From f327e2eb7b687c272aa5c2bcf4904d135aeaf378 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Jun 2024 13:10:12 +0200 Subject: [PATCH] Move Finch tests to Req.FinchTest --- test/req/finch_test.exs | 402 ++++++++++++++++++++++++++++++++++ test/req/steps_test.exs | 462 +++------------------------------------- test/req_test.exs | 6 +- test/test_helper.exs | 24 ++- 4 files changed, 451 insertions(+), 443 deletions(-) diff --git a/test/req/finch_test.exs b/test/req/finch_test.exs index 7c50057..07c01f2 100644 --- a/test/req/finch_test.exs +++ b/test/req/finch_test.exs @@ -1,5 +1,407 @@ defmodule Req.FinchTest do use ExUnit.Case, async: true + import TestHelper, only: [start_http_server: 1, start_tcp_server: 1] + + describe "run" do + test ":finch_request" do + %{url: url} = + start_http_server(fn conn -> + Plug.Conn.send_resp(conn, 200, "ok") + end) + + pid = self() + + fun = fn req, finch_request, finch_name, finch_opts -> + {:ok, resp} = Finch.request(finch_request, finch_name, finch_opts) + send(pid, resp) + {req, Req.Response.new(status: resp.status, headers: resp.headers, body: "finch_request")} + end + + assert Req.get!(url, finch_request: fun).body == "finch_request" + assert_received %Finch.Response{body: "ok"} + end + + test ":finch_request error" do + fun = fn req, _finch_request, _finch_name, _finch_opts -> + {req, %ArgumentError{message: "exec error"}} + end + + assert_raise ArgumentError, "exec error", fn -> + Req.get!("http://localhost", finch_request: fun, retry: false) + end + end + + test ":finch_request with invalid return" do + fun = fn _, _, _, _ -> :ok end + + assert_raise RuntimeError, ~r"expected adapter to return \{request, response\}", fn -> + Req.get!("http://localhost", finch_request: fun) + end + end + + test "pool timeout" do + %{url: url} = + start_http_server(fn conn -> + Plug.Conn.send_resp(conn, 200, "ok") + end) + + options = [pool_timeout: 0] + + assert_raise RuntimeError, ~r/unable to provide a connection within the timeout/, fn -> + Req.get!(url, options) + end + end + + test ":receive_timeout" do + pid = self() + + %{url: url} = + start_tcp_server(fn socket -> + assert {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) + send(pid, :ping) + body = "ok" + + Process.sleep(1000) + + data = """ + HTTP/1.1 200 OK + content-length: #{byte_size(body)} + + #{body} + """ + + :ok = :gen_tcp.send(socket, data) + end) + + req = Req.new(url: url, receive_timeout: 50, retry: false) + assert {:error, %Req.TransportError{reason: :timeout}} = Req.request(req) + assert_received :ping + end + + test "Req.HTTPError" do + %{url: url} = + start_tcp_server(fn socket -> + assert {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) + :ok = :gen_tcp.send(socket, "bad\r\n") + end) + + req = Req.new(url: url, retry: false) + {:error, %Req.HTTPError{protocol: :http1, reason: :invalid_status_line}} = Req.request(req) + end + + test ":connect_options :protocol" do + %{url: url} = + start_http_server(fn conn -> + assert Plug.Conn.get_http_protocol(conn) == :"HTTP/2" + Plug.Conn.send_resp(conn, 200, "ok") + end) + + req = Req.new(url: url, connect_options: [protocols: [:http2]], retry: false) + assert Req.request!(req).body == "ok" + end + + test ":connect_options :proxy" do + %{url: url} = + start_http_server(fn conn -> + Plug.Conn.send_resp(conn, 200, "ok") + end) + + # Bandit will forward request to itself + # Not quite a proper forward proxy server, but good enough + proxy = {:http, "localhost", url.port, []} + + req = Req.new(base_url: url, connect_options: [proxy: proxy]) + assert Req.request!(req).body == "ok" + end + + test ":connect_options :hostname" do + %{url: url} = + start_http_server(fn conn -> + assert ["example.com:" <> _] = Plug.Conn.get_req_header(conn, "host") + Plug.Conn.send_resp(conn, 200, "ok") + end) + + req = Req.new(base_url: url, connect_options: [hostname: "example.com"]) + assert Req.request!(req).body == "ok" + end + + test ":connect_options :transport_opts" do + %{url: url} = + start_http_server(fn conn -> + Plug.Conn.send_resp(conn, 200, "ok") + end) + + req = Req.new(connect_options: [transport_opts: [cacertfile: "bad.pem"]]) + + assert_raise File.Error, ~r/could not read file "bad.pem"/, fn -> + Req.request!(req, url: %{url | scheme: "https"}) + end + end + + defmodule ExamplePlug do + def init(options), do: options + + def call(conn, []) do + Plug.Conn.send_resp(conn, 200, "ok") + end + end + + test ":inet6" do + start_supervised!( + {Plug.Cowboy, scheme: :http, plug: ExamplePlug, ref: ExamplePlug.IPv4, port: 0} + ) + + start_supervised!( + {Plug.Cowboy, + scheme: :http, + plug: ExamplePlug, + ref: ExamplePlug.IPv6, + port: 0, + net: :inet6, + ipv6_v6only: true} + ) + + ipv4_port = :ranch.get_port(ExamplePlug.IPv4) + ipv6_port = :ranch.get_port(ExamplePlug.IPv6) + + req = Req.new(url: "http://localhost:#{ipv4_port}") + assert Req.request!(req).body == "ok" + + req = Req.new(url: "http://localhost:#{ipv4_port}", inet6: true) + assert Req.request!(req).body == "ok" + + req = Req.new(url: "http://localhost:#{ipv6_port}", inet6: true) + assert Req.request!(req).body == "ok" + + req = Req.new(url: "http://[::1]:#{ipv6_port}") + assert Req.request!(req).body == "ok" + end + + test ":connect_options bad option" do + assert_raise ArgumentError, "unknown option :timeou. Did you mean :timeout?", fn -> + Req.get!("http://localhost", connect_options: [timeou: 0]) + end + end + + test ":finch and :connect_options" do + assert_raise ArgumentError, "cannot set both :finch and :connect_options", fn -> + Req.request!(finch: MyFinch, connect_options: [timeout: 0]) + end + end + + def send_telemetry_metadata_pid(_name, _measurements, metadata, _) do + send(metadata.request.private.pid, :telemetry_private) + :ok + end + + test ":finch_private", %{test: test} do + on_exit(fn -> :telemetry.detach("#{test}") end) + + :ok = + :telemetry.attach( + "#{test}", + [:finch, :request, :stop], + &__MODULE__.send_telemetry_metadata_pid/4, + nil + ) + + %{url: url} = + start_http_server(fn conn -> + Plug.Conn.send_resp(conn, 200, "finch_private") + end) + + assert Req.get!(url, finch_private: %{pid: self()}).body == "finch_private" + assert_received :telemetry_private + end + + test "into: fun" do + %{url: url} = + start_tcp_server(fn socket -> + {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) + + data = """ + HTTP/1.1 200 OK + transfer-encoding: chunked + trailer: x-foo, x-bar + + 6\r + chunk1\r + 6\r + chunk2\r + 0\r + x-foo: foo\r + x-bar: bar\r + \r + """ + + :ok = :gen_tcp.send(socket, data) + end) + + pid = self() + + resp = + Req.get!( + url: url, + into: fn {:data, data}, acc -> + send(pid, {:data, data}) + {:cont, acc} + end + ) + + assert resp.status == 200 + assert resp.headers["transfer-encoding"] == ["chunked"] + assert resp.headers["trailer"] == ["x-foo, x-bar"] + + assert resp.trailers["x-foo"] == ["foo"] + assert resp.trailers["x-bar"] == ["bar"] + + assert_receive {:data, "chunk1"} + assert_receive {:data, "chunk2"} + refute_receive _ + end + + test "into: fun with halt" do + # try fixing `** (exit) shutdown` on CI by starting custom server + defmodule StreamPlug do + def init(options), do: options + + def call(conn, []) do + conn = Plug.Conn.send_chunked(conn, 200) + {:ok, conn} = Plug.Conn.chunk(conn, "foo") + {:ok, conn} = Plug.Conn.chunk(conn, "bar") + conn + end + end + + start_supervised!({Plug.Cowboy, plug: StreamPlug, scheme: :http, port: 0}) + url = "http://localhost:#{:ranch.get_port(StreamPlug.HTTP)}" + + resp = + Req.get!( + url: url, + into: fn {:data, data}, {req, resp} -> + resp = update_in(resp.body, &(&1 <> data)) + {:halt, {req, resp}} + end + ) + + assert resp.status == 200 + assert resp.body == "foo" + end + + test "into: fun handle error" do + assert {:error, %Req.TransportError{reason: :econnrefused}} = + Req.get( + url: "http://localhost:9999", + retry: false, + into: fn {:data, data}, {req, resp} -> + resp = update_in(resp.body, &(&1 <> data)) + {:halt, {req, resp}} + end + ) + end + + test "into: collectable" do + %{url: url} = + start_tcp_server(fn socket -> + {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) + + data = """ + HTTP/1.1 200 OK + transfer-encoding: chunked + trailer: x-foo, x-bar + + 6\r + chunk1\r + 6\r + chunk2\r + 0\r + x-foo: foo\r + x-bar: bar\r + \r + """ + + :ok = :gen_tcp.send(socket, data) + end) + + resp = + Req.get!( + url: url, + into: [] + ) + + assert resp.status == 200 + assert resp.headers["transfer-encoding"] == ["chunked"] + assert resp.headers["trailer"] == ["x-foo, x-bar"] + + assert resp.trailers["x-foo"] == ["foo"] + assert resp.trailers["x-bar"] == ["bar"] + + assert resp.body == ["chunk1", "chunk2"] + end + + test "into: collectable handle error" do + assert {:error, %Req.TransportError{reason: :econnrefused}} = + Req.get( + url: "http://localhost:9999", + retry: false, + into: IO.stream() + ) + end + + # TODO + @tag :skip + test "into: fun with content-encoding" do + %{url: url} = + start_http_server(fn conn -> + conn + |> Plug.Conn.put_resp_header("content-encoding", "gzip") + |> Plug.Conn.send_resp(200, :zlib.gzip("foo")) + end) + + pid = self() + + fun = fn {:data, data}, acc -> + send(pid, {:data, data}) + {:cont, acc} + end + + assert Req.get!(url: url, into: fun).body == "" + assert_received {:data, "foo"} + refute_receive _ + end + + test "into: :self" do + %{url: url} = + start_http_server(fn conn -> + conn = Plug.Conn.send_chunked(conn, 200) + {:ok, conn} = Plug.Conn.chunk(conn, "foo") + {:ok, conn} = Plug.Conn.chunk(conn, "bar") + conn + end) + + resp = Req.get!(url: url, into: :self) + assert resp.status == 200 + assert {:ok, [data: "foo"]} = Req.parse_message(resp, assert_receive(_)) + assert {:ok, [data: "bar"]} = Req.parse_message(resp, assert_receive(_)) + assert {:ok, [:done]} = Req.parse_message(resp, assert_receive(_)) + refute_receive _ + end + + test "into: :self cancel" do + %{url: url} = + start_http_server(fn conn -> + conn = Plug.Conn.send_chunked(conn, 200) + {:ok, conn} = Plug.Conn.chunk(conn, "foo") + {:ok, conn} = Plug.Conn.chunk(conn, "bar") + conn + end) + + resp = Req.get!(url: url, into: :self) + assert resp.status == 200 + assert :ok = Req.cancel_async_response(resp) + end + end describe "pool_options" do test "defaults" do diff --git a/test/req/steps_test.exs b/test/req/steps_test.exs index 26e55b6..2b2ec69 100644 --- a/test/req/steps_test.exs +++ b/test/req/steps_test.exs @@ -1,6 +1,6 @@ defmodule Req.StepsTest do use ExUnit.Case, async: true - import TestHelper, only: [start_server: 1] + import TestHelper, only: [start_http_server: 1, start_tcp_server: 1] require Logger setup do @@ -25,7 +25,7 @@ defmodule Req.StepsTest do describe "put_base_url" do test "it works" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "ok") end) @@ -39,7 +39,7 @@ defmodule Req.StepsTest do test "with absolute url" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "ok") end) @@ -48,16 +48,16 @@ defmodule Req.StepsTest do test "with base path" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> assert conn.request_path == "/api/v2/foo" Plug.Conn.send_resp(conn, 200, "ok") end) - assert Req.get!("/foo", base_url: url <> "/api/v2", retry: false).body == "ok" - assert Req.get!("foo", base_url: url <> "/api/v2").body == "ok" - assert Req.get!("/foo", base_url: url <> "/api/v2/").body == "ok" - assert Req.get!("foo", base_url: url <> "/api/v2/").body == "ok" - assert Req.get!("", base_url: url <> "/api/v2/foo").body == "ok" + assert Req.get!("/foo", base_url: "#{url}/api/v2", retry: false).body == "ok" + assert Req.get!("foo", base_url: "#{url}/api/v2").body == "ok" + assert Req.get!("/foo", base_url: "#{url}/api/v2/").body == "ok" + assert Req.get!("foo", base_url: "#{url}/api/v2/").body == "ok" + assert Req.get!("", base_url: "#{url}/api/v2/foo").body == "ok" end test "function" do @@ -94,7 +94,7 @@ defmodule Req.StepsTest do @tag :tmp_dir test ":netrc", c do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> expected = "Basic " <> Base.encode64("foo:bar") case Plug.Conn.get_req_header(conn, "authorization") do @@ -145,7 +145,7 @@ defmodule Req.StepsTest do @tag :tmp_dir test "{:netrc, path}", c do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> expected = "Basic " <> Base.encode64("foo:bar") case Plug.Conn.get_req_header(conn, "authorization") do @@ -198,7 +198,7 @@ defmodule Req.StepsTest do # here for locality test "body" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn) Plug.Conn.send_resp(conn, 200, body) end) @@ -214,7 +214,7 @@ defmodule Req.StepsTest do test "body stream" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn) Plug.Conn.send_resp(conn, 200, body) end) @@ -230,7 +230,7 @@ defmodule Req.StepsTest do test "json" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> assert {:ok, ~s|{"a":1}|, conn} = Plug.Conn.read_body(conn) assert ["application/json"] = Plug.Conn.get_req_header(conn, "accept") assert ["application/json"] = Plug.Conn.get_req_header(conn, "content-type") @@ -296,7 +296,7 @@ defmodule Req.StepsTest do test "stream" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> assert {:ok, body, conn} = Plug.Conn.read_body(conn) body = :zlib.gunzip(body) Plug.Conn.send_resp(conn, 200, body) @@ -315,7 +315,7 @@ defmodule Req.StepsTest do test "nil body" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> assert Plug.Conn.get_req_header(conn, "content-encoding") == [] Plug.Conn.send_resp(conn, 200, "ok") end) @@ -332,7 +332,7 @@ defmodule Req.StepsTest do test "into: binary" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "foo") end) @@ -360,7 +360,7 @@ defmodule Req.StepsTest do test "into: binary with gzip" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> ["zstd, br, gzip"] = Plug.Conn.get_req_header(conn, "accept-encoding") conn @@ -386,7 +386,7 @@ defmodule Req.StepsTest do test "into: fun" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "foo") end) @@ -417,7 +417,7 @@ defmodule Req.StepsTest do test "into: collectable" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "foo") end) @@ -446,7 +446,7 @@ defmodule Req.StepsTest do test "into: :self" do %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> Plug.Conn.send_resp(conn, 200, "foo") end) @@ -642,7 +642,7 @@ defmodule Req.StepsTest do test "multiple codecs with multiple headers" do %{url: url} = - start_socket(fn socket -> + start_tcp_server(fn socket -> assert {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) body = "foo" |> :zlib.gzip() |> :ezstd.compress() @@ -1650,7 +1650,7 @@ defmodule Req.StepsTest do pid = self() %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> case Plug.Conn.get_req_header(conn, "if-modified-since") do [] -> send(pid, :cache_miss) @@ -1688,7 +1688,7 @@ defmodule Req.StepsTest do {:ok, _} = Agent.start_link(fn -> 0 end, name: :counter) %{url: url} = - start_server(fn conn -> + start_http_server(fn conn -> case Plug.Conn.get_req_header(conn, "if-modified-since") do [] -> send(pid, :cache_miss) @@ -1893,420 +1893,6 @@ defmodule Req.StepsTest do end end - describe "run_finch" do - test ":finch_request" do - %{url: url} = - start_server(fn conn -> - Plug.Conn.send_resp(conn, 200, "ok") - end) - - pid = self() - - fun = fn req, finch_request, finch_name, finch_opts -> - {:ok, resp} = Finch.request(finch_request, finch_name, finch_opts) - send(pid, resp) - {req, Req.Response.new(status: resp.status, headers: resp.headers, body: "finch_request")} - end - - assert Req.get!(url, finch_request: fun).body == "finch_request" - assert_received %Finch.Response{body: "ok"} - end - - test ":finch_request error", c do - fun = fn req, _finch_request, _finch_name, _finch_opts -> - {req, %ArgumentError{message: "exec error"}} - end - - assert_raise ArgumentError, "exec error", fn -> - Req.get!(c.url, finch_request: fun, retry: false) - end - end - - test ":finch_request with invalid return", c do - fun = fn _, _, _, _ -> :ok end - - assert_raise RuntimeError, ~r"expected adapter to return \{request, response\}", fn -> - Req.get!(c.url, finch_request: fun) - end - end - - test "pool timeout" do - %{url: url} = - start_server(fn conn -> - Plug.Conn.send_resp(conn, 200, "ok") - end) - - options = [pool_timeout: 0] - - assert_raise RuntimeError, ~r/unable to provide a connection within the timeout/, fn -> - Req.get!(url, options) - end - end - - test ":receive_timeout" do - pid = self() - - %{url: url} = - start_socket(fn socket -> - assert {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) - send(pid, :ping) - body = "ok" - - Process.sleep(1000) - - data = """ - HTTP/1.1 200 OK - content-length: #{byte_size(body)} - - #{body} - """ - - :ok = :gen_tcp.send(socket, data) - end) - - req = Req.new(url: url, receive_timeout: 50, retry: false) - assert {:error, %Req.TransportError{reason: :timeout}} = Req.request(req) - assert_received :ping - end - - test "Req.HTTPError" do - %{url: url} = - start_socket(fn socket -> - assert {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) - :ok = :gen_tcp.send(socket, "bad\r\n") - end) - - req = Req.new(url: url, retry: false) - {:error, %Req.HTTPError{protocol: :http1, reason: :invalid_status_line}} = Req.request(req) - end - - test ":connect_options :protocol", c do - Bypass.stub(c.bypass, "GET", "/", fn conn -> - {_, %{version: :"HTTP/2"}} = conn.adapter - Plug.Conn.send_resp(conn, 200, "ok") - end) - - req = Req.new(url: c.url, connect_options: [protocols: [:http2]]) - assert Req.request!(req).body == "ok" - end - - test ":connect_options :proxy", c do - Bypass.expect(c.bypass, "GET", "/foo/bar", fn conn -> - Plug.Conn.send_resp(conn, 200, "ok") - end) - - # Bypass will forward request to itself - # Not quite a proper forward proxy server, but good enough - test_proxy = {:http, "localhost", c.bypass.port, []} - - req = Req.new(base_url: c.url, connect_options: [proxy: test_proxy]) - assert Req.request!(req, url: "/foo/bar").body == "ok" - end - - test ":connect_options :hostname" do - %{url: url} = - start_server(fn conn -> - assert ["example.com:" <> _] = Plug.Conn.get_req_header(conn, "host") - Plug.Conn.send_resp(conn, 200, "ok") - end) - - req = Req.new(base_url: url, connect_options: [hostname: "example.com"]) - assert Req.request!(req).body == "ok" - end - - test ":connect_options :transport_opts", c do - req = Req.new(connect_options: [transport_opts: [cacertfile: "bad.pem"]]) - - assert_raise File.Error, ~r/could not read file "bad.pem"/, fn -> - Req.request!(req, url: "https://localhost:#{c.bypass.port}") - end - end - - defmodule ExamplePlug do - def init(options), do: options - - def call(conn, []) do - Plug.Conn.send_resp(conn, 200, "ok") - end - end - - test ":inet6" do - start_supervised!( - {Plug.Cowboy, scheme: :http, plug: ExamplePlug, ref: ExamplePlug.IPv4, port: 0} - ) - - start_supervised!( - {Plug.Cowboy, - scheme: :http, - plug: ExamplePlug, - ref: ExamplePlug.IPv6, - port: 0, - net: :inet6, - ipv6_v6only: true} - ) - - ipv4_port = :ranch.get_port(ExamplePlug.IPv4) - ipv6_port = :ranch.get_port(ExamplePlug.IPv6) - - req = Req.new(url: "http://localhost:#{ipv4_port}") - assert Req.request!(req).body == "ok" - - req = Req.new(url: "http://localhost:#{ipv4_port}", inet6: true) - assert Req.request!(req).body == "ok" - - req = Req.new(url: "http://localhost:#{ipv6_port}", inet6: true) - assert Req.request!(req).body == "ok" - - req = Req.new(url: "http://[::1]:#{ipv6_port}") - assert Req.request!(req).body == "ok" - end - - test ":connect_options bad option", c do - assert_raise ArgumentError, "unknown option :timeou. Did you mean :timeout?", fn -> - Req.get!(c.url, connect_options: [timeou: 0]) - end - end - - test ":finch and :connect_options" do - assert_raise ArgumentError, "cannot set both :finch and :connect_options", fn -> - Req.request!(finch: MyFinch, connect_options: [timeout: 0]) - end - end - - def send_telemetry_metadata_pid(_name, _measurements, metadata, _) do - send(metadata.request.private.pid, :telemetry_private) - :ok - end - - test ":finch_private", c do - on_exit(fn -> :telemetry.detach("#{c.test}") end) - - :ok = - :telemetry.attach( - "#{c.test}", - [:finch, :request, :stop], - &__MODULE__.send_telemetry_metadata_pid/4, - nil - ) - - %{url: url} = - start_server(fn conn -> - Plug.Conn.send_resp(conn, 200, "finch_private") - end) - - assert Req.get!(url, finch_private: %{pid: self()}).body == "finch_private" - assert_received :telemetry_private - end - - test "into: fun" do - %{url: url} = - start_socket(fn socket -> - {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) - - data = """ - HTTP/1.1 200 OK - transfer-encoding: chunked - trailer: x-foo, x-bar - - 6\r - chunk1\r - 6\r - chunk2\r - 0\r - x-foo: foo\r - x-bar: bar\r - \r - """ - - :ok = :gen_tcp.send(socket, data) - end) - - pid = self() - - resp = - Req.get!( - url: url, - into: fn {:data, data}, acc -> - send(pid, {:data, data}) - {:cont, acc} - end - ) - - assert resp.status == 200 - assert resp.headers["transfer-encoding"] == ["chunked"] - assert resp.headers["trailer"] == ["x-foo, x-bar"] - - assert resp.trailers["x-foo"] == ["foo"] - assert resp.trailers["x-bar"] == ["bar"] - - assert_receive {:data, "chunk1"} - assert_receive {:data, "chunk2"} - refute_receive _ - end - - test "into: fun with halt" do - # try fixing `** (exit) shutdown` on CI by starting custom server - defmodule StreamPlug do - def init(options), do: options - - def call(conn, []) do - conn = Plug.Conn.send_chunked(conn, 200) - {:ok, conn} = Plug.Conn.chunk(conn, "foo") - {:ok, conn} = Plug.Conn.chunk(conn, "bar") - conn - end - end - - start_supervised!({Plug.Cowboy, plug: StreamPlug, scheme: :http, port: 0}) - url = "http://localhost:#{:ranch.get_port(StreamPlug.HTTP)}" - - resp = - Req.get!( - url: url, - into: fn {:data, data}, {req, resp} -> - resp = update_in(resp.body, &(&1 <> data)) - {:halt, {req, resp}} - end - ) - - assert resp.status == 200 - assert resp.body == "foo" - end - - test "into: fun handle error" do - assert {:error, %Req.TransportError{reason: :econnrefused}} = - Req.get( - url: "http://localhost:9999", - retry: false, - into: fn {:data, data}, {req, resp} -> - resp = update_in(resp.body, &(&1 <> data)) - {:halt, {req, resp}} - end - ) - end - - test "into: collectable" do - %{url: url} = - start_socket(fn socket -> - {:ok, "GET / HTTP/1.1\r\n" <> _} = :gen_tcp.recv(socket, 0) - - data = """ - HTTP/1.1 200 OK - transfer-encoding: chunked - trailer: x-foo, x-bar - - 6\r - chunk1\r - 6\r - chunk2\r - 0\r - x-foo: foo\r - x-bar: bar\r - \r - """ - - :ok = :gen_tcp.send(socket, data) - end) - - resp = - Req.get!( - url: url, - into: [] - ) - - assert resp.status == 200 - assert resp.headers["transfer-encoding"] == ["chunked"] - assert resp.headers["trailer"] == ["x-foo, x-bar"] - - assert resp.trailers["x-foo"] == ["foo"] - assert resp.trailers["x-bar"] == ["bar"] - - assert resp.body == ["chunk1", "chunk2"] - end - - test "into: collectable handle error" do - assert {:error, %Req.TransportError{reason: :econnrefused}} = - Req.get( - url: "http://localhost:9999", - retry: false, - into: IO.stream() - ) - end - - # TODO - @tag :skip - test "into: fun with content-encoding" do - %{url: url} = - start_server(fn conn -> - conn - |> Plug.Conn.put_resp_header("content-encoding", "gzip") - |> Plug.Conn.send_resp(200, :zlib.gzip("foo")) - end) - - pid = self() - - fun = fn {:data, data}, acc -> - send(pid, {:data, data}) - {:cont, acc} - end - - assert Req.get!(url: url, into: fun).body == "" - assert_received {:data, "foo"} - refute_receive _ - end - - test "into: :self" do - %{url: url} = - start_server(fn conn -> - conn = Plug.Conn.send_chunked(conn, 200) - {:ok, conn} = Plug.Conn.chunk(conn, "foo") - {:ok, conn} = Plug.Conn.chunk(conn, "bar") - conn - end) - - resp = Req.get!(url: url, into: :self) - assert resp.status == 200 - assert {:ok, [data: "foo"]} = Req.parse_message(resp, assert_receive(_)) - assert {:ok, [data: "bar"]} = Req.parse_message(resp, assert_receive(_)) - assert {:ok, [:done]} = Req.parse_message(resp, assert_receive(_)) - refute_receive _ - end - - test "into: :self cancel" do - %{url: url} = - start_server(fn conn -> - conn = Plug.Conn.send_chunked(conn, 200) - {:ok, conn} = Plug.Conn.chunk(conn, "foo") - {:ok, conn} = Plug.Conn.chunk(conn, "bar") - conn - end) - - resp = Req.get!(url: url, into: :self) - assert resp.status == 200 - assert :ok = Req.cancel_async_response(resp) - end - end - - defp start_socket(fun) do - {:ok, listen_socket} = :gen_tcp.listen(0, mode: :binary, active: false) - {:ok, port} = :inet.port(listen_socket) - pid = ExUnit.Callbacks.start_supervised!({Task, fn -> accept(listen_socket, fun) end}) - %{pid: pid, url: "http://localhost:#{port}"} - end - - defp accept(listen_socket, fun) do - case :gen_tcp.accept(listen_socket) do - {:ok, socket} -> - fun.(socket) - :ok = :gen_tcp.close(socket) - - {:error, :closed} -> - :ok - end - - accept(listen_socket, fun) - end - def create_tar(files, options \\ []) when is_list(files) do options = Keyword.validate!(options, compressed: false) compressed = Keyword.fetch!(options, :compressed) diff --git a/test/req_test.exs b/test/req_test.exs index 800e391..85a6d55 100644 --- a/test/req_test.exs +++ b/test/req_test.exs @@ -1,6 +1,6 @@ defmodule ReqTest do use ExUnit.Case, async: true - import TestHelper, only: [start_server: 1] + import TestHelper, only: [start_http_server: 1] doctest Req, only: [ @@ -76,7 +76,7 @@ defmodule ReqTest do test "async enumerable" do %{url: origin_url} = - start_server(fn conn -> + start_http_server(fn conn -> conn = Plug.Conn.send_chunked(conn, 200) {:ok, conn} = Plug.Conn.chunk(conn, "foo") {:ok, conn} = Plug.Conn.chunk(conn, "bar") @@ -85,7 +85,7 @@ defmodule ReqTest do end) %{url: echo_url} = - start_server(fn conn -> + start_http_server(fn conn -> {:ok, body, conn} = Plug.Conn.read_body(conn) Plug.Conn.send_resp(conn, 200, body) end) diff --git a/test/test_helper.exs b/test/test_helper.exs index aadfcfe..783a60b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,5 +1,5 @@ defmodule TestHelper do - def start_server(plug) do + def start_http_server(plug) do options = [ scheme: :http, port: 0, @@ -10,7 +10,27 @@ defmodule TestHelper do pid = ExUnit.Callbacks.start_supervised!({Bandit, options}) {:ok, {_ip, port}} = ThousandIsland.listener_info(pid) - %{pid: pid, url: "http://localhost:#{port}"} + %{pid: pid, url: URI.new!("http://localhost:#{port}")} + end + + def start_tcp_server(fun) do + {:ok, listen_socket} = :gen_tcp.listen(0, mode: :binary, active: false) + {:ok, port} = :inet.port(listen_socket) + pid = ExUnit.Callbacks.start_supervised!({Task, fn -> accept(listen_socket, fun) end}) + %{pid: pid, url: URI.new!("http://localhost:#{port}")} + end + + defp accept(listen_socket, fun) do + case :gen_tcp.accept(listen_socket) do + {:ok, socket} -> + fun.(socket) + :ok = :gen_tcp.close(socket) + + {:error, :closed} -> + :ok + end + + accept(listen_socket, fun) end end