From a1de71615b105d1a50d11a821b7dddbda4efc22d Mon Sep 17 00:00:00 2001 From: Amir Hasanbasic <43892661+hamir-suspect@users.noreply.github.com> Date: Tue, 14 May 2024 11:03:04 +0200 Subject: [PATCH] Sync with upstream (#2) * Exclude empty paths from spec (#583) * Exclude empty paths from spec * fix: assert_operation_response header lookup (#584) * fix: assert_operation_response header lookup * Release version 3.18.1 * Fix 'AllOf cast returns a map, but I expected a struct' (#592) * Add failing test * Cast result of AllOf cast into a struct * Shorter module name * Add missing NoneCache test * Release version 3.18.2 * Relax dependency constraint on ymlr to allow version ~> 5.0 (#586) * relax dependency on ymlr, and fix some tests * test with more elixir versions * Update Elixir version test matrix (#602) * Update Elixir version test matrix * Fix map key order dependent test * Release version 3.18.3 * Support response code ranges See: https://swagger.io/docs/specification/describing-responses/ * Release version 3.19.0 * Add notice that body params are not merged into Conn.params whne using cast and validate plug (#589) * Set nonces on - - + """ + @doc """ + Initializes the plug. + + ## Options + + * `:csp_nonce_assign_key` - Optional. An assign key to find the CSP nonce value used + for assets. Supports either `atom()` or a map of type `%{optional(:script) => atom()}`. + + ## Example + + get "/oauth2-redirect.html", + OpenApiSpex.Plug.SwaggerUIOAuth2Redirect, + csp_nonce_assign_key: %{script: :script_src_nonce} + """ @impl Plug - def init(_opts), do: [] + def init(opts) when is_list(opts) do + Map.new(opts) + end @impl Plug - def call(conn, _opts) do - html = render() + def call(conn, config) do + html = render(OpenApiSpex.Plug.SwaggerUI.get_nonce(conn, config, :script)) conn |> put_resp_content_type("text/html") @@ -94,5 +112,5 @@ defmodule OpenApiSpex.Plug.SwaggerUIOAuth2Redirect do end require EEx - EEx.function_from_string(:defp, :render, @html, []) + EEx.function_from_string(:defp, :render, @html, [:script_src_nonce]) end diff --git a/lib/open_api_spex/test/test_assertions.ex b/lib/open_api_spex/test/test_assertions.ex index 42f95b2e..809044d6 100644 --- a/lib/open_api_spex/test/test_assertions.ex +++ b/lib/open_api_spex/test/test_assertions.ex @@ -136,7 +136,7 @@ defmodule OpenApiSpex.TestAssertions do case operation_lookup[operation_id] do nil -> flunk( - "Failed to resolve schema. Unable to find a response for operation_id: #{operation_id} for response status code: #{conn.status}" + "Failed to resolve a response schema for operation_id: #{operation_id} for status code: #{conn.status}" ) operation -> @@ -154,20 +154,27 @@ defmodule OpenApiSpex.TestAssertions do ) :: term | no_return defp validate_operation_response(conn, %Operation{operationId: operation_id} = operation, spec) do - content_type = Utils.content_type_from_header(conn) + content_type = Utils.content_type_from_header(conn, :response) + + responses = Map.get(operation, :responses, %{}) + code_range = String.first(to_string(conn.status)) <> "XX" + + response = + Map.get(responses, conn.status) || + Map.get(responses, "#{conn.status}") || + Map.get(responses, :"#{conn.status}") || + Map.get(responses, code_range) || + Map.get(responses, :"#{code_range}", %{}) resolved_schema = - get_in(operation, [ - Access.key!(:responses), - Access.key!(conn.status), - Access.key!(:content), - content_type, - Access.key!(:schema) - ]) + response + |> Map.get(:content, %{}) + |> Map.get(content_type, %{}) + |> Map.get(:schema) if is_nil(resolved_schema) do flunk( - "Failed to resolve schema! Unable to find a response for operation_id: #{operation_id} for response status code: #{conn.status} and content type #{content_type}" + "Failed to resolve a response schema for operation_id: #{operation_id} for status code: #{conn.status} and content type: #{content_type}" ) end diff --git a/mix.exs b/mix.exs index 78459d52..49eaee63 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule OpenApiSpex.Mixfile do use Mix.Project @source_url "https://github.com/open-api-spex/open_api_spex" - @version "3.18.0" + @version "3.19.0" def project do [ @@ -70,7 +70,7 @@ defmodule OpenApiSpex.Mixfile do {:phoenix, "~> 1.3", only: [:dev, :test]}, {:plug, "~> 1.7"}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", optional: true}, - {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", optional: true} + {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", optional: true} ] end diff --git a/test/cast/all_of_test.exs b/test/cast/all_of_test.exs index a491dc25..9f2fbabe 100644 --- a/test/cast/all_of_test.exs +++ b/test/cast/all_of_test.exs @@ -355,6 +355,7 @@ defmodule OpenApiSpex.CastAllOfTest do test "with schema having x-type" do value = %{fur: true, meow: true} - assert {:ok, _} = cast(value: value, schema: CatSchema.schema()) + + assert {:ok, %CatSchema{fur: true, meow: true}} = cast(value: value, schema: CatSchema.schema()) end end diff --git a/test/cast/object_test.exs b/test/cast/object_test.exs index add91d24..84261c18 100644 --- a/test/cast/object_test.exs +++ b/test/cast/object_test.exs @@ -415,7 +415,11 @@ defmodule OpenApiSpex.ObjectTest do } } - assert {:error, [error1, error2]} = cast(value: %{"age" => 0, "name" => "N"}, schema: schema) + assert {:error, [_error1, _error2] = errors} = + cast(value: %{"age" => 0, "name" => "N"}, schema: schema) + + error1 = Enum.find(errors, &(&1.path == [:age])) + error2 = Enum.find(errors, &(&1.path == [:name])) assert %Error{} = error1 assert error1.reason == :minimum diff --git a/test/paths_test.exs b/test/paths_test.exs index 300e9497..4bec9a9f 100644 --- a/test/paths_test.exs +++ b/test/paths_test.exs @@ -12,8 +12,13 @@ defmodule OpenApiSpex.PathsTest do "/api/pets/{id}" => pets_path_item } = paths - assert pets_path_item.patch.operationId == "OpenApiSpexTest.PetController.update" - assert pets_path_item.put.operationId == "OpenApiSpexTest.PetController.update (2)" + refute Map.has_key?(paths, "/api/noapi") + refute Map.has_key?(paths, "/api/noapi_with_struct") + + operation_ids = [pets_path_item.put.operationId, pets_path_item.patch.operationId] + + assert "OpenApiSpexTest.PetController.update" in operation_ids + assert "OpenApiSpexTest.PetController.update (2)" in operation_ids end end end diff --git a/test/plug/none_cache_test.exs b/test/plug/none_cache_test.exs new file mode 100644 index 00000000..50c69d02 --- /dev/null +++ b/test/plug/none_cache_test.exs @@ -0,0 +1,28 @@ +defmodule OpenApiSpex.Plug.NoneCacheTest do + use ExUnit.Case, async: true + + alias OpenApiSpex.Plug.NoneCache + alias OpenApiSpexTest.ApiSpec + + setup do + [spec: ApiSpec.spec()] + end + + describe "get/1" do + test "returns nil", %{spec: spec} do + assert is_nil(NoneCache.get(spec)) + end + end + + describe "put/2" do + test "returns :ok", %{spec: spec} do + assert :ok = NoneCache.put(spec, %{}) + end + end + + describe "erase/1" do + test "returns :ok", %{spec: spec} do + assert :ok = NoneCache.erase(spec) + end + end +end diff --git a/test/plug/swagger_ui_test.exs b/test/plug/swagger_ui_test.exs index e53b21d6..4506e7bc 100644 --- a/test/plug/swagger_ui_test.exs +++ b/test/plug/swagger_ui_test.exs @@ -13,4 +13,37 @@ defmodule OpenApiSpec.Plug.SwaggerUITest do assert conn.resp_body =~ ~r[pathname.+?/ui] assert String.contains?(conn.resp_body, token) end + + describe "nonces" do + test "omits nonces if not configured" do + conn = Plug.Test.conn(:get, "/ui") |> SwaggerUI.call(@opts) + refute String.contains?(conn.resp_body, "nonce") + end + + test "renders with single key" do + conn = + Plug.Test.conn(:get, "/ui") + |> Plug.Conn.assign(:nonce, "my_nonce") + |> SwaggerUI.call(Map.put(@opts, :csp_nonce_assign_key, :nonce)) + + assert String.match?(conn.resp_body, ~r/ Plug.Conn.assign(:style_src_nonce, "my_style_nonce") + |> Plug.Conn.assign(:script_src_nonce, "my_script_nonce") + |> SwaggerUI.call( + Map.put(@opts, :csp_nonce_assign_key, %{ + script: :script_src_nonce, + style: :style_src_nonce + }) + ) + + assert String.match?(conn.resp_body, ~r/ Plug.Test.conn("/api/response_code_ranges") + |> Plug.Conn.put_req_header("content-type", "application/json") + + conn = OpenApiSpexTest.Router.call(conn, []) + + assert conn.status == 200 + TestAssertions.assert_operation_response(conn, "response_code_ranges") + end + test "missing operation id" do conn = :get @@ -129,14 +141,11 @@ defmodule OpenApiSpex.TestAssertionsTest do conn = OpenApiSpexTest.Router.call(conn, []) assert conn.status == 200 - try do - TestAssertions.assert_operation_response(conn, "not_a_real_operation_id") - raise RuntimeError, "Should flunk" - rescue - e in ExUnit.AssertionError -> - assert e.message =~ - "Failed to resolve schema. Unable to find a response for operation_id: not_a_real_operation_id for response status code: 200" - end + assert_raise( + ExUnit.AssertionError, + ~r/Failed to resolve a response schema for operation_id: not_a_real_operation_id for status code: 200/, + fn -> TestAssertions.assert_operation_response(conn, "not_a_real_operation_id") end + ) end test "invalid schema" do @@ -149,14 +158,29 @@ defmodule OpenApiSpex.TestAssertionsTest do assert conn.status == 200 - try do - TestAssertions.assert_operation_response(conn, "showPetById") - raise RuntimeError, "Should flunk" - rescue - e in ExUnit.AssertionError -> - assert e.message =~ - "Value does not conform to schema PetResponse: Failed to cast value to one of: no schemas validate at" - end + assert_raise( + ExUnit.AssertionError, + ~r/Value does not conform to schema PetResponse: Failed to cast value to one of: no schemas validate at/, + fn -> TestAssertions.assert_operation_response(conn, "showPetById") end + ) + end + + test "returns an error when the response content-type does not match the schema" do + conn = + :get + |> Plug.Test.conn("/api/pets") + |> Plug.Conn.put_req_header("content-type", "application/json") + |> Plug.Conn.put_resp_header("content-type", "unexpected-content-type") + + conn = OpenApiSpexTest.Router.call(conn, []) + + assert conn.status == 200 + + assert_raise( + ExUnit.AssertionError, + ~r/Failed to resolve a response schema for operation_id: showPetById for status code: 200 and content type: unexpected-content-type/, + fn -> TestAssertions.assert_operation_response(conn, "showPetById") end + ) end end end