From 3fa216921fbc80db94ea8748d51d09f016ae0de4 Mon Sep 17 00:00:00 2001 From: Alberto Sartori Date: Sat, 15 Apr 2023 15:12:37 +0200 Subject: [PATCH 1/3] Revert "Merge pull request #93 from Gigitsu/feature/ParametricRoute" This reverts commit 8e4b4d82c593ec43da6ef6f74a046b19b249c6a5, reversing changes made to edf2d9b3e8470e69f40c02fde89e409332412d1d. --- lib/bypass/instance.ex | 56 ++++++------------------------------------ lib/bypass/plug.ex | 5 ++-- test/bypass_test.exs | 42 ------------------------------- 3 files changed, 10 insertions(+), 93 deletions(-) diff --git a/lib/bypass/instance.ex b/lib/bypass/instance.ex index faef7ea..2a5c69e 100644 --- a/lib/bypass/instance.ex +++ b/lib/bypass/instance.ex @@ -4,7 +4,6 @@ defmodule Bypass.Instance do use GenServer, restart: :transient import Bypass.Utils - import Plug.Router.Utils, only: [build_path_match: 1] def start_link(opts \\ []) do GenServer.start_link(__MODULE__, [opts]) @@ -135,7 +134,6 @@ defmodule Bypass.Instance do route, new_route( fun, - path, case expect do :expect -> :once_or_more :expect_once -> :once @@ -276,46 +274,17 @@ defmodule Bypass.Instance do end defp route_info(method, path, %{expectations: expectations} = _state) do - segments = build_path_match(path) |> elem(1) - route = - expectations - |> Enum.reduce_while( - {:any, :any, %{}}, - fn - {{^method, path_pattern}, %{path_parts: path_parts}}, acc -> - case match_route(segments, path_parts) do - {true, params} -> {:halt, {method, path_pattern, params}} - {false, _} -> {:cont, acc} - end - - _, acc -> - {:cont, acc} - end - ) - - {route, Map.get(expectations, route)} - end - - defp match_route(path, route) when length(path) == length(route) do - path - |> Enum.zip(route) - |> Enum.reduce_while( - {true, %{}}, - fn - {value, {param, _, _}}, {_, params} -> - {:cont, {true, Map.put(params, Atom.to_string(param), value)}} - - {segment, segment}, acc -> - {:cont, acc} + case Map.get(expectations, {method, path}, :no_expectations) do + :no_expectations -> + {:any, :any} - _, _ -> - {:halt, {false, nil}} + _ -> + {method, path} end - ) - end - defp match_route(_, _), do: {false, nil} + {route, Map.get(expectations, route)} + end defp do_up(port, ref) do plug_opts = [self()] @@ -398,25 +367,16 @@ defmodule Bypass.Instance do |> length end - defp new_route(fun, path_parts, expected) when is_list(path_parts) do + defp new_route(fun, expected) do %{ fun: fun, expected: expected, - path_parts: path_parts, retained_plugs: %{}, results: [], request_count: 0 } end - defp new_route(fun, :any, expected) do - new_route(fun, [], expected) - end - - defp new_route(fun, path, expected) do - new_route(fun, build_path_match(path) |> elem(1), expected) - end - defp cowboy_opts(port, ref, socket) do [ref: ref, port: port, transport_options: [num_acceptors: 5, socket: socket]] end diff --git a/lib/bypass/plug.ex b/lib/bypass/plug.ex index b0e3d3e..253e62d 100644 --- a/lib/bypass/plug.ex +++ b/lib/bypass/plug.ex @@ -4,9 +4,8 @@ defmodule Bypass.Plug do def init([pid]), do: pid def call(%{method: method, request_path: request_path} = conn, pid) do - {method, path, path_params} = Bypass.Instance.call(pid, {:get_route, method, request_path}) - route = {method, path} - conn = Plug.Conn.fetch_query_params(%{conn | params: path_params}) + route = Bypass.Instance.call(pid, {:get_route, method, request_path}) + ref = make_ref() case Bypass.Instance.call(pid, {:get_expect_fun, route}) do {:ok, ref, fun} -> diff --git a/test/bypass_test.exs b/test/bypass_test.exs index 6f41000..d256721 100644 --- a/test/bypass_test.exs +++ b/test/bypass_test.exs @@ -346,48 +346,6 @@ defmodule BypassTest do end) end - test "Bypass.stub/4 does not raise if request with parameters is made" do - :stub |> specific_route_with_params - end - - test "Bypass.expect/4 can be used to define a specific route with parameters" do - :expect |> specific_route_with_params - end - - test "Bypass.expect_once/4 can be used to define a specific route with parameters" do - :expect_once |> specific_route_with_params - end - - defp specific_route_with_params(expect_fun) do - bypass = Bypass.open() - method = "POST" - pattern = "/this/:resource/get/:id" - path = "/this/my_resource/get/1234" - - apply(Bypass, expect_fun, [ - bypass, - method, - pattern, - fn conn -> - assert conn.method == method - assert conn.request_path == path - - assert conn.params == %{ - "resource" => "my_resource", - "id" => "1234", - "q_param_1" => "a", - "q_param_2" => "b" - } - - Plug.Conn.send_resp(conn, 200, "") - end - ]) - - capture_log(fn -> - assert {:ok, 200, ""} = request(bypass.port, path <> "?q_param_1=a&q_param_2=b") - end) - end - test "All routes to a Bypass.expect/4 call must be called" do :expect |> all_routes_must_be_called end From a0fcafbd5929b3e333b3f8886d4ae10bdb2b1981 Mon Sep 17 00:00:00 2001 From: Alberto Sartori Date: Mon, 17 Apr 2023 09:36:51 +0200 Subject: [PATCH 2/3] Fix ranch requirement too strict --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index ba85a76..42398d5 100644 --- a/mix.exs +++ b/mix.exs @@ -28,7 +28,7 @@ defmodule Bypass.Mixfile do [ {:plug_cowboy, "~> 2.0"}, {:plug, "~> 1.7"}, - {:ranch, "~> 1.7.1"}, + {:ranch, "~> 1.7"}, {:ex_doc, "> 0.0.0", only: :dev}, {:espec, "~> 1.6", only: [:dev, :test]}, {:mint, "~> 1.1", only: :test} From 532596514ea1f997afc9c4cc1c6034727e612a9f Mon Sep 17 00:00:00 2001 From: Timmo Verlaan Date: Wed, 25 Oct 2023 13:07:43 +0200 Subject: [PATCH 3/3] Allow IP address to be given per instance This is an improvement of the current global listen_ip env config. This way one can use the full 127/8 localhost range for testing. Also see https://datatracker.ietf.org/doc/html/rfc6890#section-2.2.2 --- lib/bypass.ex | 6 ++++-- lib/bypass/instance.ex | 22 +++++++++++++++------- test/bypass_test.exs | 30 ++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/bypass.ex b/lib/bypass.ex index 4d7ae77..8adcdc7 100644 --- a/lib/bypass.ex +++ b/lib/bypass.ex @@ -5,7 +5,7 @@ defmodule Bypass do |> String.split("") |> Enum.fetch!(1) - defstruct pid: nil, port: nil + defstruct pid: nil, port: nil, ip: nil @typedoc """ Represents a Bypass server process. @@ -25,6 +25,7 @@ defmodule Bypass do ## Options - `port` - Optional TCP port to listen to requests. + - `ip` - Optional TCP IP to listen to requests. Default is `{127, 0, 0, 1}` ## Examples @@ -43,8 +44,9 @@ defmodule Bypass do def open(opts \\ []) do pid = start_instance(opts) port = Bypass.Instance.call(pid, :port) + ip = Bypass.Instance.call(pid, :ip) debug_log("Did open connection #{inspect(pid)} on port #{inspect(port)}") - bypass = %Bypass{pid: pid, port: port} + bypass = %Bypass{pid: pid, port: port, ip: ip} setup_framework_integration(test_framework(), bypass) bypass end diff --git a/lib/bypass/instance.ex b/lib/bypass/instance.ex index 2a5c69e..10cb3c2 100644 --- a/lib/bypass/instance.ex +++ b/lib/bypass/instance.ex @@ -24,16 +24,20 @@ defmodule Bypass.Instance do def init([opts]) do # Get a free port from the OS - case :ranch_tcp.listen(so_reuseport() ++ [ip: listen_ip(), port: Keyword.get(opts, :port, 0)]) do + case :ranch_tcp.listen( + so_reuseport() ++ + [ip: Keyword.get(opts, :ip, listen_ip()), port: Keyword.get(opts, :port, 0)] + ) do {:ok, socket} -> - {:ok, port} = :inet.port(socket) + {:ok, {ip, port}} = :inet.sockname(socket) :erlang.port_close(socket) ref = make_ref() - socket = do_up(port, ref) + socket = do_up(ip, port, ref) state = %{ expectations: %{}, + ip: ip, port: port, ref: ref, socket: socket, @@ -75,8 +79,12 @@ defmodule Bypass.Instance do {:reply, port, state} end - defp do_handle_call(:up, _from, %{port: port, ref: ref, socket: nil} = state) do - socket = do_up(port, ref) + defp do_handle_call(:ip, _, %{ip: ip} = state) do + {:reply, ip, state} + end + + defp do_handle_call(:up, _from, %{ip: ip, port: port, ref: ref, socket: nil} = state) do + socket = do_up(ip, port, ref) {:reply, :ok, %{state | socket: socket}} end @@ -286,9 +294,9 @@ defmodule Bypass.Instance do {route, Map.get(expectations, route)} end - defp do_up(port, ref) do + defp do_up(ip, port, ref) do plug_opts = [self()] - {:ok, socket} = :ranch_tcp.listen(so_reuseport() ++ [ip: listen_ip(), port: port]) + {:ok, socket} = :ranch_tcp.listen(so_reuseport() ++ [ip: ip, port: port]) cowboy_opts = cowboy_opts(port, ref, socket) {:ok, _pid} = Plug.Cowboy.http(Bypass.Plug, plug_opts, cowboy_opts) socket diff --git a/test/bypass_test.exs b/test/bypass_test.exs index d256721..c5e3493 100644 --- a/test/bypass_test.exs +++ b/test/bypass_test.exs @@ -39,6 +39,32 @@ defmodule BypassTest do assert(is_map(bypass2) and bypass2.__struct__ == Bypass) end + test "Bypass.open can specify an ip to operate on with expect" do + specify_ip({127, 0, 0, 2}, :expect) + end + + test "Bypass.open can specify an ip to operate on with expect_once" do + specify_ip({127, 1, 2, 3}, :expect_once) + end + + defp specify_ip(ip, expect_fun) do + port = 9876 + bypass = Bypass.open(ip: ip, port: port) + address = ip |> :inet.ntoa() |> to_string() + + apply(Bypass, expect_fun, [ + bypass, + fn conn -> + assert address == conn.host + Plug.Conn.send_resp(conn, 200, "") + end + ]) + + assert {:ok, 200, ""} = request(port, "/", "GET", address) + bypass2 = Bypass.open(ip: ip, port: port) + assert(is_map(bypass2) and bypass2.__struct__ == Bypass) + end + test "Bypass.down takes down the socket with expect" do :expect |> down_socket end @@ -388,8 +414,8 @@ defmodule BypassTest do "high-level" HTTP client, since they do connection pooling and we will sometimes get a connection closed error and not a failed to connect error, when we test Bypass.down. """ - def request(port, path \\ "/example_path", method \\ "POST") do - with {:ok, conn} <- Mint.HTTP.connect(:http, "127.0.0.1", port, mode: :passive), + def request(port, path \\ "/example_path", method \\ "POST", ip \\ "127.0.0.1") do + with {:ok, conn} <- Mint.HTTP.connect(:http, ip, port, mode: :passive), {:ok, conn, ref} <- Mint.HTTP.request(conn, method, path, [], "") do receive_responses(conn, ref, 100, []) end