From 15608fbb30dfdd9e8e98fad5f4d8c8116b0fd2f9 Mon Sep 17 00:00:00 2001 From: Dan Schultzer <1254724+danschultzer@users.noreply.github.com> Date: Sat, 18 Nov 2023 15:07:11 -0800 Subject: [PATCH] Improve errors --- CHANGELOG.md | 4 + lib/assent.ex | 103 +++++++++-------- lib/assent/config.ex | 8 +- lib/assent/http_adapter.ex | 21 +++- lib/assent/jwt_adapter/assent_jwt.ex | 16 +-- lib/assent/strategies/oauth.ex | 12 +- lib/assent/strategies/oauth2.ex | 16 +-- lib/assent/strategies/oidc.ex | 8 +- lib/assent/strategies/twitter.ex | 2 +- lib/assent/strategies/vk.ex | 20 +++- lib/assent/strategy.ex | 14 +-- test/assent/strategies/auth0_test.exs | 3 +- test/assent/strategies/facebook_test.exs | 6 - test/assent/strategies/instagram_test.exs | 2 +- test/assent/strategies/oauth2_test.exs | 71 ++++++------ test/assent/strategies/oauth_test.exs | 134 ++++++++++++---------- test/assent/strategies/oidc_test.exs | 65 +++++------ test/assent/strategies/vk_test.exs | 9 +- test/assent/strategy_test.exs | 16 +-- 19 files changed, 291 insertions(+), 239 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97251c7..69ac31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.2.8 (TBA) + +- More expressive errors now including the whole HTTP response where applicable + ## v0.2.7 (2023-09-12) * `Assent.Strategy.Strava` added diff --git a/lib/assent.ex b/lib/assent.ex index 69f4d0b..8dafe08 100644 --- a/lib/assent.ex +++ b/lib/assent.ex @@ -6,74 +6,81 @@ defmodule Assent do end defmodule CallbackCSRFError do - defexception [:message] + defexception [:key] - @spec new(binary()) :: %__MODULE__{} - def new(key) do - %__MODULE__{message: "CSRF detected with param key #{inspect key}"} + def message(exception) do + "CSRF detected with param key #{inspect exception.key}" end end defmodule MissingParamError do - defexception [:message, :params] - - @spec new(binary(), map()) :: %__MODULE__{} - def new(key, params) do - %__MODULE__{ - message: "Expected #{inspect key} to exist in params, but only found the following keys: #{inspect Map.keys(params)}", - params: params - } + defexception [:expected_key, :params] + + def message(exception) do + expected_key = inspect exception.expected_key + params = inspect Map.keys(exception.params) + + "Expected #{expected_key} in params, got: #{params}" end end defmodule RequestError do - defexception [:message, :error] + defexception [:message, :response] + + alias Assent.HTTPAdapter.HTTPResponse + + def message(exception) do + """ + #{exception.message} + + #{HTTPResponse.format(exception.response)} + """ + end + end + + defmodule InvalidResponseError do + defexception [:response] alias Assent.HTTPAdapter.HTTPResponse - @spec unexpected(HTTPResponse.t()) :: %__MODULE__{} - def unexpected(response) do - %__MODULE__{ - message: - """ - An unexpected success response was received: - - #{inspect response.body} - """, - error: :unexpected_response - } + def message(exception) do + """ + An invalid response was received. + + #{HTTPResponse.format(exception.response)} + """ end + end + + defmodule UnexpectedResponseError do + defexception [:response] - @spec invalid(HTTPResponse.t()) :: %__MODULE__{} - def invalid(response) do - %__MODULE__{ - message: - """ - Server responded with status: #{response.status} + alias Assent.HTTPAdapter.HTTPResponse - Headers:#{Enum.reduce(response.headers, "", fn {k, v}, acc -> acc <> "\n#{k}: #{v}" end)} + def message(exception) do + """ + An unexpected response was received. - Body: - #{inspect response.body} - """, - error: :invalid_server_response - } + #{HTTPResponse.format(exception.response)} + """ end + end + + defmodule ServerUnreachableError do + defexception [:http_adapter, :request_url, :reason] + + def message(exception) do + [url | _rest] = String.split(exception.request_url, "?", parts: 2) - @spec unreachable(atom(), binary(), term()) :: %__MODULE__{} - def unreachable(adapter, url, error) do - %__MODULE__{ - message: - """ - Server was unreachable with #{inspect adapter}. + """ + The server was unreachable. - Failed with the following error: - #{inspect error} + HTTP Adapter: #{inspect exception.http_adapter} + Request URL: #{url} - URL: #{url} - """, - error: :unreachable - } + Reason: + #{inspect exception.reason} + """ end end diff --git a/lib/assent/config.ex b/lib/assent/config.ex index 3a7e9b0..7f0afef 100644 --- a/lib/assent/config.ex +++ b/lib/assent/config.ex @@ -6,7 +6,11 @@ defmodule Assent.Config do defmodule MissingKeyError do @type t :: %__MODULE__{} - defexception [:message] + defexception [:key] + + def message(exception) do + "Key #{inspect exception.key} not found in config" + end end @type t :: Keyword.t() @@ -18,7 +22,7 @@ defmodule Assent.Config do def fetch(config, key) do case Keyword.fetch(config, key) do {:ok, value} -> {:ok, value} - :error -> {:error, MissingKeyError.exception("Key `:#{key}` not found in config")} + :error -> {:error, MissingKeyError.exception(key: key)} end end diff --git a/lib/assent/http_adapter.ex b/lib/assent/http_adapter.ex index 09ba33b..e30647d 100644 --- a/lib/assent/http_adapter.ex +++ b/lib/assent/http_adapter.ex @@ -21,12 +21,31 @@ defmodule Assent.HTTPAdapter do @type header :: {binary(), binary()} @type t :: %__MODULE__{ + http_adapter: atom(), + request_url: binary(), status: integer(), headers: [header()], body: binary() | term() } - defstruct status: 200, headers: [], body: "" + defstruct http_adapter: nil, request_url: nil, status: 200, headers: [], body: "" + + def format(response) do + [request_url | _rest] = String.split(response.request_url, "?", parts: 2) + + """ + HTTP Adapter: #{inspect response.http_adapter} + Request URL: #{request_url} + + Response status: #{response.status} + + Response headers: + #{Enum.reduce(response.headers, "", fn {k, v}, acc -> acc <> "\n#{k}: #{v}" end)} + + Response body: + #{inspect response.body} + """ + end end @type method :: :get | :post diff --git a/lib/assent/jwt_adapter/assent_jwt.ex b/lib/assent/jwt_adapter/assent_jwt.ex index c38574a..3ba8f1f 100644 --- a/lib/assent/jwt_adapter/assent_jwt.ex +++ b/lib/assent/jwt_adapter/assent_jwt.ex @@ -29,7 +29,7 @@ defmodule Assent.JWTAdapter.AssentJWT do case encode_json_base64(header, opts) do {:ok, encoded_header} -> {:ok, encoded_header} - {:error, error} -> {:error, %Error{message: "Failed to encode header", reason: error, data: header}} + {:error, error} -> {:error, Error.exception(message: "Failed to encode header", reason: error, data: header)} end end @@ -43,7 +43,7 @@ defmodule Assent.JWTAdapter.AssentJWT do defp encode_claims(claims, opts) do case encode_json_base64(claims, opts) do {:ok, encoded_claims} -> {:ok, encoded_claims} - {:error, error} -> {:error, %Error{message: "Failed to encode claims", reason: error, data: claims}} + {:error, error} -> {:error, Error.exception(message: "Failed to encode claims", reason: error, data: claims)} end end @@ -52,7 +52,7 @@ defmodule Assent.JWTAdapter.AssentJWT do case sign_message(message, alg, secret_or_private_key) do {:ok, signature} -> {:ok, "#{message}.#{Base.url_encode64(signature, padding: false)}"} - {:error, error} -> {:error, %Error{message: "Failed to sign JWT", reason: error, data: {message, alg}}} + {:error, error} -> {:error, Error.exception(message: "Failed to sign JWT", reason: error, data: {message, alg})} end end @@ -139,7 +139,7 @@ defmodule Assent.JWTAdapter.AssentJWT do defp split(token) do case String.split(token, ".") do [header, claims, signature] -> {:ok, %{header: header, claims: claims, signature: signature}} - parts -> {:error, %Error{message: "JWT must have exactly three parts", reason: :invalid_format, data: parts}} + parts -> {:error, Error.exception(message: "JWT must have exactly three parts", reason: :invalid_format, data: parts)} end end @@ -150,7 +150,7 @@ defmodule Assent.JWTAdapter.AssentJWT do {:ok, alg} <- fetch_alg(header) do {:ok, alg, header} else - {:error, error} -> {:error, %Error{message: "Failed to decode header", reason: error, data: header}} + {:error, error} -> {:error, Error.exception(message: "Failed to decode header", reason: error, data: header)} end end @@ -177,14 +177,14 @@ defmodule Assent.JWTAdapter.AssentJWT do {:ok, claims} <- decode_json(claims, json_library) do {:ok, claims} else - {:error, error} -> {:error, %Error{message: "Failed to decode claims", reason: error, data: claims}} + {:error, error} -> {:error, Error.exception(message: "Failed to decode claims", reason: error, data: claims)} end end defp decode_signature(signature) do case decode_base64_url(signature) do {:ok, signature} -> {:ok, signature} - {:error, error} -> {:error, %Error{message: "Failed to decode signature", reason: error, data: signature}} + {:error, error} -> {:error, Error.exception(message: "Failed to decode signature", reason: error, data: signature)} end end @@ -193,7 +193,7 @@ defmodule Assent.JWTAdapter.AssentJWT do case verify_message(message, signature, alg, secret_or_public_key) do {:ok, verified} -> {:ok, verified} - {:error, error} -> {:error, %Error{message: "Failed to verify signature", reason: error, data: {message, signature, alg}}} + {:error, error} -> {:error, Error.exception(message: "Failed to verify signature", reason: error, data: {message, signature, alg})} end end diff --git a/lib/assent/strategies/oauth.ex b/lib/assent/strategies/oauth.ex index 7e96d20..078453d 100644 --- a/lib/assent/strategies/oauth.ex +++ b/lib/assent/strategies/oauth.ex @@ -46,7 +46,7 @@ defmodule Assent.Strategy.OAuth do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, RequestError} + alias Assent.{Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, InvalidResponseError, RequestError, UnexpectedResponseError} @doc """ Generate authorization URL for request phase. @@ -244,8 +244,8 @@ defmodule Assent.Strategy.OAuth do defp process_token_response({:ok, %HTTPResponse{status: 200, body: %{"oauth_token" => _, "oauth_token_secret" => _} = token}}), do: {:ok, token} defp process_token_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, RequestError.unexpected(response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, RequestError.invalid(response)} + defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} + defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} defp process_response({:error, error}), do: {:error, error} defp build_authorize_url({:ok, token}, config) do @@ -298,10 +298,10 @@ defmodule Assent.Strategy.OAuth do end defp fetch_oauth_token(%{"oauth_token" => code}), do: {:ok, code} - defp fetch_oauth_token(params), do: {:error, MissingParamError.new("oauth_token", params)} + defp fetch_oauth_token(params), do: {:error, MissingParamError.exception(expected_key: "oauth_token", params: params)} defp fetch_oauth_verifier(%{"oauth_verifier" => code}), do: {:ok, code} - defp fetch_oauth_verifier(params), do: {:error, MissingParamError.new("oauth_verifier", params)} + defp fetch_oauth_verifier(params), do: {:error, MissingParamError.exception(expected_key: "oauth_verifier", params: params)} defp get_access_token(config, oauth_token, oauth_verifier) do with {:ok, site} <- Config.fetch(config, :site) do @@ -340,6 +340,6 @@ defmodule Assent.Strategy.OAuth do end defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} - defp process_user_response({:error, %HTTPResponse{status: 401}}), do: {:error, %RequestError{message: "Unauthorized token"}} + defp process_user_response({:error, %HTTPResponse{status: 401} = response}), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} defp process_user_response(any), do: process_response(any) end diff --git a/lib/assent/strategies/oauth2.ex b/lib/assent/strategies/oauth2.ex index ee200ea..b94b05e 100644 --- a/lib/assent/strategies/oauth2.ex +++ b/lib/assent/strategies/oauth2.ex @@ -65,7 +65,7 @@ defmodule Assent.Strategy.OAuth2 do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{CallbackCSRFError, CallbackError, Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, RequestError} + alias Assent.{CallbackCSRFError, CallbackError, Config, HTTPAdapter.HTTPResponse, JWTAdapter, MissingParamError, InvalidResponseError, RequestError, UnexpectedResponseError} @doc """ Generate authorization URL for request phase. @@ -144,21 +144,21 @@ defmodule Assent.Strategy.OAuth2 do error = params["error"] error_uri = params["error_uri"] - {:error, %CallbackError{message: message, error: error, error_uri: error_uri}} + {:error, CallbackError.exception(message: message, error: error, error_uri: error_uri)} end defp check_error_params(_params), do: :ok defp fetch_code_param(%{"code" => code}), do: {:ok, code} - defp fetch_code_param(params), do: {:error, MissingParamError.new("code", params)} + defp fetch_code_param(params), do: {:error, MissingParamError.exception(expected_key: "code", params: params)} defp maybe_check_state(%{state: stored_state}, %{"state" => provided_state}) do case Assent.constant_time_compare(stored_state, provided_state) do true -> :ok - false -> {:error, CallbackCSRFError.new("state")} + false -> {:error, CallbackCSRFError.exception(key: "state")} end end defp maybe_check_state(%{state: _state}, params) do - {:error, MissingParamError.new("state", params)} + {:error, MissingParamError.exception(expected_key: "state", params: params)} end defp maybe_check_state(_session_params, _params), do: :ok @@ -262,8 +262,8 @@ defmodule Assent.Strategy.OAuth2 do defp process_access_token_response({:ok, %HTTPResponse{status: status, body: %{"access_token" => _} = token}}) when status in [200, 201], do: {:ok, token} defp process_access_token_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, RequestError.unexpected(response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, RequestError.invalid(response)} + defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} + defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} defp process_response({:error, error}), do: {:error, error} defp fetch_user_with_strategy(config, token, strategy) do @@ -350,6 +350,6 @@ defmodule Assent.Strategy.OAuth2 do end defp process_user_response({:ok, %HTTPResponse{status: 200, body: user}}), do: {:ok, user} - defp process_user_response({:error, %HTTPResponse{status: 401}}), do: {:error, %RequestError{message: "Unauthorized token"}} + defp process_user_response({:error, %HTTPResponse{status: 401} = response}), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} defp process_user_response(any), do: process_response(any) end diff --git a/lib/assent/strategies/oidc.ex b/lib/assent/strategies/oidc.ex index 618e153..e037658 100644 --- a/lib/assent/strategies/oidc.ex +++ b/lib/assent/strategies/oidc.ex @@ -67,7 +67,7 @@ defmodule Assent.Strategy.OIDC do @behaviour Assent.Strategy alias Assent.Strategy, as: Helpers - alias Assent.{Config, HTTPAdapter.HTTPResponse, RequestError, Strategy.OAuth2} + alias Assent.{Config, HTTPAdapter.HTTPResponse, RequestError, UnexpectedResponseError, InvalidResponseError, Strategy.OAuth2} @doc """ Generates an authorization URL for request phase. @@ -119,8 +119,8 @@ defmodule Assent.Strategy.OIDC do defp process_openid_configuration_response({:ok, %HTTPResponse{status: 200, body: configuration}}), do: {:ok, configuration} defp process_openid_configuration_response(any), do: process_response(any) - defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, RequestError.unexpected(response)} - defp process_response({:error, %HTTPResponse{} = response}), do: {:error, RequestError.invalid(response)} + defp process_response({:ok, %HTTPResponse{} = response}), do: {:error, UnexpectedResponseError.exception(response: response)} + defp process_response({:error, %HTTPResponse{} = response}), do: {:error, InvalidResponseError.exception(response: response)} defp process_response({:error, error}), do: {:error, error} defp fetch_from_openid_config(config, key) do @@ -439,7 +439,7 @@ defmodule Assent.Strategy.OIDC do _any -> {:ok, body} end end - defp process_userinfo_response({:error, %HTTPResponse{status: 401}}, _openid_config, _config), do: {:error, %RequestError{message: "Unauthorized token"}} + defp process_userinfo_response({:error, %HTTPResponse{status: 401} = response}, _openid_config, _config), do: {:error, RequestError.exception(message: "Unauthorized token", response: response)} defp process_userinfo_response(any, _openid_config, _config), do: process_response(any) defp process_jwt(body, openid_config, config) do diff --git a/lib/assent/strategies/twitter.ex b/lib/assent/strategies/twitter.ex index df7ef1e..7e0ab86 100644 --- a/lib/assent/strategies/twitter.ex +++ b/lib/assent/strategies/twitter.ex @@ -33,7 +33,7 @@ defmodule Assent.Strategy.Twitter do @impl true def callback(config, params) do case Map.has_key?(params, "denied") do - true -> {:error, %CallbackError{message: "The user denied the authorization request"}} + true -> {:error, CallbackError.exception(message: "The user denied the authorization request")} false -> Base.callback(config, params, __MODULE__) end end diff --git a/lib/assent/strategies/vk.ex b/lib/assent/strategies/vk.ex index 36f87ee..f0d0d15 100644 --- a/lib/assent/strategies/vk.ex +++ b/lib/assent/strategies/vk.ex @@ -73,8 +73,20 @@ defmodule Assent.Strategy.VK do {:ok, user} end - defp handle_user_response({:ok, user}, _token), - do: {:error, %Assent.RequestError{message: "Retrieved invalid response: #{inspect user}"}} - defp handle_user_response({:error, error}, _token), - do: {:error, error} + + defp handle_user_response({:ok, user}, _token) do + { + :error, + RuntimeError.exception(""" + Retrieved an invalid response fetching VK user. + + User response: + #{inspect user} + """) + } + end + + defp handle_user_response({:error, error}, _token) do + {:error, error} + end end diff --git a/lib/assent/strategy.ex b/lib/assent/strategy.ex index 6f443a4..f1620a9 100644 --- a/lib/assent/strategy.ex +++ b/lib/assent/strategy.ex @@ -26,7 +26,7 @@ defmodule Assent.Strategy do end end """ - alias Assent.{Config, HTTPAdapter.HTTPResponse, RequestError} + alias Assent.{Config, HTTPAdapter.HTTPResponse, ServerUnreachableError} @callback authorize_url(Config.t()) :: {:ok, %{:url => binary(), optional(atom()) => any()}} | {:error, term()} @callback callback(Config.t(), map()) :: {:ok, %{:user => map(), optional(atom()) => any()}} | {:error, term()} @@ -51,16 +51,14 @@ defmodule Assent.Strategy do end end - defp parse_status_response({:ok, %{status: status} = resp}, _http_adapter, _url) when status in 200..399 do - {:ok, resp} + defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) when status in 200..399 do + {:ok, %{resp | http_adapter: http_adapter, request_url: url}} end - defp parse_status_response({:ok, %{status: status} = resp}, _http_adapter, _url) when status in 400..599 do - {:error, resp} + defp parse_status_response({:ok, %{status: status} = resp}, http_adapter, url) when status in 400..599 do + {:error, %{resp | http_adapter: http_adapter, request_url: url}} end defp parse_status_response({:error, error}, http_adapter, url) do - [url | _rest] = String.split(url, "?", parts: 2) - - {:error, RequestError.unreachable(http_adapter, url, error)} + {:error, ServerUnreachableError.exception(reason: error, http_adapter: http_adapter, request_url: url)} end @doc """ diff --git a/test/assent/strategies/auth0_test.exs b/test/assent/strategies/auth0_test.exs index 497cd5d..17817fc 100644 --- a/test/assent/strategies/auth0_test.exs +++ b/test/assent/strategies/auth0_test.exs @@ -34,7 +34,8 @@ defmodule Assent.Strategy.Auth0Test do test "requires domain or site configuration", %{config: config} do config = Keyword.take(config, [:client_id, :redirect_uri]) - assert Auth0.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:site` not found in config"}} + assert {:error, %MissingKeyError{} = error} = Auth0.authorize_url(config) + assert error.key == :site assert {:ok, %{url: url}} = Auth0.authorize_url(config ++ [site: "https://localhost"]) assert url =~ "https://localhost/authorize" diff --git a/test/assent/strategies/facebook_test.exs b/test/assent/strategies/facebook_test.exs index b424650..49cc5d7 100644 --- a/test/assent/strategies/facebook_test.exs +++ b/test/assent/strategies/facebook_test.exs @@ -41,11 +41,5 @@ defmodule Assent.Strategy.FacebookTest do assert {:ok, %{user: user}} = Facebook.callback(config, params) assert user == Map.put(@user, "picture", TestServer.url("/1000001/picture")) end - - test "handles error", %{config: config, callback_params: params} do - TestServer.stop() - - assert {:error, %Assent.RequestError{error: :unreachable}} = Facebook.callback(config, params) - end end end diff --git a/test/assent/strategies/instagram_test.exs b/test/assent/strategies/instagram_test.exs index 59d1aba..7e2c2af 100644 --- a/test/assent/strategies/instagram_test.exs +++ b/test/assent/strategies/instagram_test.exs @@ -44,7 +44,7 @@ defmodule Assent.Strategy.InstagramTest do test "handles error", %{config: config, callback_params: params} do TestServer.stop() - assert {:error, %Assent.RequestError{error: :unreachable}} = Instagram.callback(config, params) + assert {:error, %Assent.ServerUnreachableError{}} = Instagram.callback(config, params) end end end diff --git a/test/assent/strategies/oauth2_test.exs b/test/assent/strategies/oauth2_test.exs index 0ad5d6b..be75e8b 100644 --- a/test/assent/strategies/oauth2_test.exs +++ b/test/assent/strategies/oauth2_test.exs @@ -1,6 +1,9 @@ defmodule Assent.Strategy.OAuth2Test do use Assent.Test.OAuth2TestCase + alias Assent.InvalidResponseError + alias Assent.ServerUnreachableError + alias Assent.UnexpectedResponseError alias Assent.{CallbackCSRFError, CallbackError, Config.MissingKeyError, JWTAdapter.AssentJWT, MissingParamError, RequestError, Strategy.OAuth2} @client_id "s6BhdRkqt3" @@ -82,7 +85,7 @@ defmodule Assent.Strategy.OAuth2Test do config = Keyword.delete(config, :session_params) assert {:error, %MissingKeyError{} = error} = OAuth2.callback(config, params) - assert error.message == "Key `:session_params` not found in config" + assert error.key == :session_params end test "with error params", %{config: config, callback_params: %{"state" => state}} do @@ -98,7 +101,8 @@ defmodule Assent.Strategy.OAuth2Test do params = Map.delete(params, "code") assert {:error, %MissingParamError{} = error} = OAuth2.callback(config, params) - assert error.message == "Expected \"code\" to exist in params, but only found the following keys: [\"state\"]" + assert Exception.message(error) == "Expected \"code\" in params, got: [\"state\"]" + assert error.expected_key == "code" assert error.params == %{"state" => "state_test_value"} end @@ -106,7 +110,8 @@ defmodule Assent.Strategy.OAuth2Test do params = Map.delete(params, "state") assert {:error, %MissingParamError{} = error} = OAuth2.callback(config, params) - assert error.message == "Expected \"state\" to exist in params, but only found the following keys: [\"code\"]" + assert Exception.message(error) == "Expected \"state\" in params, got: [\"code\"]" + assert error.expected_key == "state" assert error.params == %{"code" => "code_test_value"} end @@ -114,7 +119,8 @@ defmodule Assent.Strategy.OAuth2Test do params = Map.put(params, "state", "invalid") assert {:error, %CallbackCSRFError{} = error} = OAuth2.callback(config, params) - assert error.message == "CSRF detected with param key \"state\"" + assert Exception.message(error) == "CSRF detected with param key \"state\"" + assert error.key == "state" end test "with state param without state in session_params", %{config: config, callback_params: params} do @@ -140,29 +146,27 @@ defmodule Assent.Strategy.OAuth2Test do oauth_token_url = TestServer.url("/oauth/token") TestServer.stop() - assert {:error, %RequestError{} = error} = OAuth2.callback(config, params) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect" - assert error.message =~ "URL: #{oauth_token_url}" + assert {:error, %ServerUnreachableError{} = error} = OAuth2.callback(config, params) + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == oauth_token_url + assert {:failed_connect, _} = error.reason end test "with access token error with 200 response", %{config: config, callback_params: params} do expect_oauth2_access_token_request(params: %{"error" => "error", "error_description" => "Error description"}) - assert {:error, %RequestError{} = error} = OAuth2.callback(config, params) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" - assert error.message =~ "%{\"error\" => \"error\", \"error_description\" => \"Error description\"}" + assert {:error, %UnexpectedResponseError{} = error} = OAuth2.callback(config, params) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"error" => "error", "error_description" => "Error description"} end test "with access token error with 500 response", %{config: config, callback_params: params} do expect_oauth2_access_token_request(status_code: 500, params: %{error: "Error"}) - assert {:error, %RequestError{} = error} = OAuth2.callback(config, params) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"error\" => \"Error\"}" + assert {:error, %InvalidResponseError{} = error} = OAuth2.callback(config, params) + assert error.response.status == 500 + assert error.response.body == %{"error" => "Error"} end test "with missing `:user_url`", %{config: config, callback_params: params} do @@ -171,7 +175,7 @@ defmodule Assent.Strategy.OAuth2Test do expect_oauth2_access_token_request() assert {:error, %MissingKeyError{} = error} = OAuth2.callback(config, params) - assert error.message == "Key `:user_url` not found in config" + assert error.key == :user_url end test "with invalid token type", %{config: config, callback_params: params} do @@ -185,11 +189,11 @@ defmodule Assent.Strategy.OAuth2Test do expect_oauth2_access_token_request() - assert {:error, %RequestError{} = error} = OAuth2.callback(config, params) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect" - assert error.message =~ "URL: http://localhost:8888/api/user" + assert {:error, %ServerUnreachableError{} = error} = OAuth2.callback(config, params) + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == config[:user_url] + assert {:failed_connect, _} = error.reason end test "with unauthorized `:user_url`", %{config: config, callback_params: params} do @@ -198,7 +202,8 @@ defmodule Assent.Strategy.OAuth2Test do assert {:error, %RequestError{} = error} = OAuth2.callback(config, params) assert error.message == "Unauthorized token" - refute error.error + assert error.response.status == 401 + assert error.response.body == %{"error" => "Unauthorized"} end @user_api_params %{name: "Dan Schultzer", email: "foo@example.com", uid: "1"} @@ -374,19 +379,18 @@ defmodule Assent.Strategy.OAuth2Test do test "with refresh token error with 200 response", %{config: config} do expect_oauth2_access_token_request(params: %{"error" => "error", "error_description" => "Error description"}) - assert {:error, %RequestError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" - assert error.message =~ "%{\"error\" => \"error\", \"error_description\" => \"Error description\"}" + assert {:error, %UnexpectedResponseError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"error" => "error", "error_description" => "Error description"} end test "with fresh token error with 500 response", %{config: config} do expect_oauth2_access_token_request(status_code: 500, params: %{error: "Error"}) - assert {:error, %RequestError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"error\" => \"Error\"}" + assert {:error, %InvalidResponseError{} = error} = OAuth2.refresh_access_token(config, %{"refresh_token" => "refresh_token_test_value"}) + assert Exception.message(error) =~ "An invalid response was received." + assert error.response.status == 500 + assert error.response.body == %{"error" => "Error"} end test "returns token", %{config: config} do @@ -420,7 +424,8 @@ defmodule Assent.Strategy.OAuth2Test do test "with missing `:site` config", %{config: config, token: token} do config = Keyword.delete(config, :site) - assert OAuth2.request(config, token, :get, "/info") == {:error, %MissingKeyError{message: "Key `:site` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth2.request(config, token, :get, "/info") + assert error.key == :site end test "with missing `access_token` in token", %{config: config, token: token} do diff --git a/test/assent/strategies/oauth_test.exs b/test/assent/strategies/oauth_test.exs index 85cfc71..eae0f06 100644 --- a/test/assent/strategies/oauth_test.exs +++ b/test/assent/strategies/oauth_test.exs @@ -1,7 +1,8 @@ defmodule Assent.Strategy.OAuthTest do use Assent.Test.OAuthTestCase - alias Assent.{Config.MissingKeyError, MissingParamError, RequestError, Strategy.OAuth} + alias Assent.UnexpectedResponseError + alias Assent.{Config.MissingKeyError, InvalidResponseError, MissingParamError, ServerUnreachableError, Strategy.OAuth} @private_key """ -----BEGIN RSA PRIVATE KEY----- @@ -48,79 +49,86 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:redirect_uri` config", %{config: config} do config = Keyword.delete(config, :redirect_uri) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:redirect_uri` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :redirect_uri end test "with missing `:site` config", %{config: config} do config = Keyword.delete(config, :site) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:site` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :site end test "with missing `:consumer_key` config", %{config: config} do config = Keyword.delete(config, :consumer_key) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:consumer_key` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :consumer_key end test "with missing `:consumer_secret` config", %{config: config} do config = Keyword.delete(config, :consumer_secret) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:consumer_secret` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :consumer_secret end test "with unreachable request token url", %{config: config} do request_token_url = TestServer.url("/request_token") TestServer.stop() - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect, " - assert error.message =~ "URL: #{request_token_url}" + assert {:error, %ServerUnreachableError{} = error} = OAuth.authorize_url(config) + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == request_token_url + assert {:failed_connect, _} = error.reason end test "with unexpected successful response", %{config: config} do expect_oauth_request_token_request(params: %{"error_code" => 215, "error_message" => "Bad Authentication data."}) - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" - assert error.message =~ "%{\"error_code\" => \"215\", \"error_message\" => \"Bad Authentication data.\"}" + assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.http_adapter == Assent.HTTPAdapter.Httpc + assert error.response.request_url == TestServer.url("/request_token") + assert error.response.status == 200 + assert error.response.body == %{"error_code" => "215", "error_message" => "Bad Authentication data."} end test "with error response", %{config: config} do expect_oauth_request_token_request(status_code: 500, params: %{"error_code" => 215, "error_message" => "Bad Authentication data."}) - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"error_code\" => \"215\", \"error_message\" => \"Bad Authentication data.\"}" + assert {:error, %InvalidResponseError{} = error} = OAuth.authorize_url(config) + assert Exception.message(error) =~ "An invalid response was received." + assert error.response.http_adapter == Assent.HTTPAdapter.Httpc + assert error.response.request_url == TestServer.url("/request_token") + assert error.response.status == 500 + assert error.response.body == %{"error_code" => "215", "error_message" => "Bad Authentication data."} end test "with json error response", %{config: config} do expect_oauth_request_token_request(status_code: 500, content_type: "application/json", params: %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]}) - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"errors\" => [%{\"code\" => 215, \"message\" => \"Bad Authentication data.\"}]}" + assert {:error, %InvalidResponseError{} = error} = OAuth.authorize_url(config) + assert error.response.status == 500 + assert error.response.body == %{"errors" => [%{"code" => 215, "message" => "Bad Authentication data."}]} end test "with missing `oauth_token` in access token response", %{config: config} do expect_oauth_request_token_request(params: %{oauth_token_secret: "hdhd0244k9j7ao03"}) - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:\n\n%{\"oauth_token_secret\" => \"hdhd0244k9j7ao03\"}\n" + assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"oauth_token_secret" => "hdhd0244k9j7ao03"} end test "with missing `oauth_token_secret` in access token response", %{config: config} do expect_oauth_request_token_request(params: %{oauth_token: "hh5s93j4hdidpola"}) - assert {:error, %RequestError{} = error} = OAuth.authorize_url(config) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:\n\n%{\"oauth_token\" => \"hh5s93j4hdidpola\"}\n" + assert {:error, %UnexpectedResponseError{} = error} = OAuth.authorize_url(config) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"oauth_token" => "hh5s93j4hdidpola"} end test "returns url", %{config: config} do @@ -206,7 +214,8 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:private_key` config", %{config: config} do config = Keyword.delete(config, :private_key) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:private_key` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :private_key end test "returns url", %{config: config} do @@ -266,7 +275,8 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:consumer_secret` config", %{config: config} do config = Keyword.delete(config, :consumer_secret) - assert OAuth.authorize_url(config) == {:error, %MissingKeyError{message: "Key `:consumer_secret` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.authorize_url(config) + assert error.key == :consumer_secret end test "returns url", %{config: config} do @@ -292,7 +302,8 @@ defmodule Assent.Strategy.OAuthTest do params = Map.delete(params, "oauth_token") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) - assert error.message == "Expected \"oauth_token\" to exist in params, but only found the following keys: [\"oauth_verifier\"]" + assert Exception.message(error) == "Expected \"oauth_token\" in params, got: [\"oauth_verifier\"]" + assert error.expected_key == "oauth_token" assert error.params == %{"oauth_verifier" => "hfdp7dh39dks9884"} end @@ -300,60 +311,60 @@ defmodule Assent.Strategy.OAuthTest do params = Map.delete(params, "oauth_verifier") assert {:error, %MissingParamError{} = error} = OAuth.callback(config, params) - assert error.message == "Expected \"oauth_verifier\" to exist in params, but only found the following keys: [\"oauth_token\"]" + assert Exception.message(error) == "Expected \"oauth_verifier\" in params, got: [\"oauth_token\"]" + assert error.expected_key == "oauth_verifier" assert error.params == %{"oauth_token" => "hh5s93j4hdidpola"} end test "with missing `:site` config", %{config: config, callback_params: callback_params} do config = Keyword.delete(config, :site) - assert OAuth.callback(config, callback_params) == {:error, %MissingKeyError{message: "Key `:site` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.callback(config, callback_params) + assert error.key == :site end test "with unreachable token url", %{config: config, callback_params: callback_params} do access_token_url = TestServer.url("/access_token") TestServer.stop() - assert {:error, %RequestError{} = error} = OAuth.callback(config, callback_params) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect, " - assert error.message =~ "URL: #{access_token_url}" + assert {:error, %ServerUnreachableError{} = error} = OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == access_token_url + assert {:failed_connect, _} = error.reason end test "with token url error response", %{config: config, callback_params: callback_params} do expect_oauth_access_token_request(status_code: 500, params: %{error: "Unknown error"}) - assert {:error, %RequestError{} = error} = OAuth.callback(config, callback_params) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"error\" => \"Unknown error\"}" + assert {:error, %InvalidResponseError{} = error} = OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "Response status: 500" + assert error.response.body == %{"error" => "Unknown error"} end test "with missing `oauth_token` in access token response", %{config: config, callback_params: callback_params} do expect_oauth_access_token_request(params: %{oauth_token_secret: "token_secret"}) - assert {:error, %RequestError{} = error} = OAuth.callback(config, callback_params) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:\n\n%{\"oauth_token_secret\" => \"token_secret\"}\n" + assert {:error, %UnexpectedResponseError{} = error} = OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"oauth_token_secret" => "token_secret"} end test "with missing `oauth_token_secret` in access token response", %{config: config, callback_params: callback_params} do expect_oauth_access_token_request(params: %{oauth_token: "token"}) - assert {:error, %RequestError{} = error} = OAuth.callback(config, callback_params) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:\n\n%{\"oauth_token\" => \"token\"}\n" + assert {:error, %UnexpectedResponseError{} = error} = OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "An unexpected response was received." + assert error.response.body == %{"oauth_token" => "token"} end test "bubbles up user request error response", %{config: config, callback_params: callback_params} do expect_oauth_access_token_request() expect_oauth_user_request(%{error: "Unknown error"}, status_code: 500) - assert {:error, %RequestError{} = error} = OAuth.callback(config, callback_params) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 500" - assert error.message =~ "%{\"error\" => \"Unknown error\"}" + assert {:error, %InvalidResponseError{} = error} = OAuth.callback(config, callback_params) + assert Exception.message(error) =~ "Response status: 500" + assert error.response.body == %{"error" => "Unknown error"} end test "normalizes data", %{config: config, callback_params: callback_params} do @@ -388,7 +399,8 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:site` config", %{config: config, token: token} do config = Keyword.delete(config, :site) - assert OAuth.request(config, token, :get, "/info") == {:error, %MissingKeyError{message: "Key `:site` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert error.key == :site end test "with missing `oauth_token` in token", %{config: config, token: token} do @@ -402,24 +414,26 @@ defmodule Assent.Strategy.OAuthTest do test "with missing `:consumer_key` config", %{config: config, token: token} do config = Keyword.delete(config, :consumer_key) - assert OAuth.request(config, token, :get, "/info") == {:error, %MissingKeyError{message: "Key `:consumer_key` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert error.key == :consumer_key end test "with missing `:consumer_secret` config", %{config: config, token: token} do config = Keyword.delete(config, :consumer_secret) - assert OAuth.request(config, token, :get, "/info") == {:error, %MissingKeyError{message: "Key `:consumer_secret` not found in config"}} + assert {:error, %MissingKeyError{} = error} = OAuth.request(config, token, :get, "/info") + assert error.key == :consumer_secret end test "with network error", %{config: config, token: token} do info_url = TestServer.url("/info") TestServer.stop() - assert {:error, %Assent.RequestError{} = error} = OAuth.request(config, token, :get, "/info") - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect, " - assert error.message =~ "URL: #{info_url}" + assert {:error, %ServerUnreachableError{} = error} = OAuth.request(config, token, :get, "/info") + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == info_url + assert {:failed_connect, _} = error.reason end test "fetches", %{config: config, token: token} do diff --git a/test/assent/strategies/oidc_test.exs b/test/assent/strategies/oidc_test.exs index 7cb86b6..389584d 100644 --- a/test/assent/strategies/oidc_test.exs +++ b/test/assent/strategies/oidc_test.exs @@ -1,7 +1,7 @@ defmodule Assent.Strategy.OIDCTest do use Assent.Test.OIDCTestCase - alias Assent.{JWTAdapter.AssentJWT, RequestError, Strategy.OIDC} + alias Assent.{JWTAdapter.AssentJWT, InvalidResponseError, RequestError, ServerUnreachableError, UnexpectedResponseError, Strategy.OIDC} describe "authorize_url/2" do test "generates url and state", %{config: config} do @@ -167,27 +167,24 @@ defmodule Assent.Strategy.OIDCTest do openid_config_url = TestServer.url("/.well-known/openid-configuration") TestServer.stop() - assert {:error, %RequestError{} = error} = OIDC.callback(config, params) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect" - assert error.message =~ "URL: #{openid_config_url}" + assert {:error, %ServerUnreachableError{} = error} = OIDC.callback(config, params) + assert Exception.message(error) =~ "The server was unreachable." + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == openid_config_url + assert {:failed_connect, _} = error.reason end test "with unexpected openid config url response", %{config: config, openid_config: openid_config, callback_params: params} do expect_openid_config_request(openid_config, status_code: 201) - assert {:error, %RequestError{} = error} = OIDC.callback(config, params) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" + assert {:error, %UnexpectedResponseError{}} = OIDC.callback(config, params) end test "with 404 openid config url", %{config: config, openid_config: openid_config, callback_params: params} do expect_openid_config_request(openid_config, status_code: 404) - assert {:error, %RequestError{} = error} = OIDC.callback(config, params) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 404" + assert {:error, %InvalidResponseError{} = error} = OIDC.callback(config, params) + assert error.response.status == 404 end test "with invalid id_token", %{config: config, openid_config: openid_config, callback_params: params} do @@ -245,14 +242,14 @@ defmodule Assent.Strategy.OIDCTest do expect_openid_config_request([], status_code: 500) - assert {:error, %RequestError{error: :invalid_server_response}} = OIDC.validate_id_token(config, id_token) + assert {:error, %InvalidResponseError{}} = OIDC.validate_id_token(config, id_token) end test "with no `:client_id`", %{config: config, id_token: id_token} do config = Keyword.delete(config, :client_id) assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.message == "Key `:client_id` not found in config" + assert error.key == :client_id end test "with missing `issuer` in OpenID configuration", %{config: config, id_token: id_token} do @@ -270,7 +267,7 @@ defmodule Assent.Strategy.OIDCTest do config = Keyword.delete(config, :client_secret) assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.message == "Key `:client_secret` not found in config" + assert error.key == :client_secret end for key <- ~w(iss sub aud exp iat) do @@ -377,7 +374,7 @@ defmodule Assent.Strategy.OIDCTest do config = Keyword.delete(config, :session_params) assert {:error, %Assent.Config.MissingKeyError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.message == "Key `:session_params` not found in config" + assert error.key == :session_params end test "without nonce", %{config: config, id_token: id_token} do @@ -433,19 +430,17 @@ defmodule Assent.Strategy.OIDCTest do jwks_uri_url = TestServer.url("/jwks_uri.json") TestServer.stop() - assert {:error, %RequestError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect" - assert error.message =~ "URL: #{jwks_uri_url}" + assert {:error, %ServerUnreachableError{} = error} = OIDC.validate_id_token(config, id_token) + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == jwks_uri_url + assert {:failed_connect, _} = error.reason end test "with unexpected `jwk_uri` url response", %{config: config, id_token: id_token} do expect_oidc_jwks_uri_request(status_code: 201) - assert {:error, %RequestError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" + assert {:error, %UnexpectedResponseError{} = error} = OIDC.validate_id_token(config, id_token) + assert error.response.status == 201 end test "with 404 `jwks_uri` url", %{config: config, id_token: id_token} do @@ -453,9 +448,8 @@ defmodule Assent.Strategy.OIDCTest do Plug.Conn.send_resp(conn, 404, "") end) - assert {:error, %RequestError{} = error} = OIDC.validate_id_token(config, id_token) - assert error.error == :invalid_server_response - assert error.message =~ "Server responded with status: 404" + assert {:error, %InvalidResponseError{} = error} = OIDC.validate_id_token(config, id_token) + assert error.response.status == 404 end test "with missing keys in `jwks_uri` url", %{config: config, id_token: id_token} do @@ -514,7 +508,7 @@ defmodule Assent.Strategy.OIDCTest do config = Keyword.delete(config, :openid_configuration) expect_openid_config_request([], status_code: 500) - assert {:error, %RequestError{error: :invalid_server_response}} = OIDC.fetch_userinfo(config, access_token) + assert {:error, %InvalidResponseError{}} = OIDC.fetch_userinfo(config, access_token) end test "with missing `userinfo_endpoint` in OpenID configuration", %{config: config, access_token: access_token} do @@ -528,19 +522,16 @@ defmodule Assent.Strategy.OIDCTest do openid_configuration = Map.put(config[:openid_configuration], "userinfo_endpoint", "http://localhost:8888/userinfo") config = Keyword.put(config, :openid_configuration, openid_configuration) - assert {:error, %RequestError{} = error} = OIDC.fetch_userinfo(config, access_token) - assert error.error == :unreachable - assert error.message =~ "Server was unreachable with Assent.HTTPAdapter.Httpc." - assert error.message =~ "{:failed_connect" - assert error.message =~ "URL: http://localhost:8888/userinfo" + assert {:error, %ServerUnreachableError{} = error} = OIDC.fetch_userinfo(config, access_token) + assert error.http_adapter == Assent.HTTPAdapter.Httpc + assert error.request_url == "http://localhost:8888/userinfo" + assert {:failed_connect, _} = error.reason end test "with unexpected `userinfo_endpoint` url response", %{config: config, access_token: access_token} do expect_oidc_userinfo_request(gen_id_token(alg: "HS256"), status_code: 201) - assert {:error, %RequestError{} = error} = OIDC.fetch_userinfo(config, access_token) - assert error.error == :unexpected_response - assert error.message =~ "An unexpected success response was received:" + assert {:error, %UnexpectedResponseError{}} = OIDC.fetch_userinfo(config, access_token) end test "with unauthorized `userinfo_endpoint`", %{config: config, access_token: access_token} do @@ -548,7 +539,7 @@ defmodule Assent.Strategy.OIDCTest do assert {:error, %RequestError{} = error} = OIDC.fetch_userinfo(config, access_token) assert error.message == "Unauthorized token" - refute error.error + assert error.response.status == 401 end test "with jwt response with invalid signature", %{config: config, access_token: access_token} do diff --git a/test/assent/strategies/vk_test.exs b/test/assent/strategies/vk_test.exs index b1a17bd..17c0864 100644 --- a/test/assent/strategies/vk_test.exs +++ b/test/assent/strategies/vk_test.exs @@ -51,10 +51,13 @@ defmodule Assent.Strategy.VKTest do assert user == @user end - test "handles error", %{config: config, callback_params: params} do - TestServer.stop() + test "handles invalid user response", %{config: config, callback_params: params} do + expect_oauth2_access_token_request(uri: "/access_token", params: @token_response) + expect_oauth2_user_request(%{"a" => 1}, [uri: "/method/users.get"]) - assert {:error, %Assent.RequestError{error: :unreachable}} = VK.callback(config, params) + assert {:error, %RuntimeError{} = error} = VK.callback(config, params) + assert error.message =~ "Retrieved an invalid response fetching VK user" + assert error.message =~ "%{\"a\" => 1}" end end end diff --git a/test/assent/strategy_test.exs b/test/assent/strategy_test.exs index 4dc5573..35a2150 100644 --- a/test/assent/strategy_test.exs +++ b/test/assent/strategy_test.exs @@ -77,31 +77,31 @@ defmodule Assent.StrategyTest do test "request/5" do assert Strategy.request(:get, "http-adapter", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [], body: nil}} + {:ok, %HTTPResponse{status: 200, headers: [], body: nil, http_adapter: HTTPMock, request_url: "http-adapter"}} assert Strategy.request(:get, "http-adapter-with-opts", nil, [], http_adapter: {HTTPMock, a: 1}) == - {:ok, %HTTPResponse{status: 200, headers: [], body: [a: 1]}} + {:ok, %HTTPResponse{status: 200, headers: [], body: [a: 1], http_adapter: HTTPMock, request_url: "http-adapter-with-opts"}} assert Strategy.request(:get, "json-encoded-body", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}}} + {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body"}} assert Strategy.request(:get, "json-encoded-body-already-decoded", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}}} + {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/json"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body-already-decoded"}} assert Strategy.request(:get, "json-encoded-body-text/javascript-header", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "text/javascript"}], body: %{"a" => 1}}} + {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "text/javascript"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "json-encoded-body-text/javascript-header"}} assert {:error, %Jason.DecodeError{}} = Strategy.request(:get, "invalid-json-body", nil, [], http_adapter: HTTPMock) assert Strategy.request(:get, "json-no-headers", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [], body: Jason.encode!(%{"a" => 1})}} + {:ok, %HTTPResponse{status: 200, headers: [], body: Jason.encode!(%{"a" => 1}), http_adapter: HTTPMock, request_url: "json-no-headers"}} assert Strategy.request(:get, "form-data-body", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => "1"}}} + {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => "1"}, http_adapter: HTTPMock, request_url: "form-data-body"}} assert Strategy.request(:get, "form-data-body-already-decoded", nil, [], http_adapter: HTTPMock) == - {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => 1}}} + {:ok, %HTTPResponse{status: 200, headers: [{"content-type", "application/x-www-form-urlencoded"}], body: %{"a" => 1}, http_adapter: HTTPMock, request_url: "form-data-body-already-decoded"}} end defmodule CustomJWTAdapter do