Files
Jyotisha/frontend/tests/database-admin-owner-recovery.test.ts
T
Jesse_Chen 754505d40a fix(admin): preserve revoked owners and 503 status
Owner recovery lived in the identity-only migration path and could clear historical revocations on conflict. Admin layout also rethrew authorization 503s, turning service-unavailable failures into 500 responses.\n\nMove recovery behind the RBAC migration sequence, exclude revoked candidates without mutating their history, and terminate layout redirects at a no-store 503 route.
2026-08-07 11:33:48 +08:00

170 lines
6.5 KiB
TypeScript

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
const runnerPath = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url));
const recoveryMigration = readFileSync(
new URL("../supabase/migrations/20260807010000_recover_initial_admin_owner.sql", import.meta.url),
"utf8",
);
const ids = {
first: "30000000-0000-4000-8000-000000000001",
second: "30000000-0000-4000-8000-000000000002",
third: "30000000-0000-4000-8000-000000000003",
blocked: "30000000-0000-4000-8000-000000000004",
unsynced: "30000000-0000-4000-8000-000000000005",
};
const ownerCountSql = `
select count(*)
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
join public.admin_roles ar on ar.id = aur.role_id
where au.revoked_at is null and ar.code = 'owner'
`;
test("Owner recovery grants only one currently loginable synced identity admin", () => {
const fixture = startPostgresFixture();
const schemaSql = (sql: string) => fixture.psqlAs(
"schema_owner",
"schema-owner-test-password",
sql,
);
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, /applied 20260807010000_recover_initial_admin_owner\.sql/);
assert.equal(fixture.psql(ownerCountSql), "0", "a truly empty database stays empty");
schemaSql(`
alter table identity.users disable trigger identity_user_business_auth_sync;
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
values ('${ids.unsynced}', 'Unsynced Admin', 'unsynced-admin@example.com', true, now(), 'admin');
alter table identity.users enable trigger identity_user_business_auth_sync;
`);
assert.equal(
fixture.psql(`select count(*) from auth.users where id = '${ids.unsynced}'`),
"0",
);
assert.throws(
() => schemaSql(recoveryMigration),
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 0/,
"an unsynced identity account is not a truly empty database",
);
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users
(id, name, email, email_verified, email_verified_at, role, banned, ban_expires)
values
('${ids.first}', 'First Admin', 'first-admin@example.com', true, now(), ' user , admin ', true, now() - interval '1 hour'),
('${ids.blocked}', 'Blocked Admin', 'blocked-admin@example.com', true, now(), 'admin', true, null)
`);
schemaSql(recoveryMigration);
assert.equal(
fixture.psql(`
select count(*)
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
join public.admin_roles ar on ar.id = aur.role_id
where au.user_id = '${ids.first}' and au.revoked_at is null and ar.code = 'owner'
`),
"1",
);
assert.equal(fixture.psql(ownerCountSql), "1");
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
values ('${ids.second}', 'Second Admin', 'second-admin@example.com', true, now(), 'admin')
`);
schemaSql(recoveryMigration);
assert.equal(
fixture.psql(`
select string_agg(aur.admin_user_id::text, ',' order by aur.admin_user_id)
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
join public.admin_roles ar on ar.id = aur.role_id
where au.revoked_at is null and ar.code = 'owner'
`),
ids.first,
"an existing active Owner makes recovery a no-op",
);
schemaSql(`
alter table public.admin_users disable trigger admin_users_require_active_owner;
update public.admin_users
set revoked_at = timestamptz '2026-08-07 01:23:45+00',
revoked_by = '${ids.first}'
where user_id = '${ids.first}';
alter table public.admin_users enable trigger admin_users_require_active_owner;
`);
schemaSql(recoveryMigration);
assert.equal(
fixture.psql(`
select string_agg(aur.admin_user_id::text, ',' order by aur.admin_user_id)
from public.admin_users au
join public.admin_user_roles aur on aur.admin_user_id = au.user_id
join public.admin_roles ar on ar.id = aur.role_id
where au.revoked_at is null and ar.code = 'owner'
`),
ids.second,
"a revoked historical admin is excluded instead of being restored",
);
assert.equal(
fixture.psql(`
select count(*)
from public.admin_users
where user_id = '${ids.first}'
and revoked_at = timestamptz '2026-08-07 01:23:45+00'
and revoked_by = '${ids.first}'
`),
"1",
"recovery preserves the historical revocation fields",
);
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
insert into identity.users (id, name, email, email_verified, email_verified_at, role)
values ('${ids.third}', 'Third Admin', 'third-admin@example.com', true, now(), 'admin')
`);
schemaSql(`
alter table public.admin_user_roles disable trigger admin_user_roles_require_active_owner;
delete from public.admin_user_roles
where role_id = (select id from public.admin_roles where code = 'owner');
alter table public.admin_user_roles enable trigger admin_user_roles_require_active_owner;
`);
assert.throws(
() => schemaSql(recoveryMigration),
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 2/,
);
assert.equal(fixture.psql(ownerCountSql), "0");
fixture.psqlAs("identity_runtime", "identity-runtime-test-password", `
update identity.users set role = 'user' where id in ('${ids.second}', '${ids.third}')
`);
assert.throws(
() => schemaSql(recoveryMigration),
/admin_owner_recovery_requires_exactly_one_active_identity_admin: found 0/,
"a revoked identity admin remains ineligible even when it is the only admin role",
);
} finally {
fixture.stop();
}
});