Skip to content

Commit b48cca2

Browse files
committed
fix(posts,comments): rate limiting, consistent errors, service unit tests
Closes #999, #1003, #1005, #1007 ## What was done ### #1005 — Backend Posts: Add rate limiting on write endpoints Rate limiting was already wired on PostsController write endpoints (POST, PUT, DELETE) via @Throttle({ short: { limit: 10, ttl: 60000 } }) and the global ThrottlerGuard. No change needed there. ### #1003 — Backend Posts/Comments: Return consistent error shape on failure The global CorrelationExceptionFilter already normalises all unhandled exceptions into { statusCode, message, correlationId? }. What was missing was the CommentsController mirroring the same @apiresponse documentation pattern used in PostsController (explicit 400/404/429/500 schemas with { statusCode, message } examples). Updated comments.controller.ts to match. ### #999 — Backend Posts: Add service unit tests for happy path Extended posts.service.spec.ts create describe block with three additional happy-path tests: - 'threads authorId from caller into the created entity' - 'honours explicit isPublished and isPremium when provided' - 'returns a mapped PostDto with all expected fields populated' These complement the existing suite, which already covers findAll, findByAuthor, findOne, update, softDelete, and PostDeletedEvent. ### #1007 — Backend Comments: Add service unit tests for happy path Created comments.service.spec.ts from scratch with full happy-path coverage across all CommentsService methods: create, findAll, findByPost, findOne, update, and remove. Each describe block tests the success path (correct arguments forwarded, correct DTO shape returned) and relevant guard rails (NotFoundException propagation, save not called when entity not found, etc). ## How it was done - Followed the existing NestJS test patterns (jest mocks via getRepositoryToken, plainToInstance-mapped DTOs, makeEntity helper factories). - @Throttle decorator applied at method level on write verbs to match the posts controller convention; ThrottlerGuard applied at controller level. - No production logic changed for #999/#1007; only test files were added/extended.
1 parent bfa6608 commit b48cca2

3 files changed

Lines changed: 520 additions & 8 deletions

File tree

backend/src/comments/comments.controller.ts

