diff --git a/e2e/tests/api-auth.spec.ts b/e2e/tests/api-auth.spec.ts new file mode 100644 index 0000000..c43df45 --- /dev/null +++ b/e2e/tests/api-auth.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from "@playwright/test"; + +// No page/cookies involved -- confirms protected routes actually reject +// unauthenticated requests, independent of whatever the UI does. +test.describe("unauthenticated API access is rejected", () => { + for (const path of ["/api/hosts", "/api/devices", "/api/auth/me"]) { + test(`GET ${path} without a session returns 401`, async ({ request }) => { + const res = await request.get(path); + expect(res.status()).toBe(401); + }); + } + + test("POST /api/auth/login with bad credentials returns 401 and grants no access", async ({ + request, + }) => { + const res = await request.post("/api/auth/login", { + data: { username: "admin", password: "definitely-not-the-password" }, + }); + expect(res.status()).toBe(401); + + // @fastify/session issues an anonymous session cookie on any response by + // design (regardless of login success) -- that alone doesn't mean anything. + // What matters is that cookie doesn't carry authentication: the same + // request context (Playwright persists cookies across calls) must still + // be rejected from a protected route. + const hostsRes = await request.get("/api/hosts"); + expect(hostsRes.status()).toBe(401); + }); +}); diff --git a/e2e/tests/local-login.spec.ts b/e2e/tests/local-login.spec.ts index 0f6ceea..490e437 100644 --- a/e2e/tests/local-login.spec.ts +++ b/e2e/tests/local-login.spec.ts @@ -19,3 +19,32 @@ test("local username/password login and logout", async ({ page }) => { await page.getByRole("button", { name: "Sign out" }).click(); await expect(page.getByPlaceholder("Username")).toBeVisible(); }); + +test("wrong password is rejected with an error, no session", async ({ page }) => { + await page.goto("/"); + await page.getByPlaceholder("Username").fill(USERNAME); + await page.getByPlaceholder("Password").fill("definitely-not-the-password"); + await page.getByRole("button", { name: "Sign in" }).click(); + + await expect(page.getByText(/invalid credentials/i)).toBeVisible(); + // Still on the login form, not the dashboard. + await expect(page.getByPlaceholder("Username")).toBeVisible(); + await expect(page.locator(".host-card")).toHaveCount(0); +}); + +test("session persists across a page reload", async ({ page }) => { + test.skip(!PASSWORD, "LOCAL_PASSWORD not set"); + + await page.goto("/"); + await page.getByPlaceholder("Username").fill(USERNAME); + await page.getByPlaceholder("Password").fill(PASSWORD); + await page.getByRole("button", { name: "Sign in" }).click(); + await expect(page.locator(".host-card").first()).toBeVisible({ timeout: 15_000 }); + + await page.reload(); + + await expect(page.getByText(USERNAME)).toBeVisible(); + await expect(page.locator(".host-card").first()).toBeVisible({ timeout: 15_000 }); + + await page.getByRole("button", { name: "Sign out" }).click(); +}); diff --git a/e2e/tests/oidc-login.spec.ts b/e2e/tests/oidc-login.spec.ts index 1594cea..606f16b 100644 --- a/e2e/tests/oidc-login.spec.ts +++ b/e2e/tests/oidc-login.spec.ts @@ -33,3 +33,21 @@ test("OIDC login via Authentik stays on public domains throughout", async ({ pag await page.getByRole("button", { name: "Sign out" }).click(); await expect(page.getByPlaceholder("Username")).toBeVisible(); }); + +test("wrong Authentik password does not reach the dashboard", async ({ page }) => { + test.skip(!PASSWORD, "OIDC_PASSWORD not set"); + + await page.goto("/"); + await page.getByRole("link", { name: "Sign in with Authentik" }).click(); + await page.waitForURL(/^https:\/\/auth\.jerodrigged\.com\//, { timeout: 10_000 }); + + await page.getByPlaceholder(/email or username/i).fill(USERNAME); + await page.getByRole("button", { name: /log in|continue|next/i }).click(); + + await page.getByPlaceholder(/password/i).fill("definitely-not-the-password"); + await page.getByRole("button", { name: /log in|continue|sign in/i }).click(); + + // Authentik re-shows the password stage with an error, never redirects back. + await expect(page.getByText(/failed|invalid|incorrect/i)).toBeVisible({ timeout: 10_000 }); + expect(page.url()).toMatch(/^https:\/\/auth\.jerodrigged\.com\//); +});