91 lines
4.1 KiB
TypeScript
91 lines
4.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { RegistrationService } from "../../apps/api/src/registration.js";
|
|
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
|
import { wp604OperationMatrix } from "../fixtures/wp6-04-audit.js";
|
|
|
|
const roots: string[] = [];
|
|
const services: RegistrationService[] = [];
|
|
const now = Date.parse("2026-08-04T09:30:00.000Z");
|
|
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), "dada-wp6-04-integration-"));
|
|
roots.push(root);
|
|
const registration = new RegistrationService({
|
|
challengePepper: Buffer.alloc(32, 0xb1),
|
|
clock: () => now,
|
|
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
|
databasePath: join(root, "dada.sqlite3"),
|
|
invitePepper: Buffer.alloc(32, 0xb2),
|
|
inviteCodeGenerator: () => "fixture-invite-code",
|
|
resend: new MockResendAdapter(),
|
|
sessionPepper: Buffer.alloc(32, 0xb3),
|
|
});
|
|
services.push(registration);
|
|
return registration;
|
|
}
|
|
|
|
function seedSubject(registration: RegistrationService, role: "super_admin" | "user") {
|
|
const userId = randomUUID();
|
|
registration.database.prepare(`
|
|
INSERT INTO users (
|
|
user_id, normalized_email, role, status, counts_toward_stage_limit,
|
|
registration_id, created_at
|
|
) VALUES (?, ?, ?, 'active', ?, ?, ?)
|
|
`).run(userId, `${role}-${userId}@example.invalid`, role, role === "user" ? 1 : 0, randomUUID(), now);
|
|
if (role === "super_admin") {
|
|
registration.database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(userId);
|
|
}
|
|
return userId;
|
|
}
|
|
|
|
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-WP6-AUD-001-sensitive-operations", () => {
|
|
it("writes invite and user status audits in the same transaction as the mutation", () => {
|
|
const registration = fixture();
|
|
const adminId = seedSubject(registration, "super_admin");
|
|
const userId = seedSubject(registration, "user");
|
|
const adminOperations = registration as RegistrationService & {
|
|
createAdminInvite(input: { actorId: string; expiresAt: number; maxUses: number }): { code: string; inviteId: string };
|
|
changeUserStatus(userId: string, status: "suspended" | "deleted", actorId: string): void;
|
|
};
|
|
|
|
const invite = adminOperations.createAdminInvite({ actorId: adminId, expiresAt: now + 86_400_000, maxUses: 1 });
|
|
adminOperations.changeUserStatus(userId, "suspended", adminId);
|
|
expect(registration.database.prepare(`
|
|
SELECT operation_type, target_ref FROM admin_operation_logs
|
|
WHERE operation_type IN ('invite_create', 'user_status_change') ORDER BY occurred_at
|
|
`).all()).toEqual([
|
|
{ operation_type: "invite_create", target_ref: invite.inviteId },
|
|
{ operation_type: "user_status_change", target_ref: userId },
|
|
]);
|
|
|
|
registration.database.exec(`
|
|
CREATE TRIGGER force_user_status_audit_failure BEFORE INSERT ON admin_operation_logs
|
|
WHEN NEW.operation_type = 'user_status_change'
|
|
BEGIN SELECT RAISE(ABORT, 'forced_audit_failure'); END;
|
|
`);
|
|
expect(() => adminOperations.changeUserStatus(userId, "deleted", adminId)).toThrow("forced_audit_failure");
|
|
expect(registration.database.prepare("SELECT status FROM users WHERE user_id = ?").get(userId)).toEqual({ status: "suspended" });
|
|
|
|
const evidenceRoot = process.env.DADA_EVIDENCE_DIR_WP6_AUD;
|
|
if (evidenceRoot) {
|
|
mkdirSync(evidenceRoot, { recursive: true });
|
|
writeFileSync(resolve(evidenceRoot, "operation-matrix.json"), `${JSON.stringify({ operations: wp604OperationMatrix }, null, 2)}\n`);
|
|
writeFileSync(resolve(evidenceRoot, "db-diff.json"), `${JSON.stringify({
|
|
audit_failure_rollback: { user_status: "suspended" },
|
|
tables: { admin_operation_logs: "append_only", private_content_access_logs: "append_only_separate" },
|
|
}, null, 2)}\n`);
|
|
}
|
|
});
|
|
});
|