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
196 changes: 196 additions & 0 deletions backend/src/posts/dto/post.dto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { CreatePostDto, UpdatePostDto } from './post.dto';

async function validateDto<T extends object>(cls: new () => T, plain: object) {
const dto = plainToInstance(cls, plain);
return validate(dto);
}

describe('CreatePostDto', () => {
const valid = { title: 'Hello', content: 'World' };

it('accepts a valid payload', async () => {
const errors = await validateDto(CreatePostDto, valid);
expect(errors).toHaveLength(0);
});

describe('title', () => {
it('rejects missing title', async () => {
const errors = await validateDto(CreatePostDto, { content: 'Body' });
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('rejects empty title', async () => {
const errors = await validateDto(CreatePostDto, { ...valid, title: '' });
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('rejects title exceeding 500 characters', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
title: 'a'.repeat(501),
});
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('rejects non-string title', async () => {
const errors = await validateDto(CreatePostDto, { ...valid, title: 123 });
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('accepts title exactly 500 characters', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
title: 'a'.repeat(500),
});
expect(errors).toHaveLength(0);
});
});

describe('content', () => {
it('rejects missing content', async () => {
const errors = await validateDto(CreatePostDto, { title: 'T' });
expect(errors.some((e) => e.property === 'content')).toBe(true);
});

it('rejects empty content', async () => {
const errors = await validateDto(CreatePostDto, { ...valid, content: '' });
expect(errors.some((e) => e.property === 'content')).toBe(true);
});

it('rejects content exceeding 10000 characters', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
content: 'a'.repeat(10001),
});
expect(errors.some((e) => e.property === 'content')).toBe(true);
});

it('accepts content exactly 10000 characters', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
content: 'a'.repeat(10000),
});
expect(errors).toHaveLength(0);
});
});

describe('isPublished', () => {
it('accepts boolean true', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
isPublished: true,
});
expect(errors).toHaveLength(0);
});

it('accepts boolean false', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
isPublished: false,
});
expect(errors).toHaveLength(0);
});

it('rejects non-boolean value', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
isPublished: 'yes',
});
expect(errors.some((e) => e.property === 'isPublished')).toBe(true);
});

it('is optional (absent is valid)', async () => {
const errors = await validateDto(CreatePostDto, valid);
expect(errors).toHaveLength(0);
});
});

describe('isPremium', () => {
it('accepts boolean true', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
isPremium: true,
});
expect(errors).toHaveLength(0);
});

it('rejects non-boolean value', async () => {
const errors = await validateDto(CreatePostDto, {
...valid,
isPremium: 1,
});
expect(errors.some((e) => e.property === 'isPremium')).toBe(true);
});

it('is optional (absent is valid)', async () => {
const errors = await validateDto(CreatePostDto, valid);
expect(errors).toHaveLength(0);
});
});
});

describe('UpdatePostDto', () => {
it('accepts an empty object (all fields optional)', async () => {
const errors = await validateDto(UpdatePostDto, {});
expect(errors).toHaveLength(0);
});

it('accepts a fully populated valid payload', async () => {
const errors = await validateDto(UpdatePostDto, {
title: 'Updated',
content: 'New body',
isPublished: true,
isPremium: false,
});
expect(errors).toHaveLength(0);
});

describe('title', () => {
it('rejects empty string', async () => {
const errors = await validateDto(UpdatePostDto, { title: '' });
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('rejects title exceeding 500 characters', async () => {
const errors = await validateDto(UpdatePostDto, {
title: 'a'.repeat(501),
});
expect(errors.some((e) => e.property === 'title')).toBe(true);
});

it('rejects non-string title', async () => {
const errors = await validateDto(UpdatePostDto, { title: 42 });
expect(errors.some((e) => e.property === 'title')).toBe(true);
});
});

describe('content', () => {
it('rejects empty string', async () => {
const errors = await validateDto(UpdatePostDto, { content: '' });
expect(errors.some((e) => e.property === 'content')).toBe(true);
});

it('rejects content exceeding 10000 characters', async () => {
const errors = await validateDto(UpdatePostDto, {
content: 'a'.repeat(10001),
});
expect(errors.some((e) => e.property === 'content')).toBe(true);
});
});

describe('isPublished', () => {
it('rejects non-boolean value', async () => {
const errors = await validateDto(UpdatePostDto, { isPublished: 'true' });
expect(errors.some((e) => e.property === 'isPublished')).toBe(true);
});
});

describe('isPremium', () => {
it('rejects non-boolean value', async () => {
const errors = await validateDto(UpdatePostDto, { isPremium: 0 });
expect(errors.some((e) => e.property === 'isPremium')).toBe(true);
});
});
});
62 changes: 38 additions & 24 deletions backend/src/posts/dto/post.dto.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Expose } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
MaxLength,
MinLength,
} from 'class-validator';

export class PostDto {
@ApiProperty({
example: 'post-123',
description: 'Unique post identifier',
})
@ApiProperty({ example: 'post-123', description: 'Unique post identifier' })
@Expose()
id: string;

@ApiProperty({
example: 'My First Post',
description: 'Post title',
})
@ApiProperty({ example: 'My First Post', description: 'Post title' })
@Expose()
title: string;

Expand All @@ -23,17 +25,11 @@ export class PostDto {
@Expose()
content: string;

@ApiProperty({
example: 'author-456',
description: 'Author user ID',
})
@ApiProperty({ example: 'author-456', description: 'Author user ID' })
@Expose()
authorId: string;

@ApiProperty({
example: true,
description: 'Whether post is published',
})
@ApiProperty({ example: true, description: 'Whether post is published' })
@Expose()
isPublished: boolean;

Expand All @@ -44,10 +40,7 @@ export class PostDto {
@Expose()
isPremium: boolean;

@ApiProperty({
example: 5,
description: 'Number of likes',
})
@ApiProperty({ example: 5, description: 'Number of likes' })
@Expose()
likesCount: number;

Expand Down Expand Up @@ -81,6 +74,10 @@ export class CreatePostDto {
minLength: 1,
maxLength: 500,
})
@IsNotEmpty()
@IsString()
@MinLength(1)
@MaxLength(500)
title: string;

@ApiProperty({
Expand All @@ -89,18 +86,26 @@ export class CreatePostDto {
minLength: 1,
maxLength: 10000,
})
@IsNotEmpty()
@IsString()
@MinLength(1)
@MaxLength(10000)
content: string;

@ApiPropertyOptional({
example: true,
description: 'Whether to publish post immediately (default: false)',
})
@IsOptional()
@IsBoolean()
isPublished?: boolean;

@ApiPropertyOptional({
example: false,
description: 'Whether this is a premium post (default: false)',
})
@IsOptional()
@IsBoolean()
isPremium?: boolean;
}

Expand All @@ -111,6 +116,10 @@ export class UpdatePostDto {
minLength: 1,
maxLength: 500,
})
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(500)
title?: string;

@ApiPropertyOptional({
Expand All @@ -119,17 +128,22 @@ export class UpdatePostDto {
minLength: 1,
maxLength: 10000,
})
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(10000)
content?: string;

@ApiPropertyOptional({
example: true,
description: 'Whether to publish post',
})
@ApiPropertyOptional({ example: true, description: 'Whether to publish post' })
@IsOptional()
@IsBoolean()
isPublished?: boolean;

@ApiPropertyOptional({
example: false,
description: 'Whether this is a premium post',
})
@IsOptional()
@IsBoolean()
isPremium?: boolean;
}
Loading
Loading