import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { connect, type Database } from '@tursodatabase/database'; const mocks = vi.hoisted(() => ({ audit: vi.fn(), })); vi.mock('$lib/server/audit', () => ({ recordIsacAudit: mocks.audit, })); import { applyAuthorityChange } from './authority-change'; import type { AuthUser } from './session'; const operator = { id: 'operator-1', aal: 2, authLevel: 2, aal2VerifiedAt: new Date().toISOString(), } as AuthUser; async function createDatabase(): Promise { const database = await connect(':memory:'); await database.exec(` CREATE TABLE users(id TEXT PRIMARY KEY, role TEXT NOT NULL); CREATE TABLE roles(id TEXT PRIMARY KEY); CREATE TABLE permissions(id TEXT PRIMARY KEY, name TEXT NOT NULL); CREATE TABLE role_permissions(role_id TEXT, permission_id TEXT); CREATE TABLE user_roles(user_id TEXT, role_id TEXT, PRIMARY KEY(user_id,role_id)); CREATE TABLE user_permissions(user_id TEXT, permission_id TEXT); CREATE TABLE user_status(user_id TEXT PRIMARY KEY, blocked_at TEXT, banned_until TEXT); CREATE TABLE deleted_accounts(user_id TEXT PRIMARY KEY); CREATE TABLE sessions(id TEXT PRIMARY KEY, user_id TEXT); CREATE TABLE mfa_totp_factors(id TEXT PRIMARY KEY, user_id TEXT, state TEXT); CREATE TABLE mfa_recovery_codes(id TEXT PRIMARY KEY, user_id TEXT, consumed_at TEXT, revoked_at TEXT); INSERT INTO users VALUES('operator-1','user'),('target-1','user'); INSERT INTO roles VALUES('role-admin'),('role-user'); INSERT INTO permissions VALUES('permission-system-admin','system.admin'),('permission-users-view','users.view'); INSERT INTO role_permissions VALUES('role-admin','permission-system-admin'),('role-user','permission-users-view'); INSERT INTO user_roles VALUES('operator-1','role-admin'); INSERT INTO mfa_totp_factors VALUES('operator-factor','operator-1','active'); INSERT INTO sessions VALUES('target-session','target-1'); `); return database; } const access = (roleIds: string[], permissionIds: string[]) => ({ roleIds, permissionIds, }); describe('shared authority changes', () => { let database: Database; beforeEach(async () => { database = await createDatabase(); mocks.audit.mockReset(); mocks.audit.mockResolvedValue(undefined); }); afterEach(async () => database.close()); it('cannot revert an administrator grant by removing the final active authority', async () => { await expect( applyAuthorityChange({ database, operator, targetId: 'operator-1', ...access([], []), expectedCurrent: access(['role-admin'], []), }), ).rejects.toMatchObject({ status: 409 }); const role = await ( await database.prepare('SELECT role_id FROM user_roles WHERE user_id=?') ).get('operator-1'); expect(role.role_id).toBe('role-admin'); }); it('requires an MFA-ready target when reversion restores administrator authority', async () => { await expect( applyAuthorityChange({ database, operator, targetId: 'target-1', ...access(['role-admin'], []), expectedCurrent: access([], []), }), ).rejects.toMatchObject({ status: 400 }); const role = await ( await database.prepare('SELECT role_id FROM user_roles WHERE user_id=?') ).get('target-1'); expect(role).toBeUndefined(); }); it('requires recent AAL2 authentication for administrator reversion', async () => { await database.exec(` INSERT INTO mfa_totp_factors VALUES('target-factor','target-1','active'); INSERT INTO mfa_recovery_codes VALUES('target-code','target-1',NULL,NULL); `); await expect( applyAuthorityChange({ database, operator: { ...operator, aal2VerifiedAt: new Date(Date.now() - 11 * 60_000).toISOString(), } as never, targetId: 'target-1', ...access(['role-admin'], []), expectedCurrent: access([], []), }), ).rejects.toMatchObject({ status: 403 }); }); it('revokes sessions and appends reversion audit evidence in the transaction', async () => { await database.exec(` INSERT INTO mfa_totp_factors VALUES('target-factor','target-1','active'); INSERT INTO mfa_recovery_codes VALUES('target-code','target-1',NULL,NULL); `); const result = await applyAuthorityChange({ database, operator, targetId: 'target-1', ...access(['role-admin'], []), expectedCurrent: access([], []), audit: { actorId: 'operator-1', category: 'audit_control', action: 'audit.reverted', entityId: 'event-1', before: access([], []), after: { originalEventId: 'event-1', restored: access(['role-admin'], []), }, }, }); expect(result.after).toEqual(access(['role-admin'], [])); expect( await ( await database.prepare('SELECT COUNT(*) AS count FROM sessions') ).get(), ).toMatchObject({ count: 0 }); expect(mocks.audit).toHaveBeenCalledWith( expect.objectContaining({ action: 'audit.reverted', entityId: 'event-1', database, }), ); }); it('rolls back authority and session changes when audit append fails', async () => { await database.exec(` INSERT INTO mfa_totp_factors VALUES('target-factor','target-1','active'); INSERT INTO mfa_recovery_codes VALUES('target-code','target-1',NULL,NULL); `); mocks.audit.mockRejectedValueOnce(new Error('audit failed')); await expect( applyAuthorityChange({ database, operator, targetId: 'target-1', ...access(['role-admin'], []), expectedCurrent: access([], []), }), ).rejects.toThrow('audit failed'); const role = await ( await database.prepare('SELECT role_id FROM user_roles WHERE user_id=?') ).get('target-1'); const sessions = await ( await database.prepare('SELECT COUNT(*) AS count FROM sessions') ).get(); expect(role).toBeUndefined(); expect(Number(sessions.count)).toBe(1); }); });