Shuffle/e2e/support/gotrue.ts
cschlaefke f302c56904 test(auth): add Playwright project with api tests for KTO-01 (GoTrue, Mailpit, db helper)
Refs #38

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-03 10:29:18 +01:00

134 lines
4.7 KiB
TypeScript

import { randomBytes, randomUUID } from 'node:crypto';
import { authConfig, env, TERMS_VERSION } from './auth-config';
/** Zufaellige E-Mail-Adresse je Test (Spec 7: jeder Test nutzt eigene Adressen). */
export function randomEmail(prefix = 'e2e'): string {
return `${prefix}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}@example.test`;
}
/** Zufaelliges, ausreichend langes Passwort (AUTH-CFG-05: mind. 10 Zeichen). */
export function randomPassword(): string {
return `Pw-${randomBytes(8).toString('hex')}-x`;
}
/** Zufaellige IPv4 fuer `X-Forwarded-For` (nur Projekt `throttle`, nie gegen die CLI, R13). */
export function randomIp(): string {
const octet = () => 1 + Math.floor(Math.random() * 253);
return `${octet()}.${octet()}.${octet()}.${octet()}`;
}
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
/** Wartet etwas mehr als AUTH-CFG-08 (1 s), damit Folgemails nicht an 429 scheitern. */
export const waitMailInterval = () => sleep(authConfig.maxFrequencySeconds * 1000 + 300);
export interface GoTrueResponse<T = any> {
status: number;
body: T;
headers: Headers;
/** Dauer in Millisekunden. */
ms: number;
}
export interface ClientOptions {
/** Basis-URL inkl. `/auth/v1` (Standard: Kong). */
baseUrl?: string;
/** Beim Zugriff ueber Kong noetig, direkt auf GoTrue nicht. */
apikey?: string | null;
/** Zusatz-Header, z. B. `X-Forwarded-For` (nur `throttle`). */
headers?: Record<string, string>;
}
/** Minimaler GoTrue-Client (REST, wie `supabase_flutter`). */
export class GoTrueClient {
readonly baseUrl: string;
private readonly apikey: string | null;
private readonly extraHeaders: Record<string, string>;
constructor(opts: ClientOptions = {}) {
this.baseUrl = opts.baseUrl ?? `${env.supabaseUrl}/auth/v1`;
this.apikey = opts.apikey === undefined ? env.anonKey : opts.apikey;
this.extraHeaders = opts.headers ?? {};
}
async request<T = any>(
method: string,
path: string,
body?: unknown,
token?: string,
extra: Record<string, string> = {},
): Promise<GoTrueResponse<T>> {
const headers: Record<string, string> = { 'content-type': 'application/json', ...this.extraHeaders, ...extra };
if (this.apikey) headers.apikey = this.apikey;
if (token) headers.authorization = `Bearer ${token}`;
const started = Date.now();
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await res.text();
let parsed: any = text;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
/* Body ist kein JSON */
}
return { status: res.status, body: parsed, headers: res.headers, ms: Date.now() - started };
}
signUp(email: string, password: string, data: Record<string, unknown> | null = validMetadata()) {
return this.request('POST', '/signup', { email, password, ...(data ? { data } : {}) });
}
signIn(email: string, password: string) {
return this.request('POST', '/token?grant_type=password', { email, password });
}
refresh(refreshToken: string) {
return this.request('POST', '/token?grant_type=refresh_token', { refresh_token: refreshToken });
}
recover(email: string) {
return this.request('POST', '/recover', { email });
}
resend(email: string, type: 'signup' = 'signup') {
return this.request('POST', '/resend', { email, type });
}
/** `verifyOTP(tokenHash, type)`. */
verify(tokenHash: string, type: 'email' | 'signup' | 'recovery') {
return this.request('POST', '/verify', { token_hash: tokenHash, type });
}
getUser(accessToken: string) {
return this.request('GET', '/user', undefined, accessToken);
}
updateUser(accessToken: string, attributes: Record<string, unknown>) {
return this.request('PUT', '/user', attributes, accessToken);
}
/** Legt ein bestaetigtes Konto direkt an (Service-Role, nur lokal), fuer Tests, die die Anmeldung pruefen. */
async createConfirmedUser(
email: string,
password: string,
data: Record<string, unknown> = validMetadata(),
) {
return this.request(
'POST',
'/admin/users',
{ email, password, email_confirm: true, user_metadata: data },
env.serviceRoleKey,
);
}
}
/** Gueltige Registrierungs-Metadaten (Spec 4.2). */
export function validMetadata(overrides: Record<string, unknown> = {}): Record<string, unknown> {
const data: Record<string, unknown> = {
display_name: 'E2E Tester',
age_confirmed: true,
terms_accepted: true,
terms_version: TERMS_VERSION,
locale: 'en',
...overrides,
};
for (const key of Object.keys(data)) if (data[key] === undefined) delete data[key];
return data;
}
export const uniqueId = () => randomUUID();