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 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/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/authorizer.ex b/lib/exhal/authorizer.ex new file mode 100644 index 0000000..1848bcd --- /dev/null +++ b/lib/exhal/authorizer.ex @@ -0,0 +1,35 @@ +defprotocol ExHal.Authorizer do + @typedoc """ + The value of the `Authorization` header field. + """ + @type credentials :: String.t() + + @typedoc """ + A URL. + """ + @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 """ + + 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(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 8e16878..14a6ab6 100644 --- a/lib/exhal/client.ex +++ b/lib/exhal/client.ex @@ -5,57 +5,91 @@ 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, 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 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: 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]} - @spec new(Keyword.t()) :: __MODULE__.t() - def new(headers) do - new(headers, follow_redirect: true) + # deprecated call patterns + def new(headers) when is_list(headers) do + %__MODULE__{headers: normalize_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) + updated_headers = merge_headers(client.headers, normalize_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) @@ -64,60 +98,63 @@ 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) + {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() + @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() + @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() + @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(Authorizer.authorization(client.authorizer, 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] @@ -150,4 +187,8 @@ 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 end 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 new file mode 100644 index 0000000..ca520ee --- /dev/null +++ b/lib/exhal/http_client.ex @@ -0,0 +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 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/null_authorizer.ex b/lib/exhal/null_authorizer.ex new file mode 100644 index 0000000..582fa8b --- /dev/null +++ b/lib/exhal/null_authorizer.ex @@ -0,0 +1,25 @@ +defmodule ExHal.NullAuthorizer do + @moduledoc """ + + A placeholder authorizer that adds nothing to the request. + + """ + + @typedoc """ + An authorizer that always responds :no_auth + """ + @opaque t :: %__MODULE__{} + + defstruct([]) + + @spec new() :: t() + def new(), do: %__MODULE__{} + + 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 new file mode 100644 index 0000000..ee6dd32 --- /dev/null +++ b/lib/exhal/simple_authorizer.ex @@ -0,0 +1,41 @@ +defmodule ExHal.SimpleAuthorizer do + @moduledoc """ + + An authorizer that always sets the `Authorization` header + field to a fixed value. + + """ + + 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.credentials()) :: t() + @doc """ + + Create a new #{__MODULE__}. + + """ + def new(url_prefix, authorization_str), + do: %__MODULE__{authorization: authorization_str, url_prefix: url_prefix} + + 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/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 d49555a..02fd2ff 100644 --- a/test/exhal/client_test.exs +++ b/test/exhal/client_test.exs @@ -1,107 +1,235 @@ -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, Document, NonHalResponse, ResponseHeader} - 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 + import Mox + setup :verify_on_exit! - # background + describe ".new" do + test ".new/0" do + assert %Client{} = Client.new() + end - 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) + test "(empty_headers)" do + assert %Client{} = Client.new([]) + end - actual_value == expected_value + 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 -end -defmodule ExHal.ClientHttpRequestTest do - use ExUnit.Case, async: false - use RequestStubbing + 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 - alias ExHal.{Client, Document, NonHalResponse, ResponseHeader} + describe ".set_authorizer/2" do + test "first time" do + test_auther = authorizer_factory() - test ".get w/ normal link", %{client: client} do - thing_hal = hal_str("http://example.com/thing") + assert %Client{authorizer: test_auther} == Client.set_authorizer(Client.new(), test_auther) + end - 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/") + test "last one in wins time" do + test_auther1 = authorizer_factory() + test_auther2 = authorizer_factory() - assert {:ok, "http://example.com/thing"} = ExHal.url(target) + assert %Client{authorizer: test_auther2} == + Client.new() + |> Client.set_authorizer(test_auther1) + |> Client.set_authorizer(test_auther2) end end - test ".post w/ normal link", %{client: client} do - new_thing_hal = hal_str("http://example.com/new-thing") + describe ".get/2" do + test "w/ normal link" do + client = Client.new() - 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 "http://example.com/", _headers, _opts -> + {:ok, %HTTPoison.Response{body: hal_str("http://example.com/thing"), status_code: 200}} + end) - assert {:ok, "http://example.com/new-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 + + 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(authorizer_factory(%{})) + + ExHal.HttpClientMock + |> 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 - 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}} = - Client.post(client, "http://example.com/", "post body") + describe ".post" do + test "w/ normal link" do + client = Client.new + 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" do + client = Client.new() + + 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(authorizer_factory("Bearer mytoken")) + + ExHal.HttpClientMock + |> expect(:post, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _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") - 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") + describe ".put" do + test "w/ normal link" do + client = Client.new() + + new_thing_hal = hal_str("http://example.com/new-thing") + + 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(authorizer_factory("Bearer mytoken")) + + ExHal.HttpClientMock + |> expect(:put, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _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" do + client = Client.new() + + new_thing_hal = hal_str("http://example.com/new-thing") + + ExHal.HttpClientMock + |> expect(:patch, fn "http://example.com/", new_thing_hal, _headers, _opts -> + {:ok, %HTTPoison.Response{body: new_thing_hal, status_code: 200}} + end) - 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") + 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(target) + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) + end + + test "w/ auth" do + client = + Client.new() + |> Client.set_authorizer(authorizer_factory("Bearer mytoken")) + + ExHal.HttpClientMock + |> expect(:patch, fn _url, _body, %{"Authorization" => "Bearer mytoken"}, _opts -> + {:ok, %HTTPoison.Response{body: "{}", status_code: 200}} + end) + + Client.patch(client, "http://example.com/thing", "patch body") end end - # Background + # 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) - setup do - {:ok, client: %Client{}} + actual_value == expected_value end - def hal_str(url) 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 b1e293e..8ccb38a 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 @@ -11,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]) @@ -21,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 @@ -55,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", %{ @@ -67,13 +69,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 @@ -84,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 @@ -102,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 @@ -125,25 +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"} - } - }) + 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 @@ -153,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 4fab6bc..2caa773 100644 --- a/test/exhal/navigation_test.exs +++ b/test/exhal/navigation_test.exs @@ -1,63 +1,76 @@ -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} + 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") + + ExHal.ClientMock + |> expect(:post, fn _client, "http://example.com/", "post body", _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - 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") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.post(doc, "single", "post body") - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + 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") + + ExHal.ClientMock + |> expect(:patch, fn _client, "http://example.com/", "patch body", _headers -> + {:ok, Document.parse!(new_thing_hal), %ResponseHeader{status_code: 200}} + end) - 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") + assert {:ok, repr = %Document{}, %ResponseHeader{status_code: 200}} = + Navigation.patch(doc, "single", "patch body") - assert {:ok, "http://example.com/new-thing"} = ExHal.url(target) + assert {:ok, "http://example.com/new-thing"} = ExHal.url(repr) end end @@ -65,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 @@ -83,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/null_authorizer_test.exs b/test/exhal/null_authorizer_test.exs new file mode 100644 index 0000000..f32a8b0 --- /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 %{} == 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..a843eb0 --- /dev/null +++ b/test/exhal/simple_authorizer_test.exs @@ -0,0 +1,32 @@ +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 %{} == + Authorizer.authorization(simple_authorizer_factory(), "http://malware.com") + end + + test "subtly alien resource" do + assert %{} == + Authorizer.authorization(simple_authorizer_factory(), "http://mallory.example.com") + end + + test "recognized resource" do + assert %{"Authorization" => "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 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) 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/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 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)