Page MenuHomePhorge

No OneTemporary

Size
33 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index f9b155e..8d059c8 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,189 +1,190 @@
defmodule OpenApiSpex.Cast do
alias OpenApiSpex.{Reference, Schema}
alias OpenApiSpex.Reference
alias OpenApiSpex.Cast.{
AllOf,
AnyOf,
Array,
Discriminator,
Error,
Integer,
Object,
OneOf,
Primitive,
String
}
@type schema_or_reference :: Schema.t() | Reference.t()
@type t :: %__MODULE__{
value: term(),
schema: schema_or_reference | nil,
schemas: map(),
path: [atom() | String.t() | integer()],
key: atom() | nil,
index: integer,
errors: [Error.t()]
}
defstruct value: nil,
schema: nil,
schemas: %{},
path: [],
key: nil,
index: 0,
errors: []
@doc ~S"""
Cast and validate a value against the given schema.
Recognizes all the types defined in Open API (itself a superset of JSON Schema).
JSON Schema types:
[https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1](https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1)
Open API primitive types:
[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types)
For an `:object` schema type, the cast operation returns a map with atom keys.
## Examples
iex> alias OpenApiSpex.{Cast, Schema}
iex> schema = %Schema{type: :string}
iex> Cast.cast(schema, "a string")
{:ok, "a string"}
iex> Cast.cast(schema, :not_a_string)
{
:error,
[
%OpenApiSpex.Cast.Error{
reason: :invalid_type,
type: :string,
value: :not_a_string
}
]
}
iex> schema = %Schema{
...> type: :object,
...> properties: %{
...> name: nil
- ...> }
+ ...> },
+ ...> additionalProperties: false
...> }
iex> Cast.cast(schema, %{"name" => "spex"})
{:ok, %{name: "spex"}}
iex> Cast.cast(schema, %{"bad" => "spex"})
{
:error,
[
%OpenApiSpex.Cast.Error{
name: "bad",
path: ["bad"],
reason: :unexpected_field,
value: %{"bad" => "spex"}
}
]
}
"""
@spec cast(schema_or_reference | nil, term(), map()) :: {:ok, term()} | {:error, [Error.t()]}
def cast(schema, value, schemas \\ %{}) do
ctx = %__MODULE__{schema: schema, value: value, schemas: schemas}
cast(ctx)
end
@spec cast(t()) :: {:ok, term()} | {:error, [Error.t()]}
# nil schema
def cast(%__MODULE__{value: value, schema: nil}),
do: {:ok, value}
def cast(%__MODULE__{schema: %Reference{}} = ctx) do
schema = Reference.resolve_schema(ctx.schema, ctx.schemas)
cast(%{ctx | schema: schema})
end
# nullable: true
def cast(%__MODULE__{value: nil, schema: %{nullable: true}}) do
{:ok, nil}
end
# nullable: false
def cast(%__MODULE__{value: nil} = ctx) do
error(ctx, {:null_value})
end
# Enum
def cast(%__MODULE__{schema: %{enum: []}} = ctx) do
cast(%{ctx | schema: %{ctx.schema | enum: nil}})
end
# Enum
def cast(%__MODULE__{schema: %{enum: enum}} = ctx) when is_list(enum) do
with {:ok, value} <- cast(%{ctx | schema: %{ctx.schema | enum: nil}}) do
OpenApiSpex.Cast.Enum.cast(%{ctx | value: value})
end
end
## Specific types
def cast(%__MODULE__{schema: %{type: :object, discriminator: discriminator}} = ctx)
when is_map(discriminator),
do: Discriminator.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, anyOf: schemas}} = ctx) when is_list(schemas),
do: AnyOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, allOf: schemas}} = ctx) when is_list(schemas),
do: AllOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, oneOf: schemas}} = ctx) when is_list(schemas),
do: OneOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: :object}} = ctx),
do: Object.cast(ctx)
def cast(%__MODULE__{schema: %{type: :boolean}} = ctx),
do: Primitive.cast_boolean(ctx)
def cast(%__MODULE__{schema: %{type: :integer}} = ctx),
do: Integer.cast(ctx)
def cast(%__MODULE__{schema: %{type: :number}} = ctx),
do: Primitive.cast_number(ctx)
def cast(%__MODULE__{schema: %{type: :string}} = ctx),
do: String.cast(ctx)
def cast(%__MODULE__{schema: %{type: :array}} = ctx),
do: Array.cast(ctx)
def cast(%__MODULE__{schema: %{type: _other}} = ctx),
do: error(ctx, {:invalid_schema_type})
def cast(%{} = ctx), do: cast(struct(__MODULE__, ctx))
def cast(ctx) when is_list(ctx), do: cast(struct(__MODULE__, ctx))
# Add an error
def error(ctx, error_args) do
error = Error.new(ctx, error_args)
{:error, [error | ctx.errors]}
end
def ok(%__MODULE__{value: value}), do: {:ok, value}
def success(%__MODULE__{schema: schema} = ctx, schema_properties)
when is_list(schema_properties) do
schema_without_successful_validation_property =
Enum.reduce(schema_properties, schema, fn property, schema ->
%{schema | property => nil}
end)
{:cast, %{ctx | schema: schema_without_successful_validation_property}}
end
def success(%__MODULE__{schema: _schema} = ctx, schema_property) do
success(ctx, [schema_property])
end
end
diff --git a/lib/open_api_spex/cast/object.ex b/lib/open_api_spex/cast/object.ex
index a87c067..e2f1aac 100644
--- a/lib/open_api_spex/cast/object.ex
+++ b/lib/open_api_spex/cast/object.ex
@@ -1,145 +1,174 @@
defmodule OpenApiSpex.Cast.Object do
@moduledoc false
alias OpenApiSpex.Cast
alias OpenApiSpex.Cast.Error
def cast(%{value: value} = ctx) when not is_map(value) do
Cast.error(ctx, {:invalid_type, :object})
end
def cast(%{value: value, schema: %{properties: nil}}) do
{:ok, value}
end
def cast(%{value: value, schema: schema} = ctx) do
+ original_value = value
schema_properties = schema.properties || %{}
with :ok <- check_unrecognized_properties(ctx, schema_properties),
value = cast_atom_keys(value, schema_properties),
ctx = %{ctx | value: value},
+ ctx = cast_additional_properties(ctx, original_value),
:ok <- check_required_fields(ctx, schema),
:ok <- check_max_properties(ctx),
:ok <- check_min_properties(ctx),
{:ok, value} <- cast_properties(%{ctx | schema: schema_properties}) do
value_with_defaults = apply_defaults(value, schema_properties)
ctx = to_struct(%{ctx | value: value_with_defaults})
{:ok, ctx}
end
end
# When additionalProperties is true, extra properties are allowed in input
- defp check_unrecognized_properties(%{schema: %{additionalProperties: true}}, _expected_keys) do
+ defp check_unrecognized_properties(%{schema: %{additionalProperties: ap}}, _expected_keys)
+ when ap in [nil, true] do
:ok
end
defp check_unrecognized_properties(%{value: value} = ctx, expected_keys) do
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
schema_keys = expected_keys |> Map.keys() |> Enum.map(&to_string/1)
extra_keys = input_keys -- schema_keys
if extra_keys == [] do
:ok
else
[name | _] = extra_keys
ctx = %{ctx | path: [name | ctx.path]}
Cast.error(ctx, {:unexpected_field, name})
end
end
defp check_required_fields(%{value: input_map} = ctx, schema) do
required = schema.required || []
input_keys = Map.keys(input_map)
missing_keys = required -- input_keys
if missing_keys == [] do
:ok
else
errors =
Enum.map(missing_keys, fn key ->
ctx = %{ctx | path: [key | ctx.path]}
Error.new(ctx, {:missing_field, key})
end)
{:error, ctx.errors ++ errors}
end
end
defp check_max_properties(%{schema: %{maxProperties: max_properties}} = ctx)
when is_integer(max_properties) do
count = ctx.value |> Map.keys() |> length()
if count > max_properties do
Cast.error(ctx, {:max_properties, max_properties, count})
else
:ok
end
end
defp check_max_properties(_ctx), do: :ok
defp check_min_properties(%{schema: %{minProperties: min_properties}} = ctx)
when is_integer(min_properties) do
count = ctx.value |> Map.keys() |> length()
if count < min_properties do
Cast.error(ctx, {:min_properties, min_properties, count})
else
:ok
end
end
defp check_min_properties(_ctx), do: :ok
defp cast_atom_keys(input_map, properties) do
Enum.reduce(properties, %{}, fn {key, _}, output ->
string_key = to_string(key)
case input_map do
%{^key => value} -> Map.put(output, key, value)
%{^string_key => value} -> Map.put(output, key, value)
_ -> output
end
end)
end
defp cast_properties(%{value: object, schema: schema_properties} = ctx) do
Enum.reduce(object, {:ok, %{}}, fn
{key, value}, {:ok, output} ->
cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output)
_, error ->
error
end)
end
+ # Pass additional properties through when `additionalProperties` is true.
+ # Map string keys are not converted to atoms. That would require calling `String.to_atom/1`, which is not safe.
+ defp cast_additional_properties(%{schema: %{additionalProperties: ap}} = ctx, original_value)
+ when ap in [nil, true] do
+ recognized_keys = Map.keys(ctx.schema.properties || %{})
+ # Create MapSet with both atom and string versions of the property keys
+ recognized_keys = MapSet.new(recognized_keys ++ Enum.map(recognized_keys, &to_string/1))
+
+ additional_properties =
+ Enum.reduce(original_value, %{}, fn {key, value}, props ->
+ if MapSet.member?(recognized_keys, key) do
+ props
+ else
+ Map.put(props, key, value)
+ end
+ end)
+
+ updated_value = Map.merge(ctx.value, additional_properties)
+
+ %{ctx | value: updated_value}
+ end
+
+ defp cast_additional_properties(ctx, _original_value) do
+ ctx
+ end
+
defp cast_property(%{key: key, schema: schema_properties} = ctx, output) do
prop_schema = Map.get(schema_properties, key)
path = [key | ctx.path]
with {:ok, value} <- Cast.cast(%{ctx | path: path, schema: prop_schema}) do
{:ok, Map.put(output, key, value)}
end
end
defp apply_defaults(object_value, schema_properties) do
Enum.reduce(schema_properties, object_value, &apply_default/2)
end
defp apply_default({_key, %{default: nil}}, object_value), do: object_value
defp apply_default({key, %{default: default_value}}, object_value) do
if Map.has_key?(object_value, key) do
object_value
else
Map.put(object_value, key, default_value)
end
end
defp apply_default(_, object_value), do: object_value
defp to_struct(%{value: value = %_{}}), do: value
defp to_struct(%{value: value, schema: %{"x-struct": nil}}), do: value
defp to_struct(%{value: value, schema: %{"x-struct": module}}),
do: struct(module, value)
end
diff --git a/test/cast/object_test.exs b/test/cast/object_test.exs
index ab07637..ddc067a 100644
--- a/test/cast/object_test.exs
+++ b/test/cast/object_test.exs
@@ -1,205 +1,226 @@
defmodule OpenApiSpex.ObjectTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Object, Error}
defp cast(ctx), do: Object.cast(struct(Cast, ctx))
describe "cast/3" do
test "when input is not an object" do
schema = %Schema{type: :object}
assert {:error, [error]} = cast(value: ["hello"], schema: schema)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.value == ["hello"]
end
test "input map can have atom keys" do
- schema = %Schema{type: :object}
+ schema = %Schema{type: :object, properties: %{one: %Schema{type: :string}}}
assert {:ok, map} = cast(value: %{one: "one"}, schema: schema)
assert map == %{one: "one"}
end
test "converting string keys to atom keys when properties are defined" do
schema = %Schema{
type: :object,
properties: %{
one: nil
}
}
assert {:ok, map} = cast(value: %{"one" => "one"}, schema: schema)
assert map == %{one: "one"}
end
test "properties:nil, given unknown input property" do
schema = %Schema{type: :object}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"unknown" => "hello"}, schema: schema) ==
{:ok, %{"unknown" => "hello"}}
end
test "with empty schema properties, given unknown input property" do
- schema = %Schema{type: :object, properties: %{}}
+ schema = %Schema{type: :object, properties: %{}, additionalProperties: false}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"unknown" => "hello"}, schema: schema)
assert %Error{} = error
assert error.reason == :unexpected_field
assert error.name == "unknown"
assert error.path == ["unknown"]
end
test "with schema properties set, given known input property" do
schema = %Schema{
type: :object,
properties: %{age: nil}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"age" => "hello"}, schema: schema) == {:ok, %{age: "hello"}}
end
test "unexpected field" do
schema = %Schema{
type: :object,
- properties: %{}
+ properties: %{},
+ additionalProperties: false
}
assert {:error, [error]} = cast(value: %{foo: "foo"}, schema: schema)
assert %Error{} = error
assert error.reason == :unexpected_field
assert error.path == ["foo"]
end
test "required fields" do
schema = %Schema{
type: :object,
properties: %{age: nil, name: nil},
required: [:age, :name]
}
assert {:error, [error, error2]} = cast(value: %{}, schema: schema)
assert %Error{} = error
assert error.reason == :missing_field
assert error.name == :age
assert error.path == [:age]
assert error2.reason == :missing_field
assert error2.name == :name
assert error2.path == [:name]
end
test "fields with default values" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
assert cast(value: %{}, schema: schema) == {:ok, %{name: "Rubi"}}
assert cast(value: %{"name" => "Jane"}, schema: schema) == {:ok, %{name: "Jane"}}
assert cast(value: %{name: "Robin"}, schema: schema) == {:ok, %{name: "Robin"}}
end
test "explicitly passing nil for fields with default values (not nullable)" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
assert {:error, [%{reason: :null_value}]} = cast(value: %{"name" => nil}, schema: schema)
assert {:error, [%{reason: :null_value}]} = cast(value: %{name: nil}, schema: schema)
end
test "explicitly passing nil for fields with default values (nullable)" do
schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi", nullable: true}}
}
assert cast(value: %{"name" => nil}, schema: schema) == {:ok, %{name: nil}}
assert cast(value: %{name: nil}, schema: schema) == {:ok, %{name: nil}}
end
test "default values in nested schemas" do
child_schema = %Schema{
type: :object,
properties: %{name: %Schema{type: :string, default: "Rubi"}}
}
parent_schema = %Schema{
type: :object,
properties: %{child: child_schema}
}
assert cast(value: %{child: %{}}, schema: parent_schema) == {:ok, %{child: %{name: "Rubi"}}}
assert cast(value: %{child: %{"name" => "Jane"}}, schema: parent_schema) ==
{:ok, %{child: %{name: "Jane"}}}
end
test "cast property against schema" do
schema = %Schema{
type: :object,
properties: %{age: %Schema{type: :integer}}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"age" => "hello"}, schema: schema)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.path == [:age]
end
+ test "allow unrecognized fields when additionalProperties is true" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: true
+ }
+
+ assert cast(value: %{"foo" => "foo"}, schema: schema) == {:ok, %{"foo" => "foo"}}
+ end
+
+ test "allow unrecognized fields when additionalProperties is nil" do
+ schema = %Schema{
+ type: :object,
+ properties: %{},
+ additionalProperties: nil
+ }
+
+ assert cast(value: %{"foo" => "foo"}, schema: schema) == {:ok, %{"foo" => "foo"}}
+ end
+
defmodule User do
defstruct [:name]
end
test "optionally casts to struct" do
schema = %Schema{
type: :object,
"x-struct": User,
properties: %{
name: %Schema{type: :string}
}
}
assert {:ok, user} = cast(value: %{"name" => "Name"}, schema: schema)
assert user == %User{name: "Name"}
end
test "validates maxProperties" do
schema = %Schema{
type: :object,
properties: %{
one: nil,
two: nil
},
maxProperties: 1
}
assert {:error, [error]} = cast(value: %{one: "one", two: "two"}, schema: schema)
assert %Error{} = error
assert error.reason == :max_properties
assert {:ok, _} = cast(value: %{one: "one"}, schema: schema)
end
test "validates minProperties" do
schema = %Schema{
type: :object,
properties: %{
one: nil,
two: nil
},
minProperties: 1
}
assert {:error, [error]} = cast(value: %{}, schema: schema)
assert %Error{} = error
assert error.reason == :min_properties
assert {:ok, _} = cast(value: %{one: "one"}, schema: schema)
end
end
end
diff --git a/test/operation2_test.exs b/test/operation2_test.exs
index 8c52c97..17dda0a 100644
--- a/test/operation2_test.exs
+++ b/test/operation2_test.exs
@@ -1,276 +1,265 @@
defmodule OpenApiSpex.Operation2Test do
use ExUnit.Case
alias OpenApiSpex.{Operation, Operation2, Schema, Components, Reference}
alias OpenApiSpex.Cast.Error
defmodule SchemaFixtures do
@user %Schema{
type: :object,
properties: %{
user: %Schema{
type: :object,
properties: %{
email: %Schema{type: :string}
}
}
}
}
@user_list %Schema{
type: :array,
items: @user
}
@schemas %{"User" => @user, "UserList" => @user_list}
def user, do: @user
def user_list, do: @user_list
def schemas, do: @schemas
end
defmodule ParameterFixtures do
alias OpenApiSpex.{Schema, Operation}
def parameters do
%{
"member" => Operation.parameter(:member, :query, :boolean, "Membership flag")
}
end
end
defmodule OperationFixtures do
@user_index %Operation{
operationId: "UserController.index",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name"),
Operation.parameter(:age, :query, :integer, "Filter by user age"),
%Reference{"$ref": "#/components/parameters/member"},
Operation.parameter(
:include_archived,
:query,
%Schema{type: :boolean, default: false},
"Example of a default value"
)
],
responses: %{
200 => Operation.response("User", "application/json", SchemaFixtures.user())
}
}
def user_index, do: @user_index
@create_user %Operation{
operationId: "UserController.create",
parameters: [
Operation.parameter(:name, :query, :string, "Filter by user name")
],
requestBody:
Operation.request_body("request body", "application/json", SchemaFixtures.user(),
required: true
),
responses: %{
200 => Operation.response("User list", "application/json", SchemaFixtures.user_list())
}
}
def create_user, do: @create_user
end
defmodule SpecModule do
@behaviour OpenApiSpex.OpenApi
@impl OpenApiSpex.OpenApi
def spec do
paths = %{
"/users" => %{
"post" => OperationFixtures.create_user()
}
}
%OpenApiSpex.OpenApi{
info: nil,
paths: paths,
components: %Components{
schemas: SchemaFixtures.schemas(),
parameters: ParameterFixtures.parameters()
}
}
end
end
defmodule RenderError do
def init(_) do
nil
end
def call(_conn, _errors) do
raise "should not have errors"
end
end
describe "cast/4" do
test "cast request body" do
conn = create_conn(%{"user" => %{"email" => "foo@bar.com"}})
assert {:ok, conn} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SpecModule.spec().components
)
assert %Plug.Conn{} = conn
end
test "cast request body - invalid data type" do
conn = create_conn(%{"user" => %{"email" => 123}})
assert {:error, errors} =
Operation2.cast(
OperationFixtures.create_user(),
conn,
"application/json",
SpecModule.spec().components
)
assert [error] = errors
assert %Error{} = error
assert error.reason == :invalid_type
end
test "casts valid query params and respects defaults" do
valid_query_params = %{"name" => "Rubi", "age" => "31", "member" => "true"}
assert {:ok, conn} = do_index_cast(valid_query_params)
assert conn.params == %{age: 31, member: true, name: "Rubi", include_archived: false}
end
test "casts valid query params and overrides defaults" do
valid_query_params = %{
"name" => "Rubi",
"age" => "31",
"member" => "true",
"include_archived" => "true"
}
assert {:ok, conn} = do_index_cast(valid_query_params)
assert conn.params == %{age: 31, member: true, name: "Rubi", include_archived: true}
end
- test "validate undefined query param name" do
- query_params = %{"unknown" => "asdf"}
-
- assert {:error, [error]} = do_index_cast(query_params)
-
- assert %Error{} = error
- assert error.reason == :unexpected_field
- assert error.name == "unknown"
- assert error.path == ["unknown"]
- end
-
test "validate invalid data type for query param" do
query_params = %{"age" => "asdf"}
assert {:error, [error]} = do_index_cast(query_params)
assert %Error{} = error
assert error.reason == :invalid_type
assert error.type == :integer
assert error.value == "asdf"
end
test "validate missing required query param" do
parameter =
Operation.parameter(:name, :query, :string, "Filter by user name", required: true)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{}, operation: operation)
assert %Error{} = error
assert error.reason == :missing_field
assert error.name == :name
end
test "validate missing content-type header for required requestBody" do
conn = :post |> Plug.Test.conn("/api/users/") |> Plug.Conn.fetch_query_params()
operation = OperationFixtures.create_user()
assert {:error, [%Error{reason: :missing_header, name: "content-type"}]} =
Operation2.cast(
operation,
conn,
nil,
SpecModule.spec().components
)
end
test "validate invalid content-type header for required requestBody" do
conn =
create_conn(%{})
|> Plug.Conn.put_req_header("content-type", "text/html")
operation = OperationFixtures.create_user()
assert {:error, [%Error{reason: :invalid_header, name: "content-type"}]} =
Operation2.cast(
operation,
conn,
"text/html",
SpecModule.spec().components
)
end
test "validate invalid value for integer range" do
parameter =
Operation.parameter(
:age,
:query,
%Schema{type: :integer, minimum: 1, maximum: 99},
"Filter by user age",
required: true
)
operation = %{OperationFixtures.user_index() | parameters: [parameter]}
assert {:error, [error]} = do_index_cast(%{"age" => 100}, operation: operation)
assert %Error{} = error
assert error.reason == :maximum
assert {:error, [error]} = do_index_cast(%{"age" => 0}, operation: operation)
assert %Error{} = error
assert error.reason == :minimum
end
defp do_index_cast(query_params, opts \\ []) do
conn =
:get
|> Plug.Test.conn("/api/users?" <> URI.encode_query(query_params))
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> build_params()
operation = opts[:operation] || OperationFixtures.user_index()
Operation2.cast(
operation,
conn,
"application/json",
SpecModule.spec().components
)
end
defp create_conn(body_params) do
:post
|> Plug.Test.conn("/api/users")
|> Plug.Conn.put_req_header("content-type", "application/json")
|> Plug.Conn.fetch_query_params()
|> Map.put(:body_params, body_params)
|> build_params()
end
defp build_params(conn) do
params =
conn.path_params
|> Map.merge(conn.query_params)
|> Map.merge(conn.body_params)
%{conn | params: params}
end
end
end
diff --git a/test/plug/cast_and_validate/custom_error_user_controller_test.exs b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
index d4ca93e..b07546b 100644
--- a/test/plug/cast_and_validate/custom_error_user_controller_test.exs
+++ b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
@@ -1,37 +1,37 @@
defmodule OpenApiSpex.Plug.CastAndValidate.CustomErrorUserControllerTest do
use ExUnit.Case, async: true
describe "query params - param with custom error handling" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
@tag :capture_log
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
assert conn.resp_body == "Invalid boolean. Got: string"
end
@tag :capture_log
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
- assert conn.resp_body == "Unexpected field: inValid2"
+ assert conn.resp_body == "Invalid boolean. Got: string"
end
end
end
diff --git a/test/plug/cast_and_validate_test.exs b/test/plug/cast_and_validate_test.exs
index c2c5467..abb611c 100644
--- a/test/plug/cast_and_validate_test.exs
+++ b/test/plug/cast_and_validate_test.exs
@@ -1,159 +1,159 @@
defmodule OpenApiSpex.Plug.CastTest do
use ExUnit.Case
describe "query params - basics" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
end
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn("/api/users?validParam=123&inValidParam=123&inValid2=hi")
+ |> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
error_resp = Jason.decode!(conn.resp_body)
assert error_resp == %{
"errors" => [
%{
- "message" => "Unexpected field: inValid2",
- "source" => %{"pointer" => "/inValid2"},
+ "message" => "Invalid boolean. Got: string",
+ "source" => %{"pointer" => "/validParam"},
"title" => "Invalid value"
}
]
}
end
test "with requestBody" do
body =
Jason.encode!(%{
phone_number: "123-456-789",
postal_address: "123 Lane St"
})
conn =
:post
|> Plug.Test.conn("/api/users/123/contact_info", body)
|> Plug.Conn.put_req_header("content-type", "application/json")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
end
describe "query params - param with custom error handling" do
test "Valid Param" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
test "Invalid value" do
conn =
:get
|> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
end
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
- assert conn.resp_body == "Unexpected field: inValid2"
+ assert conn.resp_body == "Invalid boolean. Got: string"
end
end
describe "body params" do
test "Valid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
|> OpenApiSpexTest.Router.call([])
assert conn.body_params == %OpenApiSpexTest.Schemas.UserRequest{
user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
}
}
assert Jason.decode!(conn.resp_body) == %{
"data" => %{
"email" => "foo@bar.com",
"id" => 1234,
"inserted_at" => nil,
"name" => "asdf",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
end
test "Invalid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "*1234",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
resp_data = Jason.decode!(conn.resp_body)
assert resp_data ==
%{
"errors" => [
%{
"message" => "Invalid format. Expected ~r/[a-zA-Z][a-zA-Z0-9_]+/",
"source" => %{"pointer" => "/user/name"},
"title" => "Invalid value"
}
]
}
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Wed, Nov 27, 3:40 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40681
Default Alt Text
(33 KB)

Event Timeline