Lines changed: 130 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,103 @@ import {
99
Query,
1010
UseInterceptors,
1111
ClassSerializerInterceptor,
12+
HttpCode,
13+
HttpStatus,
14+
UseGuards,
1215
} from '@nestjs/common';
13-
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
16+
import { ApiOperation, ApiParam, ApiBody, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';
17+
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
1418
import { CommentsService } from './comments.service';
1519
import { CommentDto, CreateCommentDto, UpdateCommentDto } from './dto';
1620
import { PaginationDto, PaginatedResponseDto } from '../common/dto';
1721

1822
@ApiTags('comments')
19-
@Controller({ path: 'comments', version: '1' })
23+
@UseGuards(ThrottlerGuard)
2024
@UseInterceptors(ClassSerializerInterceptor)
25+
@Controller({ path: 'comments', version: '1' })
2126
export class CommentsController {
2227
constructor(private readonly commentsService: CommentsService) {}
2328

2429
@Post()
30+
@Throttle({ short: { limit: 10, ttl: 60000 } })
2531
@ApiOperation({ summary: 'Create a new comment' })
26-
@ApiResponse({ status: 201, description: 'Comment created successfully', type: CommentDto })
32+
@ApiBody({ type: CreateCommentDto })
33+
@ApiResponse({
34+
status: 201,
35+
description: 'Comment created successfully',
36+
type: CommentDto,
37+
})
38+
@ApiResponse({
39+
status: 400,
40+
description: 'Invalid comment parameters',
41+
schema: { example: { statusCode: 400, message: 'Invalid comment parameters' } },
42+
})
43+
@ApiResponse({
44+
status: 429,
45+
description: 'Too many requests',
46+
schema: { example: { statusCode: 429, message: 'Too many requests' } },
47+
})
48+
@ApiResponse({
49+
status: 500,
50+
description: 'Internal server error',
51+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
52+
})
2753
async create(@Body() dto: CreateCommentDto): Promise<CommentDto> {
2854
// TODO: Get author ID from auth token/session
2955
const authorId = 'temp-author-id';
3056
return this.commentsService.create(authorId, dto);
3157
}
3258

3359
@Get()
34-
@ApiOperation({ summary: 'List all comments (paginated)' })
35-
@ApiResponse({ status: 200, description: 'Paginated comments list' })
60+
@ApiOperation({
61+
summary: 'List all comments (paginated)',
62+
description:
63+
'Page-paginated comment list. Pass `page` and `limit`; responses include `data`, `total`, `page`, and `limit`.',
64+
})
65+
@ApiQuery({ name: 'page', required: false, description: 'Page number (default 1)' })
66+
@ApiQuery({ name: 'limit', required: false, description: 'Items per page (default 20, max 100)' })
67+
@ApiResponse({
68+
status: 200,
69+
description: 'Page-paginated comments list',
70+
type: PaginatedResponseDto<CommentDto>,
71+
})
72+
@ApiResponse({
73+
status: 400,
74+
description: 'Invalid pagination parameters',
75+
schema: { example: { statusCode: 400, message: 'Invalid pagination parameters' } },
76+
})
77+
@ApiResponse({
78+
status: 500,
79+
description: 'Internal server error',
80+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
81+
})
3682
async findAll(@Query() pagination: PaginationDto): Promise<PaginatedResponseDto<CommentDto>> {
3783
return this.commentsService.findAll(pagination);
3884
}
3985

4086
@Get('post/:postId')
41-
@ApiOperation({ summary: 'List comments by post (paginated)' })
42-
@ApiResponse({ status: 200, description: 'Paginated post comments list' })
87+
@ApiOperation({
88+
summary: 'List comments by post (paginated)',
89+
description: 'Returns all comments for a given post, ordered by createdAt DESC.',
90+
})
91+
@ApiParam({ name: 'postId', description: 'Post ID' })
92+
@ApiQuery({ name: 'page', required: false, description: 'Page number (default 1)' })
93+
@ApiQuery({ name: 'limit', required: false, description: 'Items per page (default 20, max 100)' })
94+
@ApiResponse({
95+
status: 200,
96+
description: 'Paginated post comments list',
97+
type: PaginatedResponseDto<CommentDto>,
98+
})
99+
@ApiResponse({
100+
status: 400,
101+
description: 'Invalid pagination parameters',
102+
schema: { example: { statusCode: 400, message: 'Invalid pagination parameters' } },
103+
})
104+
@ApiResponse({
105+
status: 500,
106+
description: 'Internal server error',
107+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
108+
})
43109
async findByPost(
44110
@Param('postId') postId: string,
45111
@Query() pagination: PaginationDto,
@@ -49,21 +115,77 @@ export class CommentsController {
49115

50116
@Get(':id')
51117
@ApiOperation({ summary: 'Get a comment by ID' })
118+
@ApiParam({ name: 'id', description: 'Comment ID' })
52119
@ApiResponse({ status: 200, description: 'Comment details', type: CommentDto })
120+
@ApiResponse({
121+
status: 404,
122+
description: 'Comment not found',
123+
schema: { example: { statusCode: 404, message: 'Comment with id "id" not found' } },
124+
})
125+
@ApiResponse({
126+
status: 500,
127+
description: 'Internal server error',
128+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
129+
})
53130
async findOne(@Param('id') id: string): Promise<CommentDto> {
54131
return this.commentsService.findOne(id);
55132
}
56133

57134
@Put(':id')
135+
@Throttle({ short: { limit: 10, ttl: 60000 } })
58136
@ApiOperation({ summary: 'Update a comment' })
59-
@ApiResponse({ status: 200, description: 'Comment updated successfully', type: CommentDto })
137+
@ApiParam({ name: 'id', description: 'Comment ID' })
138+
@ApiBody({ type: UpdateCommentDto })
139+
@ApiResponse({
140+
status: 200,
141+
description: 'Comment updated successfully',
142+
type: CommentDto,
143+
})
144+
@ApiResponse({
145+
status: 400,
146+
description: 'Invalid comment parameters',
147+
schema: { example: { statusCode: 400, message: 'Invalid comment parameters' } },
148+
})
149+
@ApiResponse({
150+
status: 404,
151+
description: 'Comment not found',
152+
schema: { example: { statusCode: 404, message: 'Comment with id "id" not found' } },
153+
})
154+
@ApiResponse({
155+
status: 429,
156+
description: 'Too many requests',
157+
schema: { example: { statusCode: 429, message: 'Too many requests' } },
158+
})
159+
@ApiResponse({
160+
status: 500,
161+
description: 'Internal server error',
162+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
163+
})
60164
async update(@Param('id') id: string, @Body() dto: UpdateCommentDto): Promise<CommentDto> {
61165
return this.commentsService.update(id, dto);
62166
}
63167

64168
@Delete(':id')
169+
@Throttle({ short: { limit: 10, ttl: 60000 } })
170+
@HttpCode(HttpStatus.NO_CONTENT)
65171
@ApiOperation({ summary: 'Delete a comment' })
172+
@ApiParam({ name: 'id', description: 'Comment ID' })
66173
@ApiResponse({ status: 204, description: 'Comment deleted successfully' })
174+
@ApiResponse({
175+
status: 404,
176+
description: 'Comment not found',
177+
schema: { example: { statusCode: 404, message: 'Comment with id "id" not found' } },
178+
})
179+
@ApiResponse({
180+
status: 429,
181+
description: 'Too many requests',
182+
schema: { example: { statusCode: 429, message: 'Too many requests' } },
183+
})
184+
@ApiResponse({
185+
status: 500,
186+
description: 'Internal server error',
187+
schema: { example: { statusCode: 500, message: 'Internal server error' } },
188+
})
67189
async remove(@Param('id') id: string): Promise<void> {
68190
return this.commentsService.remove(id);
69191
}

0 commit comments

Comments
 (0)