Skip to content
Open
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
83 changes: 83 additions & 0 deletions BackendAcademy/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ArgumentMetadata, BadRequestException, ValidationPipe } from '@nestjs/common';
import { IsNotEmpty, IsString } from 'class-validator';
import { AppController } from './app.controller';
import { AppService } from './app.service';

const validationPipeOptions = {
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
};

class SampleCourseDto {
@IsString()
@IsNotEmpty()
title!: string;

@IsString()
@IsNotEmpty()
description!: string;
}

describe('AppController', () => {
let appController: AppController;

Expand All @@ -19,4 +37,69 @@ describe('AppController', () => {
expect(appController.getHello()).toBe('RustAcademy BackendAcademy API');
});
});

describe('Global Request Validation (BA-120)', () => {
let pipe: ValidationPipe;

beforeEach(() => {
pipe = new ValidationPipe(validationPipeOptions);
});

it('should reject unknown DTO properties without stripping or silently accepting them', async () => {
const metadata: ArgumentMetadata = {
type: 'body',
metatype: SampleCourseDto,
data: '',
};

const payloadWithExtraneousField = {
title: 'Soroban 101',
description: 'Introduction to Soroban smart contracts',
maliciousParam: 'unexpected',
isAdmin: true,
};

await expect(
pipe.transform(payloadWithExtraneousField, metadata),
).rejects.toThrow(BadRequestException);
});

it('should include property name in validation error response when unknown fields are present', async () => {
const metadata: ArgumentMetadata = {
type: 'body',
metatype: SampleCourseDto,
data: '',
};

try {
await pipe.transform({ title: 'Rust', description: 'Basics', extraField: 123 }, metadata);
fail('Expected ValidationPipe to reject unknown property');
} catch (err) {
expect(err).toBeInstanceOf(BadRequestException);
const res = (err as BadRequestException).getResponse() as any;
expect(res.message).toEqual(
expect.arrayContaining([expect.stringContaining('property extraField should not exist')]),
);
}
});

it('should successfully accept valid payloads conforming to target DTO', async () => {
const metadata: ArgumentMetadata = {
type: 'body',
metatype: SampleCourseDto,
data: '',
};

const validPayload = {
title: 'Rust Basics',
description: 'Learn memory safety without garbage collection',
};

const result = await pipe.transform(validPayload, metadata);
expect(result).toBeInstanceOf(SampleCourseDto);
expect(result.title).toBe('Rust Basics');
expect(result.description).toBe('Learn memory safety without garbage collection');
});
});
});

14 changes: 12 additions & 2 deletions BackendAcademy/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { APP_GUARD, APP_PIPE } from '@nestjs/core';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { AppController } from './app.controller';
import { AppService } from './app.service';
Expand Down Expand Up @@ -57,6 +57,16 @@ import { CertificatesModule } from './certificates/certificates.module';
// token), so the rate limiting configured above is actually
// enforced on every request rather than just being available.
{ provide: APP_GUARD, useClass: ThrottlerGuard },
// Registers global strict validation pipe: rejects unknown DTO properties
// without stripping or silently accepting them.
{
provide: APP_PIPE,
useValue: new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
},
],
})
export class AppModule {}
13 changes: 7 additions & 6 deletions BackendAcademy/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import helmet from 'helmet';
import { AppModule } from './app.module';

export const validationPipeOptions = {
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
};

// Application entry point: creates the Nest app, applies global
// middleware/pipes, optionally sets up Swagger docs, and starts
// listening on the configured port.
Expand All @@ -25,18 +31,13 @@ async function bootstrap() {
// liveness/readiness/status checks remain reachable at their bare
// paths for monitoring tools that expect them un-prefixed).
app.setGlobalPrefix('api', { exclude: ['health'] });

// Global validation pipe applied to all incoming request bodies/params:
// - whitelist: strips properties not defined on the target DTO
// - transform: converts payloads into instances of their DTO classes
// - forbidNonWhitelisted: rejects requests containing any property
// not defined on the DTO (rather than silently dropping them)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
new ValidationPipe(validationPipeOptions),
);

const configService = app.get(ConfigService);
Expand Down