From 48c1a0a0f7be51d43e4e8008a97d08eac46296bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 10:54:00 +0200 Subject: [PATCH 1/5] Add Finch to supervision tree --- lib/diff/application.ex | 1 + mix.exs | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/diff/application.ex b/lib/diff/application.ex index 7802519..c6dbead 100644 --- a/lib/diff/application.ex +++ b/lib/diff/application.ex @@ -14,6 +14,7 @@ defmodule Diff.Application do goth_spec(), {Task.Supervisor, name: Diff.Tasks}, {Phoenix.PubSub, name: Diff.PubSub}, + {Finch, name: Diff.Finch, pools: %{default: [size: 50, count: 1, conn_max_idle_time: 10_000]}}, Diff.Package.Supervisor, DiffWeb.Endpoint ] diff --git a/mix.exs b/mix.exs index 03f8b97..f0cf8d2 100644 --- a/mix.exs +++ b/mix.exs @@ -37,6 +37,7 @@ defmodule Diff.MixProject do {:bandit, "~> 1.0"}, {:tidewave, "~> 0.5", only: [:dev]}, {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, + {:finch, "~> 0.22.0"}, {:gettext, "~> 1.0"}, {:git_diff, github: "ericmj/git_diff", branch: "ericmj/fix-modes"}, {:goth, "~> 1.0"}, From 88b252cef6feb01b4483e7106ff76d0a03860f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 10:58:07 +0200 Subject: [PATCH 2/5] Replace hackney with Finch in Diff.HTTP Delete get_stream/2, put_stream/3, and the read_response/1 helper. Reimplement get/2 and put/3 against Finch. Add Bypass + tests. --- lib/diff/http.ex | 62 +++++++++-------------------------------- mix.exs | 1 + mix.lock | 6 ++++ test/diff/http_test.exs | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 test/diff/http_test.exs diff --git a/lib/diff/http.ex b/lib/diff/http.ex index e9f940d..2ed41d4 100644 --- a/lib/diff/http.ex +++ b/lib/diff/http.ex @@ -5,62 +5,26 @@ defmodule Diff.HTTP do require Logger def get(url, headers) do - :hackney.get(url, headers) - |> read_response() - end - - def put(url, headers, body) do - :hackney.put(url, headers, body) - |> read_response() - end - - def get_stream(url, headers) do - case :hackney.get(url, headers) do - {:ok, status, headers, ref} -> - stream = - Stream.unfold(:ok, fn :ok -> - case :hackney.stream_body(ref) do - :done -> - nil - - {:ok, data} -> - {data, :ok} - - {:error, reason} -> - raise "failed to stream body of #{url}, reason: #{inspect(reason)}" - end - end) + req = Finch.build(:get, url, headers) - {:ok, status, headers, stream} + case Finch.request(req, Diff.Finch) do + {:ok, %Finch.Response{status: status, headers: headers, body: body}} -> + {:ok, status, headers, body} - {:error, reason} -> - {:error, reason} + {:error, exception} -> + {:error, exception} end end - def put_stream(url, headers, stream) do - case :hackney.put(url, headers, :stream) do - {:ok, ref} -> - Enum.reduce_while(stream, :ok, fn chunk, :ok -> - case :hackney.send_body(ref, chunk) do - :ok -> {:cont, :ok} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - - ref - |> :hackney.start_response() - |> read_response() + def put(url, headers, body) do + req = Finch.build(:put, url, headers, body) - {:error, reason} -> - {:error, reason} - end - end + case Finch.request(req, Diff.Finch) do + {:ok, %Finch.Response{status: status, headers: headers, body: body}} -> + {:ok, status, headers, body} - defp read_response(result) do - with {:ok, status, headers, ref} <- result, - {:ok, body} <- :hackney.body(ref) do - {:ok, status, headers, body} + {:error, exception} -> + {:error, exception} end end diff --git a/mix.exs b/mix.exs index f0cf8d2..f67f421 100644 --- a/mix.exs +++ b/mix.exs @@ -35,6 +35,7 @@ defmodule Diff.MixProject do defp deps do [ {:bandit, "~> 1.0"}, + {:bypass, "~> 2.1", only: :test}, {:tidewave, "~> 0.5", only: [:dev]}, {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, {:finch, "~> 0.22.0"}, diff --git a/mix.lock b/mix.lock index 5b1da46..0021131 100644 --- a/mix.lock +++ b/mix.lock @@ -1,9 +1,13 @@ %{ "bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "circular_buffer": {:hex, :circular_buffer, "1.0.1", "01f0e3d5fe945080692cf6521c0988e9dbb5dc312831cefe77e5b63a4e658160", [:mix], [], "hexpm", "7d4ece3137d49c1f8dd0b3e0aa7c484f4d83a0be5d4b516c282085c1d5f2d7b9"}, + "cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.16.1", "318d385d55f657e9a5005838c4e426e13dcd724a691438384b6165a69687e531", [:make, :rebar3], [], "hexpm", "58f1e425a9e04176f1d30e20116f57c4e90ef0e187552e9741c465bdf4044f70"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, @@ -46,7 +50,9 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.19.2", "e4950525b22c6789dfb38a3f95d47171ba159da3fc5a33be9643b43d5e8adb98", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b6fce20a56af5e60fa5dfecf3f907bb98ec981be43c79a3809a499bc3d133de0"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.8.1", "5aa391a5e8d1ac3192e36a3bcaff12b5fd6ef6c7e29b53a38e63a860783e77d0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c200288673d5bc86a0ab7dc6a2a069176a74e5d573ef62740a1c517458a5f26"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, "sentry": {:hex, :sentry, "13.0.1", "6056b3d49c97ee73e1380456e64cbb9b0f7c4e49cce058a89cb4e6a9b415440e", [:mix], [{:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "8c8894c6e3c3a10fcd7e3e4c1d67ef64bd892d847d29f663a9635538943189ac"}, diff --git a/test/diff/http_test.exs b/test/diff/http_test.exs new file mode 100644 index 0000000..2ccea4b --- /dev/null +++ b/test/diff/http_test.exs @@ -0,0 +1,50 @@ +defmodule Diff.HTTPTest do + use ExUnit.Case, async: true + + setup do + bypass = Bypass.open() + {:ok, bypass: bypass} + end + + describe "get/2" do + test "returns status, headers, body on 200", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/foo", fn conn -> + conn + |> Plug.Conn.put_resp_header("x-thing", "bar") + |> Plug.Conn.resp(200, "hello") + end) + + assert {:ok, 200, headers, "hello"} = + Diff.HTTP.get("http://localhost:#{bypass.port}/foo", [{"accept", "*/*"}]) + + assert {"x-thing", "bar"} in headers + end + + test "passes request headers through", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/foo", fn conn -> + assert Plug.Conn.get_req_header(conn, "x-test") == ["sent"] + Plug.Conn.resp(conn, 204, "") + end) + + assert {:ok, 204, _, ""} = + Diff.HTTP.get("http://localhost:#{bypass.port}/foo", [{"x-test", "sent"}]) + end + + test "returns error tuple when server unreachable" do + assert {:error, _} = Diff.HTTP.get("http://localhost:1/never", []) + end + end + + describe "put/3" do + test "sends body and returns response", %{bypass: bypass} do + Bypass.expect_once(bypass, "PUT", "/blob", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + assert body == "payload" + Plug.Conn.resp(conn, 201, "ok") + end) + + assert {:ok, 201, _, "ok"} = + Diff.HTTP.put("http://localhost:#{bypass.port}/blob", [], "payload") + end + end +end From 50db19e2d0c360d7d7feecacac95517ee54b0fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 11:02:50 +0200 Subject: [PATCH 3/5] Replace hackney with Finch in Diff.Hex.Adapter Rewrites both :hex_http behaviour callbacks to use Finch with manual redirect following (up to 5 hops, preserving method/body except on 303), and adds ExUnit tests using Bypass covering the main HTTP flows. --- lib/diff/hex/adapter.ex | 116 ++++++++++++++++++++++--------- test/diff/hex/adapter_test.exs | 121 +++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 32 deletions(-) create mode 100644 test/diff/hex/adapter_test.exs diff --git a/lib/diff/hex/adapter.ex b/lib/diff/hex/adapter.ex index 867c3ee..3da6454 100644 --- a/lib/diff/hex/adapter.ex +++ b/lib/diff/hex/adapter.ex @@ -1,64 +1,116 @@ defmodule Diff.Hex.Adapter do @behaviour :hex_http - @opts [follow_redirect: true, max_redirect: 5] + @max_redirects 5 + @redirect_statuses [301, 302, 303, 307, 308] @impl true def request(method, uri, req_headers, req_body, _config) do {content_type, payload} = deconstruct_body(req_body) - req_headers = prepare_headers(req_headers, content_type) - resp = :hackney.request(method, uri, req_headers, payload, @opts) - - with {:ok, status, resp_headers, client_ref} <- resp, - {:ok, resp_body} <- :hackney.body(client_ref) do - resp_headers = Map.new(resp_headers) - {:ok, {status, resp_headers, resp_body}} - end + headers = prepare_headers(req_headers, content_type) + do_request(method, to_string(uri), headers, payload, @max_redirects) end @impl true def request_to_file(method, uri, req_headers, req_body, filename, _config) do {content_type, payload} = deconstruct_body(req_body) - req_headers = prepare_headers(req_headers, content_type) + headers = prepare_headers(req_headers, content_type) + do_request_to_file(method, to_string(uri), headers, payload, filename, @max_redirects) + end + + defp do_request(_method, _uri, _headers, _body, 0), do: {:error, :too_many_redirects} - case :hackney.request(method, uri, req_headers, payload, @opts) do - {:ok, 200, resp_headers, ref} -> - resp_headers = Map.new(resp_headers) + defp do_request(method, uri, headers, body, redirects_left) do + req = Finch.build(method, uri, headers, body) - case File.open(filename, [:write, :binary], &stream_body_to_file(ref, &1)) do - {:ok, :ok} -> {:ok, {200, resp_headers}} - {:ok, {:error, _} = error} -> error - {:error, reason} -> {:error, reason} + case Finch.request(req, Diff.Finch) do + {:ok, %Finch.Response{status: status, headers: resp_headers}} when status in @redirect_statuses -> + case follow(status, method, body, uri, resp_headers, redirects_left) do + {:follow, m, u, b, left} -> do_request(m, u, headers, b, left) + :no_location -> {:ok, {status, Map.new(resp_headers), <<>>}} end - {:ok, status, resp_headers, ref} -> - :hackney.skip_body(ref) - resp_headers = Map.new(resp_headers) - {:ok, {status, resp_headers}} + {:ok, %Finch.Response{status: status, headers: resp_headers, body: resp_body}} -> + {:ok, {status, Map.new(resp_headers), resp_body}} - {:error, reason} -> - {:error, reason} + {:error, exception} -> + {:error, exception} end end - defp stream_body_to_file(ref, file) do - case :hackney.stream_body(ref) do - {:ok, data} -> - :ok = IO.binwrite(file, data) - stream_body_to_file(ref, file) + defp do_request_to_file(_m, _u, _h, _b, _f, 0), do: {:error, :too_many_redirects} + + defp do_request_to_file(method, uri, headers, body, filename, redirects_left) do + req = Finch.build(method, uri, headers, body) + acc = %{status: nil, headers: [], file: nil} - :done -> - :ok + case Finch.stream(req, Diff.Finch, acc, &stream_collect(&1, &2, filename)) do + {:ok, %{status: status, headers: resp_headers, file: file}} when status in @redirect_statuses -> + if file, do: File.close(file) + + case follow(status, method, body, uri, resp_headers, redirects_left) do + {:follow, m, u, b, left} -> do_request_to_file(m, u, headers, b, filename, left) + :no_location -> {:ok, {status, Map.new(resp_headers)}} + end + + {:ok, %{status: status, headers: resp_headers, file: file}} -> + if file, do: File.close(file) + {:ok, {status, Map.new(resp_headers)}} {:error, reason} -> {:error, reason} end end + defp stream_collect({:status, status}, acc, _filename), do: %{acc | status: status} + defp stream_collect({:headers, hs}, acc, _filename), do: %{acc | headers: hs} + + defp stream_collect({:data, _chunk}, %{status: status} = acc, _filename) + when status in @redirect_statuses, + do: acc + + defp stream_collect({:data, chunk}, %{status: 200, file: nil} = acc, filename) do + {:ok, file} = File.open(filename, [:write, :binary]) + :ok = IO.binwrite(file, chunk) + %{acc | file: file} + end + + defp stream_collect({:data, chunk}, %{status: 200, file: file} = acc, _filename) do + :ok = IO.binwrite(file, chunk) + acc + end + + defp stream_collect({:data, _chunk}, acc, _filename), do: acc + + defp follow(status, method, body, original_uri, resp_headers, redirects_left) do + case find_header(resp_headers, "location") do + nil -> + :no_location + + location -> + {new_method, new_body} = if status == 303, do: {:get, ""}, else: {method, body} + new_uri = resolve_uri(location, original_uri) + {:follow, new_method, new_uri, new_body, redirects_left - 1} + end + end + + defp find_header(headers, name) do + case Enum.find(headers, fn {k, _v} -> String.downcase(k) == name end) do + {_, value} -> value + nil -> nil + end + end + + defp resolve_uri("http://" <> _ = absolute, _original), do: absolute + defp resolve_uri("https://" <> _ = absolute, _original), do: absolute + + defp resolve_uri(relative, original) do + original |> URI.parse() |> URI.merge(relative) |> to_string() + end + defp prepare_headers(req_headers, content_type) do if content_type do - req_headers - |> Map.put("content-type", content_type) + Map.put(req_headers, "content-type", content_type) else req_headers end diff --git a/test/diff/hex/adapter_test.exs b/test/diff/hex/adapter_test.exs new file mode 100644 index 0000000..0b007e1 --- /dev/null +++ b/test/diff/hex/adapter_test.exs @@ -0,0 +1,121 @@ +defmodule Diff.Hex.AdapterTest do + use ExUnit.Case, async: true + + setup do + bypass = Bypass.open() + {:ok, bypass: bypass, base: "http://localhost:#{bypass.port}"} + end + + describe "request/5" do + test "returns status, headers map, body on 200", %{bypass: bypass, base: base} do + Bypass.expect_once(bypass, "GET", "/p", fn conn -> + conn |> Plug.Conn.put_resp_header("x-foo", "1") |> Plug.Conn.resp(200, "hi") + end) + + assert {:ok, {200, headers, "hi"}} = + Diff.Hex.Adapter.request(:get, base <> "/p", %{}, :undefined, %{}) + + assert headers["x-foo"] == "1" + end + + test "returns 404 with body", %{bypass: bypass, base: base} do + Bypass.expect_once(bypass, "GET", "/p", fn conn -> Plug.Conn.resp(conn, 404, "nope") end) + + assert {:ok, {404, _headers, "nope"}} = + Diff.Hex.Adapter.request(:get, base <> "/p", %{}, :undefined, %{}) + end + + test "follows a 302 to a new path", %{bypass: bypass, base: base} do + Bypass.expect(bypass, fn + %{request_path: "/a"} = conn -> + conn |> Plug.Conn.put_resp_header("location", "/b") |> Plug.Conn.resp(302, "") + + %{request_path: "/b"} = conn -> + Plug.Conn.resp(conn, 200, "final") + end) + + assert {:ok, {200, _headers, "final"}} = + Diff.Hex.Adapter.request(:get, base <> "/a", %{}, :undefined, %{}) + end + + test "follows an absolute Location", %{bypass: bypass, base: base} do + Bypass.expect(bypass, fn + %{request_path: "/a"} = conn -> + conn + |> Plug.Conn.put_resp_header("location", "#{base}/b") + |> Plug.Conn.resp(301, "") + + %{request_path: "/b"} = conn -> + Plug.Conn.resp(conn, 200, "done") + end) + + assert {:ok, {200, _, "done"}} = + Diff.Hex.Adapter.request(:get, base <> "/a", %{}, :undefined, %{}) + end + + test "returns too_many_redirects after exceeding limit", %{bypass: bypass, base: base} do + Bypass.expect(bypass, fn conn -> + conn + |> Plug.Conn.put_resp_header("location", "#{base}#{conn.request_path}") + |> Plug.Conn.resp(302, "") + end) + + assert {:error, :too_many_redirects} = + Diff.Hex.Adapter.request(:get, base <> "/loop", %{}, :undefined, %{}) + end + end + + describe "request_to_file/6" do + test "streams 200 body to file", %{bypass: bypass, base: base} do + filename = + Path.join(System.tmp_dir!(), "adapter_test_#{System.unique_integer([:positive])}.bin") + + on_exit(fn -> File.rm(filename) end) + + Bypass.expect_once(bypass, "GET", "/tar", fn conn -> + Plug.Conn.resp(conn, 200, "tarball-bytes") + end) + + assert {:ok, {200, _headers}} = + Diff.Hex.Adapter.request_to_file(:get, base <> "/tar", %{}, :undefined, filename, %{}) + + assert File.read!(filename) == "tarball-bytes" + end + + test "does not write file on non-200 status", %{bypass: bypass, base: base} do + filename = + Path.join(System.tmp_dir!(), "adapter_test_#{System.unique_integer([:positive])}.bin") + + on_exit(fn -> File.rm(filename) end) + + Bypass.expect_once(bypass, "GET", "/tar", fn conn -> + Plug.Conn.resp(conn, 404, "missing") + end) + + assert {:ok, {404, _headers}} = + Diff.Hex.Adapter.request_to_file(:get, base <> "/tar", %{}, :undefined, filename, %{}) + + refute File.exists?(filename) + end + + test "follows redirect then streams to file", %{bypass: bypass, base: base} do + filename = + Path.join(System.tmp_dir!(), "adapter_test_#{System.unique_integer([:positive])}.bin") + + on_exit(fn -> File.rm(filename) end) + + Bypass.expect(bypass, fn + %{request_path: "/r"} = conn -> + conn |> Plug.Conn.put_resp_header("location", "/t") |> Plug.Conn.resp(302, "") + + %{request_path: "/t"} = conn -> + Plug.Conn.resp(conn, 200, "after-redirect") + end) + + assert {:ok, {200, _headers}} = + Diff.Hex.Adapter.request_to_file(:get, base <> "/r", %{}, :undefined, filename, %{}) + + assert File.read!(filename) == "after-redirect" + end + end +end From 2418e0c8462f7c94bb7e61763a31759bbe8df42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 11:04:19 +0200 Subject: [PATCH 4/5] Remove hackney and unused inets from extra_applications --- mix.exs | 3 +-- mix.lock | 8 -------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/mix.exs b/mix.exs index f67f421..f0b64ba 100644 --- a/mix.exs +++ b/mix.exs @@ -21,7 +21,7 @@ defmodule Diff.MixProject do def application do [ mod: {Diff.Application, []}, - extra_applications: [:logger, :inets, :runtime_tools] + extra_applications: [:logger, :runtime_tools] ] end @@ -42,7 +42,6 @@ defmodule Diff.MixProject do {:gettext, "~> 1.0"}, {:git_diff, github: "ericmj/git_diff", branch: "ericmj/fix-modes"}, {:goth, "~> 1.0"}, - {:hackney, "~> 1.15"}, {:hex_core, "~> 0.16.0"}, {:jason, "~> 1.0"}, {:logster, "~> 1.1.1"}, diff --git a/mix.lock b/mix.lock index 0021131..54e67a0 100644 --- a/mix.lock +++ b/mix.lock @@ -3,7 +3,6 @@ "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, - "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "circular_buffer": {:hex, :circular_buffer, "1.0.1", "01f0e3d5fe945080692cf6521c0988e9dbb5dc312831cefe77e5b63a4e658160", [:mix], [], "hexpm", "7d4ece3137d49c1f8dd0b3e0aa7c484f4d83a0be5d4b516c282085c1d5f2d7b9"}, "cowboy": {:hex, :cowboy, "2.15.0", "9cfe86ed7117bf045e10adbedb0170af7be57f2a3637e7be143433d8dd267396", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "179fb65140fb440a17b767ad53b755081506f9596c4db5c49c0396d8c8643668"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -18,10 +17,8 @@ "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, "git_diff": {:git, "https://github.com/ericmj/git_diff.git", "e4ee06cfd139b8a911d07e42d0ff3b15eee2b740", [branch: "ericmj/fix-modes"]}, "goth": {:hex, :goth, "1.4.5", "ee37f96e3519bdecd603f20e7f10c758287088b6d77c0147cd5ee68cf224aade", [:mix], [{:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "0fc2dce5bd710651ed179053d0300ce3a5d36afbdde11e500d57f05f398d5ed5"}, - "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "hex_core": {:hex, :hex_core, "0.16.0", "c846e75d73a84b9ab6d0c516aef21350579701a500fbf88315df27c9daa1a5af", [:rebar3], [], "hexpm", "35d4e23df07b23103f6d5cacf717b0543713ed93404f97508d195e5fd28e1af7"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, - "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"}, "lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"}, @@ -31,16 +28,13 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "makeup_gleam": {:hex, :makeup_gleam, "1.0.0", "c4032ef68d5f3abff3d3837cdefc293be727e718507813b13b501100b151420e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "cf4b1dea482dd319679d009cc057db0ca2f14c8e0451ee307575b54eae00afda"}, "makeup_syntect": {:hex, :makeup_syntect, "0.1.4", "e1230c9e0513c667b226b21c83eb182e1ab581f65af9441edab1f9ac626acba6", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}, {:rustler, "~> 0.37.1", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8.2", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5b624a434d9665786b9a352a5f3502b6c98e1996ede9936b20035ec140daef70"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.8.7", "d8d755b4ff4b449f610223dd706b4ae64155cb720d3dc09c706c079ecea189e4", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "47352f72d6ab31009ef77516b1b3a14745be97b54061fd458031b9d8294869d5"}, "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, @@ -56,12 +50,10 @@ "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, "sentry": {:hex, :sentry, "13.0.1", "6056b3d49c97ee73e1380456e64cbb9b0f7c4e49cce058a89cb4e6a9b415440e", [:mix], [{:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "8c8894c6e3c3a10fcd7e3e4c1d67ef64bd892d847d29f663a9635538943189ac"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "tidewave": {:hex, :tidewave, "0.5.6", "91f35540b5599640443f1d3a1c6166bf506e202840261a6344e384e8813c1f64", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "dc82d52b8b6ffc04680544b17cd340c7d4166bb0d63999eb960850526866b533"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, } From ecb6bcc743fd95f58b3d271d5ad669f96978ccb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Meadows-J=C3=B6nsson?= Date: Fri, 29 May 2026 21:25:32 +0200 Subject: [PATCH 5/5] Format Finch migration code --- lib/diff/application.ex | 3 ++- lib/diff/hex/adapter.ex | 6 ++++-- test/diff/hex/adapter_test.exs | 27 ++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/diff/application.ex b/lib/diff/application.ex index c6dbead..2e47c97 100644 --- a/lib/diff/application.ex +++ b/lib/diff/application.ex @@ -14,7 +14,8 @@ defmodule Diff.Application do goth_spec(), {Task.Supervisor, name: Diff.Tasks}, {Phoenix.PubSub, name: Diff.PubSub}, - {Finch, name: Diff.Finch, pools: %{default: [size: 50, count: 1, conn_max_idle_time: 10_000]}}, + {Finch, + name: Diff.Finch, pools: %{default: [size: 50, count: 1, conn_max_idle_time: 10_000]}}, Diff.Package.Supervisor, DiffWeb.Endpoint ] diff --git a/lib/diff/hex/adapter.ex b/lib/diff/hex/adapter.ex index 3da6454..fc5801d 100644 --- a/lib/diff/hex/adapter.ex +++ b/lib/diff/hex/adapter.ex @@ -24,7 +24,8 @@ defmodule Diff.Hex.Adapter do req = Finch.build(method, uri, headers, body) case Finch.request(req, Diff.Finch) do - {:ok, %Finch.Response{status: status, headers: resp_headers}} when status in @redirect_statuses -> + {:ok, %Finch.Response{status: status, headers: resp_headers}} + when status in @redirect_statuses -> case follow(status, method, body, uri, resp_headers, redirects_left) do {:follow, m, u, b, left} -> do_request(m, u, headers, b, left) :no_location -> {:ok, {status, Map.new(resp_headers), <<>>}} @@ -45,7 +46,8 @@ defmodule Diff.Hex.Adapter do acc = %{status: nil, headers: [], file: nil} case Finch.stream(req, Diff.Finch, acc, &stream_collect(&1, &2, filename)) do - {:ok, %{status: status, headers: resp_headers, file: file}} when status in @redirect_statuses -> + {:ok, %{status: status, headers: resp_headers, file: file}} + when status in @redirect_statuses -> if file, do: File.close(file) case follow(status, method, body, uri, resp_headers, redirects_left) do diff --git a/test/diff/hex/adapter_test.exs b/test/diff/hex/adapter_test.exs index 0b007e1..e632f35 100644 --- a/test/diff/hex/adapter_test.exs +++ b/test/diff/hex/adapter_test.exs @@ -77,7 +77,14 @@ defmodule Diff.Hex.AdapterTest do end) assert {:ok, {200, _headers}} = - Diff.Hex.Adapter.request_to_file(:get, base <> "/tar", %{}, :undefined, filename, %{}) + Diff.Hex.Adapter.request_to_file( + :get, + base <> "/tar", + %{}, + :undefined, + filename, + %{} + ) assert File.read!(filename) == "tarball-bytes" end @@ -93,7 +100,14 @@ defmodule Diff.Hex.AdapterTest do end) assert {:ok, {404, _headers}} = - Diff.Hex.Adapter.request_to_file(:get, base <> "/tar", %{}, :undefined, filename, %{}) + Diff.Hex.Adapter.request_to_file( + :get, + base <> "/tar", + %{}, + :undefined, + filename, + %{} + ) refute File.exists?(filename) end @@ -113,7 +127,14 @@ defmodule Diff.Hex.AdapterTest do end) assert {:ok, {200, _headers}} = - Diff.Hex.Adapter.request_to_file(:get, base <> "/r", %{}, :undefined, filename, %{}) + Diff.Hex.Adapter.request_to_file( + :get, + base <> "/r", + %{}, + :undefined, + filename, + %{} + ) assert File.read!(filename) == "after-redirect" end