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)
132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import { INestApplication } from '@nestjs/common';
|
||
import request = require('supertest');
|
||
import { InquiryController } from '../../src/inquiry/inquiry.controller';
|
||
import { InquiryService } from '../../src/inquiry/inquiry.service';
|
||
import { ProviderOrchestratorService } from '../../src/providers/strategy/provider-orchestrator.service';
|
||
import { InquiryLogService } from '../../src/logging/inquiry-log.service';
|
||
import { JwtAuthGuard } from '../../src/auth/guards/jwt-auth.guard';
|
||
import { InquiryAccessGuard } from '../../src/auth/guards/inquiry-access.guard';
|
||
import { UserRateLimitGuard } from '../../src/rate-limit/user-rate-limit.guard';
|
||
import { inquiryEndpointCases } from '../support/inquiry-endpoints';
|
||
import { createHttpTestApp } from '../support/app';
|
||
import { authenticatedGuard, passGuard } from '../support/auth';
|
||
import {
|
||
normalizedProviderError,
|
||
providerErrorCases,
|
||
successfulProviderResult,
|
||
} from '../support/mock-provider';
|
||
import { expectErrorEnvelope, expectSuccessEnvelope } from '../support/assertions';
|
||
|
||
describe('inquiry endpoints (integration)', () => {
|
||
let app: INestApplication;
|
||
const orchestrator = {
|
||
executeWithFallback: jest.fn(),
|
||
};
|
||
const inquiryLogs = {
|
||
create: jest.fn().mockResolvedValue(undefined),
|
||
};
|
||
|
||
beforeAll(async () => {
|
||
app = await createHttpTestApp(
|
||
{
|
||
controllers: [InquiryController],
|
||
providers: [
|
||
InquiryService,
|
||
{ provide: ProviderOrchestratorService, useValue: orchestrator },
|
||
{ provide: InquiryLogService, useValue: inquiryLogs },
|
||
],
|
||
},
|
||
(builder) =>
|
||
builder
|
||
.overrideGuard(JwtAuthGuard)
|
||
.useValue(authenticatedGuard())
|
||
.overrideGuard(InquiryAccessGuard)
|
||
.useValue(passGuard())
|
||
.overrideGuard(UserRateLimitGuard)
|
||
.useValue(passGuard()),
|
||
);
|
||
});
|
||
|
||
beforeEach(() => {
|
||
jest.clearAllMocks();
|
||
orchestrator.executeWithFallback.mockResolvedValue(successfulProviderResult());
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await app.close();
|
||
});
|
||
|
||
describe.each(inquiryEndpointCases)('$name', (endpoint) => {
|
||
it('returns a successful response and maps provider data', async () => {
|
||
orchestrator.executeWithFallback.mockResolvedValueOnce(
|
||
successfulProviderResult({ raw: { mapped: true, inquiryType: endpoint.inquiryType } }),
|
||
);
|
||
|
||
const response = await request(app.getHttpServer())
|
||
.post(endpoint.path)
|
||
.send(endpoint.validPayload)
|
||
.expect(201);
|
||
|
||
expectSuccessEnvelope(response);
|
||
expect(response.body.data).toMatchObject({
|
||
mapped: true,
|
||
inquiryType: endpoint.inquiryType,
|
||
});
|
||
expect(orchestrator.executeWithFallback).toHaveBeenCalledWith(
|
||
endpoint.inquiryType,
|
||
expect.objectContaining(endpoint.validPayload),
|
||
expect.objectContaining({ requestId: 'test-request-id' }),
|
||
);
|
||
});
|
||
|
||
it('returns validation errors without calling providers', async () => {
|
||
const response = await request(app.getHttpServer())
|
||
.post(endpoint.path)
|
||
.send(endpoint.invalidPayload)
|
||
.expect(400);
|
||
|
||
expectErrorEnvelope(response, 'VALIDATION_ERROR');
|
||
if (endpoint.name === 'person inquiry') {
|
||
expect(response.body.error).toMatchObject({
|
||
message: 'nationalCode must be exactly 10 digits',
|
||
messageFa: 'کد ملی باید دقیقا ۱۰ رقم باشد',
|
||
details: [
|
||
{
|
||
field: 'nationalCode',
|
||
constraints: ['nationalCode must be exactly 10 digits'],
|
||
},
|
||
],
|
||
});
|
||
}
|
||
expect(orchestrator.executeWithFallback).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('rejects malformed JSON without calling providers', async () => {
|
||
await request(app.getHttpServer())
|
||
.post(endpoint.path)
|
||
.set('Content-Type', 'application/json')
|
||
.send('{"broken":')
|
||
.expect(400);
|
||
|
||
expect(orchestrator.executeWithFallback).not.toHaveBeenCalled();
|
||
});
|
||
|
||
describe.each(providerErrorCases)('$name', (providerCase) => {
|
||
it('maps provider errors into the normalized response envelope', async () => {
|
||
orchestrator.executeWithFallback.mockRejectedValueOnce(
|
||
normalizedProviderError(providerCase.error),
|
||
);
|
||
|
||
const response = await request(app.getHttpServer())
|
||
.post(endpoint.path)
|
||
.send(endpoint.validPayload)
|
||
.expect(201);
|
||
|
||
expectErrorEnvelope(response, providerCase.code);
|
||
expect(response.body.data).toBeNull();
|
||
expect(response.body.error.providerCode).toBe(providerCase.error.providerCode);
|
||
});
|
||
});
|
||
});
|
||
});
|