Current section

Files

Jump to
oaskit lib oaskit test.ex
Raw

lib/oaskit/test.ex

defmodule Oaskit.Test do
alias Oaskit.Validation.ResponseData
alias Oaskit.Validation.ResponseValidator
alias Plug.Conn
@moduledoc """
Provides the `valid_response/3` test helper to validate API responses in your
ExUnit tests.
"""
@doc ~S"""
Validates that the given conn bears a response that is valid with regard to
the OpenAPI operation that served it, and returns the response body.
Responses returned with a JSON based content-type like `"application/json"` or
`"application/vnd.api+json"` will be decoded.
It is encouraged to wrap this function with a custom helper, typically in your
`MyAppWeb.ConnCase` test helper:
defmodule MyAppWeb.ConnCase do
# ...
# You can wrap the helper function this way so you do not have to pass
# The spec module in every call:
def valid_response(conn, status) do
Oaskit.Test.valid_response(MyAppWeb.OpenAPISpec, conn, status)
end
end
Then use the helper in your tests:
test "should return the user info", %{conn: conn} do
%{id: id} = user_fixture(username: "joe", roles: ["admin"])
conn = get(conn, ~p"/api/users/#{id}")
assert %{
"username" => "joe",
"roles" => ["admin"]
} = valid_response(conn, 200)
end
"""
def valid_response(spec_module, %Plug.Conn{} = conn, status) when is_integer(status) do
body = phoenix_response(conn, status)
operation_id = fetch_operation_id(conn)
content_type = ResponseValidator.content_type(conn)
body = maybe_parse_body(body, ResponseValidator.parse_content_type(content_type))
resp_data = %ResponseData{resp_body: body, resp_headers: conn.resp_headers, status: status}
case ResponseValidator.validate_response(resp_data, spec_module, operation_id) do
{:ok, _} ->
# We do not return the cast body because in test we validate the
# behaviour of the app from an external point of view.
body
{:error, jsv_error} ->
raise "invalid response returned by operation #{inspect(operation_id)} " <>
"with status #{inspect(status)} and content-type #{inspect(content_type)}" <>
"""
#{Exception.message(jsv_error)}
Response data:
#{inspect(body, pretty: true)}
"""
end
end
# Copy from phoenix to not depend on phoenix
def phoenix_response(%Conn{state: :unset}, _status) do
raise """
expected connection to have a response but no response was set/sent.
Please verify that you assign to "conn" after a request:
conn = get(conn, "/")
assert %{"data" => "foo"} = valid_response(Spec, conn, 200)
"""
end
def phoenix_response(%Conn{status: status, resp_body: body}, given) do
given = Conn.Status.code(given)
if given == status do
body
else
body_debug = format_error_body(body)
raise "expected response with status #{given}, got: #{status}, with body:\n#{body_debug}"
end
end
defp format_error_body(body) do
if is_binary(body) do
body
else
inspect(body)
end
end
defp fetch_operation_id(conn) do
case get_in(conn.private, [:oaskit, :operation_id]) do
nil ->
raise """
the connection was not validated by Oaskit.Plugs.ValidateRequest
This may happen if no pipeline with the Oaskit plugs is defined for
the route, if the operation is not declared above the controller function
or is explictitly disabled with `operation :my_function, false`.
"""
opid ->
opid
end
end
defp maybe_parse_body(body, {_, subtype}) do
# For now we only know how to parse JSON
cond do
subtype == "json" -> json_decode!(body)
String.ends_with?(subtype, "+json") -> json_decode!(body)
:otherwise -> body
end
end
defp json_decode!(data) do
data = IO.iodata_to_binary(data)
case data do
"" -> ""
payload -> JSV.Codec.decode!(payload)
end
end
end