Skip to content

Commit

Permalink
Customize check action (#561)
Browse files Browse the repository at this point in the history
* Add openapi schemas

* Enhance Fallback controller and error json view

* Add customization controller and initial action

* Add extra tests for follback controller

* Add extra validation tests

* Adjust custom value openapi schema description

* Adjust matching in error json
  • Loading branch information
nelsonkopliku authored Jan 30, 2025
1 parent 057a909 commit 22c0419
Show file tree
Hide file tree
Showing 13 changed files with 616 additions and 10 deletions.
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ config :joken,

config :unplug, :init_mode, :runtime

config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache

# Override with local dev.local.exs file
if File.exists?("#{__DIR__}/dev.local.exs") do
import_config "dev.local.exs"
Expand Down
22 changes: 22 additions & 0 deletions lib/wanda_web/controllers/error_json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ defmodule WandaWeb.ErrorJSON do
}
end

def render("404.json", %{reason: reason}) when not is_exception(reason, Ecto.NoResultsError) do
%{
errors: [
%{
title: "Not Found",
detail: reason
}
]
}
end

def render("404.json", _) do
%{
errors: [
Expand All @@ -21,6 +32,17 @@ defmodule WandaWeb.ErrorJSON do
}
end

def render("400.json", %{reason: reason}) do
%{
errors: [
%{
title: "Bad Request",
detail: reason
}
]
}
end

def render("403.json", %{reason: reason}) do
%{
errors: [
Expand Down
24 changes: 24 additions & 0 deletions lib/wanda_web/controllers/fallback_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ defmodule WandaWeb.FallbackController do
|> render(:"403")
end

def call(conn, {:error, :check_not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: ErrorJSON)
|> render(:"404", reason: "Referenced check was not found.")
end

def call(conn, {:error, :check_not_customizable}) do
conn
|> put_status(:forbidden)
|> put_view(json: ErrorJSON)
|> render(:"403", reason: "Referenced check is not customizable.")
end

def call(conn, {:error, :invalid_custom_values}) do
conn
|> put_status(:bad_request)
|> put_view(json: ErrorJSON)
|> render(:"400",
reason:
"Some of the custom values do not exist in the check, they're not customizable or a type mismatch occurred."
)
end

def call(conn, _) do
conn
|> put_status(:internal_server_error)
Expand Down
51 changes: 51 additions & 0 deletions lib/wanda_web/controllers/v1/checks_customizations_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule WandaWeb.V1.ChecksCustomizationsController do
use WandaWeb, :controller
use OpenApiSpex.ControllerSpecs

alias OpenApiSpex.Schema

alias WandaWeb.Schemas.{BadRequest, Forbidden}
alias WandaWeb.Schemas.V1.ChecksCustomizations.{CustomizationRequest, CustomizationResponse}

alias Wanda.ChecksCustomizations

plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true
action_fallback WandaWeb.FallbackController

operation :apply_custom_values,
summary: "Apply custom values for a specific check",
parameters: [
check_id: [
in: :path,
description: "Identifier of the specific check that is being customized",
type: %Schema{
type: :string
},
example: "ABC123"
],
group_id: [
in: :path,
description: "Identifier of the group for which a custom value should be applied",
type: %Schema{
type: :string,
format: :uuid
},
example: "00000000-0000-0000-0000-000000000001"
]
],
request_body: {"Custom Values", "application/json", CustomizationRequest},
responses: [
ok: {"Check Customizations", "application/json", CustomizationResponse},
forbidden: Forbidden.response(),
bad_request: BadRequest.response(),
unprocessable_entity: OpenApiSpex.JsonErrorResponse.response()
]

def apply_custom_values(conn, %{check_id: check_id, group_id: group_id}) do
%{values: custom_values} = OpenApiSpex.body_params(conn)

with {:ok, customization} <- ChecksCustomizations.customize(check_id, group_id, custom_values) do
render(conn, :check_customization, %{customization: customization})
end
end
end
13 changes: 13 additions & 0 deletions lib/wanda_web/controllers/v1/checks_customizations_json.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule WandaWeb.V1.ChecksCustomizationsJSON do
alias Wanda.Catalog.CheckCustomization

def check_customization(%{
customization: %CheckCustomization{
custom_values: values
}
}) do
%{
values: values
}
end
end
4 changes: 4 additions & 0 deletions lib/wanda_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ defmodule WandaWeb.Router do
get "/groups/:id/executions/last", ExecutionController, :last
post "/executions/start", ExecutionController, :start
get "/catalog", CatalogController, :catalog

post "/:check_id/customize/:group_id",
ChecksCustomizationsController,
:apply_custom_values
end

if Application.compile_env!(:wanda, :operations_enabled) do
Expand Down
42 changes: 42 additions & 0 deletions lib/wanda_web/schemas/forbidden.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defmodule WandaWeb.Schemas.Forbidden do
@moduledoc """
403 - Forbidden
"""

alias OpenApiSpex.Operation
alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(
%{
title: "Forbidden",
type: :object,
additionalProperties: false,
properties: %{
errors: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
detail: %Schema{
type: :string,
example: "The requested operation could not be performed."
},
title: %Schema{type: :string, example: "Forbidden"}
}
}
}
}
},
struct?: false
)

def response do
Operation.response(
"Forbidden",
"application/json",
__MODULE__
)
end
end
31 changes: 31 additions & 0 deletions lib/wanda_web/schemas/v1/checks_customizations/custom_value.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule WandaWeb.Schemas.V1.ChecksCustomizations.CustomValue do
@moduledoc """
Custom value to be applied or already applied to a check
"""

alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(
%{
title: "CustomValue",
description: "A single custom value to be applied or already applied to a check",
type: :object,
additionalProperties: false,
properties: %{
name: %Schema{type: :string, description: "Name of the specific value to be customized"},
value: %Schema{
description: "Overriding value",
oneOf: [
%Schema{type: :string},
%Schema{type: :number},
%Schema{type: :boolean}
]
}
},
required: [:name, :value]
},
struct?: false
)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule WandaWeb.Schemas.V1.ChecksCustomizations.CustomizationRequest do
@moduledoc """
Request to customize a check
"""

alias OpenApiSpex.Schema
alias WandaWeb.Schemas.V1.ChecksCustomizations.CustomValue

require OpenApiSpex

OpenApiSpex.schema(
%{
title: "CustomizationRequest",
description: "Request to customize a check",
type: :object,
additionalProperties: false,
minProperties: 1,
properties: %{
values: %Schema{
type: :array,
description: "List of values to customize",
items: CustomValue,
minItems: 1
}
},
required: [:values]
},
struct?: false
)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
defmodule WandaWeb.Schemas.V1.ChecksCustomizations.CustomizationResponse do
@moduledoc """
Response for a customization operation
"""
alias WandaWeb.Schemas.V1.ChecksCustomizations.CustomValue

alias OpenApiSpex.Schema

require OpenApiSpex

OpenApiSpex.schema(
%{
title: "CustomizationResponse",
description: "Response for a customization",
type: :object,
additionalProperties: false,
properties: %{
values: %Schema{
type: :array,
description: "List of the custom values applied",
items: CustomValue
}
}
},
struct?: false
)
end
18 changes: 17 additions & 1 deletion test/wanda_web/controllers/error_json_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,30 @@ defmodule WandaWeb.ErrorJSONTest do
}
end

