From fbe52d70681fa4b6105fc369d071af137a050f88 Mon Sep 17 00:00:00 2001 From: Yashin Santos Date: Sun, 4 Oct 2020 11:57:59 -0300 Subject: [PATCH 1/4] feat: create admin endpoint --- .../lib/identity/commands/create_identity.ex | 9 +++ apps/rest_api/README.md | 25 +++++- apps/rest_api/lib/controllers/admin/user.ex | 29 +++++++ apps/rest_api/lib/ports/authenticator.ex | 7 ++ apps/rest_api/lib/ports/resource_manager.ex | 22 ++++++ apps/rest_api/lib/routers/public.ex | 6 ++ apps/rest_api/lib/views/admin/user.ex | 16 ++++ .../test/controllers/admin/user_test.exs | 76 +++++++++++++++++++ apps/rest_api/test/support/mocks.ex | 5 +- config/config.exs | 1 + config/test.exs | 1 + 11 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 apps/rest_api/lib/controllers/admin/user.ex create mode 100644 apps/rest_api/lib/ports/resource_manager.ex create mode 100644 apps/rest_api/lib/views/admin/user.ex create mode 100644 apps/rest_api/test/controllers/admin/user_test.exs diff --git a/apps/resource_manager/lib/identity/commands/create_identity.ex b/apps/resource_manager/lib/identity/commands/create_identity.ex index 7ed95de..19334ab 100644 --- a/apps/resource_manager/lib/identity/commands/create_identity.ex +++ b/apps/resource_manager/lib/identity/commands/create_identity.ex @@ -82,6 +82,15 @@ defmodule ResourceManager.Identity.Commands.CreateIdentity do end end + def execute(%{"username" => _, "password_hash" => _} = params) do + params + |> CreateUser.cast_and_apply() + |> case do + {:ok, %CreateUser{} = input} -> execute(input) + error -> error + end + end + def execute(%{username: _, password_hash: _} = params) do params |> CreateUser.cast_and_apply() diff --git a/apps/rest_api/README.md b/apps/rest_api/README.md index 9b78350..bb32ad8 100644 --- a/apps/rest_api/README.md +++ b/apps/rest_api/README.md @@ -133,4 +133,27 @@ curl -X POST http://localhost:4000/api/v1/auth/protocol/openid-connect/logout \ **Response (204)**: -`No content` \ No newline at end of file +`No content` + +### Create an user + +**Request**: + +```sh +curl -X POST http://localhost:4000/admin/v1/users \ + -H "Content-Type: application/json" \ +-d '{"username":"yashu", "password":"lcpo", "scopes":["6a3a3771-9f56-4254-9497-927e441dacfc" "8a235ba0-a827-4593-92c9-6248bef4fa06"]}' +``` + +**Response (201)**: + +```json +{ + "id":"0c5fb5a7-5d86-4b11-b4e3-facf925b3e9d", + "inserted_at":"2020-10-04T13:23:45", + "is_admin":false, + "status":"active", + "update_at":"2020-10-04T13:23:45", + "username":"yashu" +} +``` \ No newline at end of file diff --git a/apps/rest_api/lib/controllers/admin/user.ex b/apps/rest_api/lib/controllers/admin/user.ex new file mode 100644 index 0000000..a797f41 --- /dev/null +++ b/apps/rest_api/lib/controllers/admin/user.ex @@ -0,0 +1,29 @@ +defmodule RestAPI.Controller.Admin.User do + @moduledoc false + + use RestAPI.Controller, :controller + + alias RestAPI.Ports.{Authenticator, ResourceManager} + alias RestAPI.Views.Admin.User + + action_fallback RestAPI.Controllers.Fallback + + def create(conn, %{"password" => password} = params) do + password_hash = Authenticator.generate_hash(password, :argon2) + + params + |> Map.put("password_hash", password_hash) + |> Map.put("password_algorithm", "argon2") + |> ResourceManager.create_identity() + |> case do + {:ok, response} -> + conn + |> put_status(:created) + |> put_view(User) + |> render("create.json", response: response) + + {:error, _any} = error -> + error + end + end +end diff --git a/apps/rest_api/lib/ports/authenticator.ex b/apps/rest_api/lib/ports/authenticator.ex index 0ab14d7..94d4af0 100644 --- a/apps/rest_api/lib/ports/authenticator.ex +++ b/apps/rest_api/lib/ports/authenticator.ex @@ -42,6 +42,9 @@ defmodule RestAPI.Ports.Authenticator do @callback sign_out_all_sessions(subject_id :: String.t(), subject_type :: String.t()) :: {:ok, count :: integer()} | possible_logout_failures() + @doc "Delegates to Authenticator.generate_hash/2" + @callback generate_hash(password :: String.t(), algorithm :: atom()) :: String.t() + @doc "Authenticates the subject using Resource Owner Flow" @spec sign_in_resource_owner(input :: map()) :: possible_sign_in_responses() def sign_in_resource_owner(input), do: implementation().sign_in_resource_owner(input) @@ -71,6 +74,10 @@ defmodule RestAPI.Ports.Authenticator do {:ok, count :: integer()} | possible_logout_failures() def sign_out_all_sessions(sub, type), do: implementation().sign_out_all_sessions(sub, type) + @doc "Generate a hash using the given password and algorithm" + @spec generate_hash(password :: String.t(), algorithm :: atom()) :: String.t() + def generate_hash(password, algorithm), do: implementation().generate_hash(password, algorithm) + defp implementation do :rest_api |> Application.get_env(__MODULE__) diff --git a/apps/rest_api/lib/ports/resource_manager.ex b/apps/rest_api/lib/ports/resource_manager.ex new file mode 100644 index 0000000..477c2b9 --- /dev/null +++ b/apps/rest_api/lib/ports/resource_manager.ex @@ -0,0 +1,22 @@ +defmodule RestAPI.Ports.ResourceManager do + @moduledoc """ + Port to access Authenticator domain commands. + """ + + @typedoc "All possible create_identity responses" + @type possible_create_identity_response :: + {:ok, struct()} | {:error, Ecto.Changeset.t() | :invalid_params} + + @doc "Delegates to Authenticator.sign_in_resource_owner/1" + @callback create_identity(input :: map()) :: possible_create_identity_response() + + @doc "Authenticates the subject using Resource Owner Flow" + @spec create_identity(input :: map()) :: possible_create_identity_response() + def create_identity(input), do: implementation().create_identity(input) + + defp implementation do + :rest_api + |> Application.get_env(__MODULE__) + |> Keyword.get(:domain) + end +end diff --git a/apps/rest_api/lib/routers/public.ex b/apps/rest_api/lib/routers/public.ex index a3dbacf..7a6ebd0 100644 --- a/apps/rest_api/lib/routers/public.ex +++ b/apps/rest_api/lib/routers/public.ex @@ -28,4 +28,10 @@ defmodule RestAPI.Routers.Public do end end end + + scope "/admin/v1", RestAPI.Controller.Admin do + # pipe_through :authenticated + + resources("/users", User, except: [:new]) + end end diff --git a/apps/rest_api/lib/views/admin/user.ex b/apps/rest_api/lib/views/admin/user.ex new file mode 100644 index 0000000..d2696ba --- /dev/null +++ b/apps/rest_api/lib/views/admin/user.ex @@ -0,0 +1,16 @@ +defmodule RestAPI.Views.Admin.User do + @moduledoc false + + use RestAPI.View + + def render("create.json", %{response: response}) do + %{ + id: response.id, + username: response.username, + status: response.status, + is_admin: response.is_admin, + inserted_at: response.inserted_at, + update_at: response.updated_at + } + end +end diff --git a/apps/rest_api/test/controllers/admin/user_test.exs b/apps/rest_api/test/controllers/admin/user_test.exs new file mode 100644 index 0000000..1fc2eea --- /dev/null +++ b/apps/rest_api/test/controllers/admin/user_test.exs @@ -0,0 +1,76 @@ +defmodule RestAPI.Controllers.Admin.User do + use RestAPI.ConnCase, async: true + + alias RestAPI.Ports.{AuthenticatorMock, ResourceManagerMock} + alias ResourceManager.Identity.Commands.Inputs.CreateUser + + @create_endpoint "/admin/v1/users" + + describe "POST #{@create_endpoint}" do + test "should render user identity response", %{conn: conn} do + password = "MyP@ssword" + + params = %{ + "username" => "Shurato", + "password" => password, + "scopes" => [ + "6a3a3771-9f56-4254-9497-927e441dacfc", + "8a235ba0-a827-4593-92c9-6248bef4fa06" + ] + } + + expect(AuthenticatorMock, :generate_hash, fn password_to_hash, :argon2 -> + assert password = password_to_hash + "password_hashed" + end) + + expect(ResourceManagerMock, :create_identity, fn input -> + assert is_map(input) + + {:ok, + %{ + id: Ecto.UUID.generate(), + inserted_at: NaiveDateTime.utc_now(), + is_admin: false, + status: "active", + updated_at: NaiveDateTime.utc_now(), + username: "Shurato" + }} + end) + + assert %{ + "id" => _id, + "inserted_at" => _inserted_at, + "is_admin" => false, + "status" => "active", + "username" => "Shurato" + } = + conn + |> post(@create_endpoint, params) + |> json_response(201) + end + + test "should return error when params is not valid", %{conn: conn} do + password = "MyP@ssword" + + expect(AuthenticatorMock, :generate_hash, fn password_to_hash, :argon2 -> + assert password = password_to_hash + "password_hashed" + end) + + expect(ResourceManagerMock, :create_identity, fn input -> + CreateUser.cast_and_apply(input) + end) + + assert %{ + "detail" => "The given params are invalid", + "error" => "bad_request", + "response" => %{"username" => ["can't be blank"]}, + "status" => 400 + } = + conn + |> post(@create_endpoint, %{"password" => password}) + |> json_response(400) + end + end +end diff --git a/apps/rest_api/test/support/mocks.ex b/apps/rest_api/test/support/mocks.ex index 0f7f537..8242c5f 100644 --- a/apps/rest_api/test/support/mocks.ex +++ b/apps/rest_api/test/support/mocks.ex @@ -1,6 +1,9 @@ for module <- [ # Authenticator domain - RestAPI.Ports.Authenticator + RestAPI.Ports.Authenticator, + + # ResourceManager domain + RestAPI.Ports.ResourceManager ] do Mox.defmock(:"#{module}Mock", for: module) end diff --git a/config/config.exs b/config/config.exs index cdfd3d0..151d3e3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -69,5 +69,6 @@ config :rest_api, RestAPI.Endpoint, config :rest_api, RestAPI.Application, children: [RestAPI.Telemetry, RestAPI.Endpoint] config :rest_api, RestAPI.Ports.Authenticator, domain: Authenticator +config :rest_api, RestAPI.Ports.ResourceManager, domain: ResourceManager import_config "#{Mix.env()}.exs" diff --git a/config/test.exs b/config/test.exs index 34bbde0..01fb1ff 100644 --- a/config/test.exs +++ b/config/test.exs @@ -49,3 +49,4 @@ config :rest_api, RestAPI.Endpoint, server: false config :rest_api, RestAPI.Ports.Authenticator, domain: RestAPI.Ports.AuthenticatorMock +config :rest_api, RestAPI.Ports.ResourceManager, domain: RestAPI.Ports.ResourceManagerMock From 585dfe13dd0dd91db039cd030efeb771d28f9537 Mon Sep 17 00:00:00 2001 From: Yashin Santos Date: Sat, 10 Oct 2020 11:18:24 -0300 Subject: [PATCH 2/4] feat: add password strong validation --- apps/rest_api/lib/controllers/admin/user.ex | 24 +++++++++---------- apps/rest_api/lib/controllers/fallback.ex | 7 ++++++ apps/rest_api/lib/views/errors/default.ex | 9 +++++++ .../test/controllers/admin/user_test.exs | 18 ++++++++++++++ 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/apps/rest_api/lib/controllers/admin/user.ex b/apps/rest_api/lib/controllers/admin/user.ex index a797f41..f01ebb2 100644 --- a/apps/rest_api/lib/controllers/admin/user.ex +++ b/apps/rest_api/lib/controllers/admin/user.ex @@ -9,18 +9,18 @@ defmodule RestAPI.Controller.Admin.User do action_fallback RestAPI.Controllers.Fallback def create(conn, %{"password" => password} = params) do - password_hash = Authenticator.generate_hash(password, :argon2) - - params - |> Map.put("password_hash", password_hash) - |> Map.put("password_algorithm", "argon2") - |> ResourceManager.create_identity() - |> case do - {:ok, response} -> - conn - |> put_status(:created) - |> put_view(User) - |> render("create.json", response: response) + with true <- ResourceManager.is_strong?(password), + password_hash <- Authenticator.generate_hash(password, :argon2), + params <- + Map.merge(params, %{"password_hash" => password_hash, "password_algorithm" => "argon2"}), + {:ok, response} <- ResourceManager.create_identity(params) do + conn + |> put_status(:created) + |> put_view(User) + |> render("create.json", response: response) + else + false -> + {:error, 422, %{error: :not_strong_enough, password: password}} {:error, _any} = error -> error diff --git a/apps/rest_api/lib/controllers/fallback.ex b/apps/rest_api/lib/controllers/fallback.ex index d4984b7..f963dca 100644 --- a/apps/rest_api/lib/controllers/fallback.ex +++ b/apps/rest_api/lib/controllers/fallback.ex @@ -42,6 +42,13 @@ defmodule RestAPI.Controllers.Fallback do |> render("changeset.json", response: changeset) end + def call(conn, {:error, status, response}) when status in [:unprocessable_entity, 422] do + conn + |> put_status(:unprocessable_entity) + |> put_view(Default) + |> render("422.json", response: response) + end + def call(conn, {:error, _unknown_error}) do conn |> put_status(:internal_server_error) diff --git a/apps/rest_api/lib/views/errors/default.ex b/apps/rest_api/lib/views/errors/default.ex index defd143..7305e36 100644 --- a/apps/rest_api/lib/views/errors/default.ex +++ b/apps/rest_api/lib/views/errors/default.ex @@ -52,6 +52,15 @@ defmodule RestAPI.Views.Errors.Default do } end + def render("422.json", %{response: response}) do + %{ + status: 422, + detail: "The given params failed in validation", + response: response, + error: "unprocessable entity" + } + end + def template_not_found(_template, _assigns) do %{ status: 501, diff --git a/apps/rest_api/test/controllers/admin/user_test.exs b/apps/rest_api/test/controllers/admin/user_test.exs index 1fc2eea..210982a 100644 --- a/apps/rest_api/test/controllers/admin/user_test.exs +++ b/apps/rest_api/test/controllers/admin/user_test.exs @@ -72,5 +72,23 @@ defmodule RestAPI.Controllers.Admin.User do |> post(@create_endpoint, %{"password" => password}) |> json_response(400) end + + test "should return error when password is not strong enough", %{conn: conn} do + password = "MyP@ssword" + + expect(ResourceManagerMock, :is_strong?, fn _input -> + false + end) + + assert %{ + "detail" => "The given params failed in validation", + "error" => "unprocessable entity", + "response" => %{"error" => "not_strong_enough", "password" => "MyP@ssword"}, + "status" => 422 + } = + conn + |> post(@create_endpoint, %{"password" => password}) + |> json_response(422) + end end end From ebeded0525b0217c213f666497bc72b06996a87d Mon Sep 17 00:00:00 2001 From: Yashin Santos Date: Sat, 10 Oct 2020 11:39:04 -0300 Subject: [PATCH 3/4] feat: use authentication in admin endpoint --- apps/rest_api/lib/routers/public.ex | 2 +- .../test/controllers/admin/user_test.exs | 70 +++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/apps/rest_api/lib/routers/public.ex b/apps/rest_api/lib/routers/public.ex index 7a6ebd0..2af2f57 100644 --- a/apps/rest_api/lib/routers/public.ex +++ b/apps/rest_api/lib/routers/public.ex @@ -30,7 +30,7 @@ defmodule RestAPI.Routers.Public do end scope "/admin/v1", RestAPI.Controller.Admin do - # pipe_through :authenticated + pipe_through :authenticated resources("/users", User, except: [:new]) end diff --git a/apps/rest_api/test/controllers/admin/user_test.exs b/apps/rest_api/test/controllers/admin/user_test.exs index 210982a..98622e4 100644 --- a/apps/rest_api/test/controllers/admin/user_test.exs +++ b/apps/rest_api/test/controllers/admin/user_test.exs @@ -7,8 +7,33 @@ defmodule RestAPI.Controllers.Admin.User do @create_endpoint "/admin/v1/users" describe "POST #{@create_endpoint}" do - test "should render user identity response", %{conn: conn} do - password = "MyP@ssword" + setup do + access_token = "my-access-token" + claims = default_claims() + + expect(AuthenticatorMock, :validate_access_token, fn token -> + assert access_token == token + {:ok, claims} + end) + + expect(AuthenticatorMock, :get_session, fn %{"jti" => jti} -> + assert claims["jti"] == jti + {:ok, success_session(claims)} + end) + + expect(AuthenticatorMock, :sign_out_session, fn jti -> + assert claims["jti"] == jti + {:ok, %{}} + end) + + {:ok, access_token: access_token} + end + + test "should render user identity response", %{ + conn: conn, + access_token: access_token + } do + password = "MyP@ssword1234" params = %{ "username" => "Shurato", @@ -46,11 +71,12 @@ defmodule RestAPI.Controllers.Admin.User do "username" => "Shurato" } = conn + |> put_req_header("authorization", "Bearer #{access_token}") |> post(@create_endpoint, params) |> json_response(201) end - test "should return error when params is not valid", %{conn: conn} do + test "should return error when params is not valid", %{conn: conn, access_token: access_token} do password = "MyP@ssword" expect(AuthenticatorMock, :generate_hash, fn password_to_hash, :argon2 -> @@ -69,11 +95,15 @@ defmodule RestAPI.Controllers.Admin.User do "status" => 400 } = conn + |> put_req_header("authorization", "Bearer #{access_token}") |> post(@create_endpoint, %{"password" => password}) |> json_response(400) end - test "should return error when password is not strong enough", %{conn: conn} do + test "should return error when password is not strong enough", %{ + conn: conn, + access_token: access_token + } do password = "MyP@ssword" expect(ResourceManagerMock, :is_strong?, fn _input -> @@ -87,8 +117,40 @@ defmodule RestAPI.Controllers.Admin.User do "status" => 422 } = conn + |> put_req_header("authorization", "Bearer #{access_token}") |> post(@create_endpoint, %{"password" => password}) |> json_response(422) end end + + defp default_claims do + %{ + "jti" => "03eds74a-c291-4b5f", + "aud" => "02eff74a-c291-4b5f-a02f-4f92d8daf693", + "azp" => "my-application", + "sub" => "272459ce-7356-4460-b461-1ecf0ebf7c4e", + "typ" => "Bearer", + "identity" => "user", + "scope" => "admin:read" + } + end + + defp success_session(claims) do + %{ + id: "02eff44a-c291-4b5f-a02f-4f92d8dbf693", + jti: claims["jti"], + subject_id: claims["sub"], + subject_type: claims["identity"], + expires_at: claims["expires_at"], + scopes: parse_scopes(claims["scope"]), + azp: claims["azp"], + claims: claims + } + end + + defp parse_scopes(scope) when is_binary(scope) do + scope + |> String.split(" ", trim: true) + |> Enum.map(& &1) + end end From 9ecdfb71fe2643528895ef95c378b863a1dea9a5 Mon Sep 17 00:00:00 2001 From: Yashin Santos Date: Sat, 10 Oct 2020 12:48:25 -0300 Subject: [PATCH 4/4] chore: use password_allowed? --- apps/rest_api/lib/controllers/admin/user.ex | 4 +- apps/rest_api/lib/controllers/fallback.ex | 19 ++-- apps/rest_api/lib/ports/resource_manager.ex | 11 ++- apps/rest_api/lib/views/errors/default.ex | 27 ++++-- .../test/controllers/admin/user_test.exs | 92 +++++++++++++------ .../test/controllers/fallback_test.exs | 2 +- 6 files changed, 106 insertions(+), 49 deletions(-) diff --git a/apps/rest_api/lib/controllers/admin/user.ex b/apps/rest_api/lib/controllers/admin/user.ex index f01ebb2..eb1a14b 100644 --- a/apps/rest_api/lib/controllers/admin/user.ex +++ b/apps/rest_api/lib/controllers/admin/user.ex @@ -9,7 +9,7 @@ defmodule RestAPI.Controller.Admin.User do action_fallback RestAPI.Controllers.Fallback def create(conn, %{"password" => password} = params) do - with true <- ResourceManager.is_strong?(password), + with true <- ResourceManager.password_allowed?(password), password_hash <- Authenticator.generate_hash(password, :argon2), params <- Map.merge(params, %{"password_hash" => password_hash, "password_algorithm" => "argon2"}), @@ -20,7 +20,7 @@ defmodule RestAPI.Controller.Admin.User do |> render("create.json", response: response) else false -> - {:error, 422, %{error: :not_strong_enough, password: password}} + {:error, 400, %{password: ["password is not strong enough"]}} {:error, _any} = error -> error diff --git a/apps/rest_api/lib/controllers/fallback.ex b/apps/rest_api/lib/controllers/fallback.ex index f963dca..0902f9f 100644 --- a/apps/rest_api/lib/controllers/fallback.ex +++ b/apps/rest_api/lib/controllers/fallback.ex @@ -14,6 +14,13 @@ defmodule RestAPI.Controllers.Fallback do |> render("400.json") end + def call(conn, {:error, status, response}) when status in [:bad_request, 400] do + conn + |> put_status(status) + |> put_view(Default) + |> render("400.json", response: response) + end + def call(conn, {:error, :unauthorized}) do conn |> put_status(:unauthorized) @@ -35,18 +42,18 @@ defmodule RestAPI.Controllers.Fallback do |> render("404.json") end - def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + def call(conn, {:error, status, response}) when status in [:unprocessable_entity, 422] do conn - |> put_status(:bad_request) + |> put_status(status) |> put_view(Default) - |> render("changeset.json", response: changeset) + |> render("422.json", response: response) end - def call(conn, {:error, status, response}) when status in [:unprocessable_entity, 422] do + def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn - |> put_status(:unprocessable_entity) + |> put_status(:bad_request) |> put_view(Default) - |> render("422.json", response: response) + |> render("changeset.json", response: changeset) end def call(conn, {:error, _unknown_error}) do diff --git a/apps/rest_api/lib/ports/resource_manager.ex b/apps/rest_api/lib/ports/resource_manager.ex index 477c2b9..2ae1538 100644 --- a/apps/rest_api/lib/ports/resource_manager.ex +++ b/apps/rest_api/lib/ports/resource_manager.ex @@ -7,13 +7,20 @@ defmodule RestAPI.Ports.ResourceManager do @type possible_create_identity_response :: {:ok, struct()} | {:error, Ecto.Changeset.t() | :invalid_params} - @doc "Delegates to Authenticator.sign_in_resource_owner/1" + @doc "Delegates to ResourceManager.create_identity/1" @callback create_identity(input :: map()) :: possible_create_identity_response() - @doc "Authenticates the subject using Resource Owner Flow" + @doc "Delegates to ResourceManager.password_allowed?/1" + @callback password_allowed?(password :: String.t()) :: boolean() + + @doc "Create a new identity with it's credentials" @spec create_identity(input :: map()) :: possible_create_identity_response() def create_identity(input), do: implementation().create_identity(input) + @doc "Checks if the given password is strong enough to be used" + @spec password_allowed?(password :: String.t()) :: boolean() + def password_allowed?(password), do: implementation().password_allowed?(password) + defp implementation do :rest_api |> Application.get_env(__MODULE__) diff --git a/apps/rest_api/lib/views/errors/default.ex b/apps/rest_api/lib/views/errors/default.ex index 7305e36..bd62a2b 100644 --- a/apps/rest_api/lib/views/errors/default.ex +++ b/apps/rest_api/lib/views/errors/default.ex @@ -3,6 +3,15 @@ defmodule RestAPI.Views.Errors.Default do use RestAPI.View + def render("400.json", %{response: response}) do + %{ + status: 400, + error: "bad_request", + detail: "The given params failed in validation", + response: response + } + end + def render("400.json", _assigns) do %{ status: 400, @@ -43,15 +52,6 @@ defmodule RestAPI.Views.Errors.Default do } end - def render("changeset.json", %{response: response}) do - %{ - status: 400, - detail: "The given params are invalid", - response: Ecto.Changeset.traverse_errors(response, &translate_error/1), - error: "bad_request" - } - end - def render("422.json", %{response: response}) do %{ status: 422, @@ -61,6 +61,15 @@ defmodule RestAPI.Views.Errors.Default do } end + def render("changeset.json", %{response: response}) do + %{ + status: 400, + detail: "The given params failed in validation", + response: Ecto.Changeset.traverse_errors(response, &translate_error/1), + error: "bad_request" + } + end + def template_not_found(_template, _assigns) do %{ status: 501, diff --git a/apps/rest_api/test/controllers/admin/user_test.exs b/apps/rest_api/test/controllers/admin/user_test.exs index 98622e4..b0b3015 100644 --- a/apps/rest_api/test/controllers/admin/user_test.exs +++ b/apps/rest_api/test/controllers/admin/user_test.exs @@ -1,8 +1,8 @@ defmodule RestAPI.Controllers.Admin.User do use RestAPI.ConnCase, async: true - alias RestAPI.Ports.{AuthenticatorMock, ResourceManagerMock} alias ResourceManager.Identity.Commands.Inputs.CreateUser + alias RestAPI.Ports.{AuthenticatorMock, ResourceManagerMock} @create_endpoint "/admin/v1/users" @@ -11,27 +11,13 @@ defmodule RestAPI.Controllers.Admin.User do access_token = "my-access-token" claims = default_claims() - expect(AuthenticatorMock, :validate_access_token, fn token -> - assert access_token == token - {:ok, claims} - end) - - expect(AuthenticatorMock, :get_session, fn %{"jti" => jti} -> - assert claims["jti"] == jti - {:ok, success_session(claims)} - end) - - expect(AuthenticatorMock, :sign_out_session, fn jti -> - assert claims["jti"] == jti - {:ok, %{}} - end) - - {:ok, access_token: access_token} + {:ok, access_token: access_token, claims: claims} end test "should render user identity response", %{ conn: conn, - access_token: access_token + access_token: access_token, + claims: claims } do password = "MyP@ssword1234" @@ -44,8 +30,22 @@ defmodule RestAPI.Controllers.Admin.User do ] } + expect(AuthenticatorMock, :validate_access_token, fn token -> + assert access_token == token + {:ok, claims} + end) + + expect(AuthenticatorMock, :get_session, fn %{"jti" => jti} -> + assert claims["jti"] == jti + {:ok, success_session(claims)} + end) + + expect(ResourceManagerMock, :password_allowed?, fn _input -> + true + end) + expect(AuthenticatorMock, :generate_hash, fn password_to_hash, :argon2 -> - assert password = password_to_hash + assert password == password_to_hash "password_hashed" end) @@ -76,11 +76,29 @@ defmodule RestAPI.Controllers.Admin.User do |> json_response(201) end - test "should return error when params is not valid", %{conn: conn, access_token: access_token} do + test "should return error when params is not valid", %{ + conn: conn, + access_token: access_token, + claims: claims + } do password = "MyP@ssword" + expect(AuthenticatorMock, :validate_access_token, fn token -> + assert access_token == token + {:ok, claims} + end) + + expect(AuthenticatorMock, :get_session, fn %{"jti" => jti} -> + assert claims["jti"] == jti + {:ok, success_session(claims)} + end) + + expect(ResourceManagerMock, :password_allowed?, fn _input -> + true + end) + expect(AuthenticatorMock, :generate_hash, fn password_to_hash, :argon2 -> - assert password = password_to_hash + assert password == password_to_hash "password_hashed" end) @@ -89,7 +107,7 @@ defmodule RestAPI.Controllers.Admin.User do end) assert %{ - "detail" => "The given params are invalid", + "detail" => "The given params failed in validation", "error" => "bad_request", "response" => %{"username" => ["can't be blank"]}, "status" => 400 @@ -102,24 +120,40 @@ defmodule RestAPI.Controllers.Admin.User do test "should return error when password is not strong enough", %{ conn: conn, - access_token: access_token + access_token: access_token, + claims: claims } do password = "MyP@ssword" - expect(ResourceManagerMock, :is_strong?, fn _input -> + expect(AuthenticatorMock, :validate_access_token, fn token -> + assert access_token == token + {:ok, claims} + end) + + expect(AuthenticatorMock, :get_session, fn %{"jti" => jti} -> + assert claims["jti"] == jti + {:ok, success_session(claims)} + end) + + expect(AuthenticatorMock, :sign_out_session, fn jti -> + assert claims["jti"] == jti + {:ok, %{}} + end) + + expect(ResourceManagerMock, :password_allowed?, fn _input -> false end) assert %{ "detail" => "The given params failed in validation", - "error" => "unprocessable entity", - "response" => %{"error" => "not_strong_enough", "password" => "MyP@ssword"}, - "status" => 422 - } = + "error" => "bad_request", + "response" => %{"password" => ["password is not strong enough"]}, + "status" => 400 + } == conn |> put_req_header("authorization", "Bearer #{access_token}") |> post(@create_endpoint, %{"password" => password}) - |> json_response(422) + |> json_response(400) end end diff --git a/apps/rest_api/test/controllers/fallback_test.exs b/apps/rest_api/test/controllers/fallback_test.exs index 0c1e30c..40f72c7 100644 --- a/apps/rest_api/test/controllers/fallback_test.exs +++ b/apps/rest_api/test/controllers/fallback_test.exs @@ -50,7 +50,7 @@ defmodule RestAPI.Controllers.FallbackTest do test "handles changeset responses", %{conn: conn} do assert %{ - "detail" => "The given params are invalid", + "detail" => "The given params failed in validation", "status" => 400, "error" => "bad_request", "response" => %{