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:
fabi
2026-07-15 20:45:59 +02:00
parent f8cba95e49
commit bbdfae09a0
67 changed files with 2441 additions and 396 deletions

View File

@@ -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);
});
});