From 49682cf38a4c97a8c41f8134e7181f09bd71ad3a Mon Sep 17 00:00:00 2001 From: xinz Date: Tue, 7 Feb 2023 17:01:26 +0800 Subject: [PATCH] Fix to support post a json request with non-object content --- lib/oasis/plug/request_validator.ex | 34 +++++++--- lib/oasis/validator.ex | 11 ++++ test/integration_test.exs | 50 +++++++++++++- test/support/gen/plug/pre_test_post_json.ex | 73 +++++++++++++++++++++ test/support/http_server.ex | 4 ++ 5 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 test/support/gen/plug/pre_test_post_json.ex diff --git a/lib/oasis/plug/request_validator.ex b/lib/oasis/plug/request_validator.ex index 8b03cf9..4573e5d 100644 --- a/lib/oasis/plug/request_validator.ex +++ b/lib/oasis/plug/request_validator.ex @@ -87,20 +87,31 @@ defmodule Oasis.Plug.RequestValidator do end defp process_body( - %{body_params: body_params, req_headers: req_headers, params: params} = conn, + %{body_params: %{"_json" => _body_params}, params: params} = conn, body_schema ) when is_map(body_schema) do - matched_schema = - req_headers - |> find_content_type() - |> schema_may_match_by_request(body_schema) + body_params = parse_and_validate_body_params(conn, body_schema) - body_params = - Oasis.Validator.parse_and_validate!(matched_schema, "body", "body_request", body_params) + if is_map(body_params) do + # Maybe the defined schema specification use the "_json" key as a property of an object. + params = Map.merge(params, body_params) + %{conn | body_params: body_params, params: params} + else + %{conn | body_params: body_params, params: body_params} + end + end - params = params |> Map.merge(body_params) + defp process_body( + conn, + body_schema + ) + when is_map(body_schema) do + + body_params = parse_and_validate_body_params(conn, body_schema) + + params = conn.params |> Map.merge(body_params) %{conn | body_params: body_params, params: params} end @@ -109,6 +120,13 @@ defmodule Oasis.Plug.RequestValidator do conn end + defp parse_and_validate_body_params(%{body_params: body_params, req_headers: req_headers}, body_schema) do + req_headers + |> find_content_type() + |> schema_may_match_by_request(body_schema) + |> Oasis.Validator.parse_and_validate!("body", "body_request", body_params) + end + defp parse_and_validate(schemas, input_params, use_in) when is_map(input_params) do Enum.reduce(schemas, input_params, fn {param_name, definition}, acc -> input_value = input_params[param_name] diff --git a/lib/oasis/validator.ex b/lib/oasis/validator.ex index ac6d2e5..38b03e3 100644 --- a/lib/oasis/validator.ex +++ b/lib/oasis/validator.ex @@ -61,6 +61,17 @@ defmodule Oasis.Validator do |> process_media_type(media_type, use_in, name, value) end + defp do_parse_and_validate!(%{schema: %{"type" => type}} = json_schema_root, "body", param_name, %{"_json" => value}) + when type == "string" and is_bitstring(value) + when type == "number" and is_number(value) + when type == "integer" and is_integer(value) + when type == "array" and is_list(value) + when type == "boolean" and is_boolean(value) do + # Since `Plug.Parsers.JSON` parses a non-map body content into a "_json" key to allow proper param merging, here + # will unwrap the "_json" key and format the input body params as a matched type to the defined OpenAPI specification. + do_parse_and_validate!(json_schema_root, "body", param_name, value) + end + defp do_parse_and_validate!(%{schema: schema} = json_schema_root, use_in, param_name, value) do try do Oasis.Parser.parse(schema, value) diff --git a/test/integration_test.exs b/test/integration_test.exs index 0433f5d..97bcf25 100644 --- a/test/integration_test.exs +++ b/test/integration_test.exs @@ -494,6 +494,55 @@ defmodule Oasis.IntegrationTest do "Find body parameter `body_request` with error: Required property addresses was not present." end + test "post application/json with non object type", %{url: url} do + start_supervised!({Finch, name: TestFinch}) + + headers = [{"content-type", "application/json"}] + body = "[{\"id\":1,\"name\":\"hello\"}]" + + assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch) + body = Jason.decode!(response.body) + assert response.status == 200 and + body["body_params"] == [%{"id" => 1, "name" => "hello"}] and + body["body_params"] == body["params"] + + headers = [{"content-type", "application/vnd.api-v1+json"}] + body = "1" + + assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch) + body = Jason.decode!(response.body) + assert response.status == 200 and + body["body_params"] == 1 and + body["body_params"] == body["params"] + + headers = [{"content-type", "application/vnd.api-v2+json"}] + body = "1.5" + + assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch) + body = Jason.decode!(response.body) + assert response.status == 200 and + body["body_params"] == 1.5 and + body["body_params"] == body["params"] + end + + test "post application/json with object type use _json key", %{url: url} do + start_supervised!({Finch, name: TestFinch}) + + headers = [{"content-type", "application/vnd.api-v3+json"}] + # valid `street_type` is ["Street", "Avenue", "Boulevard"] + body = "{\"_json\":{\"street_name\":\"S1\", \"street_type\":\"Avenue2\"}}" + + assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch) + assert response.body == "Find body parameter `body_request` with error: Value is not allowed in enum." + + body = "{\"_json\":{\"street_name\":\"S1\", \"id\":\"1\", \"street_type\":\"Avenue\"}}" + assert {:ok, response} = Finch.build(:post, "#{url}/test_post_json", headers, body) |> Finch.request(TestFinch) + body = Jason.decode!(response.body) + assert response.status == 200 and + body["body_params"]["_json"] == %{"street_name" => "S1", "street_type" => "Avenue", "id" => 1} and + body["body_params"] == body["params"] + end + test "delete request with body schema validation", %{url: url} do start_supervised!({Finch, name: TestFinch}) @@ -715,7 +764,6 @@ defmodule Oasis.IntegrationTest do end test "verify hmac auth with body", %{url: url} do - IO.puts "url: #{url}" start_supervised!({Finch, name: TestFinch}) c = Oasis.Test.Support.HMAC.case_with_body() diff --git a/test/support/gen/plug/pre_test_post_json.ex b/test/support/gen/plug/pre_test_post_json.ex new file mode 100644 index 0000000..312af25 --- /dev/null +++ b/test/support/gen/plug/pre_test_post_json.ex @@ -0,0 +1,73 @@ +defmodule Oasis.Gen.Plug.PreTestPostJSON do + use Oasis.Controller + use Plug.ErrorHandler + + plug( + Plug.Parsers, + parsers: [:json], + json_decoder: Jason, + pass: ["*/*"] + ) + + plug( + Oasis.Plug.RequestValidator, + body_schema: %{ + "required" => true, + "content" => %{ + "application/json" => %{ + "schema" => %ExJsonSchema.Schema.Root{ + schema: %{ + "items" => %{ + "type" => "object", + "properties" => %{ + "id" => %{"type" => "integer"}, + "name" => %{"type" => "string"} + } + }, + "type" => "array" + } + } + }, + "application/vnd.api-v1+json" => %{ + "schema" => %ExJsonSchema.Schema.Root{ + schema: %{ + "type" => "integer" + } + } + }, + "application/vnd.api-v2+json" => %{ + "schema" => %ExJsonSchema.Schema.Root{ + schema: %{ + "type" => "number" + } + } + }, + "application/vnd.api-v3+json" => %{ + "schema" => %ExJsonSchema.Schema.Root{ + schema: %{ + "properties" => %{ + "_json" => %{ + "type" => "object", + "properties" => %{ + "street_name" => %{"type" => "string"}, + "street_type" => %{"enum" => ["Street", "Avenue", "Boulevard"]}, + "id" => %{"type" => "integer"} + } + } + }, + "type" => "object" + } + } + }, + + } + } + ) + + def call(conn, opts) do + conn |> super(opts) |> Oasis.Gen.Plug.TestPost.call(opts) |> halt() + end + + defdelegate handle_errors(conn, error), to: Oasis.Gen.Plug.TestPost + +end diff --git a/test/support/http_server.ex b/test/support/http_server.ex index 3a164c2..86b8d87 100644 --- a/test/support/http_server.ex +++ b/test/support/http_server.ex @@ -75,6 +75,10 @@ defmodule Oasis.HTTPServer.PlugRouter do to: Oasis.Gen.Plug.PreTestPostMultipart ) + post("/test_post_json", + to: Oasis.Gen.Plug.PreTestPostJSON + ) + post("/test_post_non_validate", to: Oasis.Gen.Plug.TestPostNonValidate )