Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions BackendAcademy/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
});
});
});
9 changes: 7 additions & 2 deletions BackendAcademy/src/health/coverage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ForbiddenException, Injectable, TooManyRequestsException } from '@nestjs/common';
import {
ForbiddenException,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common';

export interface ChatMessage {
id: string;
Expand Down Expand Up @@ -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);
Expand Down
39 changes: 35 additions & 4 deletions BackendAcademy/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}