mitgliederverwaltung/test/mv_web/controllers/auth_controller_test.exs

312 lines
9.4 KiB
Elixir

defmodule MvWeb.AuthControllerTest do
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Phoenix.ConnTest
# Helper to create an unauthenticated conn (preserves sandbox metadata)
defp build_unauthenticated_conn(authenticated_conn) do
# Create new conn but preserve sandbox metadata for database access
new_conn =
build_conn()
|> init_test_session(%{})
|> fetch_flash()
# Copy sandbox metadata from authenticated conn
if authenticated_conn.private[:ecto_sandbox] do
Plug.Conn.put_private(new_conn, :ecto_sandbox, authenticated_conn.private[:ecto_sandbox])
else
new_conn
end
end
# Basic UI tests
test "GET /sign-in shows sign in form", %{conn: authenticated_conn} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
conn = get(conn, ~p"/sign-in")
assert html_response(conn, 200) =~ "Sign in"
end
test "GET /sign-out redirects to home", %{conn: authenticated_conn} do
conn = conn_with_oidc_user(authenticated_conn)
conn = get(conn, ~p"/sign-out")
assert redirected_to(conn) == ~p"/"
end
# Password authentication (LiveView)
test "password user can sign in with valid credentials via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "password@example.com",
password: "secret123",
oidc_id: nil
})
{:ok, view, _html} = live(conn, "/sign-in")
{:error, {:redirect, %{to: to}}} =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "password@example.com", password: "secret123"}
)
|> render_submit()
assert to =~ "/auth/user/password/sign_in_with_token"
end
test "password user with invalid credentials shows error via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "test@example.com",
password: "correct_password",
oidc_id: nil
})
{:ok, view, _html} = live(conn, "/sign-in")
html =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "test@example.com", password: "wrong_password"}
)
|> render_submit()
assert html =~ "Email or password was incorrect"
end
test "password user with non-existent email shows error via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
{:ok, view, _html} = live(conn, "/sign-in")
html =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "nonexistent@example.com", password: "anypassword"}
)
|> render_submit()
assert html =~ "Email or password was incorrect"
end
# Registration (LiveView)
test "user can register with valid credentials via LiveView", %{conn: authenticated_conn} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
{:ok, view, _html} = live(conn, "/register")
{:error, {:redirect, %{to: to}}} =
view
|> form("#user-password-register-with-password-wrapper form",
user: %{email: "newuser@example.com", password: "newpassword123"}
)
|> render_submit()
assert to =~ "/auth/user/password/sign_in_with_token"
end
test "registration with existing email shows error via LiveView", %{conn: authenticated_conn} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "existing@example.com",
password: "secret123",
oidc_id: nil
})
{:ok, view, _html} = live(conn, "/register")
html =
view
|> form("#user-password-register-with-password-wrapper form",
user: %{email: "existing@example.com", password: "anotherpassword"}
)
|> render_submit()
assert html =~ "has already been taken"
end
test "registration with weak password shows error via LiveView", %{conn: authenticated_conn} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
{:ok, view, _html} = live(conn, "/register")
html =
view
|> form("#user-password-register-with-password-wrapper form",
user: %{email: "weakpass@example.com", password: "123"}
)
|> render_submit()
assert html =~ "length must be greater than or equal to 8"
end
# Access control
test "unauthenticated user accessing protected route gets redirected to sign-in", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
conn = get(conn, ~p"/members")
assert redirected_to(conn) == ~p"/sign-in"
end
test "authenticated user can access protected route", %{conn: authenticated_conn} do
conn = conn_with_oidc_user(authenticated_conn)
conn = get(conn, ~p"/members")
assert conn.status == 200
end
test "password authenticated user can access protected route via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "auth@example.com",
password: "secret123",
oidc_id: nil
})
{:ok, view, _html} = live(conn, "/sign-in")
{:error, {:redirect, %{to: to}}} =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "auth@example.com", password: "secret123"}
)
|> render_submit()
assert to =~ "/auth/user/password/sign_in_with_token"
# After login, user is redirected to /auth/user/password/sign_in_with_token. Session handling for protected routes should be tested in integration or E2E tests.
end
# Edge cases
test "user with nil oidc_id can still sign in with password via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "nil_oidc@example.com",
password: "secret123",
oidc_id: nil
})
{:ok, view, _html} = live(conn, "/sign-in")
{:error, {:redirect, %{to: to}}} =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "nil_oidc@example.com", password: "secret123"}
)
|> render_submit()
assert to =~ "/auth/user/password/sign_in_with_token"
end
test "user with empty string oidc_id is handled correctly via LiveView", %{
conn: authenticated_conn
} do
# Create unauthenticated conn for this test
conn = build_unauthenticated_conn(authenticated_conn)
_user =
create_test_user(%{
email: "empty_oidc@example.com",
password: "secret123",
oidc_id: ""
})
{:ok, view, _html} = live(conn, "/sign-in")
{:error, {:redirect, %{to: to}}} =
view
|> form("#user-password-sign-in-with-password",
user: %{email: "empty_oidc@example.com", password: "secret123"}
)
|> render_submit()
assert to =~ "/auth/user/password/sign_in_with_token"
end
# OIDC/Rauthy error handling tests
describe "handle_rauthy_failure/2" do
test "Assent.ServerUnreachableError redirects to sign-in with error flash", %{
conn: authenticated_conn
} do
conn = build_unauthenticated_conn(authenticated_conn)
# Create a mock Assent.ServerUnreachableError struct with required fields
error = %Assent.ServerUnreachableError{
http_adapter: Assent.HTTPAdapter.Finch,
request_url: "https://auth.example.com/callback?token=secret123",
reason: %Mint.TransportError{reason: :econnrefused}
}
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)
assert redirected_to(conn) == ~p"/sign-in"
assert get_flash(conn, :error) ==
"The authentication server is currently unavailable. Please try again later."
end
test "Assent.InvalidResponseError redirects to sign-in with error flash", %{
conn: authenticated_conn
} do
conn = build_unauthenticated_conn(authenticated_conn)
# Create a mock Assent.InvalidResponseError struct with required field
# InvalidResponseError only has :response field (HTTPResponse struct)
error = %Assent.InvalidResponseError{
response: %Assent.HTTPAdapter.HTTPResponse{
status: 400,
headers: [],
body: "invalid_request"
}
}
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, error)
assert redirected_to(conn) == ~p"/sign-in"
assert get_flash(conn, :error) ==
"Authentication configuration error. Please contact the administrator."
end
test "unknown reason triggers catch-all and redirects to sign-in with error flash", %{
conn: authenticated_conn
} do
conn = build_unauthenticated_conn(authenticated_conn)
unknown_reason = :oops
conn = MvWeb.AuthController.failure(conn, {:rauthy, :callback}, unknown_reason)
assert redirected_to(conn) == ~p"/sign-in"
assert get_flash(conn, :error) ==
"Unable to authenticate with OIDC. Please try again."
end
end
end