From f2798938a563cf56395dad52351d05943b827736 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchon Date: Thu, 7 Sep 2023 14:12:36 -0400 Subject: [PATCH] Fix tests and format --- .../{controllers => errors}/error_html.ex | 2 +- .../{controllers => errors}/error_json.ex | 2 +- lib/elixir_boilerplate_web/errors/errors.ex | 68 +++++++++++++++++++ .../errors/templates/404.html.heex | 11 +++ .../errors/templates/error_messages.html.heex | 7 ++ .../errors/error_html_test.exs | 16 +++++ .../errors/error_json_test.exs | 13 ++++ .../{ => errors}/errors_test.exs | 4 +- test/support/conn_case.ex | 2 - 9 files changed, 118 insertions(+), 7 deletions(-) rename lib/elixir_boilerplate_web/{controllers => errors}/error_html.ex (91%) rename lib/elixir_boilerplate_web/{controllers => errors}/error_json.ex (89%) create mode 100644 lib/elixir_boilerplate_web/errors/errors.ex create mode 100644 lib/elixir_boilerplate_web/errors/templates/404.html.heex create mode 100644 lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex create mode 100644 test/elixir_boilerplate_web/errors/error_html_test.exs create mode 100644 test/elixir_boilerplate_web/errors/error_json_test.exs rename test/elixir_boilerplate_web/{ => errors}/errors_test.exs (96%) diff --git a/lib/elixir_boilerplate_web/controllers/error_html.ex b/lib/elixir_boilerplate_web/errors/error_html.ex similarity index 91% rename from lib/elixir_boilerplate_web/controllers/error_html.ex rename to lib/elixir_boilerplate_web/errors/error_html.ex index 1ecc5be2..ed649f2d 100644 --- a/lib/elixir_boilerplate_web/controllers/error_html.ex +++ b/lib/elixir_boilerplate_web/errors/error_html.ex @@ -1,4 +1,4 @@ -defmodule ElixirBoilerplateWeb.Controllers.ErrorHTML do +defmodule ElixirBoilerplateWeb.Errors.ErrorHTML do use ElixirBoilerplateWeb, :html # If you want to customize your error pages, diff --git a/lib/elixir_boilerplate_web/controllers/error_json.ex b/lib/elixir_boilerplate_web/errors/error_json.ex similarity index 89% rename from lib/elixir_boilerplate_web/controllers/error_json.ex rename to lib/elixir_boilerplate_web/errors/error_json.ex index 0ccb0a67..7d916576 100644 --- a/lib/elixir_boilerplate_web/controllers/error_json.ex +++ b/lib/elixir_boilerplate_web/errors/error_json.ex @@ -1,4 +1,4 @@ -defmodule ElixirBoilerplateWeb.Controllers.ErrorJSON do +defmodule ElixirBoilerplateWeb.Errors.ErrorJSON do # If you want to customize a particular status code, # you may add your own clauses, such as: # diff --git a/lib/elixir_boilerplate_web/errors/errors.ex b/lib/elixir_boilerplate_web/errors/errors.ex new file mode 100644 index 00000000..37308770 --- /dev/null +++ b/lib/elixir_boilerplate_web/errors/errors.ex @@ -0,0 +1,68 @@ +defmodule ElixirBoilerplateWeb.Errors do + alias Ecto.Changeset + + import Phoenix.Template, only: [embed_templates: 1] + + embed_templates("templates/*") + + @doc """ + Generates a human-readable block containing all errors in a changeset. Errors + are then localized using translations in the `ecto` domain. + + For example, you could have an `errors.po` file in the french locale: + + ``` + msgid "" + msgstr "" + "Language: fr" + + msgid "can't be blank" + msgstr "ne peut ĂȘtre vide" + ``` + """ + def changeset_to_error_messages(changeset) do + changeset + |> Changeset.traverse_errors(&translate_error/1) + |> convert_errors_to_html(changeset.data.__struct__) + end + + defp translate_error({message, options}) do + if options[:count] do + Gettext.dngettext(ElixirBoilerplate.Gettext, "errors", message, message, options[:count], options) + else + Gettext.dgettext(ElixirBoilerplate.Gettext, "errors", message, options) + end + end + + defp convert_errors_to_html(errors, schema) do + errors = Enum.reduce(errors, [], &convert_error_field(&1, &2, schema)) + + error_messages(%{errors: errors}) + end + + defp convert_error_field({field, errors}, memo, schema) when is_list(errors) do + memo ++ Enum.flat_map(errors, &convert_error_subfield(&1, field, [], schema)) + end + + defp convert_error_field({field, errors}, memo, schema) when is_map(errors) do + memo ++ Enum.flat_map(Map.keys(errors), &convert_error_subfield(&1, field, errors[&1], schema)) + end + + defp convert_error_subfield(message, field, _, _schema) when is_binary(message) do + # NOTE `schema` is available here if we want to use something like + # `schema.humanize_field(field)` to be able to display `"Email address is + # invalid"` instead of `email is invalid"`. + ["#{field} #{message}"] + end + + defp convert_error_subfield(message, field, memo, schema) when is_map(message) do + Enum.reduce(message, memo, fn {subfield, errors}, memo -> + memo ++ convert_error_field({"#{field}.#{subfield}", errors}, memo, schema) + end) + end + + defp convert_error_subfield(subfield, field, errors, schema) do + field = "#{field}.#{subfield}" + convert_error_field({field, errors}, [], schema) + end +end diff --git a/lib/elixir_boilerplate_web/errors/templates/404.html.heex b/lib/elixir_boilerplate_web/errors/templates/404.html.heex new file mode 100644 index 00000000..7902e3fb --- /dev/null +++ b/lib/elixir_boilerplate_web/errors/templates/404.html.heex @@ -0,0 +1,11 @@ + + + + + + Not found + + +

