From b18f118d376796408cf8c581cf1ab493df58dae5 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Sep 2026 12:52:59 -0700 Subject: [PATCH 1/2] fix(backend): remove test controllers from production module and add registration guard --- apps/backend/src/app.module.spec.ts | 16 + apps/backend/src/app.module.ts | 4 +- .../filters/global-exception.filter.spec.ts | 49 ++ .../src/sentiment/sentiment.controller.ts | 94 ++++ .../backend/src/sentiment/sentiment.module.ts | 3 + apps/backend/src/test-exception.controller.ts | 476 ------------------ apps/backend/src/test/test.controller.ts | 162 ------ apps/backend/test/app.e2e-spec.ts | 76 ++- 8 files changed, 194 insertions(+), 686 deletions(-) create mode 100644 apps/backend/src/app.module.spec.ts create mode 100644 apps/backend/src/sentiment/sentiment.controller.ts delete mode 100644 apps/backend/src/test-exception.controller.ts delete mode 100644 apps/backend/src/test/test.controller.ts diff --git a/apps/backend/src/app.module.spec.ts b/apps/backend/src/app.module.spec.ts new file mode 100644 index 000000000..68ce3716e --- /dev/null +++ b/apps/backend/src/app.module.spec.ts @@ -0,0 +1,16 @@ +import { AppModule } from './app.module'; + +describe('AppModule registration guard', () => { + it('prevents any controllers named test* from being registered in app.module.ts', () => { + // Retrieve registered controllers metadata from AppModule + const controllers = + (Reflect.getMetadata('controllers', AppModule) as unknown[]) || []; + + const testControllers = controllers.filter((ctrl: any) => { + const name = typeof ctrl === 'function' ? ctrl.name : String(ctrl); + return name.toLowerCase().startsWith('test'); + }); + + expect(testControllers).toEqual([]); + }); +}); diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 2cc44f5f0..a2f3a3c03 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -9,7 +9,6 @@ import { memoryStorage } from 'multer'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { TestExceptionController } from './test-exception.controller'; import { SentimentModule } from './sentiment/sentiment.module'; import { MetricsModule } from './metrics/metrics.module'; @@ -39,7 +38,6 @@ import { createThrottlerOptions, getRateLimitSettings, } from './common/rate-limit/rate-limit.config'; -import { TestController } from './test/test.controller'; import { UploadModule } from './upload/upload.module'; import { AuthModule } from './auth/auth.module'; import { UsersModule } from './users/users.module'; @@ -212,7 +210,7 @@ import { PriceAlertModule } from './price-alert/price-alert.module'; // Idempotency for write endpoints IdempotencyModule, ], - controllers: [AppController, TestController, TestExceptionController], + controllers: [AppController], providers: [ AppService, RequestContextService, diff --git a/apps/backend/src/filters/global-exception.filter.spec.ts b/apps/backend/src/filters/global-exception.filter.spec.ts index 2379bc80b..9a6eeede0 100644 --- a/apps/backend/src/filters/global-exception.filter.spec.ts +++ b/apps/backend/src/filters/global-exception.filter.spec.ts @@ -152,4 +152,53 @@ describe('GlobalExceptionFilter', () => { process['env']['NODE_ENV'] = originalNodeEnv; }); + + it('exposes error message for generic Error in development/non-production mode', () => { + const originalNodeEnv = process['env']['NODE_ENV']; + process['env']['NODE_ENV'] = 'development'; + + filter.catch(new Error('Test general error message'), mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith( + HttpStatus.INTERNAL_SERVER_ERROR, + ); + expect(mockResponse.json).toHaveBeenCalledWith({ + code: ErrorCode.SYS_INTERNAL_ERROR, + message: 'Test general error message', + requestId: 'req-123', + }); + + process['env']['NODE_ENV'] = originalNodeEnv; + }); + + it('handles BAD_REQUEST HttpException correctly', () => { + const exception = new HttpException( + 'Test HTTP exception message', + HttpStatus.BAD_REQUEST, + ); + + filter.catch(exception, mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.BAD_REQUEST); + expect(mockResponse.json).toHaveBeenCalledWith({ + code: ErrorCode.SYS_BAD_REQUEST, + message: 'Test HTTP exception message', + details: undefined, + requestId: 'req-123', + }); + }); + + it('handles unknown thrown non-Error object gracefully', () => { + filter.catch('string exception', mockArgumentsHost); + + expect(mockResponse.status).toHaveBeenCalledWith( + HttpStatus.INTERNAL_SERVER_ERROR, + ); + expect(mockResponse.json).toHaveBeenCalledWith({ + code: ErrorCode.SYS_INTERNAL_ERROR, + message: 'Internal server error', + requestId: 'req-123', + }); + }); }); + diff --git a/apps/backend/src/sentiment/sentiment.controller.ts b/apps/backend/src/sentiment/sentiment.controller.ts new file mode 100644 index 000000000..d21432ad4 --- /dev/null +++ b/apps/backend/src/sentiment/sentiment.controller.ts @@ -0,0 +1,94 @@ +import { + Controller, + Get, + Post, + Body, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiProperty, +} from '@nestjs/swagger'; +import { + SentimentService, + SentimentResponse, + HealthResponse, +} from './sentiment.service'; + +export class AnalyzeDto { + @ApiProperty({ + description: 'The text to run sentiment analysis on', + example: 'Lumenpulse is doing great!', + }) + text: string; +} + +export class SentimentResponseDto implements SentimentResponse { + @ApiProperty({ + description: 'Calculated sentiment polarity score between -1 and 1', + example: 0.85, + }) + sentiment: number; +} + +export class HealthResponseDto implements HealthResponse { + @ApiProperty({ description: 'Service health status', example: 'healthy' }) + status: string; + + @ApiProperty({ + description: 'Timestamp of health check', + example: '2026-05-27T20:58:35Z', + }) + timestamp: string; + + @ApiProperty({ + description: 'Service name identifier', + example: 'sentiment-analysis', + }) + service: string; +} + +@ApiTags('sentiment') +@Controller('sentiment') +export class SentimentController { + constructor(private readonly sentimentService: SentimentService) {} + + @Post('analyze') + @ApiOperation({ + summary: 'Analyze text sentiment polarity', + description: 'Submits text to calculate polarity scores.', + }) + @ApiResponse({ + status: 200, + description: 'Sentiment calculated successfully', + type: SentimentResponseDto, + }) + @ApiResponse({ status: 400, description: 'Bad request (text empty)' }) + @ApiResponse({ status: 503, description: 'Sentiment service unavailable' }) + async analyzeSentiment( + @Body() analyzeDto: AnalyzeDto, + ): Promise { + if (!analyzeDto || analyzeDto.text === undefined || analyzeDto.text === null) { + throw new HttpException('Text cannot be empty', HttpStatus.BAD_REQUEST); + } + return this.sentimentService.analyzeSentiment(analyzeDto.text); + } + + @Get('health') + @ApiOperation({ + summary: 'Check sentiment service health status', + description: 'Checks health of sentiment analysis service.', + }) + @ApiResponse({ + status: 200, + description: 'Sentiment service health retrieved successfully', + type: HealthResponseDto, + }) + @ApiResponse({ status: 503, description: 'Sentiment service unavailable' }) + async checkSentimentHealth(): Promise { + return this.sentimentService.checkHealth(); + } +} diff --git a/apps/backend/src/sentiment/sentiment.module.ts b/apps/backend/src/sentiment/sentiment.module.ts index 3b04bd081..a568b5d6b 100644 --- a/apps/backend/src/sentiment/sentiment.module.ts +++ b/apps/backend/src/sentiment/sentiment.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { SentimentService } from './sentiment.service'; +import { SentimentController } from './sentiment.controller'; import { ConfigModule } from '@nestjs/config'; @Module({ @@ -13,7 +14,9 @@ import { ConfigModule } from '@nestjs/config'; }), ConfigModule, ], + controllers: [SentimentController], providers: [SentimentService], exports: [SentimentService], }) export class SentimentModule {} + diff --git a/apps/backend/src/test-exception.controller.ts b/apps/backend/src/test-exception.controller.ts deleted file mode 100644 index 5c8ab71e6..000000000 --- a/apps/backend/src/test-exception.controller.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { - Controller, - Get, - HttpException, - HttpStatus, - Post, - Body, - Logger, -} from '@nestjs/common'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiProperty, -} from '@nestjs/swagger'; -import { - SentimentService, - SentimentResponse, - HealthResponse, -} from './sentiment/sentiment.service'; -import { config } from './lib/config'; - -// DTO for sentiment analysis -class AnalyzeDto { - @ApiProperty({ - description: 'The text to run sentiment analysis on', - example: 'Lumenpulse is doing great!', - }) - text: string; -} - -// Interface for test results -class SentimentTestCaseDto { - @ApiProperty({ - description: 'Test text analyzed', - example: 'I love this product!', - }) - text: string; - - @ApiProperty({ description: 'Sentiment score returned', example: 0.85 }) - sentiment?: number; - - @ApiProperty({ description: 'Expected class', example: 'positive' }) - expected: string; - - @ApiProperty({ description: 'Test status', example: 'success' }) - status: string; - - @ApiProperty({ description: 'Actual class calculated', example: 'positive' }) - actual?: string; - - @ApiProperty({ - description: 'Whether actual matches expected', - example: true, - }) - match?: boolean; - - @ApiProperty({ description: 'Error message if failed', example: '' }) - error?: string; -} - -class SentimentTestResultDto { - @ApiProperty({ - description: 'ISO timestamp of the run', - example: '2026-05-27T21:00:00Z', - }) - timestamp: string; - - @ApiProperty({ - description: 'Status of testing process', - example: 'complete', - }) - status: string; - - @ApiProperty({ - description: 'Status feedback message', - example: 'All tests finished', - }) - message?: string; - - @ApiProperty({ description: 'Total count of tests executed', example: 3 }) - totalTests: number; - - @ApiProperty({ description: 'Count of successful requests', example: 3 }) - successful: number; - - @ApiProperty({ description: 'Count of matching classifications', example: 3 }) - matches: number; - - @ApiProperty({ - description: 'Run details for each test case', - type: [SentimentTestCaseDto], - }) - testCases: SentimentTestCaseDto[]; - - @ApiProperty({ - description: 'Target Python API URL', - example: 'http://localhost:8000', - }) - pythonApiUrl: string; - - @ApiProperty({ description: 'Availability of Python service', example: true }) - serviceAvailable: boolean; -} - -class ExceptionTestResultDto { - @ApiProperty({ - description: 'Name of the test endpoint', - example: 'http-exception', - }) - endpoint: string; - - @ApiProperty({ - description: 'URL path of endpoint', - example: 'test-exception/http-exception', - }) - url: string; - - @ApiProperty({ - description: 'Current availability/status', - example: 'available', - }) - status: string; -} - -class AllTestsSummaryDto { - @ApiProperty({ description: 'Total exception endpoints tested', example: 3 }) - totalExceptionTests: number; - - @ApiProperty({ - description: 'Availability of sentiment service', - example: true, - }) - sentimentServiceAvailable: boolean; - - @ApiProperty({ - description: 'Overall combined status string', - example: 'full_service', - }) - overallStatus: string; -} - -class AllTestsResultDto { - @ApiProperty({ - description: 'Timestamp of run', - example: '2026-05-27T21:00:00Z', - }) - timestamp: string; - - @ApiProperty({ - description: 'Results of exception tests', - type: [ExceptionTestResultDto], - }) - exceptionTests: ExceptionTestResultDto[]; - - @ApiProperty({ - description: 'Results of sentiment tests', - type: SentimentTestResultDto, - nullable: true, - }) - sentimentTests: SentimentTestResultDto | null; - - @ApiProperty({ - description: 'Overall run summary metrics', - type: AllTestsSummaryDto, - }) - summary: AllTestsSummaryDto; -} - -class SentimentResponseDto implements SentimentResponse { - @ApiProperty({ - description: 'Calculated sentiment polarity score between -1 and 1', - example: 0.85, - }) - sentiment: number; - - @ApiProperty({ description: 'Classification category', example: 'positive' }) - label: string; -} - -class HealthResponseDto implements HealthResponse { - @ApiProperty({ description: 'Service health status', example: 'healthy' }) - status: string; - - @ApiProperty({ - description: 'Timestamp of health check', - example: '2026-05-27T20:58:35Z', - }) - timestamp: string; - - @ApiProperty({ - description: 'Service name identifier', - example: 'sentiment-analysis-service', - }) - service: string; -} - -@ApiTags('test-exception') -@Controller('test-exception') -export class TestExceptionController { - private readonly logger = new Logger(TestExceptionController.name); - - constructor(private readonly sentimentService?: SentimentService) {} - - // ===== Original Exception Testing Endpoints (Backward Compatible) ===== - - @Get('http-exception') - @ApiOperation({ - summary: 'Trigger standard HTTP HttpException', - description: - 'Throws a BAD_REQUEST HttpException to test exception filters.', - }) - @ApiResponse({ status: 400, description: 'Throws a Bad Request exception' }) - getHttpException() { - throw new HttpException( - 'Test HTTP exception message', - HttpStatus.BAD_REQUEST, - ); - } - - @Get('general-error') - @ApiOperation({ - summary: 'Trigger standard Javascript Error', - description: - 'Throws a generic Error to test internal server error mappings.', - }) - @ApiResponse({ status: 500, description: 'Throws generic Error' }) - getGeneralError() { - throw new Error('Test general error message'); - } - - @Get('internal-server-error') - @ApiOperation({ - summary: 'Trigger unknown error type', - description: - 'Throws an Error with unknown details to verify fallback logs.', - }) - @ApiResponse({ status: 500, description: 'Throws unknown error type' }) - getInternalServerError() { - // This will trigger the unknown error path - throw new Error('Unknown error type'); - } - - // ===== New Sentiment Analysis Endpoints ===== - - @Post('sentiment/analyze') - @ApiOperation({ - summary: 'Analyze text sentiment polarity', - description: - 'Submits text to the Python data service to calculate polarity scores.', - }) - @ApiResponse({ - status: 200, - description: 'Sentiment calculated successfully', - type: SentimentResponseDto, - }) - @ApiResponse({ status: 503, description: 'Sentiment service unavailable' }) - async analyzeSentiment( - @Body() analyzeDto: AnalyzeDto, - ): Promise { - if (!this.sentimentService) { - throw new HttpException( - 'Sentiment service is not available', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - - this.logger.log( - `Analyzing sentiment for text: ${analyzeDto.text.substring(0, 50)}...`, - ); - return this.sentimentService.analyzeSentiment(analyzeDto.text); - } - - @Get('sentiment/health') - @ApiOperation({ - summary: 'Check sentiment service health status', - description: - 'Pings the underlying Python analysis service health endpoint.', - }) - @ApiResponse({ - status: 200, - description: 'Sentiment service health retrieved successfully', - type: HealthResponseDto, - }) - @ApiResponse({ status: 503, description: 'Sentiment service unavailable' }) - async checkSentimentHealth(): Promise { - if (!this.sentimentService) { - throw new HttpException( - 'Sentiment service is not available', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } - - return this.sentimentService.checkHealth(); - } - - @Post('sentiment/test') - @ApiOperation({ - summary: 'Run sentiment test suite', - description: - 'Runs multiple hardcoded test phrases to verify sentiment classifications.', - }) - @ApiResponse({ - status: 200, - description: 'Test suite ran successfully', - type: SentimentTestResultDto, - }) - async testSentiment(): Promise { - if (!this.sentimentService) { - return { - timestamp: new Date().toISOString(), - status: 'service_unavailable', - message: 'Sentiment service is not configured', - totalTests: 0, - successful: 0, - matches: 0, - testCases: [], - pythonApiUrl: config.python.apiUrl, - serviceAvailable: false, - }; - } - - const testCases = [ - { - text: 'I love this product! It is absolutely amazing!', - expected: 'positive', - }, - { - text: 'This is terrible and awful, worst experience ever.', - expected: 'negative', - }, - { text: 'The weather is normal today.', expected: 'neutral' }, - ]; - - const results: SentimentTestCaseDto[] = []; - - for (const testCase of testCases) { - try { - const result = await this.sentimentService.analyzeSentiment( - testCase.text, - ); - - const actual = - result.sentiment > 0.05 - ? 'positive' - : result.sentiment < -0.05 - ? 'negative' - : 'neutral'; - - const match = - (result.sentiment > 0.05 && testCase.expected === 'positive') || - (result.sentiment < -0.05 && testCase.expected === 'negative') || - (result.sentiment >= -0.05 && - result.sentiment <= 0.05 && - testCase.expected === 'neutral'); - - results.push({ - text: - testCase.text.substring(0, 50) + - (testCase.text.length > 50 ? '...' : ''), - sentiment: result.sentiment, - expected: testCase.expected, - status: 'success', - actual, - match, - }); - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error); - results.push({ - text: - testCase.text.substring(0, 50) + - (testCase.text.length > 50 ? '...' : ''), - expected: testCase.expected, - status: 'failed', - error: errorMessage, - }); - } - } - - const successCount = results.filter((r) => r.status === 'success').length; - const matchCount = results.filter((r) => r.match).length; - - return { - timestamp: new Date().toISOString(), - status: successCount === testCases.length ? 'complete' : 'partial', - totalTests: testCases.length, - successful: successCount, - matches: matchCount, - testCases: results, - pythonApiUrl: config.python.apiUrl, - serviceAvailable: true, - }; - } - - // ===== Hybrid Endpoint for Testing Both ===== - - @Get('all-tests') - @ApiOperation({ - summary: 'Run exception filters and sentiment tests together', - description: - 'Bundles diagnostics for both exceptions filters and sentiment APIs.', - }) - @ApiResponse({ - status: 200, - description: 'All tests finished successfully', - type: AllTestsResultDto, - }) - async runAllTests(): Promise { - const results: AllTestsResultDto = { - timestamp: new Date().toISOString(), - exceptionTests: [], - sentimentTests: null, - summary: { - totalExceptionTests: 0, - sentimentServiceAvailable: false, - overallStatus: '', - }, - }; - - // Test exception endpoints - const exceptionEndpoints = [ - { name: 'http-exception', url: 'test-exception/http-exception' }, - { name: 'general-error', url: 'test-exception/general-error' }, - { - name: 'internal-server-error', - url: 'test-exception/internal-server-error', - }, - ]; - - for (const endpoint of exceptionEndpoints) { - results.exceptionTests.push({ - endpoint: endpoint.name, - url: endpoint.url, - status: 'available', - }); - } - - // Test sentiment if available - if (this.sentimentService) { - try { - const sentimentTest = await this.testSentiment(); - results.sentimentTests = sentimentTest; - results.summary.sentimentServiceAvailable = true; - } catch (error: unknown) { - const errorMessage = - error instanceof Error ? error.message : String(error); - results.sentimentTests = { - timestamp: new Date().toISOString(), - status: 'failed', - message: `Sentiment test failed: ${errorMessage}`, - totalTests: 0, - successful: 0, - matches: 0, - testCases: [ - { - text: 'Sentiment service test', - expected: 'n/a', - status: 'error', - error: errorMessage, - }, - ], - pythonApiUrl: config.python.apiUrl, - serviceAvailable: false, - }; - } - } - - results.summary.totalExceptionTests = exceptionEndpoints.length; - results.summary.overallStatus = this.sentimentService - ? 'full_service' - : 'basic_service'; - - return results; - } -} diff --git a/apps/backend/src/test/test.controller.ts b/apps/backend/src/test/test.controller.ts deleted file mode 100644 index 06759e653..000000000 --- a/apps/backend/src/test/test.controller.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { - Controller, - Get, - Post, - Put, - Delete, - Body, - Param, - UseGuards, -} from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; -import { FeatureFlag } from '../feature-flags/feature-flag.decorator'; -import { FeatureFlagGuard } from '../feature-flags/feature-flag.guard'; - -@ApiTags('test') -@Controller('test') -export class TestController { - @Get('hello') - @UseGuards(FeatureFlagGuard) - @FeatureFlag('test.hello') - @ApiOperation({ - summary: 'Hello World feature flagged endpoint', - description: - 'Returns hello message if "test.hello" feature flag is enabled.', - }) - @ApiResponse({ status: 200, description: 'Success message', type: String }) - @ApiResponse({ - status: 403, - description: 'Forbidden (Feature flag disabled)', - }) - getHello(): string { - return 'Hello World!'; - } - - @Post('submit') - @ApiOperation({ - summary: 'Submit diagnostic data', - description: 'Echos submitted payload back with a timestamp for testing.', - }) - @ApiResponse({ - status: 200, - description: 'Data submitted successfully', - schema: { - properties: { - message: { type: 'string', example: 'Data submitted successfully' }, - timestamp: { type: 'string', format: 'date-time' }, - receivedData: { type: 'object' }, - }, - }, - }) - submitData(@Body() body: Record): { - message: string; - timestamp: Date; - receivedData?: Record; - } { - return { - message: 'Data submitted successfully', - timestamp: new Date(), - receivedData: body, - }; - } - - @Get('error') - @ApiOperation({ - summary: 'Trigger standard Error for testing logs', - description: - 'Throws a generic Error to confirm that error log interceptors trigger.', - }) - @ApiResponse({ status: 500, description: 'Throws standard Error' }) - getError(): void { - throw new Error('Test error for logging'); - } - - @Get('not-found') - @ApiOperation({ - summary: 'Trigger Not Found Error for testing logs', - description: - 'Throws a generic not found Error to verify filter intercepts.', - }) - @ApiResponse({ status: 500, description: 'Throws resource not found error' }) - getNotFound(): void { - throw new Error('Resource not found'); - } - - @Get('redirect') - @ApiOperation({ - summary: 'Mock redirect response metadata', - description: 'Returns mock redirection data.', - }) - @ApiResponse({ - status: 200, - description: 'Redirection payload', - schema: { - properties: { - redirect: { type: 'boolean', example: true }, - destination: { type: 'string', example: '/test/hello' }, - }, - }, - }) - getRedirect(): Record { - return { redirect: true, destination: '/test/hello' }; - } - - @Put('update/:id') - @ApiOperation({ - summary: 'Update diagnostic data', - description: 'Echos updated body back with specified id.', - }) - @ApiParam({ - name: 'id', - description: 'Diagnostic item ID', - example: 'diag_123', - }) - @ApiResponse({ - status: 200, - description: 'Data updated successfully', - schema: { - properties: { - message: { type: 'string', example: 'Data updated successfully' }, - id: { type: 'string', example: 'diag_123' }, - updatedData: { type: 'object' }, - }, - }, - }) - updateData( - @Param('id') id: string, - @Body() body: Record, - ): { message: string; id: string; updatedData: Record } { - return { - message: 'Data updated successfully', - id, - updatedData: body, - }; - } - - @Delete('delete/:id') - @ApiOperation({ - summary: 'Delete diagnostic data', - description: 'Confirms deletion of item ID.', - }) - @ApiParam({ - name: 'id', - description: 'Diagnostic item ID to delete', - example: 'diag_123', - }) - @ApiResponse({ - status: 200, - description: 'Data deleted successfully', - schema: { - properties: { - message: { type: 'string', example: 'Data deleted successfully' }, - id: { type: 'string', example: 'diag_123' }, - }, - }, - }) - deleteData(@Param('id') id: string): { message: string; id: string } { - return { - message: 'Data deleted successfully', - id, - }; - } -} diff --git a/apps/backend/test/app.e2e-spec.ts b/apps/backend/test/app.e2e-spec.ts index 60877ffc5..4c32e9c1b 100644 --- a/apps/backend/test/app.e2e-spec.ts +++ b/apps/backend/test/app.e2e-spec.ts @@ -1,62 +1,48 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -// import request from 'supertest'; -import { AppModule } from './../src/app.module'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from '../src/app.module'; import { GlobalExceptionFilter } from '../src/filters/global-exception.filter'; -// import type { Server } from 'http'; +import { ErrorCode } from '../src/common/enums/error-code.enum'; +import { ErrorResponse } from '../src/interfaces/error-response.interface'; + +const makeRequest = (app: INestApplication) => + request(app.getHttpServer() as unknown as App); describe('AppController (e2e)', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); - // Apply the global exception filter for testing app.useGlobalFilters(new GlobalExceptionFilter()); await app.init(); }); - // it('/ (GET)', () => { - // // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - // return request(app.getHttpServer()); - - // return request(app.getHttpServer() as unknown as Server) - // .get('/') - // .expect(200) - // .expect('Hello World!'); - // }); - - // it('/nonexistent (GET) - should return standardized error response', () => { - // // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - // return request(app.getHttpServer()); - - // interface ErrorResponse { - // statusCode: number; - // message: string; - // error: string; - // timestamp: string; - // path: string; - // } - - // return request(app.getHttpServer() as unknown as Server) - // .get('/nonexistent') - // .expect(404) - // .then((response) => { - // const body = response.body as ErrorResponse; - - // expect(body).toHaveProperty('statusCode'); - // expect(body).toHaveProperty('message'); - // expect(body).toHaveProperty('error'); - // expect(body).toHaveProperty('timestamp'); - // expect(body).toHaveProperty('path'); - // expect(body.statusCode).toBe(404); - // expect(typeof body.message).toBe('string'); - // expect(typeof body.error).toBe('string'); - // expect(typeof body.timestamp).toBe('string'); - // expect(body.path).toBe('/nonexistent'); - // }); - // }); + afterAll(async () => { + await app.close(); + }); + + it('/ (GET)', async () => { + const res = await makeRequest(app).get('/').expect(200); + expect(res.text).toBe('Hello World!'); + }); + + it('/nonexistent (GET) - should return standardized 404 error response', async () => { + const res = await makeRequest(app).get('/nonexistent').expect(404); + const body = res.body as ErrorResponse; + + expect(body.code).toBe(ErrorCode.SYS_NOT_FOUND); + expect(body.message).toBeTruthy(); + }); + + it('verifies test controller paths /test/* and /test-exception/* are not exposed', async () => { + await makeRequest(app).get('/test/hello').expect(404); + await makeRequest(app).get('/test-exception/http-exception').expect(404); + await makeRequest(app).get('/test-exception/all-tests').expect(404); + }); }); From 273cf4390427c7aae15ddcd5f5116d3a36e06496 Mon Sep 17 00:00:00 2001 From: egwujiohaifesinachiperpetual-max Date: Sat, 26 Sep 2026 01:50:15 -0700 Subject: [PATCH 2/2] fix add --- apps/backend/src/openapi/error-response.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/openapi/error-response.dto.ts b/apps/backend/src/openapi/error-response.dto.ts index 41a55a9d6..db99a5ed5 100644 --- a/apps/backend/src/openapi/error-response.dto.ts +++ b/apps/backend/src/openapi/error-response.dto.ts @@ -54,5 +54,5 @@ export class ErrorResponseDto implements ErrorResponse { 'Correlation id for this request, also sent as the `X-Request-Id` response header.', example: '4b7c7a8e-3f0e-4c7a-9d0b-2a1f7e2c9b11', }) - requestId: string; + correlationId: string; }