From 13617c6000e85bfab358a704ecb48d5cf7616821 Mon Sep 17 00:00:00 2001 From: Michael Ruoss Date: Sat, 1 Apr 2023 10:58:48 +0200 Subject: [PATCH] tests --- .../admission_control/admission_review.ex | 44 +++--- priv/templates/bonny.gen/init/application.ex | 1 + .../admission_control/webhook_plug_test.exs | 82 +++++++++++ .../admission_control/webhook_router_test.exs | 129 ------------------ test/support/admission_control_helper.ex | 68 +++++++++ test/support/compile_time_assertions.ex | 19 --- 6 files changed, 173 insertions(+), 170 deletions(-) create mode 100644 test/bonny/admission_control/webhook_plug_test.exs delete mode 100644 test/bonny/admission_control/webhook_router_test.exs create mode 100644 test/support/admission_control_helper.ex delete mode 100644 test/support/compile_time_assertions.ex diff --git a/lib/bonny/admission_control/admission_review.ex b/lib/bonny/admission_control/admission_review.ex index 2def083..ee928eb 100644 --- a/lib/bonny/admission_control/admission_review.ex +++ b/lib/bonny/admission_control/admission_review.ex @@ -32,9 +32,9 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.allow(admission_review) - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => true}} + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => true}, webhook_type: :validating} """ @spec allow(t()) :: t() def allow(admission_review) do @@ -46,9 +46,9 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.deny(admission_review) - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false}} + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false}, webhook_type: :validating} """ @spec deny(t()) :: t() def deny(admission_review) do @@ -60,12 +60,12 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.deny(admission_review, 403, "foo") - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 403, "message" => "foo"}}} + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 403, "message" => "foo"}}, webhook_type: :validating} - iex> Bonny.AdmissionControl.AdmissionReview.deny(%Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}}, "foo") - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "foo"}}} + iex> Bonny.AdmissionControl.AdmissionReview.deny(%Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}, webhook_type: :validating}, "foo") + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "foo"}}, webhook_type: :validating} """ @spec deny(t(), integer(), binary()) :: t() @spec deny(t(), binary()) :: t() @@ -80,13 +80,13 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.add_warning(admission_review, "warning") - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["warning"]}} + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["warning"]}, webhook_type: :validating} - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["existing_warning"]}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["existing_warning"]}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.add_warning(admission_review, "new_warning") - %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["new_warning", "existing_warning"]}} + %Bonny.AdmissionControl.AdmissionReview{request: %{}, response: %{"warnings" => ["new_warning", "existing_warning"]}, webhook_type: :validating} """ @spec add_warning(t(), binary()) :: t() def add_warning(admission_review, warning) do @@ -102,13 +102,13 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.check_immutable(admission_review, ["spec", "immutable"]) - %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}} + %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}, webhook_type: :validating} - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.check_immutable(admission_review, ["spec", "immutable"]) - %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "The field .spec.immutable is immutable."}}} + %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"spec" => %{"immutable" => "new_value"}}, "oldObject" => %{"spec" => %{"immutable" => "value"}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => "The field .spec.immutable is immutable."}}, webhook_type: :validating} """ @spec check_immutable(t(), list()) :: t() def check_immutable(admission_review, field) do @@ -126,17 +126,17 @@ defmodule Bonny.AdmissionControl.AdmissionReview do ## Examples - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"]) - %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}} + %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "bar"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}, webhook_type: :validating} - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"]) - %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}} + %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}, webhook_type: :validating} - iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}} + iex> admission_review = %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{}, webhook_type: :validating} ...> Bonny.AdmissionControl.AdmissionReview.check_allowed_values(admission_review, ~w(metadata annotations some/annotation), ["foo", "bar"]) - %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => ~S(The field .metadata.annotations.some/annotation must contain one of the values in ["foo", "bar"] but it's currently set to "other".)}}} + %Bonny.AdmissionControl.AdmissionReview{request: %{"object" => %{"metadata" => %{"annotations" => %{"some/annotation" => "other"}}, "spec" => %{}}, "oldObject" => %{"spec" => %{}}}, response: %{"allowed" => false, "status" => %{"code" => 400, "message" => ~S(The field .metadata.annotations.some/annotation must contain one of the values in ["foo", "bar"] but it's currently set to "other".)}}, webhook_type: :validating} """ @spec check_allowed_values(t(), list(), list()) :: t() def check_allowed_values(admission_review, field, allowed_values) do diff --git a/priv/templates/bonny.gen/init/application.ex b/priv/templates/bonny.gen/init/application.ex index afb405a..9cd20c0 100644 --- a/priv/templates/bonny.gen/init/application.ex +++ b/priv/templates/bonny.gen/init/application.ex @@ -14,6 +14,7 @@ defmodule <%= @app_name %>.Application do defp children(:test), do: [] defp children(env) do [ + {<%= @app_name %>.Operator, conn: <%= @app_name %>.K8sConn.get!(env)} ] end diff --git a/test/bonny/admission_control/webhook_plug_test.exs b/test/bonny/admission_control/webhook_plug_test.exs new file mode 100644 index 0000000..708fb71 --- /dev/null +++ b/test/bonny/admission_control/webhook_plug_test.exs @@ -0,0 +1,82 @@ +defmodule Bonny.AdmissionControl.PlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Bonny.Test.AdmissionControlHelper + alias Bonny.AdmissionControl.AdmissionReview + alias Bonny.AdmissionControl.Plug, as: MUT + + describe "init/1" do + test "raises if webhook_type is not declared" do + assert_raise(CompileError, ~r/requires you to define the :webhook_type option/, fn -> + MUT.init(webhook_handler: SomeModule) + end) + end + + test "raises if webhook_type is not :validating or :mutating" do + assert_raise(CompileError, ~r/requires you to define the :webhook_type option/, fn -> + MUT.init(webhook_handler: SomeModule, webhook_type: :invalid) + end) + end + + test "raises if webhook_handler is not declared" do + assert_raise(CompileError, ~r/requires you to set the :webhook_handler option/, fn -> + MUT.init(webhook_type: :validating) + end) + end + + test "turns webhook_handler into {module, opts} tuple" do + opts = MUT.init(webhook_type: :validating, webhook_handler: SomeModule) + assert opts.webhook_handler == {SomeModule, []} + end + + defmodule InitTestHandler do + def init(:foo), do: :bar + end + + test "calls handler's init function if tuple is given" do + opts = MUT.init(webhook_type: :validating, webhook_handler: {InitTestHandler, :foo}) + assert opts.webhook_handler == {InitTestHandler, :bar} + end + end + + defmodule CallTestHandler do + def call(admission_webhook, opts) do + case opts[:result] do + :deny -> AdmissionReview.deny(admission_webhook) + _ -> admission_webhook + end + end + end + + describe "call/2" do + test "calls the handler and returns plug" do + response = + AdmissionControlHelper.webhook_request_conn() + |> MUT.call(%{webhook_type: :validation, webhook_handler: {CallTestHandler, []}}) + |> Map.get(:resp_body) + |> Jason.decode!() + + assert %{ + "apiVersion" => "admission.k8s.io/v1", + "kind" => "AdmissionReview", + "response" => %{ + "allowed" => true + } + } = response + end + + test "calls the handler and returns allowed false" do + response = + AdmissionControlHelper.webhook_request_conn() + |> MUT.call(%{ + webhook_type: :validation, + webhook_handler: {CallTestHandler, [result: :deny]} + }) + |> Map.get(:resp_body) + |> Jason.decode!() + + assert false == response["response"]["allowed"] + end + end +end diff --git a/test/bonny/admission_control/webhook_router_test.exs b/test/bonny/admission_control/webhook_router_test.exs deleted file mode 100644 index f2371a3..0000000 --- a/test/bonny/admission_control/webhook_router_test.exs +++ /dev/null @@ -1,129 +0,0 @@ -defmodule Bonny.AdmissionControl.WebhookRouterTest do - use ExUnit.Case, async: true - use Plug.Test - - alias Bonny.Test.Plug.WebhookHandlerCRD - alias Bonny.AdmissionControl.WebhookRouter, as: MUT - - describe "call/2" do - test "return 404 if not POST request" do - conn = - conn(:get, "/some_url") - |> Map.put(:body_params, nil) - |> MUT.call(webhook_type: :some_type) - - assert conn.status == 404 - end - - test "return 400 if no body given" do - conn = - conn(:post, "/some_url") - |> Map.put(:body_params, nil) - |> MUT.call(webhook_type: :some_type) - - assert conn.status == 400 - end - - test "return 400 for non-json body" do - conn = - conn(:post, "/some_url") - |> Map.put(:body_params, "") - |> MUT.call(webhook_type: :some_type) - - assert conn.status == 400 - end - - test "return 400 for json body that is not an admission review" do - conn = - conn(:post, "/some_url") - |> Map.put(:body_params, %{"foo" => "bar"}) - |> MUT.call(webhook_type: :some_type) - - assert conn.status == 400 - end - - test "returns allow: true if no handler" do - body = - conn(:post, "/some_url", %{ - "apiVersion" => "admission.k8s.io/v1", - "kind" => "AdmissionReview", - "request" => %{"uid" => "some_uid"} - }) - |> MUT.call(webhook_type: :some_type, handlers: []) - |> Map.get(:resp_body) - |> Jason.decode!() - - assert true == get_in(body, ~w(response allowed)) - assert "some_uid" == get_in(body, ~w(response uid)) - end - - test "returns allow: true if webhook not handled" do - opts = MUT.init(webhook_type: :validating_webhook, handlers: [WebhookHandlerCRD]) - - body = - conn(:post, "/some_url", %{ - "apiVersion" => "admission.k8s.io/v1", - "kind" => "AdmissionReview", - "request" => %{"uid" => "some_uid"} - }) - |> MUT.call(opts) - |> Map.get(:resp_body) - |> Jason.decode!() - - assert true == get_in(body, ~w(response allowed)) - assert "some_uid" == get_in(body, ~w(response uid)) - end - - test "returns handler's response if handler processes request" do - opts = MUT.init(webhook_type: :validating_webhook, handlers: [WebhookHandlerCRD]) - - body = - conn(:post, "/some_url", %{ - "apiVersion" => "admission.k8s.io/v1", - "kind" => "AdmissionReview", - "request" => %{ - "uid" => "some_uid", - "resource" => %{ - "group" => "example.com", - "version" => "v1", - "resource" => "testresourcev3s" - } - } - }) - |> MUT.call(opts) - |> Map.get(:resp_body) - |> Jason.decode!() - - assert false == get_in(body, ~w(response allowed)) - assert "some_uid" == get_in(body, ~w(response uid)) - end - - test "returns handler's response if handler passed via config" do - Application.put_env(:bonny_plug, Bonny.AdmissionControl.WebhookPlug, - handlers: [WebhookHandlerCRD] - ) - - opts = MUT.init(webhook_type: :validating_webhook) - - body = - conn(:post, "/some_url", %{ - "apiVersion" => "admission.k8s.io/v1", - "kind" => "AdmissionReview", - "request" => %{ - "uid" => "some_uid", - "resource" => %{ - "group" => "example.com", - "version" => "v1", - "resource" => "testresourcev3s" - } - } - }) - |> MUT.call(opts) - |> Map.get(:resp_body) - |> Jason.decode!() - - assert false == get_in(body, ~w(response allowed)) - assert "some_uid" == get_in(body, ~w(response uid)) - end - end -end diff --git a/test/support/admission_control_helper.ex b/test/support/admission_control_helper.ex new file mode 100644 index 0000000..0ad275b --- /dev/null +++ b/test/support/admission_control_helper.ex @@ -0,0 +1,68 @@ +defmodule Bonny.Test.AdmissionControlHelper do + use Plug.Test + + def webhook_request_conn() do + body = """ + { + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "request": { + "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", + "kind": { + "group": "example.com", + "version": "v1alpha1", + "kind": "SomeCRD" + }, + "resource": { + "group": "example.com", + "version": "v1alpha1", + "resource": "somecrds" + }, + "requestKind": { + "group": "example.com", + "version": "v1alpha1", + "kind": "SomeCRD" + }, + "requestResource": { + "group": "example.com", + "version": "v1alpha1", + "resource": "somecrds" + }, + "name": "my-deployment", + "namespace": "my-namespace", + "operation": "UPDATE", + "userInfo": { + "username": "admin", + "uid": "014fbff9a07c", + "groups": [ + "system:authenticated", + "my-admin-group" + ], + "extra": { + "some-key": [ + "some-value1", + "some-value2" + ] + } + }, + "object": { + "apiVersion": "autoscaling/v1", + "kind": "Scale" + }, + "oldObject": { + "apiVersion": "autoscaling/v1", + "kind": "Scale" + }, + "options": { + "apiVersion": "meta.k8s.io/v1", + "kind": "UpdateOptions" + }, + "dryRun": false + } + } + """ + + conn("POST", "/webhook", body) + |> put_req_header("content-type", "application/json") + end +end diff --git a/test/support/compile_time_assertions.ex b/test/support/compile_time_assertions.ex deleted file mode 100644 index b360f02..0000000 --- a/test/support/compile_time_assertions.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Bonny.Test.CompileTimeAssertions do - defmodule DidNotRaise, do: defstruct(message: nil) - - defmacro assert_compile_time_raise(expected_exception, expected_message, fun) do - actual_exception = - try do - Code.eval_quoted(fun) - %DidNotRaise{} - rescue - e -> e - end - |> Macro.escape() - - quote do - assert is_struct(unquote(actual_exception), unquote(expected_exception)) - assert Exception.message(unquote(actual_exception)) =~ unquote(expected_message) - end - end -end