80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|||
|
|
import { ADMIN_EMAIL, ADMIN_PASSWORD, BASE, requireAuth, injectAuth, apiLogin } from "./helpers";
|
||
|
|
|
||
|
|
test.describe("Auth — public endpoints", () => {
|
||
|
|
test("login page renders", async ({ page }) => {
|
||
|
|
await page.goto("/login");
|
||
|
|
await expect(page).toHaveTitle(/.+/);
|
||
|
|
const hasForm = await page.locator("form, input[type=password]").count();
|
||
|
|
expect(hasForm).toBeGreaterThan(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("protected routes reject unauthenticated requests", async ({ page }) => {
|
||
|
|
const routes = ["/api/tree", "/api/leaderboard/weekly", "/api/notifications"];
|
||
|
|
for (const route of routes) {
|
||
|
|
const res = await page.request.get(`${BASE}${route}`);
|
||
|
|
expect(res.status(), `${route} should be 401`).toBe(401);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("invalid credentials: bad captcha → 400, bad password → 401", async ({ page }) => {
|
||
|
|
// Bad captcha answer returns 400
|
||
|
|
const r1 = await page.request.post(`${BASE}/api/auth/login`, {
|
||
|
|
data: { username: "x", password: "x", challengeId: "fake", captchaAnswer: "wrong" },
|
||
|
|
});
|
||
|
|
expect([400, 429]).toContain(r1.status());
|
||
|
|
|
||
|
|
// Correct captcha, wrong password → 401
|
||
|
|
const auth = requireAuth();
|
||
|
|
const captchaRes = await page.request.get(`${BASE}/api/auth/captcha`);
|
||
|
|
const { challengeId, answer } = await captchaRes.json();
|
||
|
|
const r2 = await page.request.post(`${BASE}/api/auth/login`, {
|
||
|
|
data: { username: ADMIN_EMAIL, password: "definitely-wrong", challengeId, captchaAnswer: answer },
|
||
|
|
});
|
||
|
|
expect(r2.status()).toBe(401);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe("Auth — requires working CAPTCHA", () => {
|
||
|
|
test("admin login via API succeeds and /me returns admin role", async ({ page }) => {
|
||
|
|
const auth = requireAuth();
|
||
|
|
const res = await page.request.get(`${BASE}/api/auth/me`, {
|
||
|
|
headers: { Authorization: `Bearer ${auth.accessToken}` },
|
||
|
|
});
|
||
|
|
expect(res.status()).toBe(200);
|
||
|
|
const body = await res.json();
|
||
|
|
expect(body.user.role).toBe("admin");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("logout invalidates refresh token", async ({ page }) => {
|
||
|
|
requireAuth();
|
||
|
|
// Issue a fresh session for this test only (don't use the shared token)
|
||
|
|
const captchaRes = await page.request.get(`${BASE}/api/auth/captcha`);
|
||
|
|
const { challengeId, answer } = await captchaRes.json();
|
||
|
|
if (!answer) { test.skip(true, "CAPTCHA not solvable"); return; }
|
||
|
|
|
||
|
|
const loginRes = await page.request.post(`${BASE}/api/auth/login`, {
|
||
|
|
data: { username: ADMIN_EMAIL, password: ADMIN_PASSWORD, challengeId, captchaAnswer: answer },
|
||
|
|
});
|
||
|
|
const { accessToken, refreshToken } = await loginRes.json();
|
||
|
|
|
||
|
|
const logout = await page.request.post(`${BASE}/api/auth/logout`, {
|
||
|
|
data: { refreshToken },
|
||
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||
|
|
});
|
||
|
|
expect(logout.status()).toBe(200);
|
||
|
|
|
||
|
|
const refresh = await page.request.post(`${BASE}/api/auth/refresh`, {
|
||
|
|
data: { refreshToken },
|
||
|
|
});
|
||
|
|
expect(refresh.status()).toBe(401);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("cap page loads and stays off /login", async ({ page }) => {
|
||
|
|
await injectAuth(page);
|
||
|
|
await page.goto("/cap");
|
||
|
|
await page.waitForLoadState("networkidle", { timeout: 15_000 });
|
||
|
|
expect(page.url()).not.toContain("/login");
|
||
|
|
});
|
||
|
|
});
|