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/i, ); 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/i); }); 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/i, ); }); });