159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
database: {
|
|
prepare: vi.fn(),
|
|
exec: vi.fn(),
|
|
},
|
|
getDatabase: vi.fn(),
|
|
getPermissions: vi.fn(),
|
|
hasRecentAal2: vi.fn(),
|
|
requirePermission: vi.fn(),
|
|
getSessionUser: vi.fn(),
|
|
recordIsacAudit: vi.fn(),
|
|
countActiveFullAdministrators: vi.fn(),
|
|
runs: [] as unknown[][],
|
|
}));
|
|
|
|
vi.mock('$lib/server/auth/session', () => ({
|
|
getSessionUser: mocks.getSessionUser,
|
|
}));
|
|
|
|
vi.mock('$lib/server/auth/authorization', () => ({
|
|
getPermissions: mocks.getPermissions,
|
|
hasRecentAal2: mocks.hasRecentAal2,
|
|
requirePermission: mocks.requirePermission,
|
|
}));
|
|
|
|
vi.mock('$lib/server/auth/bootstrap', () => ({
|
|
countActiveFullAdministrators: mocks.countActiveFullAdministrators,
|
|
}));
|
|
|
|
vi.mock('$lib/server/database', () => ({
|
|
getDatabase: mocks.getDatabase,
|
|
}));
|
|
|
|
vi.mock('$lib/server/audit', () => ({
|
|
recordIsacAudit: mocks.recordIsacAudit,
|
|
}));
|
|
|
|
import { actions } from './+page.server';
|
|
|
|
const postRole = (id = '', permissions: string[] = []) => {
|
|
const form = new FormData();
|
|
form.set('id', id);
|
|
form.set('name', 'Field Operators');
|
|
form.set('description', 'Ordinary access role');
|
|
for (const permission of permissions)
|
|
form.append('permissionIds', permission);
|
|
return new Request('http://localhost/isac/roles?/save', {
|
|
method: 'POST',
|
|
body: form,
|
|
});
|
|
};
|
|
|
|
const postDelete = () => {
|
|
const form = new FormData();
|
|
form.set('id', 'role-user-operator');
|
|
form.set('confirmation', 'Existing');
|
|
return new Request('http://localhost/isac/roles?/delete', {
|
|
method: 'POST',
|
|
body: form,
|
|
});
|
|
};
|
|
|
|
describe('ISAC role authority actions', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mocks.runs.length = 0;
|
|
mocks.getSessionUser.mockResolvedValue({ id: 'operator-1' });
|
|
mocks.getPermissions.mockResolvedValue(['users.roles']);
|
|
mocks.hasRecentAal2.mockReturnValue(true);
|
|
mocks.requirePermission.mockImplementation(async (user) => user);
|
|
mocks.getDatabase.mockResolvedValue(mocks.database);
|
|
mocks.countActiveFullAdministrators.mockResolvedValue(1);
|
|
mocks.database.prepare.mockImplementation((sql: string) => ({
|
|
get: vi.fn(async (...args: unknown[]) => {
|
|
if (sql.includes('SELECT id,name,description FROM roles WHERE id=?'))
|
|
return args[0]
|
|
? { id: args[0], name: 'Existing', description: null }
|
|
: null;
|
|
if (sql.includes('SELECT id FROM roles WHERE name=')) return null;
|
|
if (sql.includes('COUNT(DISTINCT users.id)')) return { count: 1 };
|
|
return null;
|
|
}),
|
|
all: vi.fn(async (...args: unknown[]) => {
|
|
if (sql.includes('SELECT id,name FROM permissions'))
|
|
return args.map((id) => ({ id, name: 'users.view' }));
|
|
if (sql.includes('SELECT permissions.name FROM role_permissions'))
|
|
return [];
|
|
return [];
|
|
}),
|
|
run: vi.fn(async (...args: unknown[]) => {
|
|
mocks.runs.push(args);
|
|
return { changes: 1 };
|
|
}),
|
|
}));
|
|
});
|
|
|
|
it('rolls back role writes when audit recording fails', async () => {
|
|
mocks.recordIsacAudit.mockRejectedValueOnce(new Error('audit failed'));
|
|
|
|
await expect(
|
|
actions.save?.({
|
|
cookies: {},
|
|
request: postRole('', ['permission-users-view']),
|
|
} as never),
|
|
).rejects.toThrow('audit failed');
|
|
|
|
expect(mocks.database.exec.mock.calls).toEqual([
|
|
['BEGIN IMMEDIATE'],
|
|
['ROLLBACK'],
|
|
]);
|
|
expect(mocks.database.exec).not.toHaveBeenCalledWith('COMMIT');
|
|
expect(mocks.recordIsacAudit).toHaveBeenCalledWith(
|
|
expect.objectContaining({ database: mocks.database }),
|
|
);
|
|
});
|
|
|
|
it('rejects a role mutation that would leave no active full administrator', async () => {
|
|
mocks.countActiveFullAdministrators.mockResolvedValue(0);
|
|
|
|
const result = await actions.save?.({
|
|
cookies: {},
|
|
request: postRole('', ['permission-users-view']),
|
|
} as never);
|
|
|
|
expect(result).toMatchObject({ status: 409 });
|
|
expect(mocks.database.exec.mock.calls).toEqual([
|
|
['BEGIN IMMEDIATE'],
|
|
['ROLLBACK'],
|
|
]);
|
|
expect(mocks.recordIsacAudit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps role deletion and its audit evidence in one transaction', async () => {
|
|
mocks.getPermissions.mockResolvedValue(['*']);
|
|
|
|
const result = await actions.delete?.({
|
|
cookies: {},
|
|
request: postDelete(),
|
|
} as never);
|
|
|
|
expect(result).toMatchObject({
|
|
roleDeleted: true,
|
|
resultToken: expect.any(String),
|
|
});
|
|
expect(mocks.database.exec.mock.calls).toEqual([
|
|
['BEGIN IMMEDIATE'],
|
|
['COMMIT'],
|
|
]);
|
|
expect(mocks.recordIsacAudit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: 'role.deleted',
|
|
database: mocks.database,
|
|
}),
|
|
);
|
|
});
|
|
});
|