chore(infra): update dev compose, add playwright e2e suite & assets

This commit is contained in:
ahp
2026-10-20 17:51:23 +03:30
parent f6ce2bd340
commit 3c5a32394d
23 changed files with 542 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
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");
});
});