manicanldes-backend/test/admin-policy.spec.ts

128 lines
4.2 KiB
TypeScript

import { UserStore } from '../src/identity/user.store';
import { RoleStore } from '../src/identity/role.store';
import { AccessStore } from '../src/identity/access.store';
import { DatabaseService } from '../src/database/database.service';
import { PasswordService } from '../src/identity/password.service';
import type { Principal } from '../src/identity/identity.types';
import type { Prisma } from '../src/generated/prisma/client';
describe('administration use-case policy', () => {
const actor: Principal = {
userId: 'actor',
organizationId: 'org',
sessionId: 'session',
permissions: ['roles.manage'],
};
const tx = {
user: { create: jest.fn(), findFirst: jest.fn(), update: jest.fn() },
role: {
findFirst: jest.fn(),
create: jest.fn(),
update: jest.fn(),
findMany: jest.fn(),
},
auditEvent: { create: jest.fn() },
session: { deleteMany: jest.fn() },
recoveryToken: { deleteMany: jest.fn() },
userRole: { deleteMany: jest.fn(), createMany: jest.fn() },
};
const access = { mutate: jest.fn() };
const passwords = { hash: jest.fn() };
const users = new UserStore(
{} as DatabaseService,
access as unknown as AccessStore,
passwords as unknown as PasswordService,
);
const roles = new RoleStore(
{} as DatabaseService,
access as unknown as AccessStore,
);
beforeEach(() => {
jest.resetAllMocks();
access.mutate.mockImplementation(
(
_actor,
_permission,
work: (tx: Prisma.TransactionClient, current: Principal) => unknown,
) => work(tx as unknown as Prisma.TransactionClient, actor),
);
});
it('creates pending users without passing plaintext to persistence', async () => {
passwords.hash.mockResolvedValue('encoded');
tx.user.create.mockResolvedValue({ id: 'new' });
await users.create(actor, {
email: 'a@example.com',
name: 'A',
password: 'secret',
});
expect(tx.user.create).toHaveBeenCalledWith(
expect.objectContaining({
data: {
organizationId: 'org',
email: 'a@example.com',
name: 'A',
passwordHash: 'encoded',
},
}),
);
expect(tx.auditEvent.create).toHaveBeenCalledWith({
data: {
organizationId: 'org',
actorId: 'actor',
action: 'user.created',
targetId: 'new',
},
});
});
it('hides users outside the organization scope', async () => {
tx.user.findFirst.mockResolvedValue(null);
await expect(users.setStatus(actor, 'foreign', 'ACTIVE')).rejects.toThrow(
'Not Found',
);
expect(tx.user.update).not.toHaveBeenCalled();
});
it('revokes sessions and recovery on suspension', async () => {
tx.user.findFirst.mockResolvedValue({ id: 'employee', isOwner: false });
await users.setStatus(actor, 'employee', 'SUSPENDED');
expect(tx.session.deleteMany).toHaveBeenCalledWith({
where: { userId: 'employee' },
});
expect(tx.recoveryToken.deleteMany).toHaveBeenCalledWith({
where: { userId: 'employee' },
});
});
it('prevents modifying an existing role more powerful than the actor', async () => {
tx.role.findFirst.mockResolvedValue({
isSystem: false,
permissions: ['audit.read'],
});
await expect(
roles.save(actor, { name: 'Changed', permissions: [] }, 'role'),
).rejects.toThrow('Cannot grant');
expect(tx.role.update).not.toHaveBeenCalled();
});
it('allows only known role targets in the actor organization', async () => {
tx.role.findFirst.mockResolvedValue(null);
await expect(
roles.save(actor, { name: 'Changed', permissions: [] }, 'foreign'),
).rejects.toThrow('Not Found');
});
it('does not remove a target user permissions the actor cannot grant', async () => {
tx.user.findFirst.mockResolvedValue({
id: 'employee',
isOwner: false,
roles: [{ role: { permissions: ['audit.read'] } }],
});
await expect(roles.assign(actor, 'employee', [])).rejects.toThrow(
'Cannot grant',
);
expect(tx.userRole.deleteMany).not.toHaveBeenCalled();
});
it('hides missing assignment targets', async () => {
tx.user.findFirst.mockResolvedValue(null);
await expect(roles.assign(actor, 'foreign', [])).rejects.toThrow(
'Not Found',
);
});
});