dev1/tests/e2e/98-security-basics.spec.mjs

122 lines
4.9 KiB
JavaScript

// 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']);
});
});