128 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|