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.Cast.Error
  alias OpenApiSpex.{Cast, OpenApi}

  @dialyzer {:no_match, assert_schema: 3}

  @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 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
end