defmodule PhoenixSwagger.SchemaTest do @moduledoc """ Testing helper module that makes it convenient to assert that phoenix controller responses conform to a swagger spec. ## Example use YourApp.ConnCase use PhoenixSwagger.SchemaTest "priv/static/swagger.json" test "Get a user by ID", %{conn: conn, swagger_schema: schema} do user = Repo.insert! struct(User, @valid_attrs) response = conn |> get(user_path(conn, :show, user)) |> validate_resp_schema(schema, "UserResponse") |> json_response(200) assert response["data"]["id"] == user.id end Errors will be output with the json-path of the location in the response body for the error: Response JSON does not conform to swagger schema from #/definitions/UserResponse. At #/data/email: Expected "foobaz" to be an email address. { "data": { "name": "Yu Ser", "id": 141, "email": "foobaz" } } """ @doc """ Given a swagger file path, defines an ExUnit `setup_all` block that will add the resolved swagger schema to the ExUnit context. """ defmacro __using__(swagger_file) do quote do @swagger_file unquote(swagger_file) import PhoenixSwagger.SchemaTest, only: [validate_resp_schema: 3] setup_all _context do PhoenixSwagger.SchemaTest.read_swagger_schema(@swagger_file) end end end @doc false def read_swagger_schema(swagger_file) do schema = swagger_file |> File.read!() |> PhoenixSwagger.json_library().decode!() |> Map.update("definitions", %{}, &swagger_nullable_to_json_schema/1) |> ExJsonSchema.Schema.resolve() [swagger_schema: schema] end # Swagger 2.0 and JSON-Schema differ in the treatment of nulls. # When the "x-nullable" vendor extension is present in the swagger, # convert the type to an array including "null" defp swagger_nullable_to_json_schema(schema = %{"type" => type, "x-nullable" => true}) when is_binary(type) do schema = %{schema | "type" => [type, "null"]} swagger_nullable_to_json_schema(schema) end defp swagger_nullable_to_json_schema(schema = %{"$ref" => ref, "x-nullable" => true}) when is_binary(ref) do schema = schema |> Map.drop(["$ref", "x-nullable"]) |> Map.put("oneOf", [%{"type" => "null"}, %{"$ref" => ref}]) swagger_nullable_to_json_schema(schema) end defp swagger_nullable_to_json_schema(schema) when is_map(schema) do for {k, v} <- schema, into: %{}, do: {k, swagger_nullable_to_json_schema(v)} end defp swagger_nullable_to_json_schema(schema) when is_list(schema) do for v <- schema, do: swagger_nullable_to_json_schema(v) end defp swagger_nullable_to_json_schema(other), do: other @doc """ Validates a response body against a swagger schema. ## Example use MyApp.ConnCase use PhoenixSwagger.SchemaTest "priv/static/swagger.json" test "get user by ID", %{conn: conn, swagger_schema: schema} do response = conn |> get(user_path(conn, :show, 123)) |> validate_resp_schema(schema, "User") |> json_response(200) assert response["data"]["id"] == 123 end """ def validate_resp_schema(conn, swagger_schema, model_name) do response_data = conn.resp_body |> PhoenixSwagger.json_library().decode!() fragment = ExJsonSchema.Schema.get_fragment!(swagger_schema, "#/definitions/#{model_name}") result = ExJsonSchema.Validator.validate_fragment(swagger_schema, fragment, response_data) case result do :ok -> conn {:error, errors} -> headline = "Response JSON does not conform to swagger schema from #/definitions/#{model_name}." error_details = errors |> Enum.map(fn {msg, path} -> "At #{path}: #{msg}" end) |> Enum.join("\n") response_pretty = PhoenixSwagger.json_library().encode!(response_data, pretty: true) message = Enum.join([headline, error_details, response_pretty], "\n") ExUnit.Assertions.flunk(message) end end end