chore(e2e): add ESLint + Prettier; fix real findings; dedupe BASE
The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).
Rules keep the ones that catch real TEST bugs and drop the noise:
- no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
- no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
`api` arg, an unused JPEG_MAGIC import), all removed.
- no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
a page.evaluate() return.
- no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.
Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.
Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,23 +8,28 @@
|
||||
*/
|
||||
import { test, expect } from '../../fixtures/test';
|
||||
import { ADMIN_PASSWORD } from '../../fixtures/api-client';
|
||||
|
||||
const BASE = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101';
|
||||
import { BASE } from '../../helpers/env';
|
||||
|
||||
/** RFC-4648 base64url with no padding. */
|
||||
function b64u(s: string) {
|
||||
return Buffer.from(s).toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||
return Buffer.from(s)
|
||||
.toString('base64')
|
||||
.replace(/=+$/, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
}
|
||||
|
||||
test.describe('Adversarial — JWT', () => {
|
||||
test('alg:none token claiming admin role is rejected', async () => {
|
||||
const header = b64u(JSON.stringify({ alg: 'none', typ: 'JWT' }));
|
||||
const payload = b64u(JSON.stringify({
|
||||
sub: '00000000-0000-0000-0000-000000000000',
|
||||
role: 'admin',
|
||||
event_id: '00000000-0000-0000-0000-000000000000',
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
}));
|
||||
const payload = b64u(
|
||||
JSON.stringify({
|
||||
sub: '00000000-0000-0000-0000-000000000000',
|
||||
role: 'admin',
|
||||
event_id: '00000000-0000-0000-0000-000000000000',
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
})
|
||||
);
|
||||
const token = `${header}.${payload}.`;
|
||||
const res = await fetch(`${BASE}/api/v1/admin/config`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -44,7 +49,9 @@ test.describe('Adversarial — JWT', () => {
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
test('JWT with payload-tampered role=admin (re-encoded payload, original signature) is rejected', async ({ guest }) => {
|
||||
test('JWT with payload-tampered role=admin (re-encoded payload, original signature) is rejected', async ({
|
||||
guest,
|
||||
}) => {
|
||||
const g = await guest('RolePromote');
|
||||
const parts = g.jwt.split('.');
|
||||
const original = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
@@ -111,7 +118,9 @@ test.describe('Adversarial — PIN brute-force', () => {
|
||||
expect(correct.status).toBe(429);
|
||||
});
|
||||
|
||||
test('parallel wrong-PIN attempts still lock the account (counter is not lost to the race)', async ({ guest }) => {
|
||||
test('parallel wrong-PIN attempts still lock the account (counter is not lost to the race)', async ({
|
||||
guest,
|
||||
}) => {
|
||||
const g = await guest('BruteParallel');
|
||||
const wrong = g.pin === '0000' ? '1111' : '0000';
|
||||
|
||||
@@ -125,7 +134,10 @@ test.describe('Adversarial — PIN brute-force', () => {
|
||||
)
|
||||
);
|
||||
const statuses = attempts.map((r) => r.status);
|
||||
expect(statuses.filter((s) => s === 200), 'a wrong PIN must never authenticate').toHaveLength(0);
|
||||
expect(
|
||||
statuses.filter((s) => s === 200),
|
||||
'a wrong PIN must never authenticate'
|
||||
).toHaveLength(0);
|
||||
|
||||
// The in-flight requests all read `pin_locked_until` before any of them wrote it, so
|
||||
// *which* of the 10 come back 429 is genuinely racy and can't be asserted. What is NOT
|
||||
@@ -140,7 +152,10 @@ test.describe('Adversarial — PIN brute-force', () => {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ display_name: g.displayName, pin: g.pin }),
|
||||
});
|
||||
expect(correct.status, 'after 10 wrong PINs the account must be locked, even for the right PIN').toBe(429);
|
||||
expect(
|
||||
correct.status,
|
||||
'after 10 wrong PINs the account must be locked, even for the right PIN'
|
||||
).toBe(429);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,7 +203,10 @@ test.describe('Adversarial — admin password brute-force', () => {
|
||||
}
|
||||
|
||||
// The password path actually ran (budget existed) before the limiter engaged...
|
||||
expect(statuses[0], 'first attempt should be a normal wrong-password 401, not a spurious 429').toBe(401);
|
||||
expect(
|
||||
statuses[0],
|
||||
'first attempt should be a normal wrong-password 401, not a spurious 429'
|
||||
).toBe(401);
|
||||
// ...and the limiter DID engage within the window. Delete the throttle and this is never true.
|
||||
expect(
|
||||
statuses.some((s) => s === 429),
|
||||
@@ -218,7 +236,10 @@ test.describe('Adversarial — admin password brute-force', () => {
|
||||
|
||||
// The right password, while throttled, must STILL be refused — the limiter is checked before
|
||||
// the bcrypt verify, so a valid credential does not buy a way around a brute-force lockout.
|
||||
expect((await tryLogin(ADMIN_PASSWORD)).status, 'a throttled IP is refused even with the correct password').toBe(429);
|
||||
expect(
|
||||
(await tryLogin(ADMIN_PASSWORD)).status,
|
||||
'a throttled IP is refused even with the correct password'
|
||||
).toBe(429);
|
||||
});
|
||||
|
||||
test('the throttle is gated: with the limiter disabled, a burst is NOT rate-limited', async ({
|
||||
@@ -235,6 +256,9 @@ test.describe('Adversarial — admin password brute-force', () => {
|
||||
const statuses: number[] = [];
|
||||
for (let i = 0; i < 8; i++) statuses.push((await tryLogin('wrong-' + i)).status);
|
||||
|
||||
expect(statuses.every((s) => s === 401), 'with admin_login_rate_enabled=false no attempt should be 429').toBe(true);
|
||||
expect(
|
||||
statuses.every((s) => s === 401),
|
||||
'with admin_login_rate_enabled=false no attempt should be 429'
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user