test: dynamic router - some additional tests, fixes and failures (#493)

* test: Scoped router tests

* test: Initial controller tests

* docs: Router helper docs, including :unscoped

* chore: Code formatting

* fix: ensure path params are processed on strategy router

* test: Working controller tests for register, sign-in, sign-out

---------

Co-authored-by: Zach Daniel <zach@zachdaniel.dev>
This commit is contained in:
Andrew Hacking 2024-08-15 01:48:18 +10:00 committed by GitHub
parent 0618356731
commit 7c90908ad9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 244 additions and 27 deletions

View file

@ -6,10 +6,7 @@ config :ash, :disable_async?, true
config :ash_authentication_phoenix, ash_domains: [Example.Accounts]
config :ash_authentication_phoenix, AshAuthentication.JsonWebToken,
signing_secret: "All I wanna do is to thank you, even though I don't know who you are."
config :ash_authentication, AshAuthentication.Jwt,
config :ash_authentication_phoenix,
signing_secret: "Marty McFly in the past with the Delorean"
config :phoenix, :json_library, Jason

View file

@ -181,17 +181,20 @@ defmodule AshAuthentication.Phoenix.Router do
Available options are:
* `path` the path under which to mount the sign-in live-view. Defaults to `"/sign-in"`.
* `path` the path under which to mount the sign-in live-view. Defaults to `"/sign-in"` within the current router scope.
* `auth_routes_prefix` if set, this will be used instead of route helpers when determining routes.
Allows disabling `helpers: true`.
If a tuple {:unscoped, path} is provided, the path prefix will not inherit the current route scope.
* `register_path` - the path under which to mount the password strategy's registration live-view.
If not set, and registration is supported, registration will use a dynamic toggle and will not be routeable to.
If a tuple {:unscoped, path} is provided, the registration path will not inherit the current route scope.
* `reset_path` - the path under which to mount the password strategy's password reset live-view.
If not set, and password reset is supported, password reset will use a dynamic toggle and will not be routeable to.
If a tuple {:unscoped, path} is provided, the reset path will not inherit the current route scope.
* `live_view` the name of the live view to render. Defaults to
`AshAuthentication.Phoenix.SignInLive`.
* `auth_routes_prefix` the prefix to use for the auth routes. Defaults to `"/auth"`.
* `as` which is passed to the generated `live` route. Defaults to `:auth`.
* `as` which is used to prefix the generated `live_session` and `live` route name. Defaults to `:auth`.
* `otp_app` the otp app or apps to find authentication resources in. Pulls from the socket by default.
* `overrides` specify any override modules for customisation. See
`AshAuthentication.Phoenix.Overrides` for more information.
@ -240,6 +243,8 @@ defmodule AshAuthentication.Phoenix.Router do
mod -> mod
end)
sign_in_path = Phoenix.Router.scoped_path(__MODULE__, unquote(path))
register_path =
case unquote(register_path) do
nil -> nil
@ -254,8 +259,12 @@ defmodule AshAuthentication.Phoenix.Router do
value -> Phoenix.Router.scoped_path(__MODULE__, value)
end
unquote(register_path) &&
Phoenix.Router.scoped_path(__MODULE__, unquote(register_path))
auth_routes_prefix =
case unquote(auth_routes_prefix) do
nil -> nil
{:unscoped, value} -> value
value -> Phoenix.Router.scoped_path(__MODULE__, value)
end
live_session_opts = [
session:
@ -263,15 +272,11 @@ defmodule AshAuthentication.Phoenix.Router do
[
%{
"overrides" => unquote(overrides),
"auth_routes_prefix" => unquote(auth_routes_prefix),
"auth_routes_prefix" => auth_routes_prefix,
"otp_app" => unquote(otp_app),
"path" => Phoenix.Router.scoped_path(__MODULE__, unquote(path)),
"reset_path" =>
unquote(reset_path) &&
Phoenix.Router.scoped_path(__MODULE__, unquote(reset_path)),
"register_path" =>
unquote(register_path) &&
Phoenix.Router.scoped_path(__MODULE__, unquote(register_path))
"path" => sign_in_path,
"reset_path" => reset_path,
"register_path" => register_path
}
]},
on_mount: on_mount
@ -286,17 +291,15 @@ defmodule AshAuthentication.Phoenix.Router do
Keyword.put(live_session_opts, :layout, layout)
end
live_session :sign_in, live_session_opts do
live_session :"#{unquote(as)}_sign_in", live_session_opts do
live(unquote(path), unquote(live_view), :sign_in, as: unquote(as))
if unquote(reset_path) do
live(unquote(reset_path), unquote(live_view), :reset, as: :"#{unquote(as)}_reset")
if reset_path do
live(reset_path, unquote(live_view), :reset, as: :"#{unquote(as)}_reset")
end
if unquote(register_path) do
live(unquote(register_path), unquote(live_view), :register,
as: :"#{unquote(as)}_register"
)
if register_path do
live(register_path, unquote(live_view), :register, as: :"#{unquote(as)}_register")
end
end
end
@ -337,7 +340,8 @@ defmodule AshAuthentication.Phoenix.Router do
`AshAuthentication.Phoenix.Overrides` for more information. all other
options are passed to the generated `scope`.
This is completely optional.
This is completely optional, in particular, if the `reset_path` option is passed to the
`sign_in_route` helper, using the `reset_route` helper is redundant.
"""
@spec reset_route(
opts :: [
@ -394,7 +398,7 @@ defmodule AshAuthentication.Phoenix.Router do
Keyword.put(live_session_opts, :layout, layout)
end
live_session :reset, live_session_opts do
live_session :"#{unquote(as)}_reset", live_session_opts do
live("/:token", unquote(live_view), :reset, as: unquote(as))
end
end

View file

@ -7,6 +7,9 @@ defmodule AshAuthentication.Phoenix.StrategyRouter do
@impl true
def call(conn, opts) do
# ensure query params have been fetched
conn = Plug.Conn.fetch_query_params(conn)
opts
|> routes()
|> Enum.reduce_while(

113
test/controller_test.exs Normal file
View file

@ -0,0 +1,113 @@
defmodule AshAuthentication.Phoenix.ControllerTest do
@moduledoc false
use AshAuthentication.Phoenix.Test.ConnCase
describe "AshAuthentication Controller" do
test "sign-in renders", %{conn: conn} do
assert_mount_render(conn, ~p"/sign-in", "Sign in")
end
test "register renders", %{conn: conn} do
assert_mount_render(conn, ~p"/register", "Register")
end
test "reset renders", %{conn: conn} do
assert_mount_render(conn, ~p"/reset", "Request magic link")
end
test "sign-out renders", %{conn: conn} do
conn = get(conn, ~p"/sign-out")
assert html_response(conn, 200) =~ "Signed out"
assert {:error, :nosession} = live(conn)
end
defp assert_mount_render(conn, path, response) do
conn = get(conn, path)
# assert unconnected mount renders
assert html_response(conn, 200) =~ response
# assert connected mount also renders
assert {:ok, _view, html} = live(conn)
assert html =~ response
conn
end
test "register user with password", %{conn: conn} do
strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password)
email = "register@email"
password = "register.secret"
{:ok, lv, _html} = live(conn, ~p"/register")
{:ok, conn} =
lv
|> form(~s{[action="/auth/user/password/register?"]},
user: %{
strategy.identity_field => email,
strategy.password_field => password,
strategy.password_confirmation_field => password
}
)
|> render_submit()
|> follow_redirect(conn)
assert html_response(conn, 200) =~ "Success"
assert Ash.CiString.value(conn.assigns.current_user.email) == email
end
test "sign-in user with password", %{conn: conn} do
strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password)
email = "sign.in@email"
password = "sign.in.secret"
create_user!(strategy, email, password)
conn = sign_in_user(conn, strategy, email, password)
assert html_response(conn, 200) =~ "Success"
assert get_session(conn, :user) != nil
assert Ash.CiString.value(conn.assigns.current_user.email) == email
end
test "sign-out user", %{conn: conn} do
strategy = AshAuthentication.Info.strategy!(Example.Accounts.User, :password)
email = "sign.out@email"
password = "sign.out.secret"
create_user!(strategy, email, password)
conn =
conn
|> sign_in_user(strategy, email, password)
|> get(~p"/sign-out")
assert html_response(conn, 200) =~ "Signed out"
assert get_session(conn, :user) == nil
end
end
defp sign_in_user(conn, strategy, email, password) do
{:ok, lv, _html} = live(conn, ~p"/sign-in")
{:ok, conn} =
lv
|> form(~s{[action="/auth/user/password/sign_in?"]},
user: %{
strategy.identity_field => email,
strategy.password_field => password
}
)
|> render_submit()
|> follow_redirect(conn)
conn
end
defp create_user!(strategy, email, password) do
Example.Accounts.User
|> Ash.Changeset.for_create(:register_with_password, %{
strategy.identity_field => email,
strategy.password_field => password,
strategy.password_confirmation_field => password
})
|> Ash.create!()
end
end

View file

@ -23,4 +23,48 @@ defmodule AshAuthentication.Phoenix.RouterTest do
}
]}
end
test "sign_in_routes respects the inherited router scope" do
route =
AshAuthentication.Phoenix.Test.Router
|> Phoenix.Router.routes()
|> Enum.find(&(&1.path == "/nested/sign-in"))
{_, _, _, %{extra: %{session: session}}} = route.metadata.phoenix_live_view
assert session ==
{AshAuthentication.Phoenix.Router, :generate_session,
[
%{
"auth_routes_prefix" => "/nested/auth",
"otp_app" => nil,
"overrides" => [AshAuthentication.Phoenix.Overrides.Default],
"path" => "/nested/sign-in",
"register_path" => "/nested/register",
"reset_path" => "/nested/reset"
}
]}
end
test "sign_in_routes respects unscoped" do
route =
AshAuthentication.Phoenix.Test.Router
|> Phoenix.Router.routes()
|> Enum.find(&(&1.path == "/unscoped/sign-in"))
{_, _, _, %{extra: %{session: session}}} = route.metadata.phoenix_live_view
assert session ==
{AshAuthentication.Phoenix.Router, :generate_session,
[
%{
"auth_routes_prefix" => "/auth",
"otp_app" => nil,
"overrides" => [AshAuthentication.Phoenix.Overrides.Default],
"path" => "/unscoped/sign-in",
"register_path" => "/register",
"reset_path" => "/reset"
}
]}
end
end

View file

@ -1,5 +1,6 @@
defmodule AshAuthentication.Phoenix.SignInTest do
@moduledoc false
use ExUnit.Case, async: false
import Phoenix.ConnTest
import Phoenix.LiveViewTest

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.Phoenix.Test.AuthController do
|> store_in_session(user)
|> assign(:current_user, user)
|> put_status(200)
|> render("success.html")
|> render(:success)
end
@doc false
@ -27,6 +27,6 @@ defmodule AshAuthentication.Phoenix.Test.AuthController do
def sign_out(conn, _params) do
conn
|> clear_session()
|> render("sign_out.html")
|> render(:signed_out)
end
end

View file

@ -0,0 +1,6 @@
defmodule AshAuthentication.Phoenix.Test.AuthView do
use Phoenix.Component
def success(assigns), do: ~H"<p>Success</p>"
def signed_out(assigns), do: ~H"<p>Signed out</p>"
end

25
test/support/conn_case.ex Normal file
View file

@ -0,0 +1,25 @@
defmodule AshAuthentication.Phoenix.Test.ConnCase do
use ExUnit.CaseTemplate
using do
quote do
# The default endpoint for testing
@endpoint AshAuthentication.Phoenix.Test.Endpoint
use Phoenix.VerifiedRoutes,
endpoint: @endpoint,
router: AshAuthentication.Phoenix.Test.Router
#statics: ~w(assets fonts images favicon.ico robots.txt)
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import Phoenix.LiveViewTest
import AshAuthentication.Phoenix.Test.ConnCase
end
end
setup _tags do
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

View file

@ -81,6 +81,30 @@ defmodule AshAuthentication.Phoenix.Test.Router do
auth_routes AuthController, Example.Accounts.User, path: "/auth"
end
scope "/nested", AshAuthentication.Phoenix.Test do
pipe_through :browser
sign_in_route register_path: "/register",
reset_path: "/reset",
auth_routes_prefix: "/auth",
as: :nested
sign_out_route AuthController
reset_route as: :nested
end
scope "/unscoped", AshAuthentication.Phoenix.Test do
pipe_through :browser
sign_in_route register_path: {:unscoped, "/register"},
reset_path: {:unscoped, "/reset"},
auth_routes_prefix: {:unscoped, "/auth"},
as: :unscoped
sign_out_route AuthController
reset_route as: :unscoped
end
scope "/", AshAuthentication.Phoenix.Test do
pipe_through(:browser)