forked from Shared/esg
Implement normalized error handling with Persian translations across all exception types, replace legacy NestJS exceptions with AppException, and add unit, integration, and smoke tests. - Add error catalog with gateway-owned codes and Persian messages - Introduce AppException wrapping normalized error envelopes - Add translateError helper for automatic messageFa population - Remove claims module and update provider error normalization - Add unit tests for error contracts and helper functions - Add integration tests for admin and inquiry endpoints - Add smoke tests for real provider connectivity - Add test support utilities (auth mocks, assertions, app factory)
222 lines
6.8 KiB
TypeScript
222 lines
6.8 KiB
TypeScript
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<string, unknown> = {}) {
|
|
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');
|
|
});
|