From e7482c2b43889f894de21fbf959b72d194ea5db5 Mon Sep 17 00:00:00 2001 From: Johanna Larsson Date: Sun, 9 Aug 2026 19:49:27 +0100 Subject: [PATCH] Set explicit pool, support overriding This also enables passing `connect_options` as well as defining your own pool settings --- lib/latch.ex | 31 ++++++++++++--- lib/latch/client.ex | 4 +- lib/latch/config.ex | 19 ++++++++-- lib/latch/discovery.ex | 10 +++-- lib/latch/flow.ex | 2 +- lib/latch/http.ex | 71 +++++++++++++++++++++++------------ lib/latch/identity.ex | 31 +++++++-------- lib/latch/pool.ex | 21 +++++++++++ lib/latch/xrpc.ex | 4 +- mix.exs | 5 ++- mix.lock | 4 +- test/latch/client_test.exs | 4 +- test/latch/flow_test.exs | 14 +++---- test/latch/store/ets_test.exs | 24 ++++++------ test/latch/xrpc_test.exs | 33 ++++++++++++++-- test/latch_test.exs | 4 +- 16 files changed, 197 insertions(+), 84 deletions(-) create mode 100644 lib/latch/pool.ex diff --git a/lib/latch.ex b/lib/latch.ex index 4e0a00d..8a8450b 100644 --- a/lib/latch.ex +++ b/lib/latch.ex @@ -26,6 +26,7 @@ defmodule Latch do alias Latch.Flow alias Latch.Identity alias Latch.PKCE + alias Latch.Pool alias Latch.Request @type name :: atom() | pid() @@ -71,10 +72,22 @@ defmodule Latch do :persistent_term.put({__MODULE__, self()}, config) :persistent_term.put({__MODULE__, name}, config) + pool = + if Pool.start_pool?(config) do + {Finch, name: Pool.name(config.name), pools: %{default: Pool.default_pool_opts()}} + end + + children = + Enum.reject( + [ + {Latch.NonceCache, config: config, name: name}, + pool + ], + &is_nil/1 + ) + Supervisor.init( - [ - {Latch.NonceCache, config: config, name: name} - ], + children, strategy: :one_for_one ) end @@ -134,9 +147,11 @@ defmodule Latch do dpop_key = DPoP.generate_key() state = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false) - with {:ok, identity} <- Identity.resolve_handle(handle), + with {:ok, identity} <- Identity.resolve_handle(config, handle), {:ok, server} <- - Discovery.discover(identity.pds_endpoint, allow_http: Config.localhost?(config)), + Discovery.discover(config, identity.pds_endpoint, + allow_http: Config.localhost?(config) + ), {:ok, request_uri} <- Flow.par(config, server, client_id: Config.client_id(config), @@ -218,6 +233,8 @@ defmodule Latch do * `:service` - the service endpoint identifier when proxying through PDS, eg did:web:api.bsky.app#bsky_appview * `:params` - params for the method, eg `params: [actor: "did:plc:bvraa6gajy4tfr3eh2sisdkr"]` resulting in `app.bsky.actor.getProfile?actor=did:plc:bvraa6gajy4tfr3eh2sisdkr` + * `:http` - opts to pass through to the HTTP client + * `:receive_timeout` - milliseconds before timing out the request ## Examples @@ -245,6 +262,8 @@ defmodule Latch do ## Options * `service` - the service endpoint identifier when proxying through PDS, eg did:web:api.bsky.app#bsky_appview + * `:http` - opts to pass through to the HTTP client + * `:receive_timeout` - milliseconds before timing out the request ## Examples @@ -279,6 +298,8 @@ defmodule Latch do ## Options * `service` - the service endpoint identifier when proxying through PDS, eg did:web:api.bsky.app#bsky_appview + * `:http` - opts to pass through to the HTTP client + * `:receive_timeout` - milliseconds before timing out the request """ @spec upload_blob(name(), String.t(), binary(), String.t(), keyword()) :: {:ok, map()} diff --git a/lib/latch/client.ex b/lib/latch/client.ex index aa42284..27dfeaa 100644 --- a/lib/latch/client.ex +++ b/lib/latch/client.ex @@ -102,7 +102,9 @@ defmodule Latch.Client do defp do_refresh(config, session) do result = with {:ok, server} <- - Discovery.discover(session.pds_endpoint, allow_http: Config.localhost?(config)) do + Discovery.discover(config, session.pds_endpoint, + allow_http: Config.localhost?(config) + ) do Flow.refresh(config, server, session, client_id: Config.client_id(config), client_jwk: config.signing_key diff --git a/lib/latch/config.ex b/lib/latch/config.ex index 38146d5..25c06ed 100644 --- a/lib/latch/config.ex +++ b/lib/latch/config.ex @@ -13,6 +13,7 @@ defmodule Latch.Config do * `:client_name` - shown on the authorization consent screen * `:client_uri` - client home page * `:base_url_fun` - function to build the base URL for `redirect_uri` and `client_id`, because the host is frequently only known at runtime. + * `:finch` - an optional Finch override to the Req client, refer to https://req.hexdocs.pm/Req.html#new/2 for the full set of options """ @default_request_ttl 600 @@ -24,6 +25,7 @@ defmodule Latch.Config do :signing_key, :client_name, :client_uri, + :pool, request_ttl: @default_request_ttl ] @@ -40,7 +42,8 @@ defmodule Latch.Config do name: atom() | pid(), client_name: String.t() | nil, client_uri: String.t() | nil, - request_ttl: non_neg_integer() + request_ttl: non_neg_integer(), + pool: keyword() } @modes [:confidential, :public, :localhost] @@ -56,7 +59,8 @@ defmodule Latch.Config do client_name: [type: :string, required: false], client_uri: [type: :string, required: false], request_ttl: [type: :non_neg_integer, required: false, default: @default_request_ttl], - mode: [type: {:in, @modes}, required: true] + mode: [type: {:in, @modes}, required: true], + finch: [type: :keyword_list, required: false] ] @doc false @@ -76,7 +80,8 @@ defmodule Latch.Config do client_name: validated[:client_name], client_uri: validated[:client_uri], request_ttl: validated[:request_ttl], - mode: mode + mode: mode, + pool: configure_pool(validated[:finch], validated[:name]) ) end @@ -153,4 +158,12 @@ defmodule Latch.Config do defp error(key, value, message) do raise %NimbleOptions.ValidationError{key: key, value: value, message: message} end + + defp configure_pool(nil, base) do + [name: Latch.Pool.name(base)] + end + + defp configure_pool(finch, _base) do + finch + end end diff --git a/lib/latch/discovery.ex b/lib/latch/discovery.ex index 605ff5e..65e9212 100644 --- a/lib/latch/discovery.ex +++ b/lib/latch/discovery.ex @@ -9,6 +9,7 @@ defmodule Latch.Discovery do metadata is trusted. """ + alias Latch.Config alias Latch.Error.Discovery, as: DiscoveryError alias Latch.Error.Transport alias Latch.HTTP @@ -23,16 +24,17 @@ defmodule Latch.Discovery do ## Options - `:allow_http` - in localhost mode we don't have to force https """ - @spec discover(String.t(), keyword()) :: + @spec discover(Config.t(), String.t(), keyword()) :: {:ok, ServerMetadata.t()} | {:error, DiscoveryError.t() | Transport.t()} - def discover(pds_endpoint, opts \\ []) when is_binary(pds_endpoint) do + def discover(%Config{} = config, pds_endpoint, opts \\ []) when is_binary(pds_endpoint) do allow_http = Keyword.get(opts, :allow_http, false) with :ok <- require_https(pds_endpoint, allow_http), - {:ok, resource} <- HTTP.get_json(pds_endpoint <> @protected_resource_path), + {:ok, resource} <- + HTTP.get_json(config.pool, pds_endpoint <> @protected_resource_path), {:ok, issuer} <- authorization_server(resource, pds_endpoint, allow_http), - {:ok, metadata} <- HTTP.get_json(issuer <> @auth_server_path), + {:ok, metadata} <- HTTP.get_json(config.pool, issuer <> @auth_server_path), {:ok, server} <- parse_server_metadata(metadata, pds_endpoint), :ok <- verify_issuer(server, issuer, pds_endpoint) do {:ok, server} diff --git a/lib/latch/flow.ex b/lib/latch/flow.ex index 45d67a0..5710469 100644 --- a/lib/latch/flow.ex +++ b/lib/latch/flow.ex @@ -190,7 +190,7 @@ defmodule Latch.Flow do DPoP.with_nonce(config, dpop_key, url, fn nonce -> proof = DPoP.proof(dpop_key, "POST", url, nonce: nonce) - case HTTP.post_form(url, build_form.(), [{"dpop", proof}]) do + case HTTP.post_form(config.pool, url, build_form.(), [{"dpop", proof}]) do {:ok, %{status: status, body: raw, headers: headers}} -> fresh_nonce = DPoP.nonce_header(headers) diff --git a/lib/latch/http.ex b/lib/latch/http.ex index ac6641d..7054dfa 100644 --- a/lib/latch/http.ex +++ b/lib/latch/http.ex @@ -12,15 +12,18 @@ defmodule Latch.HTTP do @receive_timeout 10_000 @type body :: nil | {:json, map()} | {:raw, binary(), String.t()} + @type pool :: Finch.name() | keyword() @doc """ GETs `url`, required a 2xx response, and decodes the body as a JSON object. """ - @spec get_json(String.t()) :: + @spec get_json(pool(), String.t()) :: {:ok, map()} | {:error, Transport.t() | InvalidResponse.t()} - def get_json(url, opts \\ []) when is_binary(url) do - with {:ok, body} <- get_body(url, opts) do + def get_json(pool, url, opts \\ []) when is_binary(url) do + opts = allowed_opts(opts) + + with {:ok, body} <- get_body(pool, url, opts) do case Jason.decode(body) do {:ok, %{} = json} -> {:ok, json} _ -> {:error, %InvalidResponse{reason: :invalid_json}} @@ -31,34 +34,40 @@ defmodule Latch.HTTP do @doc """ GETs `url`, requires a 2xx response, and returns the raw response body. """ - @spec get_text(String.t()) :: + @spec get_text(pool(), String.t()) :: {:ok, String.t()} | {:error, Transport.t() | InvalidResponse.t()} - def get_text(url) when is_binary(url) do - get_body(url) + def get_text(pool, url, opts \\ []) when is_binary(url) do + opts = allowed_opts(opts) + get_body(pool, url, opts) end @doc """ POSTs `form` as `application/x-www-form-urlencoded` with `headers`, returning the raw status, body, and response headers. - Unlike `get_json/1` this neither treats non-2xx as an error nor decodes the + Unlike `get_json` this neither treats non-2xx as an error nor decodes the body: atproto OAuth endpoints return meaningful JSON and a `DPoP-Nonce` header on 4xx responses, which the caller must inspect to drive the none retry. """ - @spec post_form(String.t(), keyword() | map(), [{String.t(), String.t()}]) :: + @spec post_form(pool(), String.t(), keyword() | map(), [{String.t(), String.t()}]) :: {:ok, %{status: pos_integer(), body: binary(), headers: %{optional(binary()) => [binary()]}}} | {:error, Transport.t()} - def post_form(url, form, headers \\ []) do - options = [ - url: url, - form: form, - headers: headers, - decode_body: false, - receive_timeout: @receive_timeout, - redirect: false - ] + def post_form(pool, url, form, headers \\ [], opts \\ []) do + options = + Keyword.merge( + [ + url: url, + form: form, + headers: headers, + decode_body: false, + receive_timeout: @receive_timeout, + redirect: false, + finch: pool + ], + allowed_opts(opts) + ) case Req.post(options) do {:ok, %Req.Response{status: status, body: body, headers: resp_headers}} -> @@ -73,21 +82,25 @@ defmodule Latch.HTTP do Performs an HTTP request with the given method, headers, and optional body, returning the raw status, body, and response headers. """ - @spec request(String.t(), String.t(), [{String.t(), String.t()}], body()) :: + @spec request(pool(), String.t(), String.t(), [{String.t(), String.t()}], body(), keyword()) :: {:ok, %{status: pos_integer(), body: binary(), headers: %{optional(binary()) => [binary()]}}} | {:error, Transport.t()} - def request(method, url, headers, body \\ nil) do + def request(pool, method, url, headers, body \\ nil, http_opts \\ []) do options = [ method: method_atom(method), url: url, headers: headers, decode_body: false, receive_timeout: @receive_timeout, - redirect: false + redirect: false, + finch: pool ] - case Req.request(put_body(options, body)) do + req = put_body(options, body) + http_opts = allowed_opts(http_opts) + + case Req.request(req, http_opts) do {:ok, %Req.Response{status: status, body: resp_body, headers: resp_headers}} -> {:ok, %{status: status, body: resp_body, headers: resp_headers}} @@ -99,8 +112,16 @@ defmodule Latch.HTTP do defp method_atom("GET"), do: :get defp method_atom("POST"), do: :post - defp get_body(url, opts \\ []) when is_binary(url) do - opts = Keyword.merge([decode_body: false, receive_timeout: @receive_timeout], opts) + defp get_body(pool, url, opts) when is_binary(url) do + opts = + Keyword.merge( + [ + decode_body: false, + receive_timeout: @receive_timeout, + finch: pool + ], + opts + ) case Req.get(url, opts) do {:ok, %Req.Response{status: status, body: body}} when status in 200..299 -> @@ -122,4 +143,8 @@ defmodule Latch.HTTP do |> Keyword.put(:body, body) |> Keyword.update!(:headers, &[{"content-type", content_type} | &1]) end + + defp allowed_opts(opts) do + Keyword.take(opts, [:receive_timeout]) + end end diff --git a/lib/latch/identity.ex b/lib/latch/identity.ex index 9946e36..39202f4 100644 --- a/lib/latch/identity.ex +++ b/lib/latch/identity.ex @@ -9,6 +9,7 @@ defmodule Latch.Identity do DID. """ + alias Latch.Config alias Latch.DID alias Latch.DIDDocument alias Latch.DNS @@ -36,17 +37,17 @@ defmodule Latch.Identity do Returns structured `Latch.Error` exceptions describing resolution, identity-verifiction, and transport failures. """ - @spec resolve_handle(String.t()) :: + @spec resolve_handle(Config.t(), String.t()) :: {:ok, t()} | {:error, HandleNotFound.t() | IdentityMismatch.t() | InvalidResponse.t() | Transport.t()} - def resolve_handle(handle) when is_binary(handle) do + def resolve_handle(%Config{} = config, handle) when is_binary(handle) do handle = Handle.normalize(handle) with :ok <- validate_handle(handle), - {:ok, did} <- handle_to_did(handle), + {:ok, did} <- handle_to_did(config, handle), :ok <- validate_did(did, handle), - {:ok, document} <- did_to_document(did, handle), + {:ok, document} <- did_to_document(config, did, handle), {:ok, parsed} <- parse_did_document(document, did, handle), :ok <- confirm_bidirectional(parsed, handle) do {:ok, %__MODULE__{did: did, handle: handle, pds_endpoint: parsed.pds_endpoint}} @@ -63,7 +64,7 @@ defmodule Latch.Identity do # DNS TXT is preferred, the HTTPS well-known method is only consulated when # DNS returns no record. Conflicting DNS records hard-fail per spec. - defp handle_to_did(handle) do + defp handle_to_did(config, handle) do case dns_did(handle) do {:ok, _did} = ok -> ok @@ -72,7 +73,7 @@ defmodule Latch.Identity do {:error, %HandleNotFound{handle: handle, reason: :ambiguous_dns}} :none -> - https_did(handle) + https_did(config, handle) end end @@ -103,8 +104,8 @@ defmodule Latch.Identity do end end - defp https_did(handle) do - case HTTP.get_text("https://" <> handle <> "/.well-known/atproto-did") do + defp https_did(config, handle) do + case HTTP.get_text(config.pool, "https://" <> handle <> "/.well-known/atproto-did") do {:ok, body} -> {:ok, String.trim(body)} @@ -116,20 +117,20 @@ defmodule Latch.Identity do end end - defp did_to_document("did:plc:" <> _ = did, _handle) do - fetch_did_document(@plc_directory <> "/" <> did) + defp did_to_document(config, "did:plc:" <> _ = did, _handle) do + fetch_did_document(config, @plc_directory <> "/" <> did) end - defp did_to_document("did:web:" <> host, _handle) do - fetch_did_document("https://" <> URI.decode(host) <> "/.well-known/did.json") + defp did_to_document(config, "did:web:" <> host, _handle) do + fetch_did_document(config, "https://" <> URI.decode(host) <> "/.well-known/did.json") end - defp did_to_document(_did, handle) do + defp did_to_document(_config, _did, handle) do {:error, %HandleNotFound{handle: handle, reason: :unsupported_did_method}} end - defp fetch_did_document(url) do - HTTP.get_json(url) + defp fetch_did_document(config, url) do + HTTP.get_json(config.pool, url) end defp parse_did_document(document, did, handle) do diff --git a/lib/latch/pool.ex b/lib/latch/pool.ex new file mode 100644 index 0000000..01d9f47 --- /dev/null +++ b/lib/latch/pool.ex @@ -0,0 +1,21 @@ +defmodule Latch.Pool do + @moduledoc false + + @default_pool_opts [ + conn_max_idle_time: 30_000, + pool_max_idle_time: 60_000, + size: 50 + ] + + def name(base) do + :"latch_#{base}_pool" + end + + def default_pool_opts do + @default_pool_opts + end + + def start_pool?(config) do + name(config.name) == config.pool[:name] + end +end diff --git a/lib/latch/xrpc.ex b/lib/latch/xrpc.ex index b092f65..6ea34a8 100644 --- a/lib/latch/xrpc.ex +++ b/lib/latch/xrpc.ex @@ -89,7 +89,9 @@ defmodule Latch.XRPC do headers end - case HTTP.request(http_method, url, headers, body) do + http_opts = Keyword.get(opts, :http, []) + + case HTTP.request(config.pool, http_method, url, headers, body, http_opts) do {:ok, %{status: status, body: raw, headers: resp}} -> fresh_nonce = DPoP.nonce_header(resp) diff --git a/mix.exs b/mix.exs index bab58c3..4f793d9 100644 --- a/mix.exs +++ b/mix.exs @@ -36,13 +36,14 @@ defmodule Latch.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:req, "~> 0.5"}, + {:req, "~> 0.7"}, {:jose, "~> 1.11"}, {:jason, "~> 1.2"}, {:credo, "~> 1.7", only: [:dev, :test]}, {:mimic, "~> 2.3", only: :test}, {:nimble_options, "~> 1.1"}, - {:ex_doc, "~> 0.34", only: :dev} + {:ex_doc, "~> 0.34", only: :dev}, + {:finch, "~> 0.23"} ] end diff --git a/mix.lock b/mix.lock index 1e4e2ff..c9bdead 100644 --- a/mix.lock +++ b/mix.lock @@ -13,11 +13,11 @@ "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, - "mimic": {:hex, :mimic, "2.3.0", "88b1d13c285e57df6ea57204317bb56e49e7329668006cdcb80a9aafc73a9616", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "52771f23689398c5d41c7d05e91c2c28e10df273b784f40ca8b02e35e46850d3"}, + "mimic": {:hex, :mimic, "2.4.0", "45482266f54cd66cac66557092e1f3ef0210da471706515ef7d17b379acc62a1", [:mix], [{:ham, "~> 0.3", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "88ea63ffcb549455f9505813bbb3cec1e62ececf9992eb8c91e0b7b6695382b1"}, "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [: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", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.6.3", "7fe5e68792ff0546e45d5919104fa1764a13694cfe3e48c8a0f32ad051ae77e4", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [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", "e85b5c6c990e6c3f52bbba68e6f099118f2b8252825f96c7c3636b97a3de307d"}, + "req": {:hex, :req, "0.7.3", "b141f1b465dabc5fb8ce67bd2f15a85fc80f6c75719910699560e32ce49f62ef", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [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", "73b303030dccc2b6d023ee5ada380825ab3a7cd3863aead493db09ec420ffdf2"}, "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, } diff --git a/test/latch/client_test.exs b/test/latch/client_test.exs index 77aca8b..73a8a43 100644 --- a/test/latch/client_test.exs +++ b/test/latch/client_test.exs @@ -26,7 +26,7 @@ defmodule Latch.ClientTest do :ok = Latch.TestStore.put_session(@did, stale_session) - expect(Discovery, :discover, fn @pds, _opts -> {:ok, server} end) + expect(Discovery, :discover, fn _pool, @pds, _opts -> {:ok, server} end) expect(Flow, :refresh, fn _config, ^server, ^stale_session, opts -> assert opts[:client_id] == Latch.Config.client_id(config) @@ -63,7 +63,7 @@ defmodule Latch.ClientTest do {:ok, %{"did" => @did}} end) - expect(Discovery, :discover, fn @pds, _opts -> {:ok, server} end) + expect(Discovery, :discover, fn _pool, @pds, _opts -> {:ok, server} end) expect(Flow, :refresh, fn _config, ^server, ^stale_session, _opts -> {:ok, refreshed_session} diff --git a/test/latch/flow_test.exs b/test/latch/flow_test.exs index 731e8b2..2d761d1 100644 --- a/test/latch/flow_test.exs +++ b/test/latch/flow_test.exs @@ -23,7 +23,7 @@ defmodule Latch.FlowTest do client_jwk = DPoP.generate_key() dpop_key = DPoP.generate_key() - expect(HTTP, :post_form, fn url, form, headers -> + expect(HTTP, :post_form, fn _pool, url, form, headers -> assert url == "https://issuer.example.com/oauth/token" assert form[:grant_type] == "authorization_code" assert form[:code] == "authorization-code" @@ -82,7 +82,7 @@ defmodule Latch.FlowTest do server = make_server_metadata() - expect(HTTP, :post_form, fn url, form, headers -> + expect(HTTP, :post_form, fn _pool, url, form, headers -> assert url == "https://issuer.example.com/oauth/par" assert form[:response_type] == "code" assert form[:state] == "state" @@ -121,7 +121,7 @@ defmodule Latch.FlowTest do server = make_server_metadata() - expect(HTTP, :post_form, fn _url, _form, headers -> + expect(HTTP, :post_form, fn _config, _url, _form, headers -> assert [{"dpop", proof}] = headers [_header, payload, _signature] = String.split(proof, ".") @@ -145,7 +145,7 @@ defmodule Latch.FlowTest do }} end) - expect(HTTP, :post_form, fn _url, _form, headers -> + expect(HTTP, :post_form, fn _config, _url, _form, headers -> assert [{"dpop", proof}] = headers [_header, payload, _signature] = String.split(proof, ".") @@ -191,7 +191,7 @@ defmodule Latch.FlowTest do server = make_server_metadata() - expect(HTTP, :post_form, fn url, form, _headers -> + expect(HTTP, :post_form, fn _config, url, form, _headers -> assert url == server.par_endpoint refute Keyword.has_key?(form, :client_assertion) refute Keyword.has_key?(form, :client_assertion_type) @@ -227,7 +227,7 @@ defmodule Latch.FlowTest do server = make_server_metadata() - expect(HTTP, :post_form, fn url, form, _headers -> + expect(HTTP, :post_form, fn _config, url, form, _headers -> assert url == server.par_endpoint refute Keyword.has_key?(form, :client_assertion) refute Keyword.has_key?(form, :client_assertion_type) @@ -254,7 +254,7 @@ defmodule Latch.FlowTest do describe "refresh/3" do test "rejects a refresh when discovery returns a different issuer" do - reject(HTTP, :post_form, 3) + reject(HTTP, :post_form, 5) config = make_config() start_link_supervised!( diff --git a/test/latch/store/ets_test.exs b/test/latch/store/ets_test.exs index 8ebbb6a..6f610fb 100644 --- a/test/latch/store/ets_test.exs +++ b/test/latch/store/ets_test.exs @@ -28,8 +28,8 @@ defmodule Latch.Store.ETSTest do server = server() request_uri = "urn:ietf:params:oauth:request_uri:request" - expect(Identity, :resolve_handle, fn _handle -> {:ok, identity} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, _handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :par, fn _config, _server, opts -> send(self(), {:state, opts[:state]}) @@ -93,8 +93,8 @@ defmodule Latch.Store.ETSTest do server = server() request_uri = "urn:ietf:params:oauth:request_uri:request" - expect(Identity, :resolve_handle, fn _handle -> {:ok, identity} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, _handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :par, fn _config, _server, opts -> send(self(), {:state, opts[:state]}) @@ -152,8 +152,8 @@ defmodule Latch.Store.ETSTest do server = server() request_uri = "urn:ietf:params:oauth:request_uri:request" - expect(Identity, :resolve_handle, fn _handle -> {:ok, identity} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, _handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :par, fn _config, _server, opts -> send(self(), {:state, opts[:state]}) @@ -184,8 +184,8 @@ defmodule Latch.Store.ETSTest do server = server() request_uri = "urn:ietf:params:oauth:request_uri:request" - expect(Identity, :resolve_handle, fn _handle -> {:ok, identity} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, _handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :par, fn _config, _server, opts -> send(self(), {:state, opts[:state]}) @@ -213,7 +213,7 @@ defmodule Latch.Store.ETSTest do {:ok, session} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :refresh, fn _config, _server, old_session, _opts -> assert session == old_session @@ -258,8 +258,8 @@ defmodule Latch.Store.ETSTest do server = server() request_uri = "urn:ietf:params:oauth:request_uri:request" - expect(Identity, :resolve_handle, fn _handle -> {:ok, identity} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, _handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) expect(Flow, :par, fn _config, _server, opts -> send(self(), {:state, opts[:state]}) @@ -287,7 +287,7 @@ defmodule Latch.Store.ETSTest do {:ok, session} end) - expect(Discovery, :discover, fn _pds, _opts -> {:ok, server} end) + expect(Discovery, :discover, fn _config, _pds, _opts -> {:ok, server} end) # Refresh only happens once even when we trigger refresh 10 times # by using XPRC in a `for` below. diff --git a/test/latch/xrpc_test.exs b/test/latch/xrpc_test.exs index 45262bb..35cbb68 100644 --- a/test/latch/xrpc_test.exs +++ b/test/latch/xrpc_test.exs @@ -22,7 +22,7 @@ defmodule Latch.XRPCTest do {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} ) - expect(HTTP, :request, fn http_method, url, headers, body -> + expect(HTTP, :request, fn _pool, http_method, url, headers, body, _opts -> assert http_method == "GET" assert url == "#{session.pds_endpoint}/xrpc/#{method}?#{URI.encode_query(params)}" assert {"atproto-proxy", service} in headers @@ -50,7 +50,7 @@ defmodule Latch.XRPCTest do {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} ) - expect(HTTP, :request, fn http_method, url, headers, body -> + expect(HTTP, :request, fn _pool, http_method, url, headers, body, _opts -> assert http_method == "GET" assert url == "#{session.pds_endpoint}/xrpc/#{method}" assert {"authorization", "DPoP access-token"} in headers @@ -63,6 +63,31 @@ defmodule Latch.XRPCTest do assert {:ok, %{"did" => "string"}} = XRPC.query(config, session, method, []) end + + test "passes http opts through" do + config = make_config() + session = make_session() + method = "com.atproto.server.getSession" + + start_link_supervised!( + {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} + ) + + expect(HTTP, :request, fn _pool, http_method, url, headers, body, opts -> + assert Keyword.fetch!(opts, :receive_timeout) == 30_000 + assert http_method == "GET" + assert url == "#{session.pds_endpoint}/xrpc/#{method}" + assert {"authorization", "DPoP access-token"} in headers + assert {"dpop", _} = List.keyfind(headers, "dpop", 0) + + assert body == nil + + {:ok, %{status: 200, body: ~s|{"did": "string"}|, headers: %{}}} + end) + + assert {:ok, %{"did" => "string"}} = + XRPC.query(config, session, method, http: [receive_timeout: 30_000]) + end end describe "procedure/5" do @@ -77,7 +102,7 @@ defmodule Latch.XRPCTest do {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} ) - expect(HTTP, :request, fn http_method, url, headers, body -> + expect(HTTP, :request, fn _pool, http_method, url, headers, body, _opts -> assert http_method == "POST" assert url == "#{session.pds_endpoint}/xrpc/#{method}" assert {"atproto-proxy", service} in headers @@ -106,7 +131,7 @@ defmodule Latch.XRPCTest do {Latch.NonceCache, config: config, name: config.name, sweep_disabled: true} ) - expect(HTTP, :request, fn http_method, url, headers, body -> + expect(HTTP, :request, fn _pool, http_method, url, headers, body, _opts -> assert http_method == "POST" assert url == "#{session.pds_endpoint}/xrpc/#{method}" assert {"atproto-proxy", service} in headers diff --git a/test/latch_test.exs b/test/latch_test.exs index f7a7250..a058517 100644 --- a/test/latch_test.exs +++ b/test/latch_test.exs @@ -38,8 +38,8 @@ defmodule LatchTest do identity = %Identity{did: @did, handle: @handle, pds_endpoint: @pds} server = server() - expect(Identity, :resolve_handle, fn @handle -> {:ok, identity} end) - expect(Discovery, :discover, fn @pds, _opts -> {:ok, server} end) + expect(Identity, :resolve_handle, fn _config, @handle -> {:ok, identity} end) + expect(Discovery, :discover, fn _config, @pds, _opts -> {:ok, server} end) expect(Flow, :par, fn config, ^server, opts -> assert opts[:client_id] == Latch.Config.client_id(config) -- 2.51.2