feat: implement TASK-WP1-03 registration notice
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user