import { INestApplication } from '@nestjs/common'; import request = require('supertest'); import { AuthController } from '../../src/auth/auth.controller'; import { AuthService } from '../../src/auth/auth.service'; import { UsersController } from '../../src/users/users.controller'; import { UsersService } from '../../src/users/users.service'; import { JwtAuthGuard } from '../../src/auth/guards/jwt-auth.guard'; import { RolesGuard } from '../../src/auth/guards/roles.guard'; import { ClientType } from '../../src/common/enums/client-type.enum'; import { Role } from '../../src/common/enums/role.enum'; import { AppException } from '../../src/common/exceptions/app-exception'; import { createHttpTestApp } from '../support/app'; import { bearerTokenGuard, passGuard } from '../support/auth'; import { expectErrorEnvelope } from '../support/assertions'; function userResponse(overrides: Record = {}) { return { id: 'client-1', fullName: 'Client One', username: 'client-one', email: 'client@example.com', role: Role.USER, clientType: ClientType.EXTERNAL, appName: 'client-app', clientName: 'Client One', isActive: true, isBlocked: false, allowedInquiries: [], requestLimitPerMinute: 60, requestLimitPerDay: 10000, totalRequests: 0, createdAt: new Date('2026-01-01T00:00:00Z'), updatedAt: new Date('2026-01-01T00:00:00Z'), ...overrides, }; } describe('admin API', () => { let app: INestApplication; const authService = { login: jest.fn(), refresh: jest.fn(), logout: jest.fn(), getProfile: jest.fn(), }; const usersService = { create: jest.fn(), findAll: jest.fn(), findById: jest.fn(), update: jest.fn(), block: jest.fn(), unblock: jest.fn(), resetPassword: jest.fn(), }; beforeAll(async () => { app = await createHttpTestApp( { controllers: [AuthController, UsersController], providers: [ { provide: AuthService, useValue: authService }, { provide: UsersService, useValue: usersService }, ], }, (builder) => builder .overrideGuard(JwtAuthGuard) .useValue(bearerTokenGuard()) .overrideGuard(RolesGuard) .useValue(passGuard()), ); }); beforeEach(() => { jest.clearAllMocks(); authService.login.mockResolvedValue({ accessToken: 'access-token', refreshToken: 'refresh-token', tokenType: 'Bearer', expiresIn: 900, }); usersService.create.mockResolvedValue(userResponse()); usersService.update.mockResolvedValue(userResponse({ fullName: 'Updated Client' })); usersService.block.mockResolvedValue(userResponse({ isBlocked: true })); usersService.unblock.mockResolvedValue(userResponse({ isBlocked: false })); usersService.resetPassword.mockResolvedValue(undefined); }); afterAll(async () => { await app.close(); }); it('logs in with valid credentials', async () => { const response = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'admin', password: 'SecureP@ssw0rd' }) .expect(201); expect(response.body).toMatchObject({ accessToken: 'access-token', refreshToken: 'refresh-token', }); }); it('returns invalid credentials using the normalized error envelope', async () => { authService.login.mockRejectedValueOnce(new AppException('INVALID_CREDENTIALS')); const response = await request(app.getHttpServer()) .post('/auth/login') .send({ username: 'admin', password: 'WrongP@ssw0rd' }) .expect(401); expectErrorEnvelope(response, 'INVALID_CREDENTIALS'); }); it('rejects unauthorized admin access', async () => { const response = await request(app.getHttpServer()).post('/users').send({}).expect(401); expectErrorEnvelope(response, 'UNAUTHORIZED'); }); it('rejects expired JWTs', async () => { const response = await request(app.getHttpServer()) .post('/users') .set('Authorization', 'Bearer expired') .send({}) .expect(401); expectErrorEnvelope(response, 'UNAUTHORIZED'); }); it('creates a client', async () => { const response = await request(app.getHttpServer()) .post('/users') .set('Authorization', 'Bearer admin') .send({ fullName: 'Client One', username: 'client-one', email: 'client@example.com', password: 'SecureP@ssw0rd', role: Role.USER, clientType: ClientType.EXTERNAL, }) .expect(201); expect(response.body).toMatchObject({ id: 'client-1', username: 'client-one' }); }); it('updates a client', async () => { const response = await request(app.getHttpServer()) .patch('/users/client-1') .set('Authorization', 'Bearer admin') .send({ fullName: 'Updated Client' }) .expect(200); expect(response.body.fullName).toBe('Updated Client'); }); it('disables and enables a client through block/unblock', async () => { await request(app.getHttpServer()) .patch('/users/client-1/block') .set('Authorization', 'Bearer admin') .expect(200) .expect(({ body }) => expect(body.isBlocked).toBe(true)); await request(app.getHttpServer()) .patch('/users/client-1/unblock') .set('Authorization', 'Bearer admin') .expect(200) .expect(({ body }) => expect(body.isBlocked).toBe(false)); }); it('rejects invalid client configuration at the DTO layer', async () => { const response = await request(app.getHttpServer()) .post('/users') .set('Authorization', 'Bearer admin') .send({ fullName: 'Client One', username: 'client-one', email: 'not-an-email', password: 'short', role: 'BAD_ROLE', clientType: ClientType.EXTERNAL, }) .expect(400); expectErrorEnvelope(response, 'VALIDATION_ERROR'); expect(usersService.create).not.toHaveBeenCalled(); }); it('returns duplicate client errors', async () => { usersService.create.mockRejectedValueOnce(new AppException('USERNAME_OR_EMAIL_EXISTS')); const response = await request(app.getHttpServer()) .post('/users') .set('Authorization', 'Bearer admin') .send({ fullName: 'Client One', username: 'client-one', email: 'client@example.com', password: 'SecureP@ssw0rd', role: Role.USER, clientType: ClientType.EXTERNAL, }) .expect(409); expectErrorEnvelope(response, 'USERNAME_OR_EMAIL_EXISTS'); }); it('validates reset-password requests', async () => { const response = await request(app.getHttpServer()) .patch('/users/client-1/reset-password') .set('Authorization', 'Bearer admin') .send({ newPassword: 'short' }) .expect(400); expectErrorEnvelope(response, 'VALIDATION_ERROR'); }); it.todo('delete client once the admin API exposes DELETE /users/:id'); });