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

128 lines
5.6 KiB
TypeScript

import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } 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 { registrationNotice } from "../../packages/shared-contracts/src/registration-notice.js";
const roots: string[] = [];
const services: RegistrationService[] = [];
const writeHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
function createHarness() {
const root = mkdtempSync(join(tmpdir(), "dada-wp1-03-notice-"));
roots.push(root);
const resend = new MockResendAdapter();
const registration = new RegistrationService({
challengePepper: Buffer.alloc(32, 0x74),
clock: () => Date.parse("2026-07-28T09:30:00.000Z"),
codeGenerator: () => "418205",
currentPrivacyNoticeVersion: registrationNotice.version,
databasePath: join(root, "dada.sqlite3"),
inviteCodeGenerator: () => "DADA-WP1-03-NOTICE",
invitePepper: Buffer.alloc(32, 0x75),
resend,
sessionPepper: Buffer.alloc(32, 0x76),
});
services.push(registration);
return { registration, resend };
}
function snapshot(registration: RegistrationService) {
return {
consents: registration.database.prepare("SELECT COUNT(*) AS count FROM privacy_consents").get().count,
credits: registration.database.prepare("SELECT COUNT(*) AS count FROM credit_accounts").get().count,
invite_used: registration.database.prepare("SELECT COALESCE(SUM(used_count), 0) AS count FROM invite_codes").get().count,
sessions: registration.database.prepare("SELECT COUNT(*) AS count FROM sessions").get().count,
users: registration.database.prepare("SELECT COUNT(*) AS count FROM users").get().count,
};
}
function writeEvidence(file: string, value: unknown) {
const directory = process.env.DADA_EVIDENCE_DIR_NOTICE;
if (!directory) return;
mkdirSync(directory, { recursive: true });
writeFileSync(resolve(directory, file), `${JSON.stringify(value, null, 2)}\n`);
}
afterEach(() => {
for (const service of services.splice(0)) service.close();
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
});
describe("TDD-WP1-NOTICE-001-registration-consent", () => {
it("rejects missing/stale consent without side effects and records current consent atomically", async () => {
const { registration, resend } = createHarness();
const invite = registration.createInvite({ expiresAt: Date.parse("2026-07-29T09:30:00.000Z"), maxUses: 1 });
const sent = await registration.sendRegistrationCode({ email: "notice-api@example.invalid", inviteCode: invite.code });
const code = resend.readLatestCode("notice-api@example.invalid");
const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration });
const before = snapshot(registration);
const basePayload = {
creator_name: "Notice User",
privacy_notice_version: registrationNotice.version,
registration_id: sent.registrationId,
social_id: "@notice_user",
verification_code: code,
};
const missingConsent = await app.inject({
headers: { ...writeHeaders, "idempotency-key": "wp1-03-consent-missing-0000000000001" },
method: "POST",
payload: { ...basePayload, privacy_consent_accepted: false },
url: "/api/v1/auth/register/complete",
});
expect(missingConsent.statusCode).toBe(400);
expect(missingConsent.json()).toMatchObject({
error: { details: { field_errors: [{ message_key: "auth.privacy.consent_required" }] } },
});
expect(snapshot(registration)).toEqual(before);
const staleVersion = await app.inject({
headers: { ...writeHeaders, "idempotency-key": "wp1-03-consent-stale-00000000000001" },
method: "POST",
payload: { ...basePayload, privacy_consent_accepted: true, privacy_notice_version: "stale-notice" },
url: "/api/v1/auth/register/complete",
});
expect(staleVersion.statusCode).toBe(400);
expect(staleVersion.json()).toMatchObject({
error: { details: { field_errors: [{ message_key: "auth.privacy.notice_version_invalid" }] } },
});
expect(snapshot(registration)).toEqual(before);
const completed = await app.inject({
headers: { ...writeHeaders, "idempotency-key": "wp1-03-consent-success-0000000000001" },
method: "POST",
payload: { ...basePayload, privacy_consent_accepted: true },
url: "/api/v1/auth/register/complete",
});
expect(completed.statusCode).toBe(200);
const after = snapshot(registration);
expect(after).toEqual({ consents: 1, credits: 1, invite_used: 1, sessions: 1, users: 1 });
const consent = registration.database.prepare("SELECT notice_version, consented_at FROM privacy_consents").get();
expect(consent).toEqual({
consented_at: Date.parse("2026-07-28T09:30:00.000Z"),
notice_version: registrationNotice.version,
});
writeEvidence("response.json", {
accepted: completed.json().status,
missing_consent: missingConsent.json().error.details.field_errors[0].message_key,
stale_version: staleVersion.json().error.details.field_errors[0].message_key,
});
writeEvidence("db-diff.json", {
after,
before,
consent_recorded_at: "2026-07-28T09:30:00.000Z",
notice_content_sha256: registrationNotice.contentSha256,
notice_effective_at: registrationNotice.effectiveAt,
notice_version: registrationNotice.version,
});
await app.close();
});
});