This commit is contained in:
Felix Faerber committed 2026-05-20 15:08:45 +03:00
commit c54e888458
67 files changed
+5933

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
/**
* Shared E2E helpers. Tests run inside the dockerized stack — the api/worker
* are reachable as service names on the compose network.
*
* Pattern lifted from cipherdolls/core/test/helpers.ts: a single `api(...)`
* wrapper around fetch, a `waitForQueuesEmpty()` poller for the
* model.x.create → BullMQ → processor pipeline, and persona-style auth
* records keyed in a shared `auth` table.
*/
import IORedis from 'ioredis';
export const BASE_URL = process.env.HOSTED_GATEWAY_URL ?? 'http://localhost:8080';
const REDIS_HOST = process.env.REDIS_HOST ?? 'localhost';
const REDIS_PORT = parseInt(process.env.REDIS_PORT ?? '6379');
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export interface ApiResult<T = any> {
status: number;
body: T;
}
/**
* Issue an HTTP request to the hosted-gateway api. Accepts either a JWT or an
* API key (bare uuid OR the `t4t-live-<uuid>` form) as `auth`.
*/
export async function api<T = any>(
method: HttpMethod,
path: string,
auth?: string,
body?: any,
): Promise<ApiResult<T>> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (auth) headers.Authorization = `Bearer ${auth}`;
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let parsed: any = text;
try { parsed = JSON.parse(text); } catch { /* keep raw */ }
return { status: res.status, body: parsed as T };
}
export const get = (path: string, auth?: string) => api('GET', path, auth);
export const post = (path: string, body: any, auth?: string) => api('POST', path, auth, body);
export const del = (path: string, auth?: string) => api('DELETE', path, auth);
/**
* Wait until all BullMQ queues are empty AND stay empty for `settleMs` —
* defends against cascades where one processor enqueues the next (eg
* user.created → account.created → ledger welcomeGrant).
*/
export async function waitForQueuesEmpty(
timeoutMs = 15_000,
settleMs = 500,
): Promise<void> {
const redis = new IORedis(REDIS_PORT, REDIS_HOST, { maxRetriesPerRequest: null });
const interval = 100;
let elapsed = 0;
let emptyFor = 0;
try {
while (elapsed < timeoutMs) {
const queueKeys = [
...(await redis.keys('bull:*:waiting')),
...(await redis.keys('bull:*:active')),
...(await redis.keys('bull:*:delayed')),
];
let total = 0;
for (const key of queueKeys) {
const type = await redis.type(key);
if (type === 'list') total += await redis.llen(key);
else if (type === 'zset') total += await redis.zcard(key);
}
if (total === 0) {
emptyFor += interval;
if (emptyFor >= settleMs) return;
} else {
emptyFor = 0;
}
await new Promise((r) => setTimeout(r, interval));
elapsed += interval;
}
throw new Error(`queues not empty after ${timeoutMs}ms`);
} finally {
redis.disconnect();
}
}
/* ────────────────────────────────────────────────────────────────
Auth flow helpers — mirror cipherdolls' signIn / createApiKey shape,
but for email/password (Phase 1 doesn't wire real Google OAuth in tests).
──────────────────────────────────────────────────────────────── */
export interface AuthRecord {
email: string;
password: string;
jwt: string;
userId: string;
apiKey: string;
apiKeyId: string;
}
/** Persona registry — populated by `describe*()` factories and read by
* downstream specs. New personas added as the test suite grows. */
export const auth: Record<string, Partial<AuthRecord>> = {
alice: {},
bob: {},
carol: {},
dave: {},
};
export async function signupEmail(
email: string,
password: string,
): Promise<{ jwt: string; userId: string; verificationToken: string }> {
const { status, body } = await post('/auth/email/signup', { email, password });
if (status !== 200) throw new Error(`signup ${email} failed: ${status} ${JSON.stringify(body)}`);
return { jwt: body.token, userId: body.user.id, verificationToken: body.verificationToken };
}
export async function verifyEmail(token: string): Promise<void> {
const { status, body } = await post('/auth/email/verify', { token });
if (status !== 200) throw new Error(`verify failed: ${status} ${JSON.stringify(body)}`);
}
export async function loginEmail(
email: string,
password: string,
): Promise<{ jwt: string; userId: string }> {
const { status, body } = await post('/auth/email/login', { email, password });
if (status !== 200) throw new Error(`login ${email} failed: ${status} ${JSON.stringify(body)}`);
return { jwt: body.token, userId: body.user.id };
}
export async function createApiKey(
jwt: string,
name: string,
): Promise<{ id: string; key: string }> {
const { status, body } = await post('/api-keys', { name }, jwt);
if (status !== 200) throw new Error(`createApiKey failed: ${status} ${JSON.stringify(body)}`);
return { id: body.id, key: body.key };
}