test(auth): KTO-01 Playwright-E2E (api, web, throttle) und CI-Job e2e #42
2 changed files with 172 additions and 0 deletions
78
e2e/tests/spike/smtp-down.spec.ts
Normal file
78
e2e/tests/spike/smtp-down.spec.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* SPIKE (Issue #37), NICHT blockierend: Verhalten von /recover, /signup und /resend, wenn der
|
||||
* SMTP-Server nicht erreichbar ist, fuer bekannte vs. unbekannte Adressen.
|
||||
*
|
||||
* Laeuft gegen einen dritten GoTrue-Container, dessen SMTP-Port 1 die Verbindung ablehnt
|
||||
* (`e2e/support/start-throttle-gotrue.sh start smtp-down`, SPIKE_GOTRUE_URL). Mailpit bleibt fuer
|
||||
* die anderen Projekte unberuehrt.
|
||||
*
|
||||
* Die Tests pruefen NICHTS ausser dem Ablauf; sie erfassen Status, Body, Dauer und den Zustand in
|
||||
* der DB je Fall und schreiben ihn in die Konsole und als Anhang `smtp-down-<fall>.json`. Eine
|
||||
* Auswertung steht in docs/auth-konfiguration.md (Abschnitt „SMTP-Ausfall (Spike #37)“).
|
||||
*/
|
||||
import { test } from '@playwright/test';
|
||||
import { env } from '../../support/auth-config';
|
||||
import { userByEmail } from '../../support/db';
|
||||
import { GoTrueClient, randomEmail, randomPassword, validMetadata } from '../../support/gotrue';
|
||||
import { countMails } from '../../support/mailpit';
|
||||
|
||||
const base = env.spikeGotrueUrl;
|
||||
test.skip(!base, 'SPIKE_GOTRUE_URL nicht gesetzt (start-throttle-gotrue.sh start smtp-down)');
|
||||
|
||||
const spike = () => new GoTrueClient({ baseUrl: base!, apikey: null });
|
||||
const cli = new GoTrueClient();
|
||||
|
||||
async function record(info: typeof test.info extends () => infer R ? R : never, name: string, data: unknown) {
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
console.log(`[SPIKE smtp-down] ${name}: ${json}`);
|
||||
await info.attach(`smtp-down-${name}.json`, { body: json, contentType: 'application/json' });
|
||||
}
|
||||
|
||||
const summarize = (r: { status: number; body: any; ms: number }) => ({
|
||||
status: r.status,
|
||||
error_code: r.body?.error_code ?? null,
|
||||
msg: r.body?.msg ?? r.body?.message ?? null,
|
||||
hasUserId: Boolean(r.body?.id),
|
||||
ms: r.ms,
|
||||
});
|
||||
|
||||
test.describe('Spike #37: SMTP nicht erreichbar', () => {
|
||||
test('recover: bekannte (bestaetigte) vs. unbekannte Adresse', async () => {
|
||||
const known = randomEmail();
|
||||
await cli.createConfirmedUser(known, randomPassword());
|
||||
const unknown = randomEmail('unknown');
|
||||
const a = await spike().recover(known);
|
||||
const b = await spike().recover(unknown);
|
||||
await record(test.info(), 'recover', { known: summarize(a), unknown: summarize(b) });
|
||||
});
|
||||
|
||||
test('signup: neue vs. bestaetigte vs. unbestaetigte Adresse', async () => {
|
||||
const confirmed = randomEmail();
|
||||
await cli.createConfirmedUser(confirmed, randomPassword());
|
||||
const unconfirmed = randomEmail();
|
||||
await cli.signUp(unconfirmed, randomPassword());
|
||||
const fresh = randomEmail();
|
||||
|
||||
const rFresh = await spike().signUp(fresh, randomPassword());
|
||||
const rConfirmed = await spike().signUp(confirmed, randomPassword());
|
||||
// AUTH-CFG-08: sonst antwortet GoTrue mit 429 statt dem SMTP-Fehler.
|
||||
await new Promise((r) => setTimeout(r, 1300));
|
||||
const rUnconfirmed = await spike().signUp(unconfirmed, randomPassword());
|
||||
await record(test.info(), 'signup', {
|
||||
newAddress: { ...summarize(rFresh), userInDbAfterwards: Boolean(await userByEmail(fresh)) },
|
||||
confirmedAddress: summarize(rConfirmed),
|
||||
unconfirmedAddress: summarize(rUnconfirmed),
|
||||
mailsToFresh: await countMails(fresh),
|
||||
});
|
||||
});
|
||||
|
||||
test('resend: unbestaetigte vs. unbekannte Adresse', async () => {
|
||||
const unconfirmed = randomEmail();
|
||||
await cli.signUp(unconfirmed, randomPassword(), validMetadata());
|
||||
await new Promise((r) => setTimeout(r, 1300));
|
||||
const unknown = randomEmail('unknown');
|
||||
const a = await spike().resend(unconfirmed);
|
||||
const b = await spike().resend(unknown);
|
||||
await record(test.info(), 'resend', { unconfirmed: summarize(a), unknown: summarize(b) });
|
||||
});
|
||||
});
|
||||
94
e2e/tests/throttle/krit07-drosselung.spec.ts
Normal file
94
e2e/tests/throttle/krit07-drosselung.spec.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* KTO-01 Kriterium 7 (Drosselung), AUTH-CFG-10 und AUTH-CFG-13.
|
||||
*
|
||||
* Laeuft gegen den ZUSAETZLICHEN GoTrue-Container mit GOTRUE_RATE_LIMIT_HEADER=X-Forwarded-For
|
||||
* (e2e/support/start-throttle-gotrue.sh, THROTTLE_GOTRUE_URL), nicht gegen die CLI: Die CLI
|
||||
* drosselt nie (Spike A2, F11). Jeder Test nutzt eine eigene zufaellige IP (simuliert den Proxy,
|
||||
* R13). Es wird direkt auf GoTrue zugegriffen, ohne Kong und ohne apikey.
|
||||
*/
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { authConfig, env, errorCodes } from '../../support/auth-config';
|
||||
import { GoTrueClient, randomEmail, randomIp, randomPassword } from '../../support/gotrue';
|
||||
|
||||
const base = env.throttleGotrueUrl;
|
||||
test.skip(!base, 'THROTTLE_GOTRUE_URL nicht gesetzt (siehe e2e/support/start-throttle-gotrue.sh)');
|
||||
|
||||
const client = (ip?: string) =>
|
||||
new GoTrueClient({ baseUrl: base!, apikey: null, headers: ip ? { 'X-Forwarded-For': ip } : {} });
|
||||
|
||||
/** Konto, das GoTrue-Instanz und DB mit der CLI teilen; Anlage ohne Header ueber die CLI (Service-Role). */
|
||||
async function createUser() {
|
||||
const email = randomEmail();
|
||||
const password = randomPassword();
|
||||
const res = await new GoTrueClient().createConfirmedUser(email, password);
|
||||
expect(res.status).toBe(200);
|
||||
return { email, password };
|
||||
}
|
||||
|
||||
test.describe('KTO-01 Kriterium 7: Drosselung der Anmeldung (throttle)', () => {
|
||||
test('KTO-01 Kriterium 7: AUTH-CFG-10 30 Fehllogins einer IP -> invalid_credentials, Login #31 -> 429 over_request_rate_limit', async () => {
|
||||
const { email, password } = await createUser();
|
||||
const ip = randomIp();
|
||||
const gotrue = client(ip);
|
||||
|
||||
for (let attempt = 1; attempt <= authConfig.loginBurst; attempt++) {
|
||||
const res = await gotrue.signIn(email, `${password}-falsch`);
|
||||
expect(res.status, `Versuch ${attempt}`).toBe(400);
|
||||
expect(res.body.error_code, `Versuch ${attempt}`).toBe(errorCodes.invalidCredentials);
|
||||
}
|
||||
|
||||
// Login #31 -> 429. Der Limiter fuellt 0,5 Token/s nach; auf langsamen Rechnern darf die
|
||||
// erste 429 deshalb bis zu zwei Versuche spaeter kommen (Burst bleibt 30, AUTH-CFG-10).
|
||||
let limited = await gotrue.signIn(email, `${password}-falsch`);
|
||||
for (let extra = 0; extra < 2 && limited.status !== 429; extra++) {
|
||||
limited = await gotrue.signIn(email, `${password}-falsch`);
|
||||
}
|
||||
expect(limited.status).toBe(429);
|
||||
expect(limited.body.error_code).toBe(errorCodes.overRequestRateLimit);
|
||||
// Auch der richtige Login dieser IP wird gedrosselt (Drosselung je IP, nicht je Passwort).
|
||||
expect((await gotrue.signIn(email, password)).status).toBe(429);
|
||||
});
|
||||
|
||||
test('KTO-01 Kriterium 7: AUTH-CFG-10 die 429-Antwort traegt weder Retry-After noch X-RateLimit-* (App nennt keine Dauer)', async () => {
|
||||
const { email, password } = await createUser();
|
||||
const gotrue = client(randomIp());
|
||||
let limited;
|
||||
for (let attempt = 1; attempt <= authConfig.loginBurst + 5; attempt++) {
|
||||
const res = await gotrue.signIn(email, `${password}-falsch`);
|
||||
if (res.status === 429) {
|
||||
limited = res;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(limited, 'es gab eine 429-Antwort').toBeTruthy();
|
||||
expect(limited!.headers.get('retry-after')).toBeNull();
|
||||
const rateHeaders = [...limited!.headers.keys()].filter((h) => h.toLowerCase().startsWith('x-ratelimit'));
|
||||
expect(rateHeaders).toEqual([]);
|
||||
});
|
||||
|
||||
test('KTO-01 Kriterium 7: AUTH-CFG-10 eine zweite IP ist von der Drosselung der ersten nicht betroffen', async () => {
|
||||
const { email, password } = await createUser();
|
||||
const first = client(randomIp());
|
||||
for (let attempt = 1; attempt <= authConfig.loginBurst; attempt++) {
|
||||
await first.signIn(email, `${password}-falsch`);
|
||||
}
|
||||
expect((await first.signIn(email, `${password}-falsch`)).status).toBe(429);
|
||||
|
||||
const second = client(randomIp());
|
||||
const wrong = await second.signIn(email, `${password}-falsch`);
|
||||
expect(wrong.status).toBe(400);
|
||||
expect(wrong.body.error_code).toBe(errorCodes.invalidCredentials);
|
||||
const right = await second.signIn(email, password);
|
||||
expect(right.status).toBe(200);
|
||||
expect(right.body.access_token).toBeTruthy();
|
||||
});
|
||||
|
||||
test('KTO-01 Kriterium 7: AUTH-CFG-13 Requests ohne X-Forwarded-For werden nicht gedrosselt', async () => {
|
||||
const { email, password } = await createUser();
|
||||
const gotrue = client();
|
||||
for (let attempt = 1; attempt <= authConfig.loginBurst + 5; attempt++) {
|
||||
const res = await gotrue.signIn(email, `${password}-falsch`);
|
||||
expect(res.status, `Versuch ${attempt}`).toBe(400);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue