From 72341ea11492f2f872ce0cc7681b2cbdf2a249be Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Sun, 30 Sep 2018 20:55:19 -0600 Subject: [PATCH 1/6] authorizer protocol and basic implementations but not integrated into request flow --- lib/exhal/authorizer.ex | 12 ++++++ lib/exhal/client.ex | 61 ++++++++++++++++++++------- lib/exhal/null_authorizer.ex | 14 ++++++ lib/exhal/simple_authorizer.ex | 25 +++++++++++ test/exhal/client_test.exs | 54 +++++++++++++++++++++--- test/exhal/null_authorizer_test.exs | 16 +++++++ test/exhal/simple_authorizer_test.exs | 27 ++++++++++++ 7 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 lib/exhal/authorizer.ex create mode 100644 lib/exhal/null_authorizer.ex create mode 100644 lib/exhal/simple_authorizer.ex create mode 100644 test/exhal/null_authorizer_test.exs create mode 100644 test/exhal/simple_authorizer_test.exs diff --git a/lib/exhal/authorizer.ex b/lib/exhal/authorizer.ex new file mode 100644 index 0000000..2e6dbdf --- /dev/null +++ b/lib/exhal/authorizer.ex @@ -0,0 +1,12 @@ +defprotocol ExHal.Authorizer do + @type authorization_field_value :: String.t() + @type url :: String.t() + + @doc """ + Returns `{:ok, authorization_header_field_value}` if the authorizer + knows the resource and has credentials for it. Otherwise, returns + `:no_auth`. + """ + @spec authorization(any, url()) :: {:ok, authorization_field_value()} | :no_auth + def authorization(authorizer, url) +end diff --git a/lib/exhal/client.ex b/lib/exhal/client.ex index 8e16878..16ac762 100644 --- a/lib/exhal/client.ex +++ b/lib/exhal/client.ex @@ -5,11 +5,27 @@ defmodule ExHal.Client do ## Examples iex> ExHal.Client.new() - %ExHal.Client{} + ...> |> ExHal.Client.get("http://haltalk.herokuapp.com/") + %ExHal.Document{...} + + iex> ExHal.Client.new() + ...> |> ExHal.Client.post("http://haltalk.herokuapp.com/signup", ~s( + ...> { "username": "fred", + ...> "password": "pwnme", + ...> "real_name": "Fred Wilson" } + ...> )) + %ExHal.Document{...} + + iex> authorizer = ExHal.SimpleAuthorizer.new("http://haltalk.herokuapp.com", + ...> "Bearer my-token") + iex> ExHal.Client.new() + ...> |> ExHal.Client.add_headers("Prefer": "minimal") + ...> |> ExHal.Client.set_authorizer(authorizer) + %ExHal.Client{...} """ require Logger - alias ExHal.{Document, NonHalResponse, ResponseHeader} + alias ExHal.{Document, NonHalResponse, ResponseHeader, NullAuthorizer} @logger Application.get_env(:exhal, :logger, Logger) @@ -17,45 +33,58 @@ defmodule ExHal.Client do Represents a client configuration/connection. Create with `new` function. """ @opaque t :: %__MODULE__{} - defstruct headers: [], opts: [follow_redirect: true] + defstruct authorizer: NullAuthorizer.new(), + headers: [], + opts: [follow_redirect: true] @typedoc """ The return value of any function that makes an HTTP request. """ @type http_response :: - {:ok, Document.t() | NonHalResponse.t(), ResponseHeader.t()} - | {:error, Document.t() | NonHalResponse.t(), ResponseHeader.t() } - | {:error, Error.t()} + {:ok, Document.t | NonHalResponse.t, ResponseHeader.t} + | {:error, Document.t | NonHalResponse.t, ResponseHeader.t } + | {:error, Error.t} @doc """ Returns a new client. """ - @spec new(Keyword.t(), Keyword.t()) :: __MODULE__.t() - def new(headers, follow_redirect: follow) do - %__MODULE__{headers: headers, opts: [follow_redirect: follow]} - end + @spec new() :: t + def new(), do: %__MODULE__{} + + @spec new(Keyword.t) :: t + def new(headers: headers), do: %__MODULE__{headers: headers, opts: [follow_redirect: true]} + def new(follow_redirect: follow), do: %__MODULE__{headers: [], opts: [follow_redirect: follow]} + def new(headers: headers, follow_redirect: follow), do: %__MODULE__{headers: headers, opts: [follow_redirect: follow]} - @spec new(Keyword.t()) :: __MODULE__.t() + # deprecated call patterns def new(headers) do - new(headers, follow_redirect: true) + %__MODULE__{headers: headers, opts: [follow_redirect: true]} end - @spec new() :: __MODULE__.t() - def new() do - new([], follow_redirect: true) + @spec new(Keyword.t, Keyword.t) :: t + def new(headers, follow_redirect: follow) do + new(headers: headers, follow_redirect: follow) end @doc """ Returns client that will include the specified headers in any request made with it. """ - @spec add_headers(__MODULE__.t(), Keyword.t()) :: __MODULE__.t() + @spec add_headers(t, Keyword.t) :: t def add_headers(client, headers) do updated_headers = merge_headers(client.headers, headers) %__MODULE__{client | headers: updated_headers} end + @doc """ + Returns a client that will authorize requests using the specified authorizer. + """ + @spec set_authorizer(t, Authorizer.t) :: t + def set_authorizer(client, new_authorizer) do + %__MODULE__{client | authorizer: new_authorizer} + end + defmacrop log_req(method, url, do: block) do quote do {time, result} = :timer.tc(fn -> unquote(block) end) diff --git a/lib/exhal/null_authorizer.ex b/lib/exhal/null_authorizer.ex new file mode 100644 index 0000000..3d4e527 --- /dev/null +++ b/lib/exhal/null_authorizer.ex @@ -0,0 +1,14 @@ +defmodule ExHal.NullAuthorizer do + @typedoc """ + An authorizer that always responds :no_auth + """ + @opaque t :: %__MODULE__{} + + defstruct([]) + + def new(), do: %__MODULE__{} +end + +defimpl ExHal.Authorizer, for: ExHal.NullAuthorizer do + def authorization(_authorizer, _url), do: :no_auth +end diff --git a/lib/exhal/simple_authorizer.ex b/lib/exhal/simple_authorizer.ex new file mode 100644 index 0000000..47bcae9 --- /dev/null +++ b/lib/exhal/simple_authorizer.ex @@ -0,0 +1,25 @@ +defmodule ExHal.SimpleAuthorizer do + alias ExHal.Authorizer + + @typedoc """ + An authorizer that returns a fixed string for resources at a particular server. + """ + @opaque t :: %__MODULE__{} + + defstruct([:authorization, :url_prefix]) + + @spec new(Authorizer.url(), Authorizer.authorization_field_value()) :: __MODULE__.t() + def new(url_prefix, authorization_str), do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} +end + +defimpl ExHal.Authorizer, for: ExHal.SimpleAuthorizer do + def authorization(authorizer, url) do + url + |> String.starts_with?(authorizer.url_prefix) + |> if do + {:ok, authorizer.authorization} + else + :no_auth + end + end +end diff --git a/test/exhal/client_test.exs b/test/exhal/client_test.exs index d49555a..7976800 100644 --- a/test/exhal/client_test.exs +++ b/test/exhal/client_test.exs @@ -2,15 +2,55 @@ Code.require_file "../support/request_stubbing.exs", __DIR__ defmodule ExHal.ClientTest do use ExUnit.Case, async: true - doctest ExHal.Client - alias ExHal.Client + alias ExHal.{Client, SimpleAuthorizer} - test "adding headers to client" do - assert (%Client{} - |> Client.add_headers("hello": "bob") - |> Client.add_headers("hello": ["alice","jane"])) - |> to_have_header("hello", ["bob", "alice", "jane"]) + describe ".new" do + test ".new/0" do + assert %Client{} = Client.new + end + + test "(empty_headers)" do + assert %Client{} = Client.new([]) + end + + test "(headers)" do + assert %Client{headers: ["User-Agent": "test agent", "X-Whatever": "example"]} = + Client.new("User-Agent": "test agent", "X-Whatever": "example") + end + + test "(headers, follow_redirect: follow)" do + assert %Client{headers: ["User-Agent": "test agent"], opts: [follow_redirect: false]} = + Client.new(["User-Agent": "test agent"], follow_redirect: false) + end + end + + describe ".add_headers/1" do + test "adding headers to client" do + assert (%Client{} + |> Client.add_headers("hello": "bob") + |> Client.add_headers("hello": ["alice","jane"])) + |> to_have_header("hello", ["bob", "alice", "jane"]) + end + end + + describe ".set_authorizer/2" do + test "first time" do + test_auther = SimpleAuthorizer.new("http://example.com", "Bearer sometoken") + + assert %Client{authorizer: test_auther} == + Client.set_authorizer(Client.new, test_auther) + end + + test "last one in wins time" do + test_auther1 = SimpleAuthorizer.new("http://example.com", "Bearer sometoken") + test_auther2 = SimpleAuthorizer.new("http://myapp.com", "Bearer someothertoken") + + assert %Client{authorizer: test_auther2} == + Client.new + |> Client.set_authorizer(test_auther1) + |> Client.set_authorizer(test_auther2) + end end # background diff --git a/test/exhal/null_authorizer_test.exs b/test/exhal/null_authorizer_test.exs new file mode 100644 index 0000000..a485469 --- /dev/null +++ b/test/exhal/null_authorizer_test.exs @@ -0,0 +1,16 @@ +defmodule ExHal.NullAuthorizerTest do + use ExUnit.Case, async: true + alias ExHal.{Authorizer, NullAuthorizer} + + test ".new/0" do + assert NullAuthorizer.new() + end + + test ".authorization/2" do + assert :no_auth = Authorizer.authorization(null_authorizer_factory(), "http://example.com") + end + + defp null_authorizer_factory() do + NullAuthorizer.new() + end +end diff --git a/test/exhal/simple_authorizer_test.exs b/test/exhal/simple_authorizer_test.exs new file mode 100644 index 0000000..2b08a3f --- /dev/null +++ b/test/exhal/simple_authorizer_test.exs @@ -0,0 +1,27 @@ +defmodule ExHal.SimpleAuthorizerTest do + use ExUnit.Case, async: true + alias ExHal.{Authorizer, SimpleAuthorizer} + + test ".new/2" do + assert SimpleAuthorizer.new("http://example.com", "Bearer my-word-is-my-bond") + end + + describe ".authorization/2" do + test "alien resource" do + assert :no_auth = Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") + end + + test "subtly alien resource" do + assert :no_auth = Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") + end + + test "recognized resource" do + assert {:ok, "Bearer hello-beautiful"} = + Authorizer.authorization(simple_authorizer_factory("Bearer hello-beautiful"), "http://example.com/foo") + end + end + + defp simple_authorizer_factory(auth_string \\ "Bearer sometoken") do + SimpleAuthorizer.new("http://example.com", auth_string) + end +end From 1f1a3bf5858836644b133c8e8f8d0ce26ddb408f Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Mon, 1 Oct 2018 08:37:11 -0600 Subject: [PATCH 2/6] use authorizer to calculate `Authorization` header field value. --- config/config.exs | 11 +-- lib/exhal/client.ex | 57 +++++++---- lib/exhal/http_client.ex | 10 ++ test/exhal/client_test.exs | 153 +++++++++++++++++++++--------- test/exhal/collection_test.exs | 25 ++--- test/exhal/navigation_test.exs | 77 ++++++++------- test/exhal_test.exs | 103 ++++++++++---------- test/support/request_stubbing.exs | 33 ------- test/test_helper.exs | 3 +- 9 files changed, 264 insertions(+), 208 deletions(-) create mode 100644 lib/exhal/http_client.ex delete mode 100644 test/support/request_stubbing.exs diff --git a/config/config.exs b/config/config.exs index 6dfa82f..a848ada 100644 --- a/config/config.exs +++ b/config/config.exs @@ -15,10 +15,7 @@ use Mix.Config # format: "$date $time [$level] $metadata$message\n", # metadata: [:user_id] -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" +if :test == Mix.env do + config :exhal, :client, ExHal.ClientMock + config :exhal, :http_client, ExHal.HttpClientMock +end \ No newline at end of file diff --git a/lib/exhal/client.ex b/lib/exhal/client.ex index 16ac762..dbc562b 100644 --- a/lib/exhal/client.ex +++ b/lib/exhal/client.ex @@ -25,16 +25,17 @@ defmodule ExHal.Client do """ require Logger - alias ExHal.{Document, NonHalResponse, ResponseHeader, NullAuthorizer} + alias ExHal.{Document, NonHalResponse, ResponseHeader, Authorizer, NullAuthorizer} @logger Application.get_env(:exhal, :logger, Logger) + @http_client Application.get_env(:exhal, :http_client, HTTPoison) @typedoc """ Represents a client configuration/connection. Create with `new` function. """ @opaque t :: %__MODULE__{} defstruct authorizer: NullAuthorizer.new(), - headers: [], + headers: %{}, opts: [follow_redirect: true] @typedoc """ @@ -52,13 +53,14 @@ defmodule ExHal.Client do def new(), do: %__MODULE__{} @spec new(Keyword.t) :: t - def new(headers: headers), do: %__MODULE__{headers: headers, opts: [follow_redirect: true]} - def new(follow_redirect: follow), do: %__MODULE__{headers: [], opts: [follow_redirect: follow]} - def new(headers: headers, follow_redirect: follow), do: %__MODULE__{headers: headers, opts: [follow_redirect: follow]} + def new(headers: headers), do: %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: true]} + def new(follow_redirect: follow), do: %__MODULE__{opts: [follow_redirect: follow]} + def new(headers: headers, follow_redirect: follow), + do: %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: follow]} # deprecated call patterns - def new(headers) do - %__MODULE__{headers: headers, opts: [follow_redirect: true]} + def new(headers) when is_list(headers) do + %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: true]} end @spec new(Keyword.t, Keyword.t) :: t @@ -72,7 +74,7 @@ defmodule ExHal.Client do """ @spec add_headers(t, Keyword.t) :: t def add_headers(client, headers) do - updated_headers = merge_headers(client.headers, headers) + updated_headers = merge_headers(client.headers, normalize_headers(headers)) %__MODULE__{client | headers: updated_headers} end @@ -95,58 +97,60 @@ defmodule ExHal.Client do @callback get(__MODULE__.t, String.t, Keyword.t) :: http_response() def get(client, url, opts \\ []) do - {headers, poison_opts} = figure_headers_and_opt(opts, client) + {headers, poison_opts} = figure_headers_and_opt(opts, client, url) log_req("GET", url) do - HTTPoison.get(url, headers, poison_opts) + @http_client.get(url, headers, poison_opts) |> extract_return(client) end end @callback post(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() def post(client, url, body, opts \\ []) do - {headers, poison_opts} = figure_headers_and_opt(opts, client) + {headers, poison_opts} = figure_headers_and_opt(opts, client, url) log_req("POST", url) do - HTTPoison.post(url, body, headers, poison_opts) + @http_client.post(url, body, headers, poison_opts) |> extract_return(client) end end @callback put(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() def put(client, url, body, opts \\ []) do - {headers, poison_opts} = figure_headers_and_opt(opts, client) + {headers, poison_opts} = figure_headers_and_opt(opts, client, url) log_req("PUT", url) do - HTTPoison.put(url, body, headers, poison_opts) + @http_client.put(url, body, headers, poison_opts) |> extract_return(client) end end @callback patch(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() def patch(client, url, body, opts \\ []) do - {headers, poison_opts} = figure_headers_and_opt(opts, client) + {headers, poison_opts} = figure_headers_and_opt(opts, client, url) log_req("PATCH", url) do - HTTPoison.patch(url, body, headers, poison_opts) + @http_client.patch(url, body, headers, poison_opts) |> extract_return(client) end end # Private functions - defp figure_headers_and_opt(opts, client) do - {local_headers, local_opts} = Keyword.pop(Keyword.new(opts), :headers, []) + defp figure_headers_and_opt(opts, client, url) do + {local_headers, local_opts} = Keyword.pop(Keyword.new(opts), :headers, %{}) + + headers = client.headers + |> merge_headers(normalize_headers(local_headers)) + |> merge_headers(auth_headers(client, url)) - headers = merge_headers(client.headers, local_headers) poison_opts = merge_poison_opts(client.opts, local_opts) {headers, poison_opts} end defp merge_headers(old_headers, new_headers) do - old_headers - |> Keyword.merge(new_headers, fn _k, v1, v2 -> List.wrap(v1) ++ List.wrap(v2) end) + Map.merge(old_headers, new_headers, fn _k, v1, v2 -> List.wrap(v1) ++ List.wrap(v2) end) end @default_poison_opts [follow_redirect: true] @@ -179,4 +183,15 @@ defmodule ExHal.Client do {:error, _} -> NonHalResponse.from_httpoison_response(resp) end end + + defp normalize_headers(headers) do + Enum.into(headers, %{}, fn {k,v} -> {to_string(k), v} end) + end + + defp auth_headers(client, url) do + case Authorizer.authorization(client.authorizer, url) do + :no_auth -> %{} + {:ok, auth} -> %{"Authorization" => auth} + end + end end diff --git a/lib/exhal/http_client.ex b/lib/exhal/http_client.ex new file mode 100644 index 0000000..a9fc23c --- /dev/null +++ b/lib/exhal/http_client.ex @@ -0,0 +1,10 @@ +defmodule ExHal.HttpClient do + @callback get(String.t, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | + {:error, HTTPoison.Error.t} + @callback post(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | + {:error, HTTPoison.Error.t} + @callback put(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | + {:error, HTTPoison.Error.t} + @callback patch(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | + {:error, HTTPoison.Error.t} +end \ No newline at end of file diff --git a/test/exhal/client_test.exs b/test/exhal/client_test.exs index 7976800..b569cc0 100644 --- a/test/exhal/client_test.exs +++ b/test/exhal/client_test.exs @@ -1,5 +1,3 @@ -Code.require_file "../support/request_stubbing.exs", __DIR__ - defmodule ExHal.ClientTest do use ExUnit.Case, async: true @@ -15,12 +13,12 @@ defmodule ExHal.ClientTest do end test "(headers)" do - assert %Client{headers: ["User-Agent": "test agent", "X-Whatever": "example"]} = + assert %Client{headers: %{"User-Agent" => "test agent", "X-Whatever" => "example"}} = Client.new("User-Agent": "test agent", "X-Whatever": "example") end test "(headers, follow_redirect: follow)" do - assert %Client{headers: ["User-Agent": "test agent"], opts: [follow_redirect: false]} = + assert %Client{headers: %{"User-Agent" => "test agent"}, opts: [follow_redirect: false]} = Client.new(["User-Agent": "test agent"], follow_redirect: false) end end @@ -56,8 +54,7 @@ defmodule ExHal.ClientTest do # background defp to_have_header(client, expected_name, expected_value) do - expected_name = String.to_atom(expected_name) - {:ok, actual_value} = Keyword.fetch(client.headers, expected_name) + {:ok, actual_value} = Map.fetch(client.headers, expected_name) actual_value == expected_value end @@ -65,67 +62,133 @@ end defmodule ExHal.ClientHttpRequestTest do use ExUnit.Case, async: false - use RequestStubbing + import Mox - alias ExHal.{Client, Document, NonHalResponse, ResponseHeader} + # Make sure mocks are verified when the test exits + setup :verify_on_exit! - test ".get w/ normal link", %{client: client} do - thing_hal = hal_str("http://example.com/thing") + alias ExHal.{Client, Document, NonHalResponse, ResponseHeader, SimpleAuthorizer} - stub_request "get", url: "http://example.com/", resp_body: thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Client.get(client, "http://example.com/") + describe ".get/2" do + test "w/ normal link", %{client: client} do + ExHal.HttpClientMock + |> expect(:get, fn "http://example.com/", _headers, _opts -> + {:ok, + %HTTPoison.Response{body: hal_str("http://example.com/thing"), status_code: 200}} + end) - assert {:ok, "http://example.com/thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.get(client, "http://example.com/") + assert {:ok, "http://example.com/thing"} = ExHal.url(repr) end - end - test ".post w/ normal link", %{client: client} do - new_thing_hal = hal_str("http://example.com/new-thing") + test "w/ auth" do + client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) - stub_request "post", url: "http://example.com/", - req_body: new_thing_hal, - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Client.post(client, "http://example.com/", new_thing_hal) + ExHal.HttpClientMock + |> expect(:get, fn _url, %{"Authorization" => "Bearer sometoken"}, _opts -> + {:ok, + %HTTPoison.Response{body: "{}", status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + Client.get(client, "http://example.com/thing") end end - test ".post with empty response", %{client: client} do - stub_request "post", url: "http://example.com/", - req_body: "post body", - resp_body: "" do - assert {:ok, %NonHalResponse{}, %ResponseHeader{status_code: 200}} = + describe ".post" do + test "w/ normal link", %{client: client} do + new_thing_hal = hal_str("http://example.com/new-thing") + + ExHal.HttpClientMock + |> expect(:post, fn "http://example.com/", new_thing_hal, _headers, _opts -> + {:ok, + %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + end) + + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.post(client, "http://example.com/", new_thing_hal) + + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) + end + + test "w/ empty response", %{client: client} do + ExHal.HttpClientMock + |> expect(:post, fn "http://example.com/", _body , _headers, _opts -> + {:ok, + %HTTPoison.Response{body: "", status_code: 204}} + end) + + assert {:ok, %NonHalResponse{}, %ResponseHeader{status_code: 204}} = Client.post(client, "http://example.com/", "post body") end + + test "w/ auth" do + client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + + ExHal.HttpClientMock + |> expect(:post, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + {:ok, + %HTTPoison.Response{body: "{}", status_code: 200}} + end) + + Client.post(client, "http://example.com/thing", "post body") + end end - test ".put w/ normal link", %{client: client} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".put" do + test "w/ normal link", %{client: client} do + new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "put", url: "http://example.com/", - req_body: "the request body", - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Client.put(client, "http://example.com/", "the request body") + ExHal.HttpClientMock + |> expect(:put, fn "http://example.com/", new_thing_hal, _headers, _opts -> + {:ok, + %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.put(client, "http://example.com/", new_thing_hal) + + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) + end + + test "w/ auth" do + client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + + ExHal.HttpClientMock + |> expect(:put, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + {:ok, + %HTTPoison.Response{body: "{}", status_code: 200}} + end) + + Client.put(client, "http://example.com/thing", "put body") end + end - test ".patch w/ normal link", %{client: client} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".patch" do + test "w/ normal link", %{client: client} do + new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "patch", url: "http://example.com/", - req_body: "the request body", - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Client.patch(client, "http://example.com/", "the request body") + ExHal.HttpClientMock + |> expect(:patch, fn "http://example.com/", new_thing_hal, _headers, _opts -> + {:ok, + %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.patch(client, "http://example.com/", new_thing_hal) + + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) + end + + test "w/ auth" do + client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + + ExHal.HttpClientMock + |> expect(:patch, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + {:ok, + %HTTPoison.Response{body: "{}", status_code: 200}} + end) + + Client.patch(client, "http://example.com/thing", "patch body") end + end @@ -135,7 +198,7 @@ defmodule ExHal.ClientHttpRequestTest do {:ok, client: %Client{}} end - def hal_str(url) do + defp hal_str(url) do """ { "name": "#{url}", "_links": { diff --git a/test/exhal/collection_test.exs b/test/exhal/collection_test.exs index b1e293e..a1fab33 100644 --- a/test/exhal/collection_test.exs +++ b/test/exhal/collection_test.exs @@ -1,8 +1,7 @@ -Code.require_file "../support/request_stubbing.exs", __DIR__ - defmodule ExHal.CollectionTest do use ExUnit.Case, async: true - use RequestStubbing + import Mox + setup :verify_on_exit! alias ExHal.Document alias ExHal.Collection @@ -67,13 +66,16 @@ defmodule ExHal.CollectionTest do } do subject = Collection.to_stream(multi_page_collection_doc) - stub_request "get", url: last_page_collection_url, resp_body: last_page_collection_hal_str do - assert 3 == Enum.count(subject) - assert Enum.all? subject, fn x -> {:ok, _, %ResponseHeader{}} = x end - assert Enum.any? subject, has_doc_with_name("first") - assert Enum.any? subject, has_doc_with_name("second") - assert Enum.any? subject, has_doc_with_name("last") - end + ExHal.ClientMock + |> stub(:get, fn _client, ^last_page_collection_url, _headers -> + {:ok, Document.parse!(last_page_collection_hal_str), %ResponseHeader{status_code: 200}} + end) + + assert 3 == Enum.count(subject) + assert Enum.all? subject, fn x -> {:ok, _, %ResponseHeader{}} = x end + assert Enum.any? subject, has_doc_with_name("first") + assert Enum.any? subject, has_doc_with_name("second") + assert Enum.any? subject, has_doc_with_name("last") end test "ExHal.to_stream(sinlge_page_collection_doc) works", ctx do @@ -132,7 +134,8 @@ defmodule ExHal.CollectionTest do ] }, "_links" => - %{"next" => %{"href" => "http://example.com/?p=2"} + %{"next" => %{"href" => "http://example.com/?p=2"}, + "self" => %{"href" => "http://example.com/?p=1"} } }) end diff --git a/test/exhal/navigation_test.exs b/test/exhal/navigation_test.exs index 4fab6bc..b20a3e9 100644 --- a/test/exhal/navigation_test.exs +++ b/test/exhal/navigation_test.exs @@ -1,63 +1,68 @@ -Code.require_file "../support/request_stubbing.exs", __DIR__ - defmodule ExHal.NavigationTest do use ExUnit.Case, async: false - use RequestStubbing + import Mox alias ExHal.{Navigation,Document,Error,ResponseHeader} - test ".follow_link", %{doc: doc} do - thing_hal = hal_str("http://example.com/thing") + describe ".follow_link" do + test "regular link", %{doc: doc} do + ExHal.ClientMock + |> expect(:get, fn _client,"http://example.com/", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/thing")), %ResponseHeader{status_code: 200}} + end) - stub_request "get", url: "http://example.com/", resp_body: thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Navigation.follow_link(doc, "single") + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.follow_link(doc, "single") - assert {:ok, "http://example.com/thing"} = ExHal.url(target) + assert {:ok, "http://example.com/thing"} = ExHal.url(repr) end - assert {:ok, (target = %Document{}), %ResponseHeader{}} = - Navigation.follow_link(doc, "embedded") + test "embedded link", %{doc: doc} do + assert {:ok, (repr = %Document{}), %ResponseHeader{}} = Navigation.follow_link(doc, "embedded") - assert {:ok, "http://example.com/e"} = ExHal.url(target) + assert {:ok, "http://example.com/e"} = ExHal.url(repr) + end end - test ".post", %{doc: doc} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".post" do + test "regular link", %{doc: doc} do + new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "post", url: "http://example.com/", - req_body: "post body", - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Navigation.post(doc, "single", "post body") + ExHal.ClientMock + |> expect(:post, fn _client, "http://example.com/", "post body", _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.post(doc, "single", "post body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end end - test ".put", %{doc: doc} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".put" do + test "regular link", %{doc: doc} do + new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "put", url: "http://example.com/", - req_body: "put body", - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Navigation.put(doc, "single", "put body") + ExHal.ClientMock + |> expect(:put, fn _client, "http://example.com/", "put body", _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.put(doc, "single", "put body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end end - test ".patch", %{doc: doc} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".patch" do + test "regular link", %{doc: doc} do + new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "patch", url: "http://example.com/", - req_body: "patch body", - resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - Navigation.patch(doc, "single", "patch body") + ExHal.ClientMock + |> expect(:patch, fn _client, "http://example.com/", "patch body", _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.patch(doc, "single", "patch body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) + end end diff --git a/test/exhal_test.exs b/test/exhal_test.exs index 15b4b45..741ad01 100644 --- a/test/exhal_test.exs +++ b/test/exhal_test.exs @@ -1,5 +1,3 @@ -Code.require_file "support/request_stubbing.exs", __DIR__ - defmodule ExHalTest do use ExUnit.Case, async: true @@ -133,37 +131,32 @@ defmodule ExHalTest do defmodule HttpRequesting do use ExUnit.Case, async: false - use RequestStubbing - - setup_all do - ExVCR.Config.cassette_library_dir(__DIR__, __DIR__) - :ok - end + import Mox + setup :verify_on_exit! test ".follow_link w/ normal link" do - stub_request "get", url: "http://example.com/", resp_body: hal_str("http://example.com/") do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.follow_link(doc(), "single") + ExHal.ClientMock + |> expect(:get, fn _client, "http://example.com/", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/")), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/"} = ExHal.url(target) - end + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.follow_link(doc(), "single") + assert {:ok, "http://example.com/"} = ExHal.url(repr) end test ".follow_link w/ templated link" do - stub_request "get", url: "http://example.com/?q=test", resp_body: hal_str("http://example.com/?q=test") do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - ExHal.follow_link(doc(), "tmpl", tmpl_vars: [q: "test"]) + ExHal.ClientMock + |> expect(:get, fn _client, "http://example.com/?q=test", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/?q=test")), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/?q=test"} = ExHal.url(target) - end + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.follow_link(doc(), "tmpl", tmpl_vars: [q: "test"]) + assert {:ok, "http://example.com/?q=test"} = ExHal.url(repr) end - test ".follow_link w/ embedded link" do - stub_request "get", url: "http://example.com/embedded", resp_body: hal_str("http://example.com/embedded") do - assert {:ok, (target = %Document{}), %ResponseHeader{}} = - ExHal.follow_link(doc(), "embedded") - - assert {:ok, "http://example.com/embedded"} = ExHal.url(target) - end + test ".follow_link w/ embedded link" do + assert {:ok, (repr = %Document{}), %ResponseHeader{}} = ExHal.follow_link(doc(), "embedded") + assert {:ok, "http://example.com/embedded"} = ExHal.url(repr) end test ".follow_link w/ non-existent rel" do @@ -175,40 +168,39 @@ defmodule ExHalTest do end test ".follow_link w/ multiple links" do - stub_request "get", url: "~r/http:\/\/example.com\/[12]/", resp_body: hal_str("") do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = - ExHal.follow_link(doc(), "multiple") + ExHal.ClientMock + |> stub(:get, fn _client, "http://example.com/"<>id, _opts -> + {:ok, Document.parse!(hal_str("http://example.com/#{id}")), %ResponseHeader{status_code: 200}} + end) - assert {:ok, _} = ExHal.url(target) - end + assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.follow_link(doc(), "multiple") + assert {:ok, _} = ExHal.url(repr) end test ".follow_links w/ single link" do - stub_request "get", url: "http://example.com/", resp_body: hal_str("http://example.com/") do - assert [{:ok, (target = %Document{}), %ResponseHeader{status_code: 200}}] = ExHal.follow_links(doc(), "single") + ExHal.ClientMock + |> expect(:get, fn _client, "http://example.com/", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/")), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/"} = ExHal.url(target) - - end + assert [{:ok, (target = %Document{}), %ResponseHeader{status_code: 200}}] = ExHal.follow_links(doc(), "single") + assert {:ok, "http://example.com/"} = ExHal.url(target) end test ".follow_links w/ templated link" do - stub_request "get", url: "http://example.com/?q=test", resp_body: hal_str("http://example.com/?q=test") do - assert [{:ok, (target = %Document{}), %ResponseHeader{status_code: 200}}] = - ExHal.follow_links(doc(), "tmpl", tmpl_vars: [q: "test"]) + ExHal.ClientMock + |> expect(:get, fn _client, "http://example.com/?q=test", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/?q=test")), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/?q=test"} = ExHal.url(target) - end + assert [{:ok, (target = %Document{}), %ResponseHeader{status_code: 200}}] = ExHal.follow_links(doc(), "tmpl", tmpl_vars: [q: "test"]) + assert {:ok, "http://example.com/?q=test"} = ExHal.url(target) end test ".follow_links w/ embedded link" do - stub_request "get", url: "http://example.com/embedded", resp_body: hal_str("http://example.com/embedded") do - assert [{:ok, (target = %Document{}), %ResponseHeader{}}] = - ExHal.follow_links(doc(), "embedded") - - assert {:ok, "http://example.com/embedded"} = ExHal.url(target) - end + assert [{:ok, (target = %Document{}), %ResponseHeader{}}] = ExHal.follow_links(doc(), "embedded") + assert {:ok, "http://example.com/embedded"} = ExHal.url(target) end test ".follow_links w/ non-existent rel" do @@ -222,21 +214,26 @@ defmodule ExHalTest do test ".post w/ normal link" do new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "post", url: "http://example.com/", req_body: new_thing_hal, resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.post(doc(), "single", new_thing_hal) + ExHal.ClientMock + |> expect(:post, fn _client, "http://example.com/", new_thing_hal, _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) - end + assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.post(doc(), "single", new_thing_hal) + assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) end test ".patch w/ normal link" do new_thing_hal = hal_str("http://example.com/new-thing") - stub_request "patch", url: "http://example.com/", req_body: new_thing_hal, resp_body: new_thing_hal do - assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.patch(doc(), "single", new_thing_hal) + ExHal.ClientMock + |> expect(:patch, fn _client, "http://example.com/", new_thing_hal, _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) + + assert {:ok, (target = %Document{}), %ResponseHeader{status_code: 200}} = ExHal.patch(doc(), "single", new_thing_hal) - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) - end + assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) end defp doc do diff --git a/test/support/request_stubbing.exs b/test/support/request_stubbing.exs deleted file mode 100644 index 1b893bb..0000000 --- a/test/support/request_stubbing.exs +++ /dev/null @@ -1,33 +0,0 @@ -defmodule RequestStubbing do - defmacro __using__(_) do - quote do - use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney - import unquote(__MODULE__) - end - end - - defmacro stub_request(method, opts, test) do - quote do - use_cassette(:stub, - figure_use_cassette_opts(unquote(method), unquote(opts)), - do: unquote(test)) - end - end - - def figure_use_cassette_opts(method, opts) do - opts = Map.new(opts) - opts = case method do - "get" -> Map.merge(opts, %{req_body: ""}) - _ -> opts - end - - url = Map.fetch!(opts, :url) - - [method: method, - url: url, - request_body: Map.fetch!(opts, :req_body), - body: Map.get(opts, :resp_body, "#{String.upcase(method)} reponse from #{url}"), - status_code: Map.get(opts, :resp_status, 200) - ] - end -end diff --git a/test/test_helper.exs b/test/test_helper.exs index cfe93ef..06080fa 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,5 +3,4 @@ Application.ensure_all_started(:mox) Application.ensure_all_started(:stream_data) Mox.defmock(ExHal.ClientMock, for: ExHal.Client) - -Application.put_env(:exhal, :client, ExHal.ClientMock) +Mox.defmock(ExHal.HttpClientMock, for: ExHal.HttpClient) From 85a53dc7b0dda3baad4de0624654994013a352be Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Mon, 1 Oct 2018 12:23:06 -0600 Subject: [PATCH 3/6] update docs and bump version --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index e746089..33419e7 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,43 @@ iex> Stream.map(collection, fn follow_results -> ["http://example.com/beginning", "http://example.com/middle", "http://example.com/end"] ``` +### Authorization + +The `Authorization` header field is automatically populated using the "authorizer" specified on the client. Currently, the only implemented authorizer is `ExHal.SimpleAuthorizer`. This authorizer returns the specified `Authorization` header field value for any resource with a specified URL prefix. + +```elixir +iex> token = fetch_api_token() +iex> authorizer = ExHal.SimpleAuthorizer.new("http://example.com/", "Bearer #{token}") +iex> client = ExHal.client() +...> |> ExHal.Client.set_authorizer(authorizer) +iex> ExHal.get("http://example.com/entrypoint") # request will included `Authorization: Bearer ...` request header +%ExHal.Document{...} +``` + +You can implement your own authorizer if you want. Simply implement `ExHal.Authorizer` protocol. + +```elixir +iex> defmodule MyAuth do +...> defstruct([:p]) +...> def connect() do +...> pid = start_token_management_process() +...> %MyAuth{p: pid} +...> end +...> end +iex> defimpl ExHal.Authorizer, for: MyAuth do +...> def authorization(authorizer, url) do +...> t = GenServer.call(authorizer.p, :get_token) +...> {:ok, "Bearer #{t}"} +...> end +...> end +...> +iex> authorizer = MyAuth.connect +iex> client = ExHal.client() +...> |> ExHal.Client.set_authorizer(authorizer) +iex> ExHal.get("http://example.com/entrypoint") # request will included `Authorization: Bearer ...` request header with a token fetched from the token management process. +%ExHal.Document{...} +``` + ### Serialization Collections and Document can render themselves to a json-like From 9c260d1b08219ea0d46a60f119d787d009a450e3 Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Mon, 1 Oct 2018 12:30:25 -0600 Subject: [PATCH 4/6] uglify the code This one's for you, Andrew. --- lib/exhal/assertions.ex | 3 +- lib/exhal/client.ex | 40 ++--- lib/exhal/document.ex | 17 ++- lib/exhal/form.ex | 13 +- lib/exhal/http_client.ex | 22 +-- lib/exhal/link.ex | 34 ++--- lib/exhal/navigation.ex | 21 ++- lib/exhal/non_hal_response.ex | 6 +- lib/exhal/simple_authorizer.ex | 3 +- test/exhal/assertions_test.exs | 45 +++--- test/exhal/client_test.exs | 102 ++++++------- test/exhal/collection_test.exs | 99 ++++++------- test/exhal/document_test.exs | 98 ++++++------ test/exhal/interpreter_test.exs | 10 +- test/exhal/link_test.exs | 106 ++++++------- test/exhal/navigation_test.exs | 60 ++++---- test/exhal/non_hal_response_test.exs | 12 +- test/exhal/simple_authorizer_test.exs | 11 +- test/exhal/transcoder_test.exs | 206 ++++++++++++++++---------- 19 files changed, 498 insertions(+), 410 deletions(-) diff --git a/lib/exhal/assertions.ex b/lib/exhal/assertions.ex index 96ead22..70936e5 100644 --- a/lib/exhal/assertions.ex +++ b/lib/exhal/assertions.ex @@ -103,7 +103,8 @@ defmodule ExHal.Assertions do # internal functions - @spec p_assert_property(String.t | Document.t, String.t, (any() -> boolean()), String.t) :: any() + @spec p_assert_property(String.t() | Document.t(), String.t(), (any() -> boolean()), String.t()) :: + any() def p_assert_property(doc, prop_name, check_fn, check_desc) when is_binary(doc) do p_assert_property(Document.parse!(doc), prop_name, check_fn, check_desc) end diff --git a/lib/exhal/client.ex b/lib/exhal/client.ex index dbc562b..5e3d5ae 100644 --- a/lib/exhal/client.ex +++ b/lib/exhal/client.ex @@ -35,16 +35,16 @@ defmodule ExHal.Client do """ @opaque t :: %__MODULE__{} defstruct authorizer: NullAuthorizer.new(), - headers: %{}, - opts: [follow_redirect: true] + headers: %{}, + opts: [follow_redirect: true] @typedoc """ The return value of any function that makes an HTTP request. """ @type http_response :: - {:ok, Document.t | NonHalResponse.t, ResponseHeader.t} - | {:error, Document.t | NonHalResponse.t, ResponseHeader.t } - | {:error, Error.t} + {:ok, Document.t() | NonHalResponse.t(), ResponseHeader.t()} + | {:error, Document.t() | NonHalResponse.t(), ResponseHeader.t()} + | {:error, Error.t()} @doc """ Returns a new client. @@ -52,9 +52,12 @@ defmodule ExHal.Client do @spec new() :: t def new(), do: %__MODULE__{} - @spec new(Keyword.t) :: t - def new(headers: headers), do: %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: true]} + @spec new(Keyword.t()) :: t + def new(headers: headers), + do: %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: true]} + def new(follow_redirect: follow), do: %__MODULE__{opts: [follow_redirect: follow]} + def new(headers: headers, follow_redirect: follow), do: %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: follow]} @@ -63,7 +66,7 @@ defmodule ExHal.Client do %__MODULE__{headers: normalize_headers(headers), opts: [follow_redirect: true]} end - @spec new(Keyword.t, Keyword.t) :: t + @spec new(Keyword.t(), Keyword.t()) :: t def new(headers, follow_redirect: follow) do new(headers: headers, follow_redirect: follow) end @@ -72,7 +75,7 @@ defmodule ExHal.Client do Returns client that will include the specified headers in any request made with it. """ - @spec add_headers(t, Keyword.t) :: t + @spec add_headers(t, Keyword.t()) :: t def add_headers(client, headers) do updated_headers = merge_headers(client.headers, normalize_headers(headers)) @@ -82,7 +85,7 @@ defmodule ExHal.Client do @doc """ Returns a client that will authorize requests using the specified authorizer. """ - @spec set_authorizer(t, Authorizer.t) :: t + @spec set_authorizer(t, Authorizer.t()) :: t def set_authorizer(client, new_authorizer) do %__MODULE__{client | authorizer: new_authorizer} end @@ -95,7 +98,7 @@ defmodule ExHal.Client do end end - @callback get(__MODULE__.t, String.t, Keyword.t) :: http_response() + @callback get(__MODULE__.t(), String.t(), Keyword.t()) :: http_response() def get(client, url, opts \\ []) do {headers, poison_opts} = figure_headers_and_opt(opts, client, url) @@ -105,7 +108,7 @@ defmodule ExHal.Client do end end - @callback post(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() + @callback post(__MODULE__.t(), String.t(), <<>>, Keyword.t()) :: http_response() def post(client, url, body, opts \\ []) do {headers, poison_opts} = figure_headers_and_opt(opts, client, url) @@ -115,7 +118,7 @@ defmodule ExHal.Client do end end - @callback put(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() + @callback put(__MODULE__.t(), String.t(), <<>>, Keyword.t()) :: http_response() def put(client, url, body, opts \\ []) do {headers, poison_opts} = figure_headers_and_opt(opts, client, url) @@ -125,7 +128,7 @@ defmodule ExHal.Client do end end - @callback patch(__MODULE__.t, String.t, <<>>, Keyword.t) :: http_response() + @callback patch(__MODULE__.t(), String.t(), <<>>, Keyword.t()) :: http_response() def patch(client, url, body, opts \\ []) do {headers, poison_opts} = figure_headers_and_opt(opts, client, url) @@ -140,9 +143,10 @@ defmodule ExHal.Client do defp figure_headers_and_opt(opts, client, url) do {local_headers, local_opts} = Keyword.pop(Keyword.new(opts), :headers, %{}) - headers = client.headers - |> merge_headers(normalize_headers(local_headers)) - |> merge_headers(auth_headers(client, url)) + headers = + client.headers + |> merge_headers(normalize_headers(local_headers)) + |> merge_headers(auth_headers(client, url)) poison_opts = merge_poison_opts(client.opts, local_opts) @@ -185,7 +189,7 @@ defmodule ExHal.Client do end defp normalize_headers(headers) do - Enum.into(headers, %{}, fn {k,v} -> {to_string(k), v} end) + Enum.into(headers, %{}, fn {k, v} -> {to_string(k), v} end) end defp auth_headers(client, url) do diff --git a/lib/exhal/document.ex b/lib/exhal/document.ex index a00ed02..28d83f9 100644 --- a/lib/exhal/document.ex +++ b/lib/exhal/document.ex @@ -69,7 +69,8 @@ defmodule ExHal.Document do } end - def from_parsed_hal(client = %ExHal.Client{}, parsed_hal), do: from_parsed_hal(parsed_hal, client) + def from_parsed_hal(client = %ExHal.Client{}, parsed_hal), + do: from_parsed_hal(parsed_hal, client) @doc """ Returns true iff the document contains at least one link with the specified rel. @@ -223,10 +224,11 @@ defmodule ExHal.Document do namespaces = NsReg.from_parsed_json(parsed_json) embedded_links = embedded_links_in(client, parsed_json) - links = simple_links_in(parsed_json) - |> augment_simple_links_with_embedded_reprs(embedded_links) - |> backfill_missing_links(embedded_links) - |> expand_curies(namespaces) + links = + simple_links_in(parsed_json) + |> augment_simple_links_with_embedded_reprs(embedded_links) + |> backfill_missing_links(embedded_links) + |> expand_curies(namespaces) Enum.group_by(links, fn a_link -> a_link.rel end) end @@ -234,18 +236,17 @@ defmodule ExHal.Document do defp augment_simple_links_with_embedded_reprs(links, embedded_links) do links |> Enum.map(fn link -> - case Enum.find(embedded_links, &(Link.equal?(&1, link))) do + case Enum.find(embedded_links, &Link.equal?(&1, link)) do nil -> link embedded -> %{link | target: embedded.target} end end) end - defp backfill_missing_links(links, embedded_links) do embedded_links |> Enum.reduce(links, fn embedded, links -> - case Enum.any?(links, &(Link.equal?(embedded, &1))) do + case Enum.any?(links, &Link.equal?(embedded, &1)) do false -> [embedded | links] _ -> links end diff --git a/lib/exhal/form.ex b/lib/exhal/form.ex index ef84770..20b82e7 100644 --- a/lib/exhal/form.ex +++ b/lib/exhal/form.ex @@ -68,13 +68,10 @@ defmodule ExHal.Form do """ @spec submit(__MODULE__.t(), Client.t()) :: Client.http_response() def submit(form, client) do - apply(client_module(), + apply( + client_module(), form.method, - [client, - form.target, - encode(form), - [headers: ["Content-Type": form.content_type]] - ] + [client, form.target, encode(form), [headers: ["Content-Type": form.content_type]]] ) end @@ -124,8 +121,8 @@ defmodule ExHal.Form do defp extract_method(a_map) do Map.get_lazy(a_map, "method", fn -> raise ArgumentError, "form method missing" end) - |> String.downcase - |> String.to_atom + |> String.downcase() + |> String.to_atom() end defp extract_content_type(a_map) do diff --git a/lib/exhal/http_client.ex b/lib/exhal/http_client.ex index a9fc23c..ca520ee 100644 --- a/lib/exhal/http_client.ex +++ b/lib/exhal/http_client.ex @@ -1,10 +1,14 @@ defmodule ExHal.HttpClient do - @callback get(String.t, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | - {:error, HTTPoison.Error.t} - @callback post(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | - {:error, HTTPoison.Error.t} - @callback put(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | - {:error, HTTPoison.Error.t} - @callback patch(String.t, any, HTTPoison.Base.headers, Keyword.t) :: {:ok, HTTPoison.Response.t | HTTPoison.AsyncResponse.t} | - {:error, HTTPoison.Error.t} -end \ No newline at end of file + @callback get(String.t(), HTTPoison.Base.headers(), Keyword.t()) :: + {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} + | {:error, HTTPoison.Error.t()} + @callback post(String.t(), any, HTTPoison.Base.headers(), Keyword.t()) :: + {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} + | {:error, HTTPoison.Error.t()} + @callback put(String.t(), any, HTTPoison.Base.headers(), Keyword.t()) :: + {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} + | {:error, HTTPoison.Error.t()} + @callback patch(String.t(), any, HTTPoison.Base.headers(), Keyword.t()) :: + {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()} + | {:error, HTTPoison.Error.t()} +end diff --git a/lib/exhal/link.ex b/lib/exhal/link.ex index 4fc1b21..56bfbcb 100644 --- a/lib/exhal/link.ex +++ b/lib/exhal/link.ex @@ -12,12 +12,12 @@ defmodule ExHal.Link do A link. Links may be simple or dereferenced (from the embedded section). """ @type t :: %__MODULE__{ - rel: String.t(), - href: String.t(), - templated: boolean(), - name: String.t(), - target: Document.t() - } + rel: String.t(), + href: String.t(), + templated: boolean(), + name: String.t(), + target: Document.t() + } defstruct [:rel, :href, :templated, :name, :target] @doc """ @@ -103,9 +103,9 @@ defmodule ExHal.Link do end end - defpat simple_link(%{target: nil}) - defpat unnamed_link(%{name: nil}) - defpat embedded_link(%{target: %{}}) + defpat(simple_link(%{target: nil})) + defpat(unnamed_link(%{name: nil})) + defpat(embedded_link(%{target: %{}})) @doc """ Returns true if the links are equivalent. @@ -118,21 +118,21 @@ defmodule ExHal.Link do @spec equal?(__MODULE__.t(), __MODULE__.t()) :: boolean() def equal?(%{href: nil}, _), do: false def equal?(_, %{href: nil}), do: false - def equal?(link_a = simple_link(), link_b = simple_link()) do - link_a.rel == link_b.rel - && link_a.href == link_b.href - && link_a.name == link_b.name + + def equal?(link_a = simple_link(), link_b = simple_link()) do + link_a.rel == link_b.rel && link_a.href == link_b.href && link_a.name == link_b.name end + def equal?(link_a = embedded_link(), link_b = embedded_link()) do # both embedded and href's are comparable - link_a.rel == link_b.rel - && link_a.href == link_b.href + link_a.rel == link_b.rel && link_a.href == link_b.href end + def equal?(link_a = simple_link(), link_b = embedded_link()), do: equal?(link_b, link_a) + def equal?(link_a = embedded_link(), link_b = simple_link()) do # both embedded and href's are comparable - link_a.rel == link_b.rel - && link_a.href == link_b.href + link_a.rel == link_b.rel && link_a.href == link_b.href end # private functions diff --git a/lib/exhal/navigation.ex b/lib/exhal/navigation.ex index 3d615f1..f290e2b 100644 --- a/lib/exhal/navigation.ex +++ b/lib/exhal/navigation.ex @@ -61,8 +61,11 @@ defmodule ExHal.Navigation do tmpl_vars = Map.get(opts, :tmpl_vars, %{}) case figure_link(a_doc, name, pick_volunteer?) do - {:error, e} -> {:error, e} - {:ok, link} -> @client_module.post(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) + {:error, e} -> + {:error, e} + + {:ok, link} -> + @client_module.post(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) end end @@ -77,8 +80,11 @@ defmodule ExHal.Navigation do tmpl_vars = Map.get(opts, :tmpl_vars, %{}) case figure_link(a_doc, name, pick_volunteer?) do - {:error, e} -> {:error, e} - {:ok, link} -> @client_module.put(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) + {:error, e} -> + {:error, e} + + {:ok, link} -> + @client_module.put(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) end end @@ -93,8 +99,11 @@ defmodule ExHal.Navigation do tmpl_vars = Map.get(opts, :tmpl_vars, %{}) case figure_link(a_doc, name, pick_volunteer?) do - {:error, e} -> {:error, e} - {:ok, link} -> @client_module.patch(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) + {:error, e} -> + {:error, e} + + {:ok, link} -> + @client_module.patch(a_doc.client, Link.target_url!(link, tmpl_vars), body, opts) end end diff --git a/lib/exhal/non_hal_response.ex b/lib/exhal/non_hal_response.ex index 1e30182..defc638 100644 --- a/lib/exhal/non_hal_response.ex +++ b/lib/exhal/non_hal_response.ex @@ -11,8 +11,8 @@ defimpl ExHal.Locatable, for: ExHal.NonHalResponse do a_resp.headers |> Enum.find(fn {field_name, _} -> Regex.match?(~r/(content-)?location/i, field_name) end) |> case do - nil -> :error - {_, url} -> {:ok, url} - end + nil -> :error + {_, url} -> {:ok, url} + end end end diff --git a/lib/exhal/simple_authorizer.ex b/lib/exhal/simple_authorizer.ex index 47bcae9..155f9c4 100644 --- a/lib/exhal/simple_authorizer.ex +++ b/lib/exhal/simple_authorizer.ex @@ -9,7 +9,8 @@ defmodule ExHal.SimpleAuthorizer do defstruct([:authorization, :url_prefix]) @spec new(Authorizer.url(), Authorizer.authorization_field_value()) :: __MODULE__.t() - def new(url_prefix, authorization_str), do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} + def new(url_prefix, authorization_str), + do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} end defimpl ExHal.Authorizer, for: ExHal.SimpleAuthorizer do diff --git a/test/exhal/assertions_test.exs b/test/exhal/assertions_test.exs index 78d481c..180b4c1 100644 --- a/test/exhal/assertions_test.exs +++ b/test/exhal/assertions_test.exs @@ -13,19 +13,20 @@ defmodule ExHal.AssertionsTest do } ) - {:ok, %{ - hal: hal, - doc: ExHal.Document.parse!(hal) - } } + {:ok, + %{ + hal: hal, + doc: ExHal.Document.parse!(hal) + }} end test "eq(expected)" do - assert true == eq("foo").("foo") + assert true == eq("foo").("foo") assert false == eq("foo").("bar") end test "matches(expected)" do - assert true == matches(~r/f/).("foo") + assert true == matches(~r/f/).("foo") assert false == matches(~r/b/).("foo") end @@ -40,18 +41,18 @@ defmodule ExHal.AssertionsTest do end test "assert_property(doc, property_name, check_fn)", %{doc: doc} do - assert true == assert_property(doc, "name", eq "foo") + assert true == assert_property(doc, "name", eq("foo")) end test "assert_property(doc, property_name, failing_check)", %{doc: doc} do assert_raise ExUnit.AssertionError, ~r/eq."wrong"/, fn -> - assert_property(doc, "name", eq "wrong") + assert_property(doc, "name", eq("wrong")) end end test "assert_property(doc, nonexistent_prop_name, check_fn)", %{doc: doc} do assert_raise ExUnit.AssertionError, ~r/absent/, fn -> - assert_property(doc, "nonexistent", eq "foo") + assert_property(doc, "nonexistent", eq("foo")) end end @@ -66,18 +67,18 @@ defmodule ExHal.AssertionsTest do end test "assert_property(hal, property_name, check_fn)", %{hal: hal} do - assert true == assert_property(hal, "name", eq "foo") + assert true == assert_property(hal, "name", eq("foo")) end test "assert_property(hal, property_name, failing_check)", %{hal: hal} do assert_raise ExUnit.AssertionError, ~r/eq."wrong"/, fn -> - assert_property(hal, "name", eq "wrong") + assert_property(hal, "name", eq("wrong")) end end test "assert_property(hal, nonexistent_prop_name, check_fn)", %{hal: hal} do assert_raise ExUnit.AssertionError, ~r/absent/, fn -> - assert_property(hal, "nonexistent", eq "foo") + assert_property(hal, "nonexistent", eq("foo")) end end @@ -92,19 +93,19 @@ defmodule ExHal.AssertionsTest do end test "assert_link_target(doc, rel_with_multiple_links, check_fn)", %{doc: doc} do - assert true == assert_link_target(doc, "profile", eq "http://example.com/simple") - assert true == assert_link_target(doc, "profile", eq "http://example.com/other") + assert true == assert_link_target(doc, "profile", eq("http://example.com/simple")) + assert true == assert_link_target(doc, "profile", eq("http://example.com/other")) end test "assert_link_target(doc, rel, non_matching_target_url)", %{doc: doc} do assert_raise ExUnit.AssertionError, ~r(eq.*"http://example.com/unsupported"), fn -> - assert_link_target(doc, "profile", eq "http://example.com/unsupported") + assert_link_target(doc, "profile", eq("http://example.com/unsupported")) end end test "assert_link_target(doc, nonexistent_rel, check_fn)", %{doc: doc} do assert_raise ExUnit.AssertionError, ~r/absent/, fn -> - assert_link_target(doc, "nonexistent", eq "http://example.com/simple") + assert_link_target(doc, "nonexistent", eq("http://example.com/simple")) end end @@ -119,27 +120,27 @@ defmodule ExHal.AssertionsTest do end test "assert_link_target(hal, rel_with_multiple_links, check_fn)", %{hal: hal} do - assert true == assert_link_target(hal, "profile", eq "http://example.com/simple") - assert true == assert_link_target(hal, "profile", eq "http://example.com/other") + assert true == assert_link_target(hal, "profile", eq("http://example.com/simple")) + assert true == assert_link_target(hal, "profile", eq("http://example.com/other")) end test "assert_link_target(hal, rel, non_matching_target_url)", %{hal: hal} do assert_raise ExUnit.AssertionError, ~r(eq.*"http://example.com/unsupported"), fn -> - assert_link_target(hal, "profile", eq "http://example.com/unsupported") + assert_link_target(hal, "profile", eq("http://example.com/unsupported")) end end test "assert_link_target(hal, nonexistent_rel, check_fn)", %{hal: hal} do assert_raise ExUnit.AssertionError, ~r/absent/, fn -> - assert_link_target(hal, "nonexistent", eq "http://example.com/simple") + assert_link_target(hal, "nonexistent", eq("http://example.com/simple")) end end test "assert collection(doc) |> Enum.empty?", %{doc: doc} do - assert collection(doc) |> Enum.empty? + assert collection(doc) |> Enum.empty?() end test "assert collection(hal) |> Enum.empty?", %{hal: hal} do - assert collection(hal) |> Enum.empty? + assert collection(hal) |> Enum.empty?() end end diff --git a/test/exhal/client_test.exs b/test/exhal/client_test.exs index b569cc0..7944874 100644 --- a/test/exhal/client_test.exs +++ b/test/exhal/client_test.exs @@ -5,7 +5,7 @@ defmodule ExHal.ClientTest do describe ".new" do test ".new/0" do - assert %Client{} = Client.new + assert %Client{} = Client.new() end test "(empty_headers)" do @@ -13,22 +13,22 @@ defmodule ExHal.ClientTest do end test "(headers)" do - assert %Client{headers: %{"User-Agent" => "test agent", "X-Whatever" => "example"}} = - Client.new("User-Agent": "test agent", "X-Whatever": "example") + assert %Client{headers: %{"User-Agent" => "test agent", "X-Whatever" => "example"}} = + Client.new("User-Agent": "test agent", "X-Whatever": "example") end test "(headers, follow_redirect: follow)" do assert %Client{headers: %{"User-Agent" => "test agent"}, opts: [follow_redirect: false]} = - Client.new(["User-Agent": "test agent"], follow_redirect: false) + Client.new(["User-Agent": "test agent"], follow_redirect: false) end end describe ".add_headers/1" do test "adding headers to client" do - assert (%Client{} - |> Client.add_headers("hello": "bob") - |> Client.add_headers("hello": ["alice","jane"])) - |> to_have_header("hello", ["bob", "alice", "jane"]) + assert %Client{} + |> Client.add_headers(hello: "bob") + |> Client.add_headers(hello: ["alice", "jane"]) + |> to_have_header("hello", ["bob", "alice", "jane"]) end end @@ -36,8 +36,7 @@ defmodule ExHal.ClientTest do test "first time" do test_auther = SimpleAuthorizer.new("http://example.com", "Bearer sometoken") - assert %Client{authorizer: test_auther} == - Client.set_authorizer(Client.new, test_auther) + assert %Client{authorizer: test_auther} == Client.set_authorizer(Client.new(), test_auther) end test "last one in wins time" do @@ -45,9 +44,9 @@ defmodule ExHal.ClientTest do test_auther2 = SimpleAuthorizer.new("http://myapp.com", "Bearer someothertoken") assert %Client{authorizer: test_auther2} == - Client.new - |> Client.set_authorizer(test_auther1) - |> Client.set_authorizer(test_auther2) + Client.new() + |> Client.set_authorizer(test_auther1) + |> Client.set_authorizer(test_auther2) end end @@ -73,21 +72,23 @@ defmodule ExHal.ClientHttpRequestTest do test "w/ normal link", %{client: client} do ExHal.HttpClientMock |> expect(:get, fn "http://example.com/", _headers, _opts -> - {:ok, - %HTTPoison.Response{body: hal_str("http://example.com/thing"), status_code: 200}} + {:ok, %HTTPoison.Response{body: hal_str("http://example.com/thing"), status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.get(client, "http://example.com/") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Client.get(client, "http://example.com/") + assert {:ok, "http://example.com/thing"} = ExHal.url(repr) end test "w/ auth" do - client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + client = + Client.new() + |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) ExHal.HttpClientMock |> expect(:get, fn _url, %{"Authorization" => "Bearer sometoken"}, _opts -> - {:ok, - %HTTPoison.Response{body: "{}", status_code: 200}} + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) Client.get(client, "http://example.com/thing") @@ -100,33 +101,33 @@ defmodule ExHal.ClientHttpRequestTest do ExHal.HttpClientMock |> expect(:post, fn "http://example.com/", new_thing_hal, _headers, _opts -> - {:ok, - %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + {:ok, %HTTPoison.Response{body: new_thing_hal, status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.post(client, "http://example.com/", new_thing_hal) + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Client.post(client, "http://example.com/", new_thing_hal) assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end test "w/ empty response", %{client: client} do ExHal.HttpClientMock - |> expect(:post, fn "http://example.com/", _body , _headers, _opts -> - {:ok, - %HTTPoison.Response{body: "", status_code: 204}} + |> expect(:post, fn "http://example.com/", _body, _headers, _opts -> + {:ok, %HTTPoison.Response{body: "", status_code: 204}} end) assert {:ok, %NonHalResponse{}, %ResponseHeader{status_code: 204}} = - Client.post(client, "http://example.com/", "post body") + Client.post(client, "http://example.com/", "post body") end test "w/ auth" do - client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + client = + Client.new() + |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) ExHal.HttpClientMock |> expect(:post, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> - {:ok, - %HTTPoison.Response{body: "{}", status_code: 200}} + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) Client.post(client, "http://example.com/thing", "post body") @@ -137,61 +138,60 @@ defmodule ExHal.ClientHttpRequestTest do test "w/ normal link", %{client: client} do new_thing_hal = hal_str("http://example.com/new-thing") - ExHal.HttpClientMock + ExHal.HttpClientMock |> expect(:put, fn "http://example.com/", new_thing_hal, _headers, _opts -> - {:ok, - %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + {:ok, %HTTPoison.Response{body: new_thing_hal, status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.put(client, "http://example.com/", new_thing_hal) + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Client.put(client, "http://example.com/", new_thing_hal) assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end test "w/ auth" do - client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + client = + Client.new() + |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) ExHal.HttpClientMock |> expect(:put, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> - {:ok, - %HTTPoison.Response{body: "{}", status_code: 200}} + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) Client.put(client, "http://example.com/thing", "put body") end - end describe ".patch" do test "w/ normal link", %{client: client} do new_thing_hal = hal_str("http://example.com/new-thing") - ExHal.HttpClientMock + ExHal.HttpClientMock |> expect(:patch, fn "http://example.com/", new_thing_hal, _headers, _opts -> - {:ok, - %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + {:ok, %HTTPoison.Response{body: new_thing_hal, status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Client.patch(client, "http://example.com/", new_thing_hal) + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Client.patch(client, "http://example.com/", new_thing_hal) assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end test "w/ auth" do - client = Client.new() |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + client = + Client.new() + |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) ExHal.HttpClientMock |> expect(:patch, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> - {:ok, - %HTTPoison.Response{body: "{}", status_code: 200}} + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) Client.patch(client, "http://example.com/thing", "patch body") end - end - # Background setup do @@ -200,11 +200,11 @@ defmodule ExHal.ClientHttpRequestTest do defp hal_str(url) do """ - { "name": "#{url}", - "_links": { - "self": { "href": "#{url}" } - } + { "name": "#{url}", + "_links": { + "self": { "href": "#{url}" } } - """ + } + """ end end diff --git a/test/exhal/collection_test.exs b/test/exhal/collection_test.exs index a1fab33..8ccb38a 100644 --- a/test/exhal/collection_test.exs +++ b/test/exhal/collection_test.exs @@ -10,8 +10,9 @@ defmodule ExHal.CollectionTest do test ".to_json_hash" do parsed_hal = %{ "name" => "My Name", - "_embedded" => %{ "test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, - "_links" => %{ "self" => %{"href" => "http://example.com/my-name"}}} + "_embedded" => %{"test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, + "_links" => %{"self" => %{"href" => "http://example.com/my-name"}} + } doc = Document.from_parsed_hal(parsed_hal) assert %{"_embedded" => %{"item" => [^parsed_hal]}} = Collection.to_json_hash([doc]) @@ -20,14 +21,16 @@ defmodule ExHal.CollectionTest do test "render!/1" do parsed_hal = %{ "name" => "My Name", - "_embedded" => %{ "test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, - "_links" => %{ "self" => %{"href" => "http://example.com/my-name"}}} + "_embedded" => %{"test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, + "_links" => %{"self" => %{"href" => "http://example.com/my-name"}} + } doc = Document.from_parsed_hal(parsed_hal) + {:ok, rendered_doc, %ResponseHeader{}} = Collection.render!([doc]) - |> Document.parse! - |> Collection.to_stream + |> Document.parse!() + |> Collection.to_stream() |> Enum.at(0) assert rendered_doc == doc @@ -54,9 +57,9 @@ defmodule ExHal.CollectionTest do } do subject = Collection.to_stream(single_page_collection_doc) assert 2 == Enum.count(subject) - assert Enum.all? subject, fn x -> {:ok, _, %ResponseHeader{}} = x end - assert Enum.any? subject, has_doc_with_name("first") - assert Enum.any? subject, has_doc_with_name("second") + assert Enum.all?(subject, fn x -> {:ok, _, %ResponseHeader{}} = x end) + assert Enum.any?(subject, has_doc_with_name("first")) + assert Enum.any?(subject, has_doc_with_name("second")) end test ".to_stream(multi_page_collection_doc) contains all items", %{ @@ -72,10 +75,10 @@ defmodule ExHal.CollectionTest do end) assert 3 == Enum.count(subject) - assert Enum.all? subject, fn x -> {:ok, _, %ResponseHeader{}} = x end - assert Enum.any? subject, has_doc_with_name("first") - assert Enum.any? subject, has_doc_with_name("second") - assert Enum.any? subject, has_doc_with_name("last") + assert Enum.all?(subject, fn x -> {:ok, _, %ResponseHeader{}} = x end) + assert Enum.any?(subject, has_doc_with_name("first")) + assert Enum.any?(subject, has_doc_with_name("second")) + assert Enum.any?(subject, has_doc_with_name("last")) end test "ExHal.to_stream(sinlge_page_collection_doc) works", ctx do @@ -86,17 +89,19 @@ defmodule ExHal.CollectionTest do assert ExHal.to_stream(doc) |> is_a_stream end - # background setup do - {:ok, [non_collection_doc: non_collection_doc(), - single_page_collection_doc: single_page_collection_doc(), - multi_page_collection_doc: multi_page_collection_doc(), - empty_collection_doc: empty_collection_doc(), - truly_empty_collection_doc: truly_empty_collection_doc(), - last_page_collection_url: "http://example.com/?p=2", - last_page_collection_hal_str: last_page_collection_hal_str()]} + {:ok, + [ + non_collection_doc: non_collection_doc(), + single_page_collection_doc: single_page_collection_doc(), + multi_page_collection_doc: multi_page_collection_doc(), + empty_collection_doc: empty_collection_doc(), + truly_empty_collection_doc: truly_empty_collection_doc(), + last_page_collection_url: "http://example.com/?p=2", + last_page_collection_hal_str: last_page_collection_hal_str() + ]} end defp non_collection_doc do @@ -104,22 +109,13 @@ defmodule ExHal.CollectionTest do end defp single_page_collection_doc do - Document.from_parsed_hal(%{"_embedded" => - %{"item" => - [%{"name" => "first"}, - %{"name" => "second"} - ] - } - }) + Document.from_parsed_hal(%{ + "_embedded" => %{"item" => [%{"name" => "first"}, %{"name" => "second"}]} + }) end defp empty_collection_doc do - Document.from_parsed_hal(%{"_embedded" => - %{"item" => - [ - ] - } - }) + Document.from_parsed_hal(%{"_embedded" => %{"item" => []}}) end defp truly_empty_collection_doc do @@ -127,26 +123,22 @@ defmodule ExHal.CollectionTest do end defp multi_page_collection_doc do - Document.from_parsed_hal(%{"_embedded" => - %{"item" => - [%{"name" => "first"}, - %{"name" => "second"} - ] - }, - "_links" => - %{"next" => %{"href" => "http://example.com/?p=2"}, - "self" => %{"href" => "http://example.com/?p=1"} - } - }) + Document.from_parsed_hal(%{ + "_embedded" => %{"item" => [%{"name" => "first"}, %{"name" => "second"}]}, + "_links" => %{ + "next" => %{"href" => "http://example.com/?p=2"}, + "self" => %{"href" => "http://example.com/?p=1"} + } + }) end defp last_page_collection_hal_str do """ - {"_embedded": { - "item": [{"name": "last"}] - } - } - """ + {"_embedded": { + "item": [{"name": "last"}] + } + } + """ end defp is_a_stream(thing) do @@ -156,8 +148,11 @@ defmodule ExHal.CollectionTest do defp has_doc_with_name(expected) do fn item -> case item do - {:ok, doc, %ResponseHeader{}} -> ExHal.get_property_lazy(doc, "name", fn -> :missing end) == expected - _ -> false + {:ok, doc, %ResponseHeader{}} -> + ExHal.get_property_lazy(doc, "name", fn -> :missing end) == expected + + _ -> + false end end end diff --git a/test/exhal/document_test.exs b/test/exhal/document_test.exs index e296f32..27dae8b 100644 --- a/test/exhal/document_test.exs +++ b/test/exhal/document_test.exs @@ -1,4 +1,4 @@ -Code.require_file "../../test_helper.exs", __ENV__.file +Code.require_file("../../test_helper.exs", __ENV__.file) defmodule ExHal.DocumentTest do use ExUnit.Case, async: true @@ -6,7 +6,7 @@ defmodule ExHal.DocumentTest do alias ExHal.Document setup do - {:ok, %{client: ExHal.client}} + {:ok, %{client: ExHal.client()}} end test "ExHal parses valid, empty HAL documents", %{client: client} do @@ -28,24 +28,27 @@ defmodule ExHal.DocumentTest do assert :error = Document.url(doc_sans_self_link()) end - defp doc_with_self_link do - Document.parse! ExHal.client, ~s({"_links": { "self": {"href": "http://example.com"}}}) + Document.parse!(ExHal.client(), ~s({"_links": { "self": {"href": "http://example.com"}}})) end + defp doc_sans_self_link do - Document.parse! ExHal.client, ~s({"_links": { }}) + Document.parse!(ExHal.client(), ~s({"_links": { }})) end end test ".to_json_hash", %{client: client} do parsed_hal = %{ "name" => "My Name", - "_embedded" => %{ "test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, - "_links" => %{ "self" => %{"href" => "http://example.com/my-name"}, - "foo" => [ - %{"href" => "http://example.com/my-name"}, - %{"href" => "http://example.com/my-foo"}, - ]}} + "_embedded" => %{"test" => %{"_embedded" => %{}, "_links" => %{}, "name" => "Is Test"}}, + "_links" => %{ + "self" => %{"href" => "http://example.com/my-name"}, + "foo" => [ + %{"href" => "http://example.com/my-name"}, + %{"href" => "http://example.com/my-foo"} + ] + } + } exhal_doc = Document.from_parsed_hal(client, parsed_hal) assert ^parsed_hal = Document.to_json_hash(exhal_doc) @@ -54,9 +57,11 @@ defmodule ExHal.DocumentTest do test "parsing with null links", %{client: client} do parsed_hal = %{ "name" => "My Name", - "_links" => %{ "self" => %{"href" => "http://example.com/my-name"}, - "foo" => %{"href" => nil} - }} + "_links" => %{ + "self" => %{"href" => "http://example.com/my-name"}, + "foo" => %{"href" => nil} + } + } exhal_doc = Document.from_parsed_hal(client, parsed_hal) refute exhal_doc |> Document.has_link?("foo") @@ -65,7 +70,7 @@ defmodule ExHal.DocumentTest do defmodule DocWithProperties do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s({"one": 1}) + defp doc, do: Document.parse!(ExHal.client(), ~s({"one": 1})) test "properties can be fetched" do assert {:ok, 1} == Document.fetch(doc(), "one") @@ -110,20 +115,18 @@ defmodule ExHal.DocumentTest do assert String.contains?(Poison.encode!(doc()), ~s("one":)) assert doc() == Document.parse!(Poison.encode!(doc())) end - end - defmodule DocWithWithLinks do use ExUnit.Case, async: true @doc_str ~s({"_links": { "profile": {"href": "http://example.com"}}}) - defp doc, do: Document.parse! ExHal.client, @doc_str + defp doc, do: Document.parse!(ExHal.client(), @doc_str) test "links can be fetched" do - assert {:ok, [%ExHal.Link{href: "http://example.com", templated: false}] } = - Document.fetch(doc(), "profile") + assert {:ok, [%ExHal.Link{href: "http://example.com", templated: false}]} = + Document.fetch(doc(), "profile") end test "missing links cannot be fetched" do @@ -131,7 +134,8 @@ defmodule ExHal.DocumentTest do end test "get_links(doc(), present_rel)" do - assert [%ExHal.Link{href: "http://example.com", templated: false}] = Document.get_links(doc(), "profile") + assert [%ExHal.Link{href: "http://example.com", templated: false}] = + Document.get_links(doc(), "profile") end test "get_links(doc(), absent_rel)" do @@ -151,18 +155,22 @@ defmodule ExHal.DocumentTest do defmodule DocWithWithEmbeddedLinks do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s({"_embedded": { + defp doc, do: Document.parse!(ExHal.client(), ~s({"_embedded": { "profile": { "name": "Peter", "_links": { "self": { "href": "http://example.com"} - }}}}) + }}}})) test "embeddeds can be fetched" do - assert {:ok, [%ExHal.Link{target: %ExHal.Document{}, - href: "http://example.com", - templated: false}] } = - Document.fetch(doc(), "profile") + assert {:ok, + [ + %ExHal.Link{ + target: %ExHal.Document{}, + href: "http://example.com", + templated: false + } + ]} = Document.fetch(doc(), "profile") end test "missing links cannot be fetched" do @@ -178,21 +186,21 @@ defmodule ExHal.DocumentTest do defmodule DocWithWithRepeatedLinks do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s({"_links": { + defp doc, do: Document.parse!(ExHal.client(), ~s({"_links": { "item": [ {"href": "http://example.com/1"}, {"href": "http://example.com/2"} ] - }}) + }})) test "links can be fetched" do - assert {:ok, [_, _] } = Document.fetch(doc(), "item") + assert {:ok, [_, _]} = Document.fetch(doc(), "item") end end defmodule DocWithWithDuplicateLinksAndEmbedded do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s( + defp doc, do: Document.parse!(ExHal.client(), ~s( { "_links": { "item": [ {"href": "http://example.com/1"} @@ -207,53 +215,49 @@ defmodule ExHal.DocumentTest do } } } - ) + )) test "links can be fetched" do - assert 1 == Document.fetch(doc(), "item") |> elem(1) |> Enum.count() + assert 1 == Document.fetch(doc(), "item") |> elem(1) |> Enum.count() end end defmodule DocWithCuriedLinks do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s({"_links": { + defp doc, do: Document.parse!(ExHal.client(), ~s({"_links": { "app:foo": { "href": "http://example.com" }, "curies": [ { "name": "app", "href": "http://example.com/rels/{rel}", "templated": true } ] - } }) + } })) test "links can be fetched by decuried rels" do - assert {:ok, [%ExHal.Link{href: "http://example.com"}] } = - Document.fetch(doc(), "http://example.com/rels/foo") + assert {:ok, [%ExHal.Link{href: "http://example.com"}]} = + Document.fetch(doc(), "http://example.com/rels/foo") end test "links can be fetched by curied rels" do - assert {:ok, [%ExHal.Link{href: "http://example.com"}] } = - Document.fetch(doc(), "app:foo") + assert {:ok, [%ExHal.Link{href: "http://example.com"}]} = Document.fetch(doc(), "app:foo") end - end defmodule DocWithTemplatedLinks do use ExUnit.Case, async: true - defp doc, do: Document.parse! ExHal.client, ~s( + defp doc, do: Document.parse!(ExHal.client(), ~s( {"_links": { "search": { "href": "http://example.com/{?q}", "templated": true } - } } ) + } } )) test "templated links can be fetched" do - assert {:ok, [%ExHal.Link{href: "http://example.com/{?q}", templated: true}] } = - Document.fetch(doc(), "search") + assert {:ok, [%ExHal.Link{href: "http://example.com/{?q}", templated: true}]} = + Document.fetch(doc(), "search") end - end # Background - defp is_hal_doc?(actual) do + defp is_hal_doc?(actual) do %ExHal.Document{properties: _, links: _} = actual end - end diff --git a/test/exhal/interpreter_test.exs b/test/exhal/interpreter_test.exs index 251c855..f6056e2 100644 --- a/test/exhal/interpreter_test.exs +++ b/test/exhal/interpreter_test.exs @@ -12,7 +12,7 @@ defmodule ExHal.InterpreterTest do } """ - {:ok, doc: ExHal.Document.parse!(ExHal.client, hal)} + {:ok, doc: ExHal.Document.parse!(ExHal.client(), hal)} end test "can we make the most simple interpreter", %{doc: doc} do @@ -27,9 +27,9 @@ defmodule ExHal.InterpreterTest do defmodule MyOverreachingInterpreter do use ExHal.Interpreter - defextract :thing - defextract :thing2, from: "TheOtherThing" - defextract :thing3 + defextract(:thing) + defextract(:thing2, from: "TheOtherThing") + defextract(:thing3) end assert MyOverreachingInterpreter.to_params(doc) == %{thing: 1, thing2: 2} @@ -39,7 +39,7 @@ defmodule ExHal.InterpreterTest do defmodule MyLinkInterpreter do use ExHal.Interpreter - defextractlink :mylink, rel: "up" + defextractlink(:mylink, rel: "up") end assert MyLinkInterpreter.to_params(doc) == %{mylink: "http://example.com"} diff --git a/test/exhal/link_test.exs b/test/exhal/link_test.exs index 28b3c6b..370d531 100644 --- a/test/exhal/link_test.exs +++ b/test/exhal/link_test.exs @@ -6,57 +6,58 @@ defmodule ExHal.LinkTest do alias ExHal.Document, as: Document test ".from_links_entry w/ explicit href" do - link_entry = %{"href" => "http://example.com", - "templated" => false, - "name" => "test"} + link_entry = %{"href" => "http://example.com", "templated" => false, "name" => "test"} link = Link.from_links_entry("foo", link_entry) assert %Link{href: "http://example.com"} = link - assert %Link{templated: false} = link - assert %Link{name: "test"} = link - assert %Link{rel: "foo"} = link + assert %Link{templated: false} = link + assert %Link{name: "test"} = link + assert %Link{rel: "foo"} = link end test ".from_links_entry w/ templated href" do - link_entry = %{"href" => "http://example.com{?q}", - "templated" => true, - "name" => "test"} + link_entry = %{"href" => "http://example.com{?q}", "templated" => true, "name" => "test"} link = Link.from_links_entry("foo", link_entry) assert %Link{href: "http://example.com{?q}"} = link - assert %Link{templated: true} = link - assert %Link{name: "test"} = link - assert %Link{rel: "foo"} = link + assert %Link{templated: true} = link + assert %Link{name: "test"} = link + assert %Link{rel: "foo"} = link end test ".from_embedded w/o self link" do - embedded_doc = Document.from_parsed_hal(ExHal.client, %{ "name" => "foo" }) + embedded_doc = Document.from_parsed_hal(ExHal.client(), %{"name" => "foo"}) link = Link.from_embedded("foo", embedded_doc) - assert %Link{href: nil} = link - assert %Link{templated: false} = link - assert %Link{name: nil} = link - assert %Link{rel: "foo"} = link + assert %Link{href: nil} = link + assert %Link{templated: false} = link + assert %Link{name: nil} = link + assert %Link{rel: "foo"} = link end test ".from_embedded w/ self link" do - parsed_hal = %{ "name" => "foo", - "_links" => %{ - "self" => %{ "href" => "http://example.com" } - } - } - embedded_doc = Document.from_parsed_hal(ExHal.client, parsed_hal) + parsed_hal = %{ + "name" => "foo", + "_links" => %{ + "self" => %{"href" => "http://example.com"} + } + } + + embedded_doc = Document.from_parsed_hal(ExHal.client(), parsed_hal) link = Link.from_embedded("foo", embedded_doc) assert %Link{href: "http://example.com"} = link - assert %Link{templated: false} = link - assert %Link{name: nil} = link - assert %Link{rel: "foo"} = link + assert %Link{templated: false} = link + assert %Link{name: nil} = link + assert %Link{rel: "foo"} = link end test ".target_url w/ untemplated link w/ vars" do - assert {:ok, "http://example.com/"} = Link.target_url(normal_link(), - %{q: "hello"}) + assert {:ok, "http://example.com/"} = + Link.target_url( + normal_link(), + %{q: "hello"} + ) end test ".target_url w/ untemplated link w/o vars" do @@ -64,8 +65,11 @@ defmodule ExHal.LinkTest do end test ".target_url w/ templated link" do - assert {:ok, "http://example.com/?q=hello"} = Link.target_url(templated_link(), - %{q: "hello"}) + assert {:ok, "http://example.com/?q=hello"} = + Link.target_url( + templated_link(), + %{q: "hello"} + ) end test ".embedded?" do @@ -106,42 +110,42 @@ defmodule ExHal.LinkTest do end test "mixed links equal" do - assert Link.equal?(embedded_link("http://example.com/"), - normal_link("http://example.com/")) - assert Link.equal?(normal_link("http://example.com/"), - embedded_link("http://example.com/")) + assert Link.equal?( + embedded_link("http://example.com/"), + normal_link("http://example.com/") + ) + + assert Link.equal?( + normal_link("http://example.com/"), + embedded_link("http://example.com/") + ) end test "mixed links anon" do - refute Link.equal?(embedded_link(nil), - normal_link("http://example.com/")) - refute Link.equal?(normal_link("http://example.com/"), - embedded_link(nil)) + refute Link.equal?( + embedded_link(nil), + normal_link("http://example.com/") + ) + + refute Link.equal?( + normal_link("http://example.com/"), + embedded_link(nil) + ) end - end - def normal_link(url \\ "http://example.com/", name \\ "test") do - link_entry = %{"href" => url, - "templated" => false, - "name" => name} + link_entry = %{"href" => url, "templated" => false, "name" => name} Link.from_links_entry("foo", link_entry) end def templated_link(tmpl \\ "http://example.com/{?q}") do - link_entry = %{"href" => tmpl, - "templated" => true, - "name" => "test"} + link_entry = %{"href" => tmpl, "templated" => true, "name" => "test"} Link.from_links_entry("foo", link_entry) end def embedded_link(url \\ "http://example.com/embedded") do - parsed_hal = %{"name" => url, - "_links" => - %{ "self" => %{ "href" => url } - } - } + parsed_hal = %{"name" => url, "_links" => %{"self" => %{"href" => url}}} target_doc = Document.from_parsed_hal(parsed_hal) Link.from_embedded("foo", target_doc) diff --git a/test/exhal/navigation_test.exs b/test/exhal/navigation_test.exs index b20a3e9..2caa773 100644 --- a/test/exhal/navigation_test.exs +++ b/test/exhal/navigation_test.exs @@ -2,22 +2,25 @@ defmodule ExHal.NavigationTest do use ExUnit.Case, async: false import Mox - alias ExHal.{Navigation,Document,Error,ResponseHeader} + alias ExHal.{Navigation, Document, Error, ResponseHeader} describe ".follow_link" do test "regular link", %{doc: doc} do ExHal.ClientMock - |> expect(:get, fn _client,"http://example.com/", _headers -> - {:ok, Document.parse!(hal_str("http://example.com/thing")), %ResponseHeader{status_code: 200}} + |> expect(:get, fn _client, "http://example.com/", _headers -> + {:ok, Document.parse!(hal_str("http://example.com/thing")), + %ResponseHeader{status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.follow_link(doc, "single") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.follow_link(doc, "single") assert {:ok, "http://example.com/thing"} = ExHal.url(repr) end test "embedded link", %{doc: doc} do - assert {:ok, (repr = %Document{}), %ResponseHeader{}} = Navigation.follow_link(doc, "embedded") + assert {:ok, repr = %Document{}, %ResponseHeader{}} = + Navigation.follow_link(doc, "embedded") assert {:ok, "http://example.com/e"} = ExHal.url(repr) end @@ -32,7 +35,9 @@ defmodule ExHal.NavigationTest do {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.post(doc, "single", "post body") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.post(doc, "single", "post body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end end @@ -46,7 +51,9 @@ defmodule ExHal.NavigationTest do {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.put(doc, "single", "put body") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.put(doc, "single", "put body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end end @@ -60,9 +67,10 @@ defmodule ExHal.NavigationTest do {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} end) - assert {:ok, (repr = %Document{}), %ResponseHeader{status_code: 200}} = Navigation.patch(doc, "single", "patch body") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.patch(doc, "single", "patch body") + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) - end end @@ -70,7 +78,8 @@ defmodule ExHal.NavigationTest do assert {:ok, "http://example.com/"} = Navigation.link_target(doc, "single") assert {:ok, "http://example.com/e"} = Navigation.link_target(doc, "embedded") - assert {:ok, "http://example.com/?q=hello"} = Navigation.link_target(doc, "tmpl", tmpl_vars: %{q: "hello"}) + assert {:ok, "http://example.com/?q=hello"} = + Navigation.link_target(doc, "tmpl", tmpl_vars: %{q: "hello"}) assert {:ok, l} = Navigation.link_target(doc, "multiple") assert "http://example.com/1" == l or "http://example.com/2" == l @@ -88,26 +97,27 @@ defmodule ExHal.NavigationTest do defp doc do ExHal.Document.from_parsed_hal( - ExHal.client, - %{"_links" => - %{"single" => %{ "href" => "http://example.com/" }, - "tmpl" => %{ "href" => "http://example.com/{?q}", "templated" => true }, - "multiple" => [%{ "href" => "http://example.com/1" }, - %{ "href" => "http://example.com/2" }] - }, - "_embedded" => - %{"embedded" => %{"_links" => %{"self" => %{"href" => "http://example.com/e"}}}} - } + ExHal.client(), + %{ + "_links" => %{ + "single" => %{"href" => "http://example.com/"}, + "tmpl" => %{"href" => "http://example.com/{?q}", "templated" => true}, + "multiple" => [%{"href" => "http://example.com/1"}, %{"href" => "http://example.com/2"}] + }, + "_embedded" => %{ + "embedded" => %{"_links" => %{"self" => %{"href" => "http://example.com/e"}}} + } + } ) end def hal_str(url) do """ - { "name": "#{url}", - "_links": { - "self": { "href": "#{url}" } - } + { "name": "#{url}", + "_links": { + "self": { "href": "#{url}" } } - """ + } + """ end end diff --git a/test/exhal/non_hal_response_test.exs b/test/exhal/non_hal_response_test.exs index 7e3a4d7..64d59e5 100644 --- a/test/exhal/non_hal_response_test.exs +++ b/test/exhal/non_hal_response_test.exs @@ -4,13 +4,21 @@ defmodule ExHal.NonHalResponseTest do describe ".url/1" do test "with Location header" do - r = %NonHalResponse{status_code: 200, headers: [{"Location", "http://example.com"}], body: ""} + r = %NonHalResponse{ + status_code: 200, + headers: [{"Location", "http://example.com"}], + body: "" + } assert {:ok, "http://example.com"} == ExHal.Locatable.url(r) end test "with Content-Location header" do - r = %NonHalResponse{status_code: 200, headers: [{"Content-Location", "http://example.com"}], body: ""} + r = %NonHalResponse{ + status_code: 200, + headers: [{"Content-Location", "http://example.com"}], + body: "" + } assert {:ok, "http://example.com"} == ExHal.Locatable.url(r) end diff --git a/test/exhal/simple_authorizer_test.exs b/test/exhal/simple_authorizer_test.exs index 2b08a3f..157a3c4 100644 --- a/test/exhal/simple_authorizer_test.exs +++ b/test/exhal/simple_authorizer_test.exs @@ -8,16 +8,21 @@ defmodule ExHal.SimpleAuthorizerTest do describe ".authorization/2" do test "alien resource" do - assert :no_auth = Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") + assert :no_auth = + Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") end test "subtly alien resource" do - assert :no_auth = Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") + assert :no_auth = + Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") end test "recognized resource" do assert {:ok, "Bearer hello-beautiful"} = - Authorizer.authorization(simple_authorizer_factory("Bearer hello-beautiful"), "http://example.com/foo") + Authorizer.authorization( + simple_authorizer_factory("Bearer hello-beautiful"), + "http://example.com/foo" + ) end end diff --git a/test/exhal/transcoder_test.exs b/test/exhal/transcoder_test.exs index 2ed0d51..33d4c1e 100644 --- a/test/exhal/transcoder_test.exs +++ b/test/exhal/transcoder_test.exs @@ -22,7 +22,7 @@ defmodule ExHal.TranscoderTest do } """ - {:ok, doc: ExHal.Document.parse!(ExHal.client, hal)} + {:ok, doc: ExHal.Document.parse!(ExHal.client(), hal)} end test "can we make the most simple transcoder", %{doc: doc} do @@ -40,13 +40,14 @@ defmodule ExHal.TranscoderTest do def to_hal(val), do: val * -1 def from_hal(val), do: val * -1 end + defmodule MyOverreachingTranscoder do use ExHal.Transcoder - defproperty "thing" - defproperty "TheOtherThing", param: :thing2, value_converter: NegationConverter - defproperty "missingThing", param: :thing3 - defproperty "yer_mom", param: [:yer, :mom] + defproperty("thing") + defproperty("TheOtherThing", param: :thing2, value_converter: NegationConverter) + defproperty("missingThing", param: :thing3) + defproperty("yer_mom", param: [:yer, :mom]) end assert %{thing: 1, thing2: -2, yer: %{mom: true}} == MyOverreachingTranscoder.decode!(doc) @@ -62,13 +63,13 @@ defmodule ExHal.TranscoderTest do defmodule MySimpleTranscoder do use ExHal.Transcoder - defproperty "firstUse", param: :thing - defproperty "secondUse", param: :thing + defproperty("firstUse", param: :thing) + defproperty("secondUse", param: :thing) end encoded = MySimpleTranscoder.encode!(%{thing: "thing_value"}) - assert "thing_value" == ExHal.get_lazy(encoded, "firstUse", fn -> :missing end) + assert "thing_value" == ExHal.get_lazy(encoded, "firstUse", fn -> :missing end) assert "thing_value" == ExHal.get_lazy(encoded, "secondUse", fn -> :missing end) end @@ -83,7 +84,7 @@ defmodule ExHal.TranscoderTest do defmodule DynamicTranscoder do use ExHal.Transcoder - defproperty "thing", value_converter: DynamicConverter + defproperty("thing", value_converter: DynamicConverter) end encoded = DynamicTranscoder.encode!(%{thing: 2}, factor: 2) @@ -96,48 +97,57 @@ defmodule ExHal.TranscoderTest do defmodule MyLinkTranscoder do use ExHal.Transcoder - deflink "up", param: :mylink - deflink "none", param: :none - deflink "nolink", param: :nolink - deflink "nested", param: [:nested, :url] - deflink "fillin", param: :fillin, templated: true + deflink("up", param: :mylink) + deflink("none", param: :none) + deflink("nolink", param: :nolink) + deflink("nested", param: [:nested, :url]) + deflink("fillin", param: :fillin, templated: true) end - assert MyLinkTranscoder.decode!(doc) == %{mylink: "http://example.com/1", - nested: %{url: "http://example.com/2"}, - fillin: "http://example.com/3{?data}" - } + assert MyLinkTranscoder.decode!(doc) == %{ + mylink: "http://example.com/1", + nested: %{url: "http://example.com/2"}, + fillin: "http://example.com/3{?data}" + } - encoded = MyLinkTranscoder.encode!(%{mylink: "http://example.com/1", - nested: %{url: "http://example.com/2"}, - fillin: "http://example.com/3{?data}"}) + encoded = + MyLinkTranscoder.encode!(%{ + mylink: "http://example.com/1", + nested: %{url: "http://example.com/2"}, + fillin: "http://example.com/3{?data}" + }) assert {:ok, "http://example.com/1"} == ExHal.link_target(encoded, "up") assert {:ok, "http://example.com/2"} == ExHal.link_target(encoded, "nested") - assert {:ok, "http://example.com/3?data=INFO"} == ExHal.link_target(encoded, "fillin", tmpl_vars: [data: "INFO"]) + + assert {:ok, "http://example.com/3?data=INFO"} == + ExHal.link_target(encoded, "fillin", tmpl_vars: [data: "INFO"]) end test "don't inject a link that has a null href" do defmodule MyEmptyLinkTranscoder do use ExHal.Transcoder - deflink "present" - deflink "present_but_nil" - deflink "absent" + deflink("present") + deflink("present_but_nil") + deflink("absent") end - encoded = MyEmptyLinkTranscoder.encode!(%{present: "http://example.com/present", - present_but_nil: nil}) + encoded = + MyEmptyLinkTranscoder.encode!(%{present: "http://example.com/present", present_but_nil: nil}) + + assert {:error, %ExHal.Error{reason: "no such link: absent"}} == + ExHal.link_target(encoded, "absent") - assert {:error, %ExHal.Error{reason: "no such link: absent"}} == ExHal.link_target(encoded, "absent") - assert {:error, %ExHal.Error{reason: "no such link: present_but_nil"}} == ExHal.link_target(encoded, "present_but_nil") + assert {:error, %ExHal.Error{reason: "no such link: present_but_nil"}} == + ExHal.link_target(encoded, "present_but_nil") end test "don't try to extract links from document that has no links" do defmodule MyTinyTranscoder do use ExHal.Transcoder - deflink "up", param: :mylink + deflink("up", param: :mylink) end hal = """ @@ -146,7 +156,7 @@ defmodule ExHal.TranscoderTest do } """ - doc = ExHal.Document.parse!(ExHal.client, hal) + doc = ExHal.Document.parse!(ExHal.client(), hal) assert MyTinyTranscoder.decode!(doc) == %{} end @@ -154,7 +164,7 @@ defmodule ExHal.TranscoderTest do defmodule MyOtherMultiLinkTranscoder do use ExHal.Transcoder - deflinks "tag", param: :tag + deflinks("tag", param: :tag) end %{tag: tags} = MyOtherMultiLinkTranscoder.decode!(doc) @@ -168,7 +178,6 @@ defmodule ExHal.TranscoderTest do assert {:ok, ["urn:1", "http://2", "foo:1"]} == ExHal.link_targets(encoded, "tag") end - test "trying to extract links with value conversion", %{doc: doc} do defmodule MyLinkConverter do @behaviour ExHal.Transcoder.ValueConverter @@ -178,10 +187,12 @@ defmodule ExHal.TranscoderTest do end def from_hal(up_url) do - {id, _} = up_url - |> String.split("/") - |> List.last - |> Integer.parse + {id, _} = + up_url + |> String.split("/") + |> List.last() + |> Integer.parse() + id end end @@ -189,7 +200,7 @@ defmodule ExHal.TranscoderTest do defmodule MyLinkConversionTranscoder do use ExHal.Transcoder - deflink "up", param: :up_id, value_converter: MyLinkConverter + deflink("up", param: :up_id, value_converter: MyLinkConverter) end assert MyLinkConversionTranscoder.decode!(doc) == %{up_id: 1} @@ -201,18 +212,19 @@ defmodule ExHal.TranscoderTest do test "composable transcoders", %{doc: doc} do defmodule BaseTranscoder do use ExHal.Transcoder - defproperty "thing" - deflink "up", param: :up_url + defproperty("thing") + deflink("up", param: :up_url) end defmodule ExtTranscoder do use ExHal.Transcoder - defproperty "TheOtherThing", param: :thing2 - deflinks "tag", param: :tag + defproperty("TheOtherThing", param: :thing2) + deflinks("tag", param: :tag) end - decoded = BaseTranscoder.decode!(doc) - |> ExtTranscoder.decode!(doc) + decoded = + BaseTranscoder.decode!(doc) + |> ExtTranscoder.decode!(doc) assert %{thing: 1} = decoded assert %{thing2: 2} = decoded @@ -224,13 +236,16 @@ defmodule ExHal.TranscoderTest do assert Enum.member?(tags, "http://2") assert Enum.member?(tags, "urn:1") - params = %{thing: 1, - tag: ["urn:1", "http://2", "foo:1"], - thing2: 2, - up_url: "http://example.com/1"} + params = %{ + thing: 1, + tag: ["urn:1", "http://2", "foo:1"], + thing2: 2, + up_url: "http://example.com/1" + } - encoded = BaseTranscoder.encode!(params) - |> ExtTranscoder.encode!(params) + encoded = + BaseTranscoder.encode!(params) + |> ExtTranscoder.encode!(params) assert 1 == ExHal.get_lazy(encoded, "thing", fn -> :missing end) assert 2 == ExHal.get_lazy(encoded, "TheOtherThing", fn -> :missing end) @@ -243,16 +258,16 @@ defmodule ExHal.TranscoderTest do defmodule MartiniTranscoder do use ExHal.Transcoder - defproperty "baseIngredient", param: [:spirits, :liquor] + defproperty("baseIngredient", param: [:spirits, :liquor]) end hal = Poison.encode!(%{"baseIngredient" => "gin"}) - doc = ExHal.Document.parse!(ExHal.client, hal) + doc = ExHal.Document.parse!(ExHal.client(), hal) cocktail = MartiniTranscoder.decode!(doc) assert %{spirits: %{liquor: "gin"}} == cocktail json_patches = [ - %{"op" => "replace", "path" => "/baseIngredient", "value" => "vodka"}, + %{"op" => "replace", "path" => "/baseIngredient", "value" => "vodka"} ] bond_style = MartiniTranscoder.patch!(cocktail, json_patches) @@ -264,23 +279,23 @@ defmodule ExHal.TranscoderTest do defmodule ShakenIsNotStirredConverter do @behaviour ExHal.Transcoder.ValueConverter def from_hal(bool), do: invert_bool(bool) - def to_hal(bool), do: invert_bool(bool) + def to_hal(bool), do: invert_bool(bool) defp invert_bool(bool), do: !bool end defmodule MartiniTranscoder2 do use ExHal.Transcoder - defproperty "stirred", param: [:shaken], value_converter: ShakenIsNotStirredConverter + defproperty("stirred", param: [:shaken], value_converter: ShakenIsNotStirredConverter) end hal = Poison.encode!(%{stirred: true}) - doc = ExHal.Document.parse!(ExHal.client, hal) + doc = ExHal.Document.parse!(ExHal.client(), hal) cocktail = MartiniTranscoder2.decode!(doc) assert %{shaken: false} == cocktail json_patches = [ - %{"op" => "replace", "path" => "/stirred", "value" => false}, + %{"op" => "replace", "path" => "/stirred", "value" => false} ] bond_style = MartiniTranscoder2.patch!(cocktail, json_patches) @@ -292,13 +307,13 @@ defmodule ExHal.TranscoderTest do defmodule MartiniTranscoder3 do use ExHal.Transcoder - defproperty "serving_dish" + defproperty("serving_dish") end cocktail = %{serving_dish: "mug"} json_patches = [ - %{"op" => "replace", "path" => "/firearm", "value" => "Walther PPK"}, + %{"op" => "replace", "path" => "/firearm", "value" => "Walther PPK"} ] assert cocktail == MartiniTranscoder3.patch!(cocktail, json_patches) @@ -308,14 +323,14 @@ defmodule ExHal.TranscoderTest do defmodule MartiniTranscoder4 do use ExHal.Transcoder - defproperty "garnish_type" - defproperty "garnish_count" + defproperty("garnish_type") + defproperty("garnish_count") end cocktail = %{garnish_type: "radish", garnish_count: 1} json_patches = [ - %{"op" => "replace", "path" => "/garnish_type", "value" => "olive"}, + %{"op" => "replace", "path" => "/garnish_type", "value" => "olive"}, %{"op" => "replace", "path" => "/garnish_count", "value" => 2} ] @@ -328,14 +343,14 @@ defmodule ExHal.TranscoderTest do defmodule CompanyAutoTranscoder do use ExHal.Transcoder - defproperty "make", protected: true - defproperty "model" + defproperty("make", protected: true) + defproperty("model") end bonds_ride = %{make: "Aston Martin", model: "DB5"} json_patches = [ - %{"op" => "replace", "path" => "/make", "value" => "Mopeds-R-Us"}, + %{"op" => "replace", "path" => "/make", "value" => "Mopeds-R-Us"}, %{"op" => "replace", "path" => "/model", "value" => "Vanquish"} ] @@ -348,19 +363,31 @@ defmodule ExHal.TranscoderTest do defmodule DestinationTranscoder do use ExHal.Transcoder - deflink "missionLocation", param: :mission_locale - deflink "transportInfo", param: :transport_details + deflink("missionLocation", param: :mission_locale) + deflink("transportInfo", param: :transport_details) end links = %{"missionLocation" => %{href: "http://mi5.uk/l/moscow"}} hal = Poison.encode!(%{_links: links}) - doc = ExHal.Document.parse!(ExHal.client, hal) + doc = ExHal.Document.parse!(ExHal.client(), hal) mission = DestinationTranscoder.decode!(doc) json_patches = [ - %{"op" => "replace", "path" => "/_links/missionLocation", "value" => %{"href" => "http://mi5.uk/l/singapore"}}, - %{"op" => "replace", "path" => "/_links/transportInfo", "value" => %{"href" => "http://mi5.uk/travel/123456"}}, - %{"op" => "replace", "path" => "/_links/notALinkOrProp", "value" => %{"href" => "http://i_will_be_ignored"}}, + %{ + "op" => "replace", + "path" => "/_links/missionLocation", + "value" => %{"href" => "http://mi5.uk/l/singapore"} + }, + %{ + "op" => "replace", + "path" => "/_links/transportInfo", + "value" => %{"href" => "http://mi5.uk/travel/123456"} + }, + %{ + "op" => "replace", + "path" => "/_links/notALinkOrProp", + "value" => %{"href" => "http://i_will_be_ignored"} + } ] new_mission = DestinationTranscoder.patch!(mission, json_patches) @@ -372,22 +399,34 @@ defmodule ExHal.TranscoderTest do assert new_mission.transport_details == "http://mi5.uk/travel/123456" # an operation for on a non-declared path will be dropped - assert [] == new_mission |> Map.delete(:mission_locale) |> Map.delete(:transport_details) |> Map.keys + assert [] == + new_mission + |> Map.delete(:mission_locale) + |> Map.delete(:transport_details) + |> Map.keys() end test "add a link to a deflinks" do defmodule CoworkersTranscoder do use ExHal.Transcoder - deflinks "workChums", param: :coworkers + deflinks("workChums", param: :coworkers) end - hal = Poison.encode!(%{_links: %{"workChums" => [%{href: "http://mi5.uk/q"}, %{href: "http://mi5.uk/m"}]}}) - doc = ExHal.Document.parse!(ExHal.client, hal) + hal = + Poison.encode!(%{ + _links: %{"workChums" => [%{href: "http://mi5.uk/q"}, %{href: "http://mi5.uk/m"}]} + }) + + doc = ExHal.Document.parse!(ExHal.client(), hal) workplace = CoworkersTranscoder.decode!(doc) json_patches = [ - %{"op" => "add", "path" => "/_links/workChums/-", "value" => %{"href" => "http://mi5.uk/moneypenny"}} + %{ + "op" => "add", + "path" => "/_links/workChums/-", + "value" => %{"href" => "http://mi5.uk/moneypenny"} + } ] new_workplace = CoworkersTranscoder.patch!(workplace, json_patches) @@ -401,21 +440,26 @@ defmodule ExHal.TranscoderTest do defmodule CoworkersTranscoder2 do use ExHal.Transcoder - deflinks "workChums", param: :coworkers + deflinks("workChums", param: :coworkers) end - hal = Poison.encode!(%{_links: %{"workChums" => [%{href: "http://mi5.uk/q"}, %{href: "http://mi5.uk/m"}]}}) - doc = ExHal.Document.parse!(ExHal.client, hal) + hal = + Poison.encode!(%{ + _links: %{"workChums" => [%{href: "http://mi5.uk/q"}, %{href: "http://mi5.uk/m"}]} + }) + + doc = ExHal.Document.parse!(ExHal.client(), hal) workplace = CoworkersTranscoder2.decode!(doc) json_patches = [ - %{"op" => "replace", + %{ + "op" => "replace", "path" => "/_links/workChums", "value" => [ %{"href" => "http://spectre.org/u/jaws"}, %{"href" => "http://spectre.org/u/oddjob"} ] - } + } ] new_workplace = CoworkersTranscoder2.patch!(workplace, json_patches) From 4fa54c1f77b606ddbb2ad742ac70056569fe3788 Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Mon, 1 Oct 2018 16:51:52 -0600 Subject: [PATCH 5/6] improve Authorizer type docs --- lib/exhal/authorizer.ex | 13 ++++++++++--- lib/exhal/simple_authorizer.ex | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/exhal/authorizer.ex b/lib/exhal/authorizer.ex index 2e6dbdf..6f18292 100644 --- a/lib/exhal/authorizer.ex +++ b/lib/exhal/authorizer.ex @@ -1,12 +1,19 @@ defprotocol ExHal.Authorizer do - @type authorization_field_value :: String.t() + @typedoc """ + The value of the `Authorization` header field. + """ + @type credentials :: String.t() + + @typedoc """ + A URL. + """ @type url :: String.t() @doc """ - Returns `{:ok, authorization_header_field_value}` if the authorizer + Returns `{:ok, credentials}` if the authorizer knows the resource and has credentials for it. Otherwise, returns `:no_auth`. """ - @spec authorization(any, url()) :: {:ok, authorization_field_value()} | :no_auth + @spec authorization(any, url()) :: {:ok, credentials()} | :no_auth def authorization(authorizer, url) end diff --git a/lib/exhal/simple_authorizer.ex b/lib/exhal/simple_authorizer.ex index 155f9c4..a9043e4 100644 --- a/lib/exhal/simple_authorizer.ex +++ b/lib/exhal/simple_authorizer.ex @@ -8,7 +8,7 @@ defmodule ExHal.SimpleAuthorizer do defstruct([:authorization, :url_prefix]) - @spec new(Authorizer.url(), Authorizer.authorization_field_value()) :: __MODULE__.t() + @spec new(Authorizer.url(), Authorizer.credentials()) :: __MODULE__.t() def new(url_prefix, authorization_str), do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} end From 264a6603cdcb3bbe308490ec436b9e409272316c Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Tue, 2 Oct 2018 20:12:22 -0600 Subject: [PATCH 6/6] better authorization interface --- lib/exhal/authorizer.ex | 24 +++++- lib/exhal/client.ex | 9 +-- lib/exhal/null_authorizer.ex | 17 ++++- lib/exhal/simple_authorizer.ex | 35 ++++++--- mix.exs | 6 +- test/exhal/client_test.exs | 103 ++++++++++++++++---------- test/exhal/null_authorizer_test.exs | 2 +- test/exhal/simple_authorizer_test.exs | 10 +-- test/support/exhal/test_authorizer.ex | 7 ++ 9 files changed, 142 insertions(+), 71 deletions(-) create mode 100644 test/support/exhal/test_authorizer.ex diff --git a/lib/exhal/authorizer.ex b/lib/exhal/authorizer.ex index 6f18292..1848bcd 100644 --- a/lib/exhal/authorizer.ex +++ b/lib/exhal/authorizer.ex @@ -9,11 +9,27 @@ defprotocol ExHal.Authorizer do """ @type url :: String.t() + @typedoc """ + An object that implements the ExHal.Authorizer protocol. + """ + @type authorizer :: any() + + @typedoc """ + Name of a HTTP header field. + """ + @type header_field_name :: String.t + @doc """ - Returns `{:ok, credentials}` if the authorizer - knows the resource and has credentials for it. Otherwise, returns - `:no_auth`. + + Called before each request to calculate any header fields needed to + authorize the request. A common return would be + + %{"Authorization" => "Bearer "} + + If the URL is unrecognized or no header fields are appropriate or + needed this function should return and empty map. + """ - @spec authorization(any, url()) :: {:ok, credentials()} | :no_auth + @spec authorization(authorizer, url()) :: %{optional(header_field_name()) => String.t()} def authorization(authorizer, url) end diff --git a/lib/exhal/client.ex b/lib/exhal/client.ex index 5e3d5ae..14a6ab6 100644 --- a/lib/exhal/client.ex +++ b/lib/exhal/client.ex @@ -146,7 +146,7 @@ defmodule ExHal.Client do headers = client.headers |> merge_headers(normalize_headers(local_headers)) - |> merge_headers(auth_headers(client, url)) + |> merge_headers(Authorizer.authorization(client.authorizer, url)) poison_opts = merge_poison_opts(client.opts, local_opts) @@ -191,11 +191,4 @@ defmodule ExHal.Client do defp normalize_headers(headers) do Enum.into(headers, %{}, fn {k, v} -> {to_string(k), v} end) end - - defp auth_headers(client, url) do - case Authorizer.authorization(client.authorizer, url) do - :no_auth -> %{} - {:ok, auth} -> %{"Authorization" => auth} - end - end end diff --git a/lib/exhal/null_authorizer.ex b/lib/exhal/null_authorizer.ex index 3d4e527..582fa8b 100644 --- a/lib/exhal/null_authorizer.ex +++ b/lib/exhal/null_authorizer.ex @@ -1,4 +1,10 @@ defmodule ExHal.NullAuthorizer do + @moduledoc """ + + A placeholder authorizer that adds nothing to the request. + + """ + @typedoc """ An authorizer that always responds :no_auth """ @@ -6,9 +12,14 @@ defmodule ExHal.NullAuthorizer do defstruct([]) + @spec new() :: t() def new(), do: %__MODULE__{} -end -defimpl ExHal.Authorizer, for: ExHal.NullAuthorizer do - def authorization(_authorizer, _url), do: :no_auth + defimpl ExHal.Authorizer do + @spec authorization(Authorizer.t(), Authorizer.url()) :: %{optional(Authorizer.header_field_name()) => String.t()} + def authorization(_authorizer, _url), do: %{} + end + end + + diff --git a/lib/exhal/simple_authorizer.ex b/lib/exhal/simple_authorizer.ex index a9043e4..ee6dd32 100644 --- a/lib/exhal/simple_authorizer.ex +++ b/lib/exhal/simple_authorizer.ex @@ -1,4 +1,11 @@ defmodule ExHal.SimpleAuthorizer do + @moduledoc """ + + An authorizer that always sets the `Authorization` header + field to a fixed value. + + """ + alias ExHal.Authorizer @typedoc """ @@ -8,19 +15,27 @@ defmodule ExHal.SimpleAuthorizer do defstruct([:authorization, :url_prefix]) - @spec new(Authorizer.url(), Authorizer.credentials()) :: __MODULE__.t() + @spec new(Authorizer.url(), Authorizer.credentials()) :: t() + @doc """ + + Create a new #{__MODULE__}. + + """ def new(url_prefix, authorization_str), do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} -end -defimpl ExHal.Authorizer, for: ExHal.SimpleAuthorizer do - def authorization(authorizer, url) do - url - |> String.starts_with?(authorizer.url_prefix) - |> if do - {:ok, authorizer.authorization} - else - :no_auth + defimpl ExHal.Authorizer do + @spec authorization(Authorizer.t(), Authorizer.url()) :: %{optional(Authorizer.header_field_name()) => String.t()} + def authorization(authorizer, url) do + url + |> String.starts_with?(authorizer.url_prefix) + |> if do + %{"Authorization" => authorizer.authorization} + else + %{} + end end end + end + diff --git a/mix.exs b/mix.exs index bcfd47f..7b392f5 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,8 @@ defmodule ExHal.Mixfile do test_coverage: [tool: ExCoveralls], preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test], deps: deps(), - package: package() + package: package(), + elixirc_paths: elixirc_paths(Mix.env) ] end @@ -75,4 +76,7 @@ defmodule ExHal.Mixfile do defp override_dep({package, version}, deps_list) do Enum.reject(deps_list, fn dep -> elem(dep, 0) == package end) ++ [{package, version}] end + + defp elixirc_paths(:test), do: ["test/support", "lib"] + defp elixirc_paths(_), do: ["lib"] end diff --git a/test/exhal/client_test.exs b/test/exhal/client_test.exs index 7944874..02fd2ff 100644 --- a/test/exhal/client_test.exs +++ b/test/exhal/client_test.exs @@ -1,7 +1,11 @@ + defmodule ExHal.ClientTest do use ExUnit.Case, async: true - alias ExHal.{Client, SimpleAuthorizer} + alias ExHal.{Client, Document, NonHalResponse, ResponseHeader} + + import Mox + setup :verify_on_exit! describe ".new" do test ".new/0" do @@ -34,14 +38,14 @@ defmodule ExHal.ClientTest do describe ".set_authorizer/2" do test "first time" do - test_auther = SimpleAuthorizer.new("http://example.com", "Bearer sometoken") + test_auther = authorizer_factory() assert %Client{authorizer: test_auther} == Client.set_authorizer(Client.new(), test_auther) end test "last one in wins time" do - test_auther1 = SimpleAuthorizer.new("http://example.com", "Bearer sometoken") - test_auther2 = SimpleAuthorizer.new("http://myapp.com", "Bearer someothertoken") + test_auther1 = authorizer_factory() + test_auther2 = authorizer_factory() assert %Client{authorizer: test_auther2} == Client.new() @@ -50,26 +54,10 @@ defmodule ExHal.ClientTest do end end - # background - - defp to_have_header(client, expected_name, expected_value) do - {:ok, actual_value} = Map.fetch(client.headers, expected_name) - - actual_value == expected_value - end -end - -defmodule ExHal.ClientHttpRequestTest do - use ExUnit.Case, async: false - import Mox - - # Make sure mocks are verified when the test exits - setup :verify_on_exit! - - alias ExHal.{Client, Document, NonHalResponse, ResponseHeader, SimpleAuthorizer} - describe ".get/2" do - test "w/ normal link", %{client: client} do + test "w/ normal link" do + client = Client.new() + ExHal.HttpClientMock |> expect(:get, fn "http://example.com/", _headers, _opts -> {:ok, %HTTPoison.Response{body: hal_str("http://example.com/thing"), status_code: 200}} @@ -81,22 +69,38 @@ defmodule ExHal.ClientHttpRequestTest do assert {:ok, "http://example.com/thing"} = ExHal.url(repr) end - test "w/ auth" do + test "adds credentials to request if authorizer provides some" do + client = + Client.new() + |> Client.set_authorizer(authorizer_factory("Bearer mytoken")) + + ExHal.HttpClientMock + |> expect(:get, fn _url, %{"Authorization" => "Bearer mytoken"}, _opts -> + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} + end) + + Client.get(client, "http://example.com/thing") + end + + test "doesn't add credentials to request if authorizer provides none" do client = Client.new() - |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + |> Client.set_authorizer(authorizer_factory(%{})) ExHal.HttpClientMock - |> expect(:get, fn _url, %{"Authorization" => "Bearer sometoken"}, _opts -> + |> expect(:get, fn _url, headers, _opts -> + assert ! Map.has_key?(headers, "Authorization") {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) Client.get(client, "http://example.com/thing") end + end describe ".post" do - test "w/ normal link", %{client: client} do + test "w/ normal link" do + client = Client.new new_thing_hal = hal_str("http://example.com/new-thing") ExHal.HttpClientMock @@ -110,7 +114,9 @@ defmodule ExHal.ClientHttpRequestTest do assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end - test "w/ empty response", %{client: client} do + test "w/ empty response" do + client = Client.new() + ExHal.HttpClientMock |> expect(:post, fn "http://example.com/", _body, _headers, _opts -> {:ok, %HTTPoison.Response{body: "", status_code: 204}} @@ -123,10 +129,10 @@ defmodule ExHal.ClientHttpRequestTest do test "w/ auth" do client = Client.new() - |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + |> Client.set_authorizer(authorizer_factory("Bearer mytoken")) ExHal.HttpClientMock - |> expect(:post, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + |> expect(:post, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _opts -> {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) @@ -134,8 +140,11 @@ defmodule ExHal.ClientHttpRequestTest do end end + describe ".put" do - test "w/ normal link", %{client: client} do + test "w/ normal link" do + client = Client.new() + new_thing_hal = hal_str("http://example.com/new-thing") ExHal.HttpClientMock @@ -152,10 +161,10 @@ defmodule ExHal.ClientHttpRequestTest do test "w/ auth" do client = Client.new() - |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + |> Client.set_authorizer(authorizer_factory("Bearer mytoken")) ExHal.HttpClientMock - |> expect(:put, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + |> expect(:put, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _opts -> {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) @@ -164,7 +173,9 @@ defmodule ExHal.ClientHttpRequestTest do end describe ".patch" do - test "w/ normal link", %{client: client} do + test "w/ normal link" do + client = Client.new() + new_thing_hal = hal_str("http://example.com/new-thing") ExHal.HttpClientMock @@ -181,10 +192,10 @@ defmodule ExHal.ClientHttpRequestTest do test "w/ auth" do client = Client.new() - |> Client.set_authorizer(SimpleAuthorizer.new("http://example.com", "Bearer sometoken")) + |> Client.set_authorizer(authorizer_factory("Bearer mytoken")) ExHal.HttpClientMock - |> expect(:patch, fn _url, _body, %{"Authorization" => "Bearer sometoken"}, _opts -> + |> expect(:patch, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _opts -> {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} end) @@ -192,10 +203,23 @@ defmodule ExHal.ClientHttpRequestTest do end end - # Background - setup do - {:ok, client: %Client{}} + # background + + defp client_factory(), do: %Client{} + + defp authorizer_factory(headers \\ %{"Authorization" => "Bearer mytoken"}) + defp authorizer_factory(headers) when is_map(headers) do + %ExHal.TestAuthorizer{headers: headers} + end + defp authorizer_factory(credentials) when is_binary(credentials) do + %ExHal.TestAuthorizer{headers: %{"Authorization" => credentials}} + end + + defp to_have_header(client, expected_name, expected_value) do + {:ok, actual_value} = Map.fetch(client.headers, expected_name) + + actual_value == expected_value end defp hal_str(url) do @@ -207,4 +231,5 @@ defmodule ExHal.ClientHttpRequestTest do } """ end + end diff --git a/test/exhal/null_authorizer_test.exs b/test/exhal/null_authorizer_test.exs index a485469..f32a8b0 100644 --- a/test/exhal/null_authorizer_test.exs +++ b/test/exhal/null_authorizer_test.exs @@ -7,7 +7,7 @@ defmodule ExHal.NullAuthorizerTest do end test ".authorization/2" do - assert :no_auth = Authorizer.authorization(null_authorizer_factory(), "http://example.com") + assert %{} == Authorizer.authorization(null_authorizer_factory(), "http://example.com") end defp null_authorizer_factory() do diff --git a/test/exhal/simple_authorizer_test.exs b/test/exhal/simple_authorizer_test.exs index 157a3c4..a843eb0 100644 --- a/test/exhal/simple_authorizer_test.exs +++ b/test/exhal/simple_authorizer_test.exs @@ -8,17 +8,17 @@ defmodule ExHal.SimpleAuthorizerTest do describe ".authorization/2" do test "alien resource" do - assert :no_auth = - Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") + assert %{} == + Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") end test "subtly alien resource" do - assert :no_auth = - Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") + assert %{} == + Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") end test "recognized resource" do - assert {:ok, "Bearer hello-beautiful"} = + assert %{"Authorization" => "Bearer hello-beautiful"} == Authorizer.authorization( simple_authorizer_factory("Bearer hello-beautiful"), "http://example.com/foo" diff --git a/test/support/exhal/test_authorizer.ex b/test/support/exhal/test_authorizer.ex new file mode 100644 index 0000000..3f5810c --- /dev/null +++ b/test/support/exhal/test_authorizer.ex @@ -0,0 +1,7 @@ +defmodule ExHal.TestAuthorizer do + defstruct([:headers]) + + defimpl ExHal.Authorizer do + def authorization(auther, _url), do: auther.headers + end +end