forked from Shared/esg
feat: add Persian error translations and comprehensive test suite
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)
This commit is contained in:
221
test/admin/admin.e2e-spec.ts
Normal file
221
test/admin/admin.e2e-spec.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user