test "renders 404.json" do
test "renders a default 404.json" do
assert ErrorJSON.render("404.json", %{}) == %{
errors: [
%{detail: "The requested resource was not found.", title: "Not Found"}
]
}
end

test "renders 404.json with custom reason" do
assert ErrorJSON.render("404.json", %{reason: "Custom reason."}) == %{
errors: [
%{detail: "Custom reason.", title: "Not Found"}
]
}
end

test "renders 400.json" do
assert ErrorJSON.render("400.json", %{reason: "Bad request reason."}) == %{
errors: [
%{detail: "Bad request reason.", title: "Bad Request"}
]
}
end

test "renders a default 403.json" do
assert ErrorJSON.render("403.json", %{}) == %{
errors: [
Expand Down
64 changes: 55 additions & 9 deletions test/wanda_web/controllers/fallback_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,61 @@ defmodule WandaWeb.FallbackControllerTest do
end

test "should return a 403 on forbidden requests", %{conn: conn} do
conn =
conn
|> Phoenix.Controller.accepts(["json"])
|> FallbackController.call({:error, :forbidden})
errors_raising_forbidden = [
{:forbidden, "Unauthorized to perform operation."},
{:check_not_customizable, "Referenced check is not customizable."}
]

assert %{
"errors" => [
%{"detail" => "Unauthorized to perform operation.", "title" => "Forbidden"}
]
} == json_response(conn, 403)
for {error, message} <- errors_raising_forbidden do
conn =
conn
|> Phoenix.Controller.accepts(["json"])
|> FallbackController.call({:error, error})

assert %{
"errors" => [
%{"detail" => message, "title" => "Forbidden"}
]
} == json_response(conn, 403)
end
end

test "should return a 404 on relevant errors", %{conn: conn} do
errors_raising_not_found = [
{:check_not_found, "Referenced check was not found."}
]

for {error, message} <- errors_raising_not_found do
conn =
conn
|> Phoenix.Controller.accepts(["json"])
|> FallbackController.call({:error, error})

assert %{
"errors" => [
%{"detail" => message, "title" => "Not Found"}
]
} == json_response(conn, 404)
end
end

test "should return a 400 on relevant errors", %{conn: conn} do
errors_raising_bad_request = [
{:invalid_custom_values,
"Some of the custom values do not exist in the check, they're not customizable or a type mismatch occurred."}
]

for {error, message} <- errors_raising_bad_request do
conn =
conn
|> Phoenix.Controller.accepts(["json"])
|> FallbackController.call({:error, error})

assert %{
"errors" => [
%{"detail" => message, "title" => "Bad Request"}
]
} == json_response(conn, 400)
end
end
end
Loading

0 comments on commit 22c0419

Please sign in to comment.