Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Openapi property based testing #172

Merged
merged 7 commits into from
Jun 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
~r/Function :asn1ct.compile\/2 does not exist/,
~r/lib\/mix\//,
~r/test\/support\/mix\//,
~r/test\/support\/requests_generator\.ex/,
{"test/support/api.ex"}
]
13 changes: 13 additions & 0 deletions lib/mongoose_push_web/plugs/cast_and_validate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ defmodule MongoosePushWeb.Plug.CastAndValidate do
# "Failed to cast value to one of: [] (no schemas provided)", instead of something more meaningful.
# Here we avoid it by manual modification of the pattern the request is matched against,
# so the framework can serve us as expected.
defp update_schema_and_do_call(
conn = %{params: %{"alert" => _, "data" => _}},
opts = %{operation_id: operation_id}
) do
new_schema = %OpenApiSpex.Reference{
"$ref": "#/components/schemas/Request.SendNotification.Deep.MixedNotification"
}

conn
|> update_schema(operation_id, new_schema)
|> OpenApiSpex.Plug.CastAndValidate.call(opts)
end

defp update_schema_and_do_call(
conn = %{params: %{"data" => _}},
opts = %{operation_id: operation_id}
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ defmodule MongoosePush.Mixfile do
{:telemetry, "~>0.4.1"},
{:telemetry_metrics, "~> 0.5"},
{:telemetry_metrics_prometheus_core, "~> 0.4"},
{:logfmt, "~>3.3"}
{:logfmt, "~>3.3"},
{:stream_data, "~> 0.5", only: :test}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"sparrow": {:git, "https://github.com/esl/sparrow.git", "b1896ca4fb0ca18369dd62de3d4c82f14f5bc66e", [ref: "b1896ca"]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"},
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "0.4.0", "0860e53746f4554cf453a5217a3d2648a6d3a074ae01a21869a3963c54b1d5bc", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.5", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "912e4c4421477bfb930a19a8de5b2eb967c2700880698c6d80706b8bc32532bf"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule MongoosePushWeb.APIv1NotificationControllerTest do
conn = post(conn, "/v1/notification/666", body)

assert json_response(conn, 422) ==
ControllersHelper.missing_field_response(unquote(dropped))
ControllersHelper.missing_field_response(:v1, unquote(dropped))
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ defmodule MongoosePushWeb.APIv2NotificationControllerTest do
conn = post(conn, "/v2/notification/654321", body)

assert json_response(conn, 422) ==
ControllersHelper.missing_field_response(unquote(dropped))
ControllersHelper.missing_field_response(:v2, unquote(dropped))
end
end

Expand Down Expand Up @@ -81,7 +81,7 @@ defmodule MongoosePushWeb.APIv2NotificationControllerTest do
conn = post(conn, "/v2/notification/654321", body)

assert json_response(conn, 422) ==
ControllersHelper.missing_field_response(unquote(missing))
ControllersHelper.missing_field_response(:v2, unquote(missing))
end
end

Expand Down Expand Up @@ -124,8 +124,8 @@ defmodule MongoosePushWeb.APIv2NotificationControllerTest do

assert json_response(conn, 422) ==
Map.merge(
ControllersHelper.missing_field_response("service"),
ControllersHelper.missing_field_response("alert"),
ControllersHelper.missing_field_response(:v2, "service"),
ControllersHelper.missing_field_response(:v2, "alert"),
fn _k, v1, v2 -> v1 ++ v2 end
)
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
defmodule MongoosePushWeb.APIv3NotificationControllerTest do
alias MongoosePushWeb.Support.ControllersHelper
alias MongoosePushWeb.Support.RequestsGenerator
use ExUnitProperties
use MongoosePushWeb.ConnCase, async: true
import Mox

Expand Down Expand Up @@ -31,7 +33,7 @@ defmodule MongoosePushWeb.APIv3NotificationControllerTest do
conn = post(conn, "/v3/notification/123456", body)

assert json_response(conn, 422) ==
ControllersHelper.missing_field_response(unquote(dropped))
ControllersHelper.missing_field_response(:v3, unquote(dropped))
end
end

Expand Down Expand Up @@ -83,7 +85,7 @@ defmodule MongoosePushWeb.APIv3NotificationControllerTest do
conn = post(conn, "/v3/notification/123456", body)

assert json_response(conn, 422) ==
ControllersHelper.missing_field_response(unquote(missing))
ControllersHelper.missing_field_response(:v3, unquote(missing))
end
end

Expand Down Expand Up @@ -126,8 +128,8 @@ defmodule MongoosePushWeb.APIv3NotificationControllerTest do

assert json_response(conn, 422) ==
Map.merge(
ControllersHelper.missing_field_response("service"),
ControllersHelper.missing_field_response("alert"),
ControllersHelper.missing_field_response(:v3, "service"),
ControllersHelper.missing_field_response(:v3, "alert"),
fn _k, v1, v2 -> v1 ++ v2 end
)
end
Expand Down Expand Up @@ -816,6 +818,35 @@ defmodule MongoosePushWeb.APIv3NotificationControllerTest do
post_and_assert(conn, device_id, expected_device_id, request, expected_request)
end

property "APIv3 notification decoder property-based test", %{conn: conn} do
check all(
mandatory <- RequestsGenerator.mandatory_fields(),
optionals <- RequestsGenerator.optional_fields(),
device_id <- RequestsGenerator.device_id()
) do
request = Map.merge(mandatory, optionals, fn _k, v1, v2 -> Map.merge(v1, v2) end)
expected_device_id = device_id
expected_request = create_expected_request(request)
post_and_assert(conn, device_id, expected_device_id, request, expected_request)
end
end

property "APIv3 notification with dropped one mandatory field", %{conn: conn} do
check all(
mandatory <- RequestsGenerator.mandatory_fields(),
optionals <- RequestsGenerator.optional_fields(),
device_id <- RequestsGenerator.device_id(),
dropped <- RequestsGenerator.mandatory_field()
) do
request =
Map.merge(mandatory, optionals, fn _k, v1, v2 -> Map.merge(v1, v2) end)
|> drop_field(dropped)

conn = post(conn, "/v3/notification/#{device_id}", Jason.encode!(request))
assert json_response(conn, 422) == ControllersHelper.missing_field_response(:v3, dropped)
end
end

defp post_and_assert(conn, device_id, expected_device_id, request, expected_request) do
expect(MongoosePush.Notification.MockImpl, :push, fn device_id, request ->
assert request == expected_request
Expand All @@ -838,4 +869,51 @@ defmodule MongoosePushWeb.APIv3NotificationControllerTest do

assert json_response(conn, number) == %{"reason" => to_string(error_reason)}
end

defp create_expected_request(request) do
%{
alert: fetch_alert(request["alert"]),
data: request["data"],
mode: fetch_enum_field(request["mode"]),
service: fetch_enum_field(request["service"]),
priority: fetch_enum_field(request["priority"]),
mutable_content: fetch_mutable_content(request["mutable_content"]),
tags: request["tags"],
topic: request["topic"],
time_to_live: request["time_to_live"]
}
|> drop_nil_values()
end

defp fetch_alert(alert) do
%{
body: alert["body"],
title: alert["title"],
badge: alert["badge"],
click_action: alert["click_action"],
tag: alert["tag"],
sound: alert["sound"]
}
|> drop_nil_values()
end

defp fetch_enum_field(nil), do: nil
defp fetch_enum_field(string), do: String.to_existing_atom(string)

defp fetch_mutable_content(nil), do: false
defp fetch_mutable_content(val), do: val

defp drop_nil_values(map) do
map
|> Enum.filter(fn {_, v} -> v != nil end)
|> Map.new()
end

defp drop_field(map, field) when field in ["body", "title"] do
Kernel.update_in(map, ["alert"], fn _ -> Map.drop(map["alert"], [field]) end)
end

defp drop_field(map, field) do
Map.drop(map, [field])
end
end
14 changes: 13 additions & 1 deletion test/support/controllers_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@ defmodule MongoosePushWeb.Support.ControllersHelper do
}
end

def missing_field_response(field) do
def missing_field_response(api, field) when api in [:v2, :v3] and field in ["body", "title"] do
%{
"errors" => [
%{
"message" => "Missing field: #{field}",
"source" => %{"pointer" => "/alert/#{field}"},
"title" => "Invalid value"
}
]
}
end

def missing_field_response(_api, field) do
%{
"errors" => [
%{
Expand Down
77 changes: 77 additions & 0 deletions test/support/requests_generator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule MongoosePushWeb.Support.RequestsGenerator do
# APIv2/3 requests
def mandatory_fields() do
StreamData.fixed_map(%{
"alert" =>
StreamData.fixed_map(%{
"title" => nonempty_string(),
"body" => nonempty_string()
}),
"service" => service()
})
end

def optional_fields() do
StreamData.optional_map(%{
"alert" =>
StreamData.optional_map(%{
"badge" => positive_integer(),
"click_action" => nonempty_string(),
"tag" => nonempty_string(),
"sound" => nonempty_string()
}),
"data" => data(),
"mode" => mode(),
"priority" => priority(),
"mutable_content" => boolean(),
"tags" => tags(),
"topic" => nonempty_string(),
"time_to_live" => positive_integer()
})
end

def device_id() do
nonempty_string()
end

def mandatory_field() do
one_of_strings([:service, :title, :body])
end

# basic types
defp nonempty_string() do
StreamData.string(:alphanumeric, min_length: 1)
end

defp positive_integer() do
StreamData.positive_integer()
end

defp boolean() do
StreamData.one_of([false, true])
end

defp tags() do
StreamData.list_of(nonempty_string(), min_length: 1, max_length: 5)
end

defp priority() do
one_of_strings([:normal, :high])
end

defp service() do
one_of_strings([:apns, :fcm])
end

defp mode() do
one_of_strings([:prod, :dev])
end

defp data() do
StreamData.map_of(StreamData.string(:ascii), StreamData.string(:ascii))
end

defp one_of_strings(list_of_atoms) do
StreamData.map(StreamData.one_of(list_of_atoms), &Kernel.to_string/1)
end
end