|
| 1 | +import { Test, TestingModule } from '@nestjs/testing'; |
| 2 | +import { ForbiddenException } from '@nestjs/common'; |
| 3 | +import { AdminController } from './admin.controller'; |
| 4 | +import { AdminService } from './admin.service'; |
| 5 | +import { AuditService } from './audit.service'; |
| 6 | +import { AdminRoleGuard } from './guards/admin-role.guard'; |
| 7 | +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; |
| 8 | + |
| 9 | +const mockAdminService = { enqueueReindex: jest.fn(), setFeatureFlag: jest.fn(), getFeatureFlags: jest.fn() }; |
| 10 | +const mockAuditService = { write: jest.fn(), findAll: jest.fn() }; |
| 11 | + |
| 12 | +const adminReq = (role = 'admin') => ({ user: { walletAddress: 'GADMIN', role }, ip: '127.0.0.1' }); |
| 13 | + |
| 14 | +describe('AdminController', () => { |
| 15 | + let controller: AdminController; |
| 16 | + |
| 17 | + beforeEach(async () => { |
| 18 | + jest.clearAllMocks(); |
| 19 | + const module: TestingModule = await Test.createTestingModule({ |
| 20 | + controllers: [AdminController], |
| 21 | + providers: [ |
| 22 | + { provide: AdminService, useValue: mockAdminService }, |
| 23 | + { provide: AuditService, useValue: mockAuditService }, |
| 24 | + ], |
| 25 | + }) |
| 26 | + .overrideGuard(JwtAuthGuard).useValue({ canActivate: () => true }) |
| 27 | + .overrideGuard(AdminRoleGuard).useValue({ canActivate: (ctx: any) => { |
| 28 | + const role = ctx.switchToHttp().getRequest().user?.role; |
| 29 | + if (role !== 'admin') throw new ForbiddenException('Admin role required'); |
| 30 | + return true; |
| 31 | + }}) |
| 32 | + .compile(); |
| 33 | + |
| 34 | + controller = module.get(AdminController); |
| 35 | + }); |
| 36 | + |
| 37 | + describe('POST /admin/reindex', () => { |
| 38 | + it('enqueues job and writes audit row', async () => { |
| 39 | + mockAdminService.enqueueReindex.mockResolvedValue('job-123'); |
| 40 | + const result = await controller.reindex({ fromLedger: 500 }, adminReq() as any); |
| 41 | + expect(result).toEqual({ jobId: 'job-123', fromLedger: 500, status: 'queued' }); |
| 42 | + expect(mockAdminService.enqueueReindex).toHaveBeenCalledWith(500); |
| 43 | + expect(mockAuditService.write).toHaveBeenCalledWith( |
| 44 | + expect.objectContaining({ actor: 'GADMIN', action: 'reindex', payload: expect.objectContaining({ fromLedger: 500 }) }), |
| 45 | + ); |
| 46 | + }); |
| 47 | + }); |
| 48 | + |
| 49 | + describe('GET /admin/audits', () => { |
| 50 | + it('returns paginated audit logs', async () => { |
| 51 | + mockAuditService.findAll.mockResolvedValue({ items: [], total: 0, page: 1, limit: 20 }); |
| 52 | + const result = await controller.getAudits({ page: 1, limit: 20 }); |
| 53 | + expect(mockAuditService.findAll).toHaveBeenCalledWith(1, 20, undefined); |
| 54 | + expect(result.total).toBe(0); |
| 55 | + }); |
| 56 | + }); |
| 57 | + |
| 58 | + describe('PATCH /admin/feature-flags/:key', () => { |
| 59 | + it('updates flag and writes audit row', async () => { |
| 60 | + const flag = { key: 'claims_enabled', enabled: false, updatedBy: 'GADMIN' }; |
| 61 | + mockAdminService.setFeatureFlag.mockResolvedValue(flag); |
| 62 | + const result = await controller.setFeatureFlag('claims_enabled', { enabled: false }, adminReq() as any); |
| 63 | + expect(result).toEqual(flag); |
| 64 | + expect(mockAuditService.write).toHaveBeenCalledWith( |
| 65 | + expect.objectContaining({ action: 'feature_flag_update', payload: expect.objectContaining({ key: 'claims_enabled', enabled: false }) }), |
| 66 | + ); |
| 67 | + }); |
| 68 | + }); |
| 69 | + |
| 70 | + describe('Role guard — non-admin access denied', () => { |
| 71 | + it('throws ForbiddenException for support_readonly on reindex', async () => { |
| 72 | + const guard = new AdminRoleGuard(); |
| 73 | + const ctx = { |
| 74 | + switchToHttp: () => ({ getRequest: () => ({ user: { role: 'support_readonly' } }) }), |
| 75 | + } as any; |
| 76 | + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); |
| 77 | + }); |
| 78 | + |
| 79 | + it('throws ForbiddenException when no user present', async () => { |
| 80 | + const guard = new AdminRoleGuard(); |
| 81 | + const ctx = { |
| 82 | + switchToHttp: () => ({ getRequest: () => ({}) }), |
| 83 | + } as any; |
| 84 | + expect(() => guard.canActivate(ctx)).toThrow(ForbiddenException); |
| 85 | + }); |
| 86 | + |
| 87 | + it('allows admin role through', () => { |
| 88 | + const guard = new AdminRoleGuard(); |
| 89 | + const ctx = { |
| 90 | + switchToHttp: () => ({ getRequest: () => ({ user: { role: 'admin' } }) }), |
| 91 | + } as any; |
| 92 | + expect(guard.canActivate(ctx)).toBe(true); |
| 93 | + }); |
| 94 | + }); |
| 95 | +}); |
0 commit comments