diff --git a/BackendAcademy/src/app.controller.spec.ts b/BackendAcademy/src/app.controller.spec.ts index 4f5dfe55e5..cd03082a60 100644 --- a/BackendAcademy/src/app.controller.spec.ts +++ b/BackendAcademy/src/app.controller.spec.ts @@ -1,6 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { HealthController, SUPPORTED_PROBES } from './health/health.controller'; describe('AppController', () => { let appController: AppController; @@ -19,4 +21,48 @@ describe('AppController', () => { expect(appController.getHello()).toBe('RustAcademy BackendAcademy API'); }); }); + + describe('Health Endpoint Query Parameter Validation (BA-113)', () => { + let healthController: HealthController; + + beforeEach(() => { + healthController = new HealthController(); + }); + + it('should cover accepted default probe when no query parameters are provided', () => { + const res = healthController.getHealth(); + expect(res.status).toBe('ok'); + expect(res.service).toBe('backend-academy'); + expect(res.probe).toBe('full'); + expect(res.timestamp).toBeDefined(); + }); + + it('should accept all supported probe options', () => { + for (const probe of SUPPORTED_PROBES) { + const res = healthController.getHealth(probe); + expect(res.status).toBe('ok'); + expect(res.probe).toBe(probe); + } + }); + + it('should reject unsupported probe options with BadRequestException', () => { + expect(() => healthController.getHealth('unsupported-probe')).toThrow(BadRequestException); + expect(() => healthController.getHealth('db')).toThrow(BadRequestException); + + try { + healthController.getHealth('invalid'); + } catch (err: any) { + expect(err).toBeInstanceOf(BadRequestException); + expect(err.message).toContain("Unsupported probe option 'invalid'"); + } + }); + + it('should include diagnostic fields when verbose query is true', () => { + const res = healthController.getHealth('readiness', 'true'); + expect(res.status).toBe('ok'); + expect(res.probe).toBe('readiness'); + expect(res.uptime).toBeDefined(); + expect(res.nodeVersion).toBeDefined(); + }); + }); }); diff --git a/BackendAcademy/src/health/coverage.ts b/BackendAcademy/src/health/coverage.ts index ec38d4397c..b0739f222d 100644 --- a/BackendAcademy/src/health/coverage.ts +++ b/BackendAcademy/src/health/coverage.ts @@ -1,4 +1,9 @@ -import { ForbiddenException, Injectable, TooManyRequestsException } from '@nestjs/common'; +import { + ForbiddenException, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; export interface ChatMessage { id: string; @@ -83,7 +88,7 @@ export class ModerationService { private assertRateLimit(userId: string, now: number): void { const recent = (this.rateWindows.get(userId) ?? []).filter((timestamp) => now - timestamp < this.rateWindowMs); if (recent.length >= this.maxMessagesPerWindow) { - throw new TooManyRequestsException('Chat rate limit exceeded'); + throw new HttpException('Chat rate limit exceeded', HttpStatus.TOO_MANY_REQUESTS); } recent.push(now); this.rateWindows.set(userId, recent); diff --git a/BackendAcademy/src/health/health.controller.ts b/BackendAcademy/src/health/health.controller.ts index ecc366321c..628fe984cb 100644 --- a/BackendAcademy/src/health/health.controller.ts +++ b/BackendAcademy/src/health/health.controller.ts @@ -1,16 +1,47 @@ -import { Controller, Get } from '@nestjs/common'; +import { BadRequestException, Controller, Get, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +export const SUPPORTED_PROBES = new Set(['liveness', 'readiness', 'startup', 'full']); + +export interface HealthResponse { + status: 'ok'; + service: string; + probe: string; + timestamp: string; + uptime?: number; + nodeVersion?: string; +} + @ApiTags('health') @Controller('health') -//controller for health check endpoint +// controller for health check endpoint with query probe validation (BA-113) export class HealthController { @Get() - getHealth() { - return { + getHealth( + @Query('probe') probe?: string, + @Query('verbose') verbose?: string, + ): HealthResponse { + if (probe !== undefined && !SUPPORTED_PROBES.has(probe)) { + throw new BadRequestException( + `Unsupported probe option '${probe}'. Supported probe options are: ${Array.from(SUPPORTED_PROBES).join(', ')}`, + ); + } + + const effectiveProbe = probe ?? 'full'; + const isVerbose = verbose === 'true' || verbose === '1'; + + const response: HealthResponse = { status: 'ok', service: 'backend-academy', + probe: effectiveProbe, timestamp: new Date().toISOString(), }; + + if (isVerbose) { + response.uptime = process.uptime(); + response.nodeVersion = process.version; + } + + return response; } }