diff --git a/apps/app/lib/app_web/live_helpers.ex b/apps/app/lib/app_web/live_helpers.ex
index f225607d..1ea6b60c 100644
--- a/apps/app/lib/app_web/live_helpers.ex
+++ b/apps/app/lib/app_web/live_helpers.ex
@@ -9,10 +9,11 @@ defmodule AppWeb.LiveHelpers do
end
def require_auth(socket) do
- if socket.assigns.current_user do
- socket
- else
- redirect(socket, to: "/")
+ case socket.assigns do
+ %{current_user: user} when not is_nil(user) ->
+ socket
+ _ ->
+ redirect(socket, to: "/")
end
end
diff --git a/apps/app/lib/app_web/router.ex b/apps/app/lib/app_web/router.ex
index f00ab155..fbb3a9cf 100644
--- a/apps/app/lib/app_web/router.ex
+++ b/apps/app/lib/app_web/router.ex
@@ -51,7 +51,7 @@ defmodule AppWeb.Router do
forward "/sent_emails", Bamboo.SentEmailViewerPlug
end
- if Mix.env() == :e2e do
+ if Mix.env() in [:e2e, :test] do
forward("/end-to-end", Legendary.CoreWeb.Plug.TestEndToEnd, otp_app: :app)
end
diff --git a/apps/app/lib/app_web/views/error_helpers.ex b/apps/app/lib/app_web/views/error_helpers.ex
index 2e891a28..e09b4a05 100644
--- a/apps/app/lib/app_web/views/error_helpers.ex
+++ b/apps/app/lib/app_web/views/error_helpers.ex
@@ -8,10 +8,12 @@ defmodule AppWeb.ErrorHelpers do
@doc """
Generates tag for inlined form input errors.
"""
- def error_tag(form, field) do
+ def error_tag(form, field, opts \\ []) do
+ {extra_classes, _rest_opts} = Keyword.pop(opts, :class, "")
+
Enum.map(Keyword.get_values(form.errors, field), fn error ->
content_tag(:span, translate_error(error),
- class: "invalid-feedback",
+ class: "invalid-feedback #{extra_classes}",
phx_feedback_for: input_id(form, field)
)
end)
diff --git a/apps/app/test/app_web/error_helpers_test.exs b/apps/app/test/app_web/error_helpers_test.exs
new file mode 100644
index 00000000..5b98bdbf
--- /dev/null
+++ b/apps/app/test/app_web/error_helpers_test.exs
@@ -0,0 +1,29 @@
+defmodule AppWeb.ErrorHelpersTest do
+ use AppWeb.ConnCase
+
+ import Phoenix.HTML, only: [safe_to_string: 1]
+ import Phoenix.HTML.Form, only: [form_for: 3]
+
+ import AppWeb.ErrorHelpers
+
+ def form do
+ :example
+ |> form_for(
+ "/example",
+ as: :test_params,
+ errors: [error_field: {"is an error", []}]
+ )
+ end
+
+ describe "error_tag/2" do
+ test "generates a span with an invalid-feedback class" do
+ [safe] = error_tag(form(), :error_field)
+ assert safe_to_string(safe) =~ "invalid-feedback"
+ end
+
+ test "error_tag/3" do
+ [safe] = error_tag(form(), :error_field, class: "test-class")
+ assert safe_to_string(safe) =~ "test-class"
+ end
+ end
+end
diff --git a/apps/app/test/app_web/live_helpers_test.exs b/apps/app/test/app_web/live_helpers_test.exs
new file mode 100644
index 00000000..e4bdad6b
--- /dev/null
+++ b/apps/app/test/app_web/live_helpers_test.exs
@@ -0,0 +1,46 @@
+defmodule AppWeb.LiveHelpersText do
+ use AppWeb.ConnCase
+
+ import Mock
+
+ import AppWeb.LiveHelpers
+
+ describe "assign_defaults/2" do
+ test "sets current_user" do
+ {store, _store_config} = Pow.Plug.Base.store(Application.get_env(:core, :pow))
+ socket = %Phoenix.LiveView.Socket{endpoint: AppWeb.Endpoint}
+
+ with_mock Pow.Plug, [verify_token: fn (_, _, _, _) -> {:ok, "h3110"} end] do
+ with_mock store, [get: fn (_config, _token) -> {%{id: 1234}, nil} end] do
+ new_socket = assign_defaults(socket, %{"core_auth" => "h3ll0"})
+ assert %{assigns: %{current_user: %{id: 1234}}} = new_socket
+ end
+ end
+ end
+ end
+
+ describe "require_auth/1" do
+ test "with user" do
+ user = %{id: 4567}
+ socket =
+ %Phoenix.LiveView.Socket{assigns: %{current_user: user}}
+ |> require_auth()
+
+ assert !socket.redirected
+ end
+
+ test "without user" do
+ socket = %Phoenix.LiveView.Socket{} |> require_auth()
+
+ assert socket.redirected
+ end
+
+ test "without nil user" do
+ socket =
+ %Phoenix.LiveView.Socket{assigns: %{current_user: nil}}
+ |> require_auth()
+
+ assert socket.redirected
+ end
+ end
+end
diff --git a/apps/content/lib/content/post.ex b/apps/content/lib/content/post.ex
index 97901ee0..b06803dc 100644
--- a/apps/content/lib/content/post.ex
+++ b/apps/content/lib/content/post.ex
@@ -116,15 +116,17 @@ defmodule Legendary.Content.Post do
def maybe_put_guid(changeset) do
import Legendary.Content.Router.Helpers, only: [url: 1, posts_url: 3]
- slug = changeset |> get_field(:name)
+ guid = changeset |> get_field(:guid)
- case slug do
- nil -> changeset
- _ ->
+ case guid do
+ nil ->
base = url(Legendary.CoreWeb.Endpoint)
+ slug = changeset |> get_field(:name)
changeset
|> put_default(:guid, posts_url(URI.merge(base, "/pages"), :show, slug))
+ _ ->
+ changeset
end
end
end
diff --git a/apps/content/lib/content/sitemap.ex b/apps/content/lib/content/sitemaps.ex
similarity index 98%
rename from apps/content/lib/content/sitemap.ex
rename to apps/content/lib/content/sitemaps.ex
index e2eeda00..6ab6bb47 100644
--- a/apps/content/lib/content/sitemap.ex
+++ b/apps/content/lib/content/sitemaps.ex
@@ -21,6 +21,7 @@ defmodule Legendary.Content.Sitemaps do
generate()
end
+ @spec generate :: :ok
def generate do
create do
add "", priority: 0.5, changefreq: "hourly", expires: nil
diff --git a/apps/content/lib/content_web/controllers/posts_controller.ex b/apps/content/lib/content_web/controllers/posts_controller.ex
index 65857566..8add7dfe 100644
--- a/apps/content/lib/content_web/controllers/posts_controller.ex
+++ b/apps/content/lib/content_web/controllers/posts_controller.ex
@@ -67,9 +67,7 @@ defmodule Legendary.Content.PostsController do
conn |> show_one(post, page_string)
end
end
- def show(conn, %{"id" => id, "page" => page_string}) when is_list(id) do
- show(conn, %{"id" => Enum.join(id, "/"), "page" => page_string})
- end
+ def show(conn, %{"id" => id} = params) when is_list(id), do: show(conn, Map.merge(params, %{"id" => Enum.join(id, "/")}))
def show(conn, %{"id" => id}), do: show(conn, %{"id" => id, "page" => "1"})
defp try_static_post(conn, id) do
diff --git a/apps/content/lib/content_web/templates/feeds/index.rss.eex b/apps/content/lib/content_web/templates/feeds/index.rss.eex
index d5cf78a6..89f9fac6 100644
--- a/apps/content/lib/content_web/templates/feeds/index.rss.eex
+++ b/apps/content/lib/content_web/templates/feeds/index.rss.eex
@@ -7,6 +7,7 @@
Bloop
\nBloop
" end + + describe "authenticated_for_post?/2" do + test "without password" do + assert authenticated_for_post?(nil, %Post{}) + end + + test "with post password that matches", %{conn: conn} do + with_mock Plug.Conn, [get_session: fn (_conn, :post_password) -> "password" end] do + assert authenticated_for_post?(conn, %Post{password: "password"}) + end + end + + test "with post password that does not match", %{conn: conn} do + with_mock Plug.Conn, [get_session: fn (_conn, :post_password) -> "password" end] do + refute authenticated_for_post?(conn, %Post{password: "password2"}) + end + end + end end diff --git a/apps/core/lib/auth_web/controller_callbacks.ex b/apps/core/lib/auth_web/controller_callbacks.ex index 9b7e344a..a339b7aa 100644 --- a/apps/core/lib/auth_web/controller_callbacks.ex +++ b/apps/core/lib/auth_web/controller_callbacks.ex @@ -1,4 +1,4 @@ -defmodule AuthWeb.Pow.ControllerCallbacks do +defmodule Legendary.AuthWeb.Pow.ControllerCallbacks do @moduledoc """ Hook into Pow Controllers to provide additional framework feature. In particular, we disconnect any active live views when a user logs out. This will cause the @@ -36,6 +36,7 @@ defmodule AuthWeb.Pow.ControllerCallbacks do conn = conn |> Conn.delete_session(:live_socket_id) + |> Conn.delete_session(:current_user_id) ControllerCallbacks.before_respond( Pow.Phoenix.SessionController, diff --git a/apps/core/lib/core_web/plugs/test_end_to_end.ex b/apps/core/lib/core_web/plugs/test_end_to_end.ex index ef6137af..2170c8c4 100644 --- a/apps/core/lib/core_web/plugs/test_end_to_end.ex +++ b/apps/core/lib/core_web/plugs/test_end_to_end.ex @@ -16,8 +16,12 @@ defmodule Legendary.CoreWeb.Plug.TestEndToEnd do send_resp(conn, 200, "connection has already been checked out") else {:ok, _pid} = Agent.start_link(&checkout_shared_db_conn/0, name: :db_owner_agent) - {:ok, _} = load_test_seeds(conn) - send_resp(conn, 200, "connection checked out") + case load_test_seeds(conn) do + {:ok, _} -> + send_resp(conn, 200, "connection checked out") + {:error, msg} -> + send_resp(conn, 500, msg) + end end end @@ -40,7 +44,10 @@ defmodule Legendary.CoreWeb.Plug.TestEndToEnd do Ecto.Repo.all_running() |> Enum.map(fn repo -> :ok = Ecto.Adapters.SQL.Sandbox.checkout(repo, ownership_timeout: :infinity) - :ok = Ecto.Adapters.SQL.Sandbox.mode(repo, {:shared, self()}) + case Ecto.Adapters.SQL.Sandbox.mode(repo, {:shared, self()}) do + :ok -> :ok + :already_shared -> :ok + end end) end @@ -59,14 +66,16 @@ defmodule Legendary.CoreWeb.Plug.TestEndToEnd do {:app, {:ok, app}} <- {:app, Map.fetch(conn.body_params, "app")}, true <- String.match?(seed_set, @valid_seed_set_characters), true <- String.match?(app, @valid_app_characters) do - seed_path = "apps/#{app}/test/seed_sets/#{seed_set}.exs" + + project_base = Path.expand(Path.join(__DIR__, "../../../../..")) + seed_path = Path.join(project_base, "apps/#{app}/test/seed_sets/#{seed_set}.exs") try do {result, _} = Code.eval_file(seed_path) {:ok, result} rescue e in Code.LoadError -> - {:error, "could not load a seed set at #{seed_path}: #{e}"} + {:error, e.message} end else {:app, :error} -> {:error, "app parameter is required if seed set is set"} diff --git a/apps/core/lib/core_web/router.ex b/apps/core/lib/core_web/router.ex index 279e6d9c..5b6bb2c7 100644 --- a/apps/core/lib/core_web/router.ex +++ b/apps/core/lib/core_web/router.ex @@ -31,4 +31,8 @@ defmodule Legendary.CoreWeb.Router do live_dashboard "/dashboard", metrics: Legendary.CoreWeb.Telemetry end end + + if Mix.env() in [:e2e, :test] do + forward("/end-to-end", Legendary.CoreWeb.Plug.TestEndToEnd, otp_app: :app) + end end diff --git a/apps/core/lib/core_web/views/helpers.ex b/apps/core/lib/core_web/views/helpers.ex index f1d4fca0..cfed1753 100644 --- a/apps/core/lib/core_web/views/helpers.ex +++ b/apps/core/lib/core_web/views/helpers.ex @@ -48,7 +48,7 @@ defmodule Legendary.CoreWeb.Helpers do {type, rest_opts} = Keyword.pop(opts, :type, input_type(f, field)) {classes, rest_opts} = Keyword.pop(rest_opts, :class, default_classes_for_type(type)) {label_text, rest_opts} = Keyword.pop(rest_opts, :label) - {input_helper, _rest_opts} = Keyword.pop(rest_opts, :input_helper, input_type(f, field)) + {input_helper, _rest_opts} = Keyword.pop(rest_opts, :input_helper, type) error_classes = if Keyword.get_values(f.errors, field) |> Enum.any?() do diff --git a/apps/core/lib/mix/tasks/create_admin.ex b/apps/core/lib/mix/tasks/create_admin.ex index cd1dbe92..58703b7f 100644 --- a/apps/core/lib/mix/tasks/create_admin.ex +++ b/apps/core/lib/mix/tasks/create_admin.ex @@ -9,11 +9,13 @@ defmodule Mix.Tasks.Legendary.CreateAdmin do alias Ecto.Changeset @shortdoc "Create an admin user." - def run(_) do + def run(args) do Application.ensure_all_started(:core) - email = ExPrompt.string_required("Email: ") - password = ExPrompt.password("Password: ") + {switches, _, _} = OptionParser.parse(args, strict: [email: :string, password: :string]) + + email = Keyword.get_lazy(switches, :email, fn -> ExPrompt.string_required("Email: ") end) + password = Keyword.get_lazy(switches, :password, fn -> ExPrompt.password("Password: ") end) params = %{ email: email, @@ -27,7 +29,7 @@ defmodule Mix.Tasks.Legendary.CreateAdmin do |> Repo.insert!() end - def maybe_confirm_email(changeset) do + defp maybe_confirm_email(changeset) do field_list = User.__schema__(:fields) case Enum.any?(field_list, &(&1 == :email_confirmed_at)) do diff --git a/apps/core/test/auth_web/controller_callback_test.exs b/apps/core/test/auth_web/controller_callback_test.exs new file mode 100644 index 00000000..50d6357b --- /dev/null +++ b/apps/core/test/auth_web/controller_callback_test.exs @@ -0,0 +1,33 @@ +defmodule Legendary.AuthWeb.Pow.ControllerCallbacksTest do + use Legendary.CoreWeb.ConnCase, async: true + + import Legendary.AuthWeb.Pow.ControllerCallbacks + import Plug.Conn, only: [assign: 3, get_session: 2] + + alias Legendary.Auth.User + + describe "before_respond/4" do + setup %{conn: conn} do + conn = + conn + |> init_test_session([]) + |> assign(:current_user, %User{id: 123}) + + %{conn: conn} + end + + test "sets the live_socket_id in session upon sign in", %{conn: conn} do + {:ok, conn} = before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, []) + + assert get_session(conn, "live_socket_id") == "users_sockets:123" + assert get_session(conn, "current_user_id") == 123 + end + + test "removes the live_socket_id and broadcasts a disconnect signal upon sign out", %{conn: conn, } do + {:ok, conn} = before_respond(Pow.Phoenix.SessionController, :delete, {:ok, conn}, []) + + assert get_session(conn, "live_socket_id") == nil + assert get_session(conn, "current_user_id") == nil + end + end +end diff --git a/apps/core/test/auth_web/helpers_test.exs b/apps/core/test/auth_web/helpers_test.exs index 20bc71b1..2329a19d 100644 --- a/apps/core/test/auth_web/helpers_test.exs +++ b/apps/core/test/auth_web/helpers_test.exs @@ -1,8 +1,15 @@ defmodule Legendary.AuthWeb.HelpersTest do - use Legendary.CoreWeb.ConnCase + use Legendary.CoreWeb.ConnCase, async: true import Legendary.AuthWeb.Helpers + alias Phoenix.LiveView.Socket + + describe "current_user/1" do + test "can get a user from the assigns in a socket", do: assert current_user(%Socket{assigns: %{current_user: %{id: 867}}}).id == 867 + test "can get a user from the __assigns__ in a socket", do: assert current_user(%Socket{assigns: %{__assigns__: %{current_user: %{id: 867}}}}).id == 867 + end + describe "has_role?/2" do test "with a user", %{conn: conn} do conn = diff --git a/apps/core/test/auth_web/plugs/require_admin_test.exs b/apps/core/test/auth_web/plugs/require_admin_test.exs index 35d2f09f..1f67651b 100644 --- a/apps/core/test/auth_web/plugs/require_admin_test.exs +++ b/apps/core/test/auth_web/plugs/require_admin_test.exs @@ -1,5 +1,5 @@ defmodule Legendary.AuthWeb.Plugs.RequireAdminTest do - use Legendary.CoreWeb.ConnCase + use Legendary.CoreWeb.ConnCase, async: true alias Legendary.AuthWeb.Plugs.RequireAdmin alias Legendary.Auth.User diff --git a/apps/core/test/core_web/plugs/test_end_to_end_test.exs b/apps/core/test/core_web/plugs/test_end_to_end_test.exs new file mode 100644 index 00000000..610c8405 --- /dev/null +++ b/apps/core/test/core_web/plugs/test_end_to_end_test.exs @@ -0,0 +1,38 @@ +defmodule Legendary.CoreWeb.Plug.TestEndToEndTest do + use Legendary.CoreWeb.ConnCase + + test "/db/setup can check out a connection", %{conn: conn} do + conn = post conn, "/end-to-end/db/setup", %{seed_set: "test_end_to_end_test", app: "core"} + + assert response(conn, 200) =~ "connection checked out" + end + + test "/db/setup twice is a no-op", %{conn: conn} do + conn = post conn, "/end-to-end/db/setup", %{seed_set: "test_end_to_end_test", app: "core"} + conn = post conn, "/end-to-end/db/setup", %{seed_set: "test_end_to_end_test", app: "core"} + + assert response(conn, 200) =~ "connection has already been checked out" + end + + test "/db/setup with no-existent seed set is an error", %{conn: conn} do + conn = post conn, "/end-to-end/db/setup", %{seed_set: "oops", app: "core"} + + assert response(conn, 500) =~ "could not load" + assert response(conn, 500) =~ "oops.exs" + end + + test "/db/teardown can check in a connection", %{conn: conn} do + conn = post conn, "/end-to-end/db/setup", %{seed_set: "test_end_to_end_test", app: "core"} + conn = post conn, "/end-to-end/db/teardown" + + assert response(conn, 200) =~ "checked in database connection" + end + + test "/db/teardown checking in twice is a no-op", %{conn: conn} do + conn = post conn, "/end-to-end/db/setup", %{seed_set: "test_end_to_end_test", app: "core"} + conn = post conn, "/end-to-end/db/teardown" + conn = post conn, "/end-to-end/db/teardown" + + assert response(conn, 200) =~ "connection has already been checked back in" + end +end diff --git a/apps/core/test/core_web/views/helpers_test.exs b/apps/core/test/core_web/views/helpers_test.exs index e0b8b6ba..a8d4b06b 100644 --- a/apps/core/test/core_web/views/helpers_test.exs +++ b/apps/core/test/core_web/views/helpers_test.exs @@ -1,5 +1,5 @@ defmodule Legendary.CoreWeb.HelpersTest do - use Legendary.CoreWeb.ConnCase + use Legendary.CoreWeb.ConnCase, async: true import Legendary.CoreWeb.Helpers import Ecto.Changeset, @@ -35,6 +35,17 @@ defmodule Legendary.CoreWeb.HelpersTest do changeset end + describe "current_user?/1" do + test "returns nil for a conn with no user", %{conn: conn}, do: refute conn |> setup_pow() |> current_user() + test "returns a user for a conn with a user", %{conn: conn} do + conn = + conn + |> setup_user(id: 456) + + assert current_user(conn).id == 456 + end + end + describe "has_role?/2" do test "with a user", %{conn: conn} do conn = @@ -105,6 +116,87 @@ defmodule Legendary.CoreWeb.HelpersTest do assert markup =~ "