// tests/e2e/98-security-basics.spec.mjs // @ts-check import { test, expect } from '@playwright/test'; import { loadTestUser } from '../utils/testUser.js'; test.describe('@p0 Security basics', () => { test.setTimeout(20000); test('wrong creds are generic; no cookie set; rate-limit headers present', async ({ page }) => { const badUser = `nope_${Date.now()}`; const badPass = 'Bad#12345!'; // A) Via raw API (check headers/body semantics) const resp = await page.request.post('/api/signin', { data: { username: badUser, password: badPass }, }); expect(resp.status()).toBe(401); const body = await resp.json(); expect(body?.error || '').toMatch(/invalid username or password/i); // no sensitive fields in body expect(body).not.toHaveProperty('id'); expect(body).not.toHaveProperty('user'); expect(body).not.toHaveProperty('token'); // standard rate-limit headers (express-rate-limit standardHeaders:true) const h = resp.headers(); expect( 'ratelimit-remaining' in h || 'ratelimit-limit' in h || 'ratelimit-reset' in h ).toBeTruthy(); // B) Via UI (no cookie; generic message) await page.context().clearCookies(); await page.goto('/signin', { waitUntil: 'networkidle' }); await page.getByPlaceholder('Username', { exact: true }).fill(badUser); await page.getByPlaceholder('Password', { exact: true }).fill(badPass); await page.getByRole('button', { name: /^Sign In$/ }).click(); // Generic UI error visible await expect(page.getByText(/invalid username or password/i)).toBeVisible(); // No auth cookie was set const cookies = await page.context().cookies(); const anyAuth = cookies.some(c => c.httpOnly && /jwt|session|auth/i.test(c.name)); expect(anyAuth).toBeFalsy(); }); test('successful signin: body has no PII/token; secure, HttpOnly cookie; minimal JIT-PII profile', async ({ page }) => { const user = loadTestUser(); await page.context().clearCookies(); await page.goto('/signin', { waitUntil: 'networkidle' }); // Capture the /api/signin response to inspect body const signinResponsePromise = page.waitForResponse(r => r.request().method() === 'POST' && r.url().includes('/api/signin') ); await page.getByPlaceholder('Username', { exact: true }).fill(user.username); await page.getByPlaceholder('Password', { exact: true }).fill(user.password); await page.getByRole('button', { name: /^Sign In$/ }).click(); const signinResp = await signinResponsePromise; expect(signinResp.status()).toBe(200); const signinBody = await signinResp.json(); // Body should be minimal (no id/email/token/user object) expect(signinBody).not.toHaveProperty('id'); expect(signinBody).not.toHaveProperty('user'); expect(signinBody).not.toHaveProperty('token'); expect(signinBody?.message || '').toMatch(/login successful/i); // Land on SignInLanding await page.waitForURL('**/signin-landing**', { timeout: 15000 }); // Cookie flags: HttpOnly + Secure; SameSite Lax/Strict (or None+Secure) const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.httpOnly && c.secure && /jwt|session|auth|aptiva/i.test(c.name) ); expect(authCookie).toBeTruthy(); if (authCookie) { // sameSite may be 'Lax' | 'Strict' | 'None' expect(['Lax', 'Strict', 'None']).toContain(authCookie.sameSite); if (authCookie.sameSite === 'None') { expect(authCookie.secure).toBeTruthy(); } } // JIT-PII: minimal profile request should NOT leak id/email const prof = await page.request.get('/api/user-profile?fields=firstname'); expect(prof.status()).toBe(200); const profJson = await prof.json(); // allowlist keys that may legitimately be present const allowed = new Set(['firstname', 'is_premium', 'is_pro_premium']); for (const k of Object.keys(profJson)) { expect(allowed.has(k)).toBeTruthy(); } expect(profJson).not.toHaveProperty('id'); expect(profJson).not.toHaveProperty('email'); }); test('username availability endpoint returns only {exists} (no leakage)', async ({ page }) => { const user = loadTestUser(); const unknown = `available_${Date.now()}`; const knownResp = await page.request.get(`/api/check-username/${encodeURIComponent(user.username)}`); expect(knownResp.status()).toBe(200); const knownJson = await knownResp.json(); expect(knownJson).toHaveProperty('exists'); const unknownResp = await page.request.get(`/api/check-username/${encodeURIComponent(unknown)}`); expect(unknownResp.status()).toBe(200); const unknownJson = await unknownResp.json(); expect(unknownJson).toHaveProperty('exists'); // Both shapes should be minimal (only 'exists') expect(Object.keys(knownJson)).toEqual(['exists']); expect(Object.keys(unknownJson)).toEqual(['exists']); }); });