138 lines
5.2 KiB
TypeScript
138 lines
5.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
closeLocalPostgresDataPools,
|
|
createLocalPostgresDataClient,
|
|
} from "../src/lib/db/local-postgres-client-core.ts";
|
|
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
|
|
|
const runnerPath = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
|
|
const actorId = "11111111-1111-4111-8111-111111111111";
|
|
const codeId = "22222222-2222-4222-8222-222222222222";
|
|
|
|
function rpcArgs(requestId: string) {
|
|
return {
|
|
p_actor_user_id: actorId,
|
|
p_actor_email: "admin@example.com",
|
|
p_actor_role: "admin",
|
|
p_request_id: requestId,
|
|
};
|
|
}
|
|
|
|
test("admin code functions reject immutable codes, revoked redemption, and roll back on audit failure", async () => {
|
|
const fixture = startPostgresFixture();
|
|
try {
|
|
const migration = spawnSync(process.execPath, [runnerPath], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
SCHEMA_DATABASE_URL: fixture.connectionUrl("schema_owner", "schema-owner-test-password"),
|
|
},
|
|
});
|
|
assert.equal(migration.status, 0, migration.stderr);
|
|
assert.match(migration.stdout, /20260805010000_reconcile_admin_redemption_audit\.sql/);
|
|
|
|
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
|
|
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
|
|
values ('${actorId}', 'Admin', 'admin@example.com', true, now(), 'admin')
|
|
`);
|
|
fixture.psql(`
|
|
insert into public.admin_users (user_id, created_by)
|
|
values ('${actorId}', '${actorId}');
|
|
insert into public.admin_user_roles (admin_user_id, role_id, assigned_by)
|
|
select '${actorId}', id, '${actorId}'
|
|
from public.admin_roles
|
|
where code = 'billing_admin';
|
|
`);
|
|
const userId = fixture.psql(`select id from identity.users where email = 'admin@example.com'`);
|
|
const admin = createLocalPostgresDataClient(
|
|
fixture.connectionUrl("admin_runtime", "admin-runtime-test-password"),
|
|
null,
|
|
null,
|
|
);
|
|
|
|
const created = await admin.rpc("admin_create_redemption_codes", {
|
|
...rpcArgs("create-1"),
|
|
p_codes: [{
|
|
codeHash: "a".repeat(64),
|
|
codeMask: "JYOTISH-****-AUD1",
|
|
credits: 5,
|
|
expiresAt: null,
|
|
note: "initial",
|
|
}],
|
|
p_reason: "create audit test code",
|
|
});
|
|
assert.equal(created.error, null);
|
|
assert.equal((created.data as Array<{ code_mask: string }>)[0]?.code_mask, "JYOTISH-****-AUD1");
|
|
assert.equal(fixture.psql("select count(*) from audit.admin_audit_logs"), "1");
|
|
assert.doesNotMatch(fixture.psql("select after_value::text from audit.admin_audit_logs"), /[a-f0-9]{64}/);
|
|
|
|
const createdId = fixture.psql("select id from public.redemption_codes where code_mask = 'JYOTISH-****-AUD1'");
|
|
const revoked = await admin.rpc("admin_revoke_redemption_code", {
|
|
...rpcArgs("revoke-1"),
|
|
p_code_id: createdId,
|
|
p_reason: "revoke audit test code",
|
|
});
|
|
assert.equal(revoked.error, null);
|
|
|
|
const app = createLocalPostgresDataClient(
|
|
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
|
|
{ id: userId, email: "admin@example.com" },
|
|
);
|
|
const redeemRevoked = await app.rpc("redeem_code", { p_code_hash: "a".repeat(64) });
|
|
assert.deepEqual(redeemRevoked.data, [{ success: false, credits: null, awarded_credits: null, error_code: "revoked_code" }]);
|
|
|
|
fixture.psql(`
|
|
insert into public.redemption_codes (id, code_hash, code_mask, credits, redeemed_by, redeemed_email, redeemed_at)
|
|
values ('${codeId}', '${"b".repeat(64)}', 'JYOTISH-****-USED', 3, '${userId}', 'admin@example.com', now())
|
|
`);
|
|
const immutable = await admin.rpc("admin_update_redemption_code", {
|
|
...rpcArgs("update-used"),
|
|
p_code_id: codeId,
|
|
p_set_note: true,
|
|
p_note: "changed",
|
|
p_set_expires_at: false,
|
|
p_expires_at: null,
|
|
p_reason: "verify redeemed codes are immutable",
|
|
});
|
|
assert.ok(immutable.error);
|
|
assert.equal(fixture.psql(`select note is null from public.redemption_codes where id = '${codeId}'`), "t");
|
|
|
|
fixture.psql(`
|
|
create or replace function audit.test_fail_admin_audit()
|
|
returns trigger language plpgsql as $$
|
|
begin
|
|
raise exception 'forced audit failure';
|
|
end;
|
|
$$;
|
|
create trigger test_fail_admin_audit
|
|
before insert on audit.admin_audit_logs
|
|
for each row execute function audit.test_fail_admin_audit()
|
|
`);
|
|
const auditFailure = await admin.rpc("admin_create_redemption_codes", {
|
|
...rpcArgs("create-audit-failure"),
|
|
p_codes: [{
|
|
codeHash: "c".repeat(64),
|
|
codeMask: "JYOTISH-****-FAIL",
|
|
credits: 7,
|
|
expiresAt: null,
|
|
note: "must rollback",
|
|
}],
|
|
p_reason: "verify audit failure rollback",
|
|
});
|
|
assert.ok(auditFailure.error);
|
|
assert.equal(
|
|
fixture.psql("select count(*) from public.redemption_codes where code_mask = 'JYOTISH-****-FAIL'"),
|
|
"0",
|
|
"audit failure must leave the redemption code unchanged",
|
|
);
|
|
fixture.psql("drop trigger test_fail_admin_audit on audit.admin_audit_logs");
|
|
} finally {
|
|
await closeLocalPostgresDataPools();
|
|
fixture.stop();
|
|
}
|
|
});
|