Packages
ash_json_api
1.4.30
1.7.1
1.7.0
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0-rc.2
1.6.0-rc.1
1.6.0-rc.0
1.5.1
1.5.0
1.4.45
1.4.44
1.4.43
1.4.42
1.4.41
1.4.40
1.4.39
1.4.38
1.4.37
1.4.36
1.4.35
1.4.34
1.4.33
1.4.32
1.4.31
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.13
1.4.12
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.8
1.3.7
1.3.6
1.3.5
1.3.4
retired
1.3.3
1.3.2
1.3.1
1.3.0
1.2.2
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.34.2
0.34.1
0.34.0
0.33.1
0.33.0
0.32.1
0.32.0
0.31.3
0.31.2
retired
0.31.1
0.31.0
0.30.1
0.30.0-rc.4
0.30.0-rc.3
0.30.0-rc.2
0.30.0-rc.1
0.30.0-rc.0
0.29.1
0.29.0
0.28.6
0.28.5
0.28.4
0.28.3
0.28.2
0.28.1
0.28.0
0.27.6
0.27.5
0.27.1
0.27.0
0.25.0
0.24.4
0.24.2
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.1
0.10.0
0.9.0
0.8.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.4
0.2.3
0.2.1
0.2.0
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
The JSON:API extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
lib/ash_json_api/test/test.ex
defmodule AshJsonApi.Test do
@moduledoc """
Utilities for testing AshJsonApi.
## Making Requests
The request testing functions get/patch/post/delete all support the following options
- `:status`: Asserts that the response has the provided status after making the request
- `:router`: The corresponding JsonApiRouter to go through. Can be set statically in config, see below for more.
- `:actor`: Sets the provided actor as the actor for the request
- `:tenant`: Sets the provided tenant as the tenant for the request
A standard test would look like this:
```elixir
test "can list posts", %{current_user: current_user} do
Domain
# GET /posts
# assert resp.status == 200
|> get("/posts", status: 200, actor: current_user, router: MyAppWeb.JsonApiRouter)
# pattern match on the data key of the response
|> assert_data_matches([
%{
"attributes" => %{
"name" => "foo"
}
}
])
end
```
"""
import Plug.Test
import Plug.Conn
require ExUnit.Assertions
import ExUnit.Assertions
@doc """
Sends a GET request to the given path. See the module docs for more.
"""
def get(domain, path, opts \\ []) do
result =
:get
|> conn(path)
|> set_req_headers(opts)
|> set_context_opts(opts)
|> maybe_set_endpoint(opts)
|> set_accept_request_header(opts)
|> call_router(domain, opts)
assert result.state == :sent
unless opts[:skip_resp_header_check] do
if 200 <= result.status and result.status <= 300 do
assert_response_header_equals(result, "content-type", "application/vnd.api+json")
end
end
if opts[:status] do
resp_body =
try do
inspect(Jason.decode!(result.resp_body), pretty: true)
rescue
_ ->
inspect(result.resp_body)
end
assert result.status == opts[:status], """
Expected to get status #{opts[:status]} but got #{result.status}.
Response body: #{resp_body}
"""
end
if Keyword.get(opts, :decode?, true) do
%{result | resp_body: Jason.decode!(result.resp_body)}
else
result
end
end
@doc """
Sends a POST request to the given path. See the module docs for more.
"""
def post(domain, path, body, opts \\ []) do
result =
:post
|> conn(path, Jason.encode!(body))
|> set_req_headers(opts)
|> set_context_opts(opts)
|> set_content_type_request_header(opts)
|> set_accept_request_header(opts)
|> call_router(domain, opts)
assert result.state == :sent
unless opts[:skip_resp_header_check] do
if 200 <= result.status and result.status <= 300 do
assert_response_header_equals(result, "content-type", "application/vnd.api+json")
end
end
if opts[:status] do
resp_body =
try do
inspect(Jason.decode!(result.resp_body), pretty: true)
rescue
_ ->
inspect(result.resp_body)
end
assert result.status == opts[:status], """
Expected to get status #{opts[:status]} but got #{result.status}.
Response body: #{resp_body}
"""
end
if Keyword.get(opts, :decode?, true) do
resp_body = Jason.decode!(result.resp_body)
# JsonXema.validate!(@schema, resp_body)
%{result | resp_body: resp_body}
else
result
end
end
if Code.ensure_loaded?(Multipart) do
@doc """
Sends a multipart POST request to the given path. See the module docs for more.
"""
def multipart_post(domain, path, body, opts \\ []) do
parser_opts =
Plug.Parsers.init(parsers: [AshJsonApi.Plug.Parser], pass: ["*/*"], json_decoder: Jason)
result =
:post
|> conn(path, Multipart.body_binary(body))
|> set_req_headers(opts)
|> set_context_opts(opts)
|> put_req_header(
"content-type",
Multipart.content_type(body, "multipart/x.ash+form-data")
)
|> set_accept_request_header(opts)
|> Plug.Parsers.call(parser_opts)
|> call_router(domain, opts)
assert result.state == :sent
unless opts[:skip_resp_header_check] do
if 200 <= result.status and result.status <= 300 do
assert_response_header_equals(result, "content-type", "application/vnd.api+json")
end
end
if opts[:status] do
resp_body =
try do
inspect(Jason.decode!(result.resp_body), pretty: true)
rescue
_ ->
inspect(result.resp_body)
end
assert result.status == opts[:status], """
Expected to get status #{opts[:status]} but got #{result.status}.
Response body: #{resp_body}
"""
end
if Keyword.get(opts, :decode?, true) do
resp_body = Jason.decode!(result.resp_body)
# JsonXema.validate!(@schema, resp_body)
%{result | resp_body: resp_body}
else
result
end
end
else
def multipart_post(_domain, _path, _body, _opts \\ []) do
raise """
Must add `:multipart` to your dependencies to test multipart posts.
`{:multipart, "~> 0.4.0", only: [:dev, :test]}`
Then run `mix deps.compile ash_json_api --force`
"""
end
end
@doc """
Sends a PATCH request to the given path. See the module docs for more.
"""
def patch(domain, path, body, opts \\ []) do
result =
:patch
|> conn(path, Jason.encode!(body))
|> set_req_headers(opts)
|> set_context_opts(opts)
|> set_content_type_request_header(opts)
|> set_accept_request_header(opts)
|> call_router(domain, opts)
assert result.state == :sent
unless opts[:skip_resp_header_check] do
if 200 <= result.status and result.status <= 300 do
assert_response_header_equals(result, "content-type", "application/vnd.api+json")
end
end
if opts[:status] do
resp_body =
try do
inspect(Jason.decode!(result.resp_body), pretty: true)
rescue
_ ->
inspect(result.resp_body)
end
assert result.status == opts[:status], """
Expected to get status #{opts[:status]} but got #{result.status}.
Response body: #{resp_body}
"""
end
if Keyword.get(opts, :decode?, true) do
resp_body = Jason.decode!(result.resp_body)
# JsonXema.validate!(@schema, resp_body)
%{result | resp_body: resp_body}
else
result
end
end
@doc """
Sends a DELETE request to the given path. See the module docs for more.
"""
def delete(domain, path, opts \\ []) do
result =
:delete
|> conn(path)
|> set_req_headers(opts)
|> set_context_opts(opts)
|> set_accept_request_header(opts)
|> call_router(domain, opts)
assert result.state == :sent
unless opts[:skip_resp_header_check] do
if 200 <= result.status and result.status <= 300 do
assert_response_header_equals(result, "content-type", "application/vnd.api+json")
end
end
if opts[:status] do
resp_body =
try do
inspect(Jason.decode!(result.resp_body), pretty: true)
rescue
_ ->
inspect(result.resp_body)
end
assert result.status == opts[:status], """
Expected to get status #{opts[:status]} but got #{result.status}.
Response body: #{resp_body}
"""
end
if Keyword.get(opts, :decode?, true) do
%{result | resp_body: Jason.decode!(result.resp_body)}
else
result
end
end
@doc """
Assert that the response body's `"data"` equals an exact value
"""
defmacro assert_data_equals(conn, expected_data) do
quote do
conn = unquote(conn)
assert %{"data" => data} = conn.resp_body
assert data == unquote(expected_data)
conn
end
end
@doc """
Assert that the response body's `"data"` matches a pattern
"""
defmacro assert_data_matches(conn, data_pattern) do
quote do
conn = unquote(conn)
assert %{"data" => unquote(data_pattern)} = conn.resp_body
conn
end
end
@doc false
defmacro assert_meta_equals(conn, expected_meta) do
quote bind_quoted: [conn: conn, expected_meta: expected_meta] do
assert %{"meta" => ^expected_meta} = conn.resp_body
conn
end
end
@doc false
def assert_response_header_equals(conn, header, value) do
assert get_resp_header(conn, header) == [value]
conn
end
@doc false
defmacro assert_attribute_equals(conn, attribute, expected_value) do
quote bind_quoted: [attribute: attribute, expected_value: expected_value, conn: conn] do
assert %{"data" => %{"attributes" => %{^attribute => ^expected_value}}} = conn.resp_body
conn
end
end
@doc false
defmacro assert_id_equals(conn, expected_value) do
quote bind_quoted: [expected_value: expected_value, conn: conn] do
assert %{"data" => %{"id" => ^expected_value}} = conn.resp_body
conn
end
end
@doc false
defmacro assert_valid_resource_object(conn, expected_type, expected_id) do
quote bind_quoted: [conn: conn, expected_type: expected_type, expected_id: expected_id] do
assert %{
"data" => %{
"type" => ^expected_type,
"id" => ^expected_id
}
} = conn.resp_body
conn
end
end
@doc false
defmacro assert_valid_resource_objects(conn, expected_type, expected_ids) do
quote bind_quoted: [conn: conn, expected_type: expected_type, expected_ids: expected_ids] do
assert %{
"data" => results
} = conn.resp_body
assert Enum.all?(results, fn
%{"type" => ^expected_type, "id" => maybe_known_id} ->
Enum.member?(expected_ids, maybe_known_id)
_ ->
false
end)
conn
end
end
@doc false
defmacro assert_invalid_resource_objects(conn, expected_type, expected_ids) do
quote bind_quoted: [conn: conn, expected_type: expected_type, expected_ids: expected_ids] do
assert %{
"data" => results
} = conn.resp_body
assert not Enum.any?(results, fn
%{"type" => ^expected_type, "id" => maybe_known_id} ->
Enum.member?(expected_ids, maybe_known_id)
_ ->
false
end)
conn
end
end
@doc false
defmacro assert_attribute_missing(conn, attribute) do
quote bind_quoted: [conn: conn, attribute: attribute] do
assert %{"data" => %{"attributes" => attributes}} = conn.resp_body
refute Map.has_key?(attributes, attribute)
conn
end
end
@doc """
Asserts that an error is in the response where each key present in the provided map
has the same value in the error.
## Example
```elixir
Domain
|> delete("/posts/1", status: 404)
|> assert_has_error(%{
"code" => "not_found",
"detail" => "No post record found with `id: 1`",
"title" => "Entity Not Found"
})
```
"""
defmacro assert_has_error(conn, fields) do
quote do
assert %{"errors" => [_ | _] = errors} = unquote(conn).resp_body
assert Enum.any?(errors, fn error ->
Enum.all?(unquote(fields), fn {key, val} ->
Map.get(error, key) == val
end)
end)
unquote(conn)
end
end
@doc """
Assert that the given function returns true for at least one included record
## Example
```elixir
Domain
|> get("/posts/\#{post.id}/?include=author", status: 200)
|> assert_has_matching_include(fn
%{"type" => "author", "id" => ^author_id} ->
true
_ ->
false
end)
```
"""
defmacro assert_has_matching_include(conn, function) do
quote do
assert %{"included" => included} = unquote(conn).resp_body
assert is_list(included)
assert Enum.any?(included, fn included ->
unquote(function).(included)
end)
unquote(conn)
end
end
@doc """
Refute that the given function returns true for at least one included record
## Example
```elixir
Domain
|> get("/posts/\#{post.id}/?include=author", status: 200)
|> refute_has_matching_include(fn
%{"type" => "author", "id" => ^author_id} ->
true
_ ->
false
end)
```
"""
defmacro refute_has_matching_include(conn, function) do
quote do
with %{"included" => included} when is_list(included) <- unquote(conn).resp_body do
refute Enum.any?(included, fn included ->
unquote(function).(included)
end)
end
unquote(conn)
end
end
@doc false
defmacro assert_equal_links(conn, expected_links) do
quote bind_quoted: [expected_links: expected_links, conn: conn] do
%{"links" => resp_links} = conn.resp_body
assert Enum.sort(Map.keys(resp_links)) == Enum.sort(Map.keys(expected_links))
for {key, value} <- expected_links do
assert AshJsonApi.Test.uri_with_query(value) ==
AshJsonApi.Test.uri_with_query(resp_links[key]),
"expected #{key} link to be #{resp_links[key]}, got: #{value}"
end
conn
end
end
@doc false
def uri_with_query(nil), do: nil
def uri_with_query(value) do
value
|> URI.parse()
|> Map.update!(:query, &URI.decode_query(&1 || %{}))
end
defp set_content_type_request_header(conn, opts) do
cond do
opts[:exclude_req_content_type_header] ->
conn
opts[:req_content_type_header] ->
conn
|> put_req_header("content-type", opts[:req_content_type_header])
true ->
conn
|> put_req_header("content-type", "application/vnd.api+json")
end
end
defp set_accept_request_header(conn, opts) do
cond do
opts[:exclude_req_accept_header] ->
conn
opts[:req_accept_header] ->
conn
|> put_req_header("accept", opts[:req_accept_header])
true ->
conn
|> put_req_header("accept", "application/vnd.api+json")
end
end
defp maybe_set_endpoint(conn, opts) do
if endpoint = opts[:phoenix_endpoint] do
put_private(conn, :phoenix_endpoint, endpoint)
else
conn
end
end
defp set_context_opts(conn, opts) do
conn
|> Ash.PlugHelpers.set_actor(opts[:actor])
|> Ash.PlugHelpers.set_tenant(opts[:tenant])
end
defp set_req_headers(conn, opts) do
opts[:headers]
|> Kernel.||([])
|> Enum.reduce(conn, fn {header, value}, conn ->
Plug.Conn.put_req_header(conn, to_string(header), to_string(value))
end)
end
defp call_router(conn, domain, opts) do
router = opts[:router] || AshJsonApi.Domain.Info.router(domain)
router.call(conn, router.init([]))
end
end