Files
tyx_AI_xhs/tests/api/wp1-01-registration.test.ts
T

125 lines
5.0 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import { RegistrationService } from "../../apps/api/src/registration.js";
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
import { isErrorEnvelope } from "../../packages/shared-contracts/src/index.js";
const fixedNow = Date.parse("2026-07-28T06:00:00.000Z");
const writeHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
const roots: string[] = [];
const services: RegistrationService[] = [];
function createRegistrationService() {
const root = mkdtempSync(join(tmpdir(), "dada-wp1-01-api-"));
roots.push(root);
const resend = new MockResendAdapter();
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x41),
clock: () => fixedNow,
codeGenerator: () => "572914",
currentPrivacyNoticeVersion: "p0a-notice-v1",
databasePath: join(root, "dada.sqlite3"),
inviteCodeGenerator: () => "DADA-WP1-API",
invitePepper: Buffer.alloc(32, 0x42),
resend,
sessionPepper: Buffer.alloc(32, 0x43),
});
services.push(registration);
return { registration, resend };
}
afterEach(() => {
for (const service of services.splice(0)) {
try { service.close(); } catch { /* already closed by the test */ }
}
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TASK-WP1-01 registration API contract", () => {
it("resolves REGISTER_SEND and REGISTER_COMPLETE to one OpenAPI operation each", async () => {
const { registration, resend } = createRegistrationService();
const invite = registration.createInvite({ expiresAt: fixedNow + 86_400_000, maxUses: 2 });
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
const sent = await app.inject({
headers: writeHeaders,
method: "POST",
payload: { email: "api-user@example.invalid", invite_code: invite.code },
url: "/api/v1/auth/register/send",
});
expect(sent.statusCode).toBe(200);
expect(sent.json()).toMatchObject({ status: "verification_sent" });
const completed = await app.inject({
headers: { ...writeHeaders, "idempotency-key": "wp1-api-idempotency-key-000000000001" },
method: "POST",
payload: {
creator_name: "API Creator",
privacy_consent_accepted: true,
privacy_notice_version: "p0a-notice-v1",
registration_id: sent.json().registration_id,
social_id: "@@api_creator",
verification_code: resend.readLatestCode("api-user@example.invalid"),
},
url: "/api/v1/auth/register/complete",
});
expect(completed.statusCode).toBe(200);
expect(completed.json()).toMatchObject({
credits: { available_balance: 10, reserved_balance: 0 },
status: "registered",
user: { role: "user", social_id: "@api_creator", status: "active" },
});
expect(completed.headers["set-cookie"]).toContain("dada_session=");
expect(completed.headers["set-cookie"]).toContain("HttpOnly");
expect(completed.headers["set-cookie"]).toContain("SameSite=Strict");
const session = await app.inject({
headers: { cookie: completed.headers["set-cookie"], host: "127.0.0.1:43121" },
method: "GET",
url: "/api/v1/auth/session",
});
expect(session.statusCode).toBe(200);
expect(session.json()).toMatchObject({
audience: "user",
authenticated: true,
credits: { available_balance: 10, reserved_balance: 0 },
});
const openapi = app.swagger() as { paths?: Record<string, Record<string, { operationId?: string }>> };
const operationIds = Object.values(openapi.paths ?? {}).flatMap((path) =>
Object.values(path).map((operation) => operation.operationId),
);
expect(operationIds.filter((id) => id === "sendRegistrationCode")).toHaveLength(1);
expect(operationIds.filter((id) => id === "completeRegistration")).toHaveLength(1);
await app.close();
});
it("returns a stable field error and does not send mail for an invalid invite", async () => {
const { registration, resend } = createRegistrationService();
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
const response = await app.inject({
headers: writeHeaders,
method: "POST",
payload: { email: "api-rejected@example.invalid", invite_code: "missing-invite" },
url: "/api/v1/auth/register/send",
});
expect(response.statusCode).toBe(409);
expect(isErrorEnvelope(response.json())).toBe(true);
expect(response.json()).toMatchObject({
error: {
code: "REGISTRATION_REJECTED",
details: { field_errors: [{ field: "invite_code", message_key: "auth.invite.not_found" }] },
message_key: "auth.registration.rejected",
},
});
expect(resend.calls).toEqual([]);
await app.close();
});
});