import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { PrismaService } from '../src/prisma/prisma.service'; import { createTestApp } from './utils/create-test-app'; import { login } from './utils/api'; interface RoleBody { id: string; key: string; name: string; isSystem: boolean; status: string; userCount: number; permissionKeys: string[]; } interface MeBody { id: string; email: string; role: string; roleKey: string; permissions: string[]; } describe('Roles (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const adminEmail = `roles-e2e-admin-${Date.now()}@example.com`; const staffEmail = `roles-e2e-staff-${Date.now()}@example.com`; const password = 'SuperSecret123!'; let adminId: string; let staffId: string; let adminToken: string; let staffToken: string; let adminRoleId: string; let staffRoleId: string; let createdRoleId: string; beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const adminRole = await prisma.role.findUniqueOrThrow({ where: { key: 'ADMIN' }, }); const staffRole = await prisma.role.findUniqueOrThrow({ where: { key: 'STAFF' }, }); adminRoleId = adminRole.id; staffRoleId = staffRole.id; const passwordHash = await bcrypt.hash(password, 12); const admin = await prisma.user.create({ data: { email: adminEmail, passwordHash, name: 'Roles E2E Admin', legacyRole: 'ADMIN', role: { connect: { key: 'ADMIN' } }, }, }); adminId = admin.id; const staff = await prisma.user.create({ data: { email: staffEmail, passwordHash, name: 'Roles E2E Staff', legacyRole: 'STAFF', role: { connect: { key: 'STAFF' } }, }, }); staffId = staff.id; adminToken = (await login(app, adminEmail, password)).accessToken; staffToken = (await login(app, staffEmail, password)).accessToken; }); afterAll(async () => { if (createdRoleId) { await prisma.rolePermission.deleteMany({ where: { roleId: createdRoleId }, }); await prisma.role.delete({ where: { id: createdRoleId } }); } const userIds = [adminId, staffId]; await prisma.refreshToken.deleteMany({ where: { userId: { in: userIds } }, }); await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); await app.close(); }); it('rejects a caller without roles:read from listing roles', () => { return request(app.getHttpServer()) .get('/api/v1/roles') .set('Authorization', `Bearer ${staffToken}`) .expect(403); }); it('lets an admin list the seeded system roles', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/roles') .set('Authorization', `Bearer ${adminToken}`) .expect(200); const roles = response.body as RoleBody[]; const keys = roles.map((role) => role.key); expect(keys).toEqual( expect.arrayContaining([ 'ADMIN', 'BEEKEEPER', 'STAFF', 'READONLY', 'API_CLIENT', ]), ); const admin = roles.find((role) => role.key === 'ADMIN'); expect(admin?.isSystem).toBe(true); expect(admin?.permissionKeys.length).toBe(83); }); it('exposes the full permission catalog', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/roles/permission-catalog') .set('Authorization', `Bearer ${adminToken}`) .expect(200); expect((response.body as unknown[]).length).toBe(83); }); it('lets an admin create a role with a chosen permission set', async () => { const response = await request(app.getHttpServer()) .post('/api/v1/roles') .set('Authorization', `Bearer ${adminToken}`) .send({ key: 'INSPECTOR_E2E', name: 'Inspector (e2e)', description: 'Read-only inspections role for e2e coverage', permissionKeys: ['inspections:read', 'colonies:read'], }) .expect(201); const created = response.body as RoleBody; createdRoleId = created.id; expect(created.permissionKeys.sort()).toEqual( ['colonies:read', 'inspections:read'].sort(), ); }); it('rejects creating a role with a duplicate key', () => { return request(app.getHttpServer()) .post('/api/v1/roles') .set('Authorization', `Bearer ${adminToken}`) .send({ key: 'INSPECTOR_E2E', name: 'Duplicate', permissionKeys: ['colonies:read'], }) .expect(409); }); it('rejects an unknown permission key', () => { return request(app.getHttpServer()) .post('/api/v1/roles') .set('Authorization', `Bearer ${adminToken}`) .send({ key: 'BOGUS_E2E', name: 'Bogus', permissionKeys: ['not-a-real-permission'], }) .expect(400); }); it('lets an admin update a role permission set', async () => { const response = await request(app.getHttpServer()) .patch(`/api/v1/roles/${createdRoleId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ permissionKeys: ['tasks:read'] }) .expect(200); const updated = response.body as RoleBody; expect(updated.permissionKeys).toEqual(['tasks:read']); }); it('blocks editing the system ADMIN role', () => { return request(app.getHttpServer()) .patch(`/api/v1/roles/${adminRoleId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ name: 'Renamed' }) .expect(403); }); it('blocks deactivating the system ADMIN role', () => { return request(app.getHttpServer()) .post(`/api/v1/roles/${adminRoleId}/deactivate`) .set('Authorization', `Bearer ${adminToken}`) .expect(403); }); it('blocks deactivating a role that still has users assigned', () => { return request(app.getHttpServer()) .post(`/api/v1/roles/${staffRoleId}/deactivate`) .set('Authorization', `Bearer ${adminToken}`) .expect(409); }); it('deactivates and reactivates an unused custom role', async () => { const deactivated = await request(app.getHttpServer()) .post(`/api/v1/roles/${createdRoleId}/deactivate`) .set('Authorization', `Bearer ${adminToken}`) .expect(201); expect((deactivated.body as RoleBody).status).toBe('INACTIVE'); const reactivated = await request(app.getHttpServer()) .post(`/api/v1/roles/${createdRoleId}/reactivate`) .set('Authorization', `Bearer ${adminToken}`) .expect(201); expect((reactivated.body as RoleBody).status).toBe('ACTIVE'); }); it("reflects the caller's role and effective permissions on /auth/me", async () => { const adminMe = await request(app.getHttpServer()) .get('/api/v1/auth/me') .set('Authorization', `Bearer ${adminToken}`) .expect(200); const adminProfile = adminMe.body as MeBody; expect(adminProfile.roleKey).toBe('ADMIN'); expect(adminProfile.permissions.length).toBe(83); const staffMe = await request(app.getHttpServer()) .get('/api/v1/auth/me') .set('Authorization', `Bearer ${staffToken}`) .expect(200); const staffProfile = staffMe.body as MeBody; expect(staffProfile.roleKey).toBe('STAFF'); expect(staffProfile.permissions).toEqual( expect.arrayContaining(['colonies:read', 'tasks:create']), ); expect(staffProfile.permissions).not.toEqual( expect.arrayContaining(['storage:create', 'users:read']), ); }); });