Broaden e2e coverage: invalid credentials, session persistence, API auth
Added: wrong-password rejection (local + OIDC), session survives a page reload, and API-level checks that /api/hosts, /api/devices, /api/auth/me all reject unauthenticated requests regardless of what the UI does. No new app bugs found this round -- one test assertion was itself wrong (expected no session cookie on failed login; @fastify/session issues an anonymous cookie on any response by design, that's normal). Fixed to assert the property that actually matters: the cookie grants no access. 9/9 tests green across 4 consecutive full-suite runs with parallel workers, no flakiness. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,3 +19,32 @@ test("local username/password login and logout", async ({ page }) => {
|
|||||||
await page.getByRole("button", { name: "Sign out" }).click();
|
await page.getByRole("button", { name: "Sign out" }).click();
|
||||||
await expect(page.getByPlaceholder("Username")).toBeVisible();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 page.getByRole("button", { name: "Sign out" }).click();
|
||||||
await expect(page.getByPlaceholder("Username")).toBeVisible();
|
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\//);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user