import { identityApp, type IdentityApp, ownerPassword, } from './helpers/identity-app'; import { BootstrapService } from '../src/identity/bootstrap.service'; describe('organization-scoped administration', () => { let ctx: IdentityApp; let userId: string; let roleId: string; let userToken: string; beforeAll(async () => { ctx = await identityApp(); }, 60000); afterAll(async () => { await ctx?.close(); }); beforeEach(async () => { await ctx.clearLimits(); }); const auth = () => ({ type: 'bearer' as const }); it('bootstraps exactly one owner', async () => { await expect( ctx.app.get(BootstrapService).createOwner('Other', { email: 'other@example.com', name: 'Other', password: ownerPassword, }), ).rejects.toThrow('Owner already exists'); expect(await ctx.db.user.count({ where: { isOwner: true } })).toBe(1); }); it('creates pending accounts with normalized emails and rejects duplicates', async () => { const payload = { name: 'Employee', email: 'EMPLOYEE@example.com', password: ownerPassword, }; const response = await ctx .api() .post('/api/v1/users') .auth(ctx.token, auth()) .send(payload) .expect(201); userId = response.body.id; expect(response.body.status).toBe('PENDING'); expect(response.body.email).toBe('employee@example.com'); expect(response.text).not.toContain('passwordHash'); await ctx .api() .post('/api/v1/users') .auth(ctx.token, auth()) .send(payload) .expect(409); expect((await ctx.login('employee@example.com')).status).toBe(401); }); it('approves accounts but leaves them without implicit permissions', async () => { await ctx .api() .patch(`/api/v1/users/${userId}/status`) .auth(ctx.token, auth()) .send({ status: 'ACTIVE' }) .expect(200); const login = await ctx.login('employee@example.com'); expect(login.status).toBe(200); userToken = login.body.accessToken; await ctx.api().get('/api/v1/users').auth(userToken, auth()).expect(403); }); it('creates and assigns roles and reflects permission changes on existing sessions', async () => { const role = await ctx .api() .post('/api/v1/roles') .auth(ctx.token, auth()) .send({ name: 'Reader', permissions: ['users.read'] }) .expect(201); roleId = role.body.id; await ctx .api() .patch(`/api/v1/users/${userId}/roles`) .auth(ctx.token, auth()) .send({ roleIds: [roleId] }) .expect(200); await ctx.api().get('/api/v1/users').auth(userToken, auth()).expect(200); await ctx .api() .patch(`/api/v1/roles/${roleId}`) .auth(ctx.token, auth()) .send({ name: 'Reader', permissions: [] }) .expect(200); await ctx.api().get('/api/v1/users').auth(userToken, auth()).expect(403); }); it('prevents delegated administrators from granting permissions they do not hold', async () => { await ctx .api() .patch(`/api/v1/roles/${roleId}`) .auth(ctx.token, auth()) .send({ name: 'Delegated', permissions: ['roles.manage', 'users.roles.assign'], }) .expect(200); await ctx .api() .post('/api/v1/roles') .auth(userToken, auth()) .send({ name: 'Escalation', permissions: ['audit.read'] }) .expect(403); await ctx .api() .patch(`/api/v1/users/${userId}/roles`) .auth(userToken, auth()) .send({ roleIds: [] }) .expect(403); }); it('protects owner accounts and system roles', async () => { const systemRole = await ctx.db.role.findFirstOrThrow({ where: { isSystem: true }, }); await ctx .api() .patch(`/api/v1/roles/${systemRole.id}`) .auth(ctx.token, auth()) .send({ name: 'Changed', permissions: [] }) .expect(403); await ctx .api() .patch(`/api/v1/users/${ctx.owner.userId}/status`) .auth(ctx.token, auth()) .send({ status: 'SUSPENDED' }) .expect(403); await ctx .api() .patch(`/api/v1/users/${userId}/roles`) .auth(ctx.token, auth()) .send({ roleIds: [systemRole.id] }) .expect(403); }); it('rejects cross-organization targets and database-level cross-organization assignments', async () => { const other = await ctx.db.organization.create({ data: { name: 'Other' } }); const otherRole = await ctx.db.role.create({ data: { organizationId: other.id, name: 'External', permissions: [] }, }); await ctx .api() .patch(`/api/v1/roles/${otherRole.id}`) .auth(ctx.token, auth()) .send({ name: 'Hijacked', permissions: [] }) .expect(404); await ctx .api() .patch(`/api/v1/users/${userId}/roles`) .auth(ctx.token, auth()) .send({ roleIds: [otherRole.id] }) .expect(404); await expect( ctx.db.userRole.create({ data: { userId, roleId: otherRole.id, organizationId: ctx.owner.organizationId, }, }), ).rejects.toThrow(); const roles = await ctx .api() .get('/api/v1/roles') .auth(ctx.token, auth()) .expect(200); expect( roles.body.some((role: { id: string }) => role.id === otherRole.id), ).toBe(false); }); it('suspends accounts and permanently revokes their sessions', async () => { await ctx .api() .patch(`/api/v1/users/${userId}/status`) .auth(ctx.token, auth()) .send({ status: 'SUSPENDED' }) .expect(200); await ctx.api().get('/api/v1/auth/me').auth(userToken, auth()).expect(401); expect((await ctx.login('employee@example.com')).status).toBe(401); await ctx .api() .patch(`/api/v1/users/${userId}/status`) .auth(ctx.token, auth()) .send({ status: 'ACTIVE' }) .expect(200); await ctx.api().get('/api/v1/auth/me').auth(userToken, auth()).expect(401); }); it('lists permissions and audit events with pagination validation', async () => { await ctx .api() .get('/api/v1/roles/permissions') .auth(ctx.token, auth()) .expect(200); const events = await ctx .api() .get('/api/v1/audit-events?limit=2') .auth(ctx.token, auth()) .expect(200); expect(events.body).toHaveLength(2); expect( events.body.every( (event: { organizationId: string }) => event.organizationId === ctx.owner.organizationId, ), ).toBe(true); await ctx .api() .get('/api/v1/users?limit=1000') .auth(ctx.token, auth()) .expect(400); }); });