test(e2e): re-point the PIN lockout specs at the property that matters
Three specs asserted the OLD policy — that three wrong PINs lock an account — which is exactly the behaviour the previous commit removed, because that threshold sat below the per-(IP, name) throttle ceiling and so let any single IP lock any guest whose display name is readable off the feed. Rewritten to assert the distinction the fix introduces, which a status code alone cannot show: both tiers answer 429, but only the account lock costs the VICTIM. The new specs read the row via db.isPinLocked rather than the response, so: - one IP hammering /recover is throttled and the account stays UNLOCKED; - a distributed guesser (counter preloaded via db.setFailedPinAttempts, since no single source can reach the threshold any more) still trips the lock, and it holds even against the correct PIN; - concurrent wrong PINs are all counted — the atomicity property the old parallel test was really about, now asserted on the counter instead of inferred from a 429 that the throttle could equally have produced. The UI spec asserts the user-visible half: after four wrong PINs Dave can still get into his own account. It also now types the PIN digit by digit rather than filling and clicking, because the 4th digit auto-submits (pin-auto-submit.spec.ts) and doing both raced the button's disabled state. The adversarial spec enables rate_limits_enabled for its own run — it is off by default in this environment, so without that the throttle tier would silently not be exercised — and restores it in afterEach so it cannot leak into other specs sharing the stack. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,11 @@ test.describe('Auth — join flow', () => {
|
||||
expect(storage.pin).toBe(original.pin);
|
||||
});
|
||||
|
||||
test('wrong PIN three times locks the account for 15 minutes', async ({ page, guest, db }) => {
|
||||
test('repeated wrong PINs are throttled without locking the guest out', async ({
|
||||
page,
|
||||
guest,
|
||||
db,
|
||||
}) => {
|
||||
const dave = await guest('Dave');
|
||||
await clearAllStorage(page);
|
||||
|
||||
@@ -85,22 +89,33 @@ test.describe('Auth — join flow', () => {
|
||||
await join.submit();
|
||||
await expect(join.recoveryPinInput).toBeVisible();
|
||||
|
||||
// Wrong PIN (real one is dave.pin)
|
||||
// Wrong PIN (real one is dave.pin), four times — one more than the OLD lock threshold of 3.
|
||||
// Typed digit by digit so the 4th character auto-submits (see pin-auto-submit.spec.ts);
|
||||
// clicking as well would double-submit and race the disabled state of the button.
|
||||
const wrong = dave.pin === '0000' ? '1111' : '0000';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await join.recoveryPinInput.fill(wrong);
|
||||
await join.recoverySubmit.click();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await join.recoveryPinInput.fill('');
|
||||
await join.recoveryPinInput.pressSequentially(wrong, { delay: 30 });
|
||||
await expect(join.recoveryError).toBeVisible();
|
||||
await expect(join.recoverySubmit).toBeEnabled();
|
||||
}
|
||||
|
||||
// Fourth attempt should hit the 429 lockout (even with the correct PIN now)
|
||||
await join.recoveryPinInput.fill(dave.pin);
|
||||
await join.recoverySubmit.click();
|
||||
await expect(join.recoveryError).toContainText(/15 Minuten/);
|
||||
// THE PROPERTY THIS TEST EXISTS FOR, stated the way a guest experiences it: Dave can still
|
||||
// get into his own account.
|
||||
//
|
||||
// The lock threshold used to be 3, BELOW the per-(IP, name) ceiling — so these very
|
||||
// keystrokes locked Dave out for 15 minutes, and anyone who can read his name off the feed
|
||||
// could do it to him on repeat. Rate limits are disabled in this environment (see
|
||||
// config `rate_limits_enabled`), so what is exercised here is purely the account-lock tier;
|
||||
// the throttle tier is covered in 07-adversarial/auth-tampering.spec.ts.
|
||||
expect(
|
||||
await db.isPinLocked(dave.userId),
|
||||
'four wrong PINs from one device must not lock a guest out of their own account'
|
||||
).toBe(false);
|
||||
|
||||
// Sanity: DB row reflects the lock
|
||||
// (The handler sets pin_locked_until directly — verify via API "recover" returning 429)
|
||||
void db; // unused for now, documenting that db.lockUserPin exists if we want shortcut path
|
||||
await join.recoveryPinInput.fill('');
|
||||
await join.recoveryPinInput.pressSequentially(dave.pin, { delay: 30 });
|
||||
await page.waitForURL('**/feed');
|
||||
});
|
||||
|
||||
test('"Anderen Namen wählen" returns to the normal join form', async ({ page, guest }) => {
|
||||
|
||||
Reference in New Issue
Block a user