Packages
open_api_spex
3.22.1
3.22.3
3.22.2
3.22.1
3.22.0
3.21.5
3.21.4
3.21.3
3.21.2
3.21.1
3.21.0
3.20.1
3.20.0
3.19.1
3.19.0
3.18.3
3.18.2
3.18.1
3.18.0
3.17.3
3.17.2
3.17.1
3.17.0
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.0
3.12.0
3.11.0
3.10.0
3.9.0
3.8.0
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
3.4.0
3.3.0
3.2.1
3.2.0
3.1.0
3.0.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
Current section
Files
Jump to
Current section
Files
lib/open_api_spex/test/test_assertions.ex
defmodule OpenApiSpex.TestAssertions do
@moduledoc """
Defines helpers for testing API responses and examples against API spec schemas.
"""
import ExUnit.Assertions
alias OpenApiSpex.Reference
alias OpenApiSpex.Cast.{Error, Utils}
alias OpenApiSpex.{Cast, Components, OpenApi, Operation, Schema}
alias OpenApiSpex.Plug.PutApiSpec
@dialyzer {:no_match, assert_schema: 3}
@json_content_regex ~r/^application\/.*json.*$/
@doc """
Asserts that `value` conforms to the schema with title `schema_title` in `api_spec`.
"""
@spec assert_schema(term, String.t(), OpenApi.t(), Cast.read_write_scope()) :: term | no_return
def assert_schema(value, schema_title, api_spec = %OpenApi{}, read_write_scope \\ nil) do
schemas = api_spec.components.schemas
schema = schemas[schema_title]
if !schema do
flunk("Schema: #{schema_title} not found in #{inspect(Map.keys(schemas))}")
end
cast_context = %Cast{
value: value,
schema: schema,
schemas: api_spec.components.schemas,
read_write_scope: read_write_scope
}
assert_schema(cast_context)
end
@doc """
Asserts that `value` conforms to the schema or reference definition.
"""
@spec assert_raw_schema(term, Schema.t() | Reference.t(), OpenApi.t() | %{}) :: term | no_return
def assert_raw_schema(value, schema, spec \\ %{})
def assert_raw_schema(value, schema = %Schema{}, spec) do
schemas = get_or_default_schemas(spec)
cast_context = %Cast{
value: value,
schema: schema,
schemas: schemas
}
assert_schema(cast_context)
end
def assert_raw_schema(value, schema = %Reference{}, spec) do
schemas = get_or_default_schemas(spec)
resolved_schema = OpenApiSpex.resolve_schema(schema, schemas)
if is_nil(resolved_schema) do
flunk("Schema: #{inspect(schema)} not found in #{inspect(spec)}")
end
cast_context = %Cast{
value: value,
schema: resolved_schema,
schemas: schemas
}
assert_schema(cast_context)
end
@spec get_or_default_schemas(OpenApi.t() | %{}) :: Components.schemas_map() | %{}
defp get_or_default_schemas(api_spec = %OpenApi{}), do: api_spec.components.schemas || %{}
defp get_or_default_schemas(input), do: input
@doc """
Asserts that `value` conforms to the schema in the given `%Cast{}` context.
"""
@spec assert_schema(Cast.t()) :: term
def assert_schema(cast_context) do
case Cast.cast(cast_context) do
{:ok, data} ->
data
{:error, errors} ->
schema = cast_context.schema
errors =
errors
|> Enum.map(fn error ->
message = Error.message(error)
path = Error.path_to_string(error)
"#{message} at #{path}"
end)
if Enum.any?(errors) do
errors_info = Enum.join(errors, "\n")
debug_info = "#{schema.title}: #{errors_info}\n#{inspect(cast_context.value)}"
flunk("Value does not conform to schema #{debug_info}")
end
end
end
@doc """
Asserts that `value` is a valid **response** schema with title `schema_title` in `api_spec`. In this case,
the presence of required `writeOnly` fields is not validated.
"""
@spec assert_response_schema(term, String.t(), OpenApi.t()) :: term | no_return
def assert_response_schema(value, schema_title, api_spec = %OpenApi{}) do
assert_schema(value, schema_title, api_spec, :read)
end
@doc """
Asserts that `value` is a valid **request** schema with title `schema_title` in `api_spec`. In this case,
the presence of required `readOnly` fields is not validated.
"""
@spec assert_request_schema(term, String.t(), OpenApi.t()) :: term | no_return
def assert_request_schema(value, schema_title, api_spec = %OpenApi{}) do
assert_schema(value, schema_title, api_spec, :write)
end
@doc """
Asserts that the response body conforms to the response schema for the operation with id `operation_id`.
"""
@spec assert_operation_response(Plug.Conn.t(), String.t() | nil) :: Plug.Conn.t()
def assert_operation_response(conn, operation_id \\ nil)
# No need to check for a schema if the response is empty
def assert_operation_response(conn, _operation_id) when conn.status == 204, do: conn
def assert_operation_response(conn, operation_id) do
{spec, operation_lookup} = PutApiSpec.get_spec_and_operation_lookup(conn)
operation_id = operation_id || conn.private.open_api_spex.operation_id
case operation_lookup[operation_id] do
nil ->
flunk(
"Failed to resolve a response schema for operation_id: #{operation_id} for status code: #{conn.status}"
)
operation ->
validate_operation_response(conn, operation, spec)
end
conn
end
if OpenApiSpex.OpenApi.json_encoder() do
@spec validate_operation_response(
Plug.Conn.t(),
Operation.t(),
OpenApi.t()
) ::
term | no_return
defp validate_operation_response(conn, %Operation{operationId: operation_id} = operation, spec) do
content_type = Utils.content_type_from_header(conn, :response)
responses = Map.get(operation, :responses, %{})
code_range = String.first(to_string(conn.status)) <> "XX"
response =
Map.get(responses, conn.status) ||
Map.get(responses, "#{conn.status}") ||
Map.get(responses, :"#{conn.status}") ||
Map.get(responses, code_range) ||
Map.get(responses, :"#{code_range}", %{})
resolved_response =
case response do
%OpenApiSpex.Reference{} = ref ->
OpenApiSpex.Reference.resolve_response(ref, spec.components.responses)
_ ->
response
end
resolved_schema =
resolved_response
|> Map.get(:content, %{})
|> Map.get(content_type, %{})
|> Map.get(:schema)
if is_nil(resolved_schema) do
flunk(
"Failed to resolve a response schema for operation_id: #{operation_id} for status code: #{conn.status} and content type: #{content_type}"
)
end
body =
if String.match?(content_type, @json_content_regex) do
OpenApiSpex.OpenApi.json_encoder().decode!(conn.resp_body)
else
conn.resp_body
end
assert_raw_schema(
body,
resolved_schema,
spec
)
end
else
defp validate_operation_response(_conn, _operation, _spec) do
flunk(
"Unable to use assert_operation_response unless a json encoder is configured. Please add :jason or :poison in your mix dependencies."
)
end
end
end