Sorry, the page you are looking for does not exist.

+ + diff --git a/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex new file mode 100644 index 00000000..56d46a2a --- /dev/null +++ b/lib/elixir_boilerplate_web/errors/templates/error_messages.html.heex @@ -0,0 +1,7 @@ +<%= if @errors != [] do %> + +<% end %> diff --git a/test/elixir_boilerplate_web/errors/error_html_test.exs b/test/elixir_boilerplate_web/errors/error_html_test.exs new file mode 100644 index 00000000..7591a646 --- /dev/null +++ b/test/elixir_boilerplate_web/errors/error_html_test.exs @@ -0,0 +1,16 @@ +defmodule ElixirBoilerplateWeb.ErrorHtmlTest do + use ElixirBoilerplate.DataCase, async: true + + # Bring render_to_string/3 for testing custom views + import Phoenix.Template + + alias ElixirBoilerplateWeb.Errors.ErrorHTML + + test "renders 404.html" do + assert render_to_string(ErrorHTML, "404", "html", []) == "Not Found" + end + + test "renders 500.html" do + assert render_to_string(ErrorHTML, "500", "html", []) == "Internal Server Error" + end +end diff --git a/test/elixir_boilerplate_web/errors/error_json_test.exs b/test/elixir_boilerplate_web/errors/error_json_test.exs new file mode 100644 index 00000000..5d8f4571 --- /dev/null +++ b/test/elixir_boilerplate_web/errors/error_json_test.exs @@ -0,0 +1,13 @@ +defmodule ElixirBoilerplateWeb.ErrorJsonTest do + use ElixirBoilerplate.DataCase, async: true + + alias ElixirBoilerplateWeb.Errors.ErrorJSON + + test "renders 404" do + assert ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} + end + + test "renders 500" do + assert ErrorJSON.render("500.json", %{}) == %{errors: %{detail: "Internal Server Error"}} + end +end diff --git a/test/elixir_boilerplate_web/errors_test.exs b/test/elixir_boilerplate_web/errors/errors_test.exs similarity index 96% rename from test/elixir_boilerplate_web/errors_test.exs rename to test/elixir_boilerplate_web/errors/errors_test.exs index a6dbaa09..57433d86 100644 --- a/test/elixir_boilerplate_web/errors_test.exs +++ b/test/elixir_boilerplate_web/errors/errors_test.exs @@ -1,8 +1,6 @@ defmodule ElixirBoilerplateWeb.ErrorsTest do use ElixirBoilerplate.DataCase, async: true - alias ElixirBoilerplateWeb.Errors - defmodule UserRole do use Ecto.Schema @@ -74,7 +72,7 @@ defmodule ElixirBoilerplateWeb.ErrorsTest do defp changeset_to_error_messages(changeset) do changeset - |> Errors.changeset_to_error_messages() + |> ElixirBoilerplateWeb.Errors.changeset_to_error_messages() |> Phoenix.HTML.Safe.to_iodata() |> IO.iodata_to_binary() end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 42b9d80a..a3d00dfe 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -26,8 +26,6 @@ defmodule ElixirBoilerplateWeb.ConnCase do import Plug.Conn import Phoenix.ConnTest - import ElixirBoilerplateWeb.Router.Helpers - # The default endpoint for testing @endpoint Endpoint end