Skip to content

Commit

Permalink
Merge branch 'dev' into hng_backend_project
Browse files Browse the repository at this point in the history
  • Loading branch information
Reaganz-Wat authored Mar 2, 2025
2 parents 3369863 + 01e3dfd commit 39098ee
Show file tree
Hide file tree
Showing 14 changed files with 381 additions and 10 deletions.
9 changes: 8 additions & 1 deletion src/modules/comments/comments.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Body, Post, Request, Get, Param } from '@nestjs/common';
import { Controller, Body, Post, Request, Get, Param, Delete } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CreateCommentDto } from './dtos/create-comment.dto';
import { CommentResponseDto } from './dtos/comment-response.dto';
Expand All @@ -25,4 +25,11 @@ export class CommentsController {
async getAComment(@Param('id') id: string): Promise<any> {
return await this.commentsService.getAComment(id);
}

@ApiOperation({ summary: 'Delete a comment' })
@ApiResponse({ status: 200, description: 'The comment has been deleted successfully.' })
@Delete(':id/delete')
async deleteAComment(@Param('id') id: string, @Request() req): Promise<any> {
return await this.commentsService.deleteAComment(id, req.user.userId);
}
}
21 changes: 21 additions & 0 deletions src/modules/comments/comments.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,25 @@ export class CommentsService {
data: { comment },
};
}

async deleteAComment(commentId: string, userId: string) {
const comment = await this.commentRepository.findOne({ where: { id: commentId }, relations: ['user'] });
if (!comment) {
throw new CustomHttpException('Comment not found', HttpStatus.NOT_FOUND);
}

const isOwner = comment.user.id === userId;

if (!isOwner) {
throw new CustomHttpException('You are not authorized to delete this comment', HttpStatus.FORBIDDEN);
}

await this.commentRepository.delete(comment);

return {
message: 'Comment deleted successfully!',
status: HttpStatus.OK,
data: { comment },
};
}
}
55 changes: 55 additions & 0 deletions src/modules/comments/tests/comments.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { HttpStatus } from '@nestjs/common';
const mockCommentRepository = () => ({
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
delete: jest.fn(),
});

const mockUserRepository = () => ({
Expand Down Expand Up @@ -75,5 +77,58 @@ describe('CommentsService', () => {
commentedBy: 'John Doe',
});
});

describe('deleteComment', () => {
it('should throw CustomHttpException if comment is not found', async () => {
commentRepository.findOne.mockResolvedValue(null);

await expect(service.deleteAComment('comment-id', 'user-id')).rejects.toThrow(CustomHttpException);
await expect(service.deleteAComment('comment-id', 'user-id')).rejects.toMatchObject({
message: 'Comment not found',
status: HttpStatus.NOT_FOUND,
});
});

it('should throw CustomHttpException if user is not the owner of the comment', async () => {
const mockOwner = { id: 'owner-id' };
const mockComment = { id: 'comment-id', user: mockOwner };

commentRepository.findOne.mockResolvedValue(mockComment);

await expect(service.deleteAComment('comment-id', 'another-user-id')).rejects.toThrow(CustomHttpException);
await expect(service.deleteAComment('comment-id', 'another-user-id')).rejects.toMatchObject({
message: 'You are not authorized to delete this comment',
status: HttpStatus.FORBIDDEN,
});
});

it('should delete a comment successfully', async () => {
const commentId = 'comment-id';
const userId = 'user-id';
const mockUser = { id: 'user-id' };
const mockComment = {
id: 'comment-id',
model_id: '1',
model_type: 'post',
comment: 'A valid comment',
user: mockUser,
};

commentRepository.findOne.mockResolvedValue(mockComment);
commentRepository.delete.mockResolvedValue(mockComment);

console.log(await commentRepository.findOne({ where: { id: commentId }, relations: ['user'] })); // Debugging

const result = await service.deleteAComment(commentId, userId);

expect(commentRepository.findOne).toHaveBeenCalledWith({ where: { id: commentId }, relations: ['user'] });
expect(commentRepository.delete).toHaveBeenCalledWith(mockComment);
expect(result).toEqual({
message: 'Comment deleted successfully!',
status: HttpStatus.OK,
data: { comment: mockComment },
});
});
});
});
});
11 changes: 10 additions & 1 deletion src/modules/jobs/jobs.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,22 @@ export class JobsService {

const { applicant_name, ...others } = jobApplicationDto;

const existingApplication = await this.jobApplicationRepository.findOne({
where: { job: { id: jobId }, applicant_name: jobApplicationDto.applicant_name },
relations: ['job'],
});

if (existingApplication) {
throw new CustomHttpException('Duplicate application', HttpStatus.BAD_REQUEST);
}

const resumeUrl = await this.s3Service.uploadFile(resume, 'resumes');

const createJobApplication = this.jobApplicationRepository.create({
...others,
applicant_name,
resume: resumeUrl,
...job,
job: job.data
});

await this.jobApplicationRepository.save(createJobApplication);
Expand Down
14 changes: 14 additions & 0 deletions src/modules/jobs/tests/jobs.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,20 @@ describe('JobsService', () => {
expect(service['jobApplicationRepository'].create).toHaveBeenCalled();
expect(service['jobApplicationRepository'].save).toHaveBeenCalled();
});

it('should throw error if duplicate application is found', async () => {
const resume = { buffer: Buffer.from('test file'), originalname: 'resume.pdf' } as Express.Multer.File;

jest.spyOn(service, 'getJob').mockResolvedValue(mockJob as any);
jest
.spyOn(service['jobApplicationRepository'], 'findOne')
.mockResolvedValue({ ...mockJobApplicationDto, resumeUrl: 'https://s3-bucket-url/resume.pdf' } as any);

await expect(service.applyForJob('jobId', mockJobApplicationDto, resume)).rejects.toThrow(
new CustomHttpException('Duplicate application', HttpStatus.CONFLICT)
);
});

});

describe('searchJobs', () => {
Expand Down
22 changes: 22 additions & 0 deletions src/modules/products/products.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,28 @@ export class ProductsController {
return this.productsService.addCommentToProduct(productId, commentDto, user.sub);
}

@ApiBearerAuth()
@UseGuards(SuperAdminGuard)
@Post('organisations/:productId/comments/:commentId')
@ApiBearerAuth()
@ApiOperation({ summary: 'Edits a comment for a product' })
@ApiParam({ name: 'id', description: 'organisation ID', example: '870ccb14-d6b0-4a50-b459-9895af803i89' })
@ApiParam({ name: 'productId', description: 'product ID', example: '126ccb14-d6b0-4a50-b459-9895af803h6y' })
@ApiBody({ type: AddCommentDto, description: 'Comment to be edited' })
@ApiResponse({ status: 201, description: 'Comment updated successfully' })
@ApiResponse({ status: 400, description: 'Bad request' })
@ApiResponse({ status: 404, description: 'Not found' })
@ApiResponse({ status: 500, description: 'Internal server error' })
async editProductComment(
@Param('productId') productId: string,
@Param('commentId') commentId: string,
@Body() commentDto: AddCommentDto,
@Req() req: any
) {
const user = req.user;
return this.productsService.editProductComment(productId, commentId, commentDto, user.sub);
}

@ApiBearerAuth()
@UseGuards(OwnershipGuard)
@Get('organisations/:orgId/products/:productId/stock')
Expand Down
35 changes: 35 additions & 0 deletions src/modules/products/products.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,41 @@ export class ProductsService {
};
}

async editProductComment(productId: string, commentId: string, commentDto: AddCommentDto, userId: string) {
const c = commentDto;

const product = await this.productRepository.findOne({ where: { id: productId } });

if (!product) {
throw new CustomHttpException(SYS_MSG.PRODUCT_NOT_FOUND, HttpStatus.NOT_FOUND);
}

const comment = await this.commentRepository.findOne({ where: { id: commentId } });

if (!comment) {
throw new CustomHttpException(SYS_MSG.COMMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
}

if (comment.user.id !== userId) {
throw new CustomHttpException(SYS_MSG.COMMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
}

await this.commentRepository.update(commentId, { comment: c.comment });

const responsePayload = {
id: comment.id,
product_id: product.id,
comment: c.comment,
user_id: userId,
created_at: new Date(),
};

return {
message: SYS_MSG.COMMENT_EDITED,
data: responsePayload,
};
}

async getProductStock(productId: string) {
const product = await this.productRepository.findOne({ where: { id: productId } });
if (!product) {
Expand Down
43 changes: 42 additions & 1 deletion src/modules/timezones/tests/timezones.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Timezone } from '../entities/timezone.entity';
import { CreateTimezoneDto } from '../dto/create-timezone.dto';
import { HttpException, HttpStatus } from '@nestjs/common';
import { HttpException, HttpStatus, NotFoundException } from '@nestjs/common';

const mockTimezoneRepository = {
findOne: jest.fn(),
Expand Down Expand Up @@ -175,3 +175,44 @@ describe('TimezonesController', () => {
});
});
});

describe('TimezonesController - deleteTimezone', () => {
let controller: TimezonesController;
let service: TimezonesService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TimezonesController],
providers: [
{
provide: TimezonesService,
useValue: {
deleteTimezone: jest.fn(),
},
},
],
}).compile();

controller = module.get<TimezonesController>(TimezonesController);
service = module.get<TimezonesService>(TimezonesService);
});

it('should successfully delete a timezone', async () => {
jest.spyOn(service, 'deleteTimezone').mockResolvedValue({
status_code: 200,
message: 'Timezone deleted successfully',
});

const response = await controller.deleteTimezone('123');
expect(response).toEqual({
status_code: 200,
message: 'Timezone deleted successfully',
});
});

it('should return NotFoundException if timezone does not exist', async () => {
jest.spyOn(service, 'deleteTimezone').mockRejectedValue(new NotFoundException());

await expect(controller.deleteTimezone('invalid-id')).rejects.toThrow(NotFoundException);
});
});
10 changes: 9 additions & 1 deletion src/modules/timezones/timezones.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller, Post, Body, Get, Res, HttpStatus, Patch, Param } from '@nestjs/common';
import { Controller, Post, Body, Get, Res, HttpStatus, Patch, Param, Delete } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
import { CreateTimezoneDto } from './dto/create-timezone.dto';
import { UpdateTimezoneDto } from './dto/update-timezone.dto';
Expand Down Expand Up @@ -38,4 +38,12 @@ export class TimezonesController {
async updateTimezone(@Param('id') id: string, @Body() updateTimezoneDto: UpdateTimezoneDto) {
return this.timezonesService.updateTimezone(id, updateTimezoneDto);
}
@Delete(':id')
@ApiBearerAuth()
@ApiOperation({ summary: 'Delete a timezone' })
@ApiResponse({ status: 200, description: 'Timezone successfully deleted.' })
@ApiResponse({ status: 404, description: 'Timezone not found.' })
async deleteTimezone(@Param('id') id: string) {
return this.timezonesService.deleteTimezone(id);
}
}
49 changes: 49 additions & 0 deletions src/modules/timezones/timezones.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TimezonesService } from './timezones.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Timezone } from './entities/timezone.entity';
import { NotFoundException } from '@nestjs/common';

const mockTimezoneRepository = {
findOne: jest.fn(),
Expand Down Expand Up @@ -34,3 +35,51 @@ describe('TimezonesService', () => {
expect(service).toBeDefined();
});
});

describe('TimezonesService - deleteTimezone', () => {
let service: TimezonesService;
let timezoneRepository: Repository<Timezone>;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TimezonesService,
{
provide: getRepositoryToken(Timezone),
useClass: Repository,
},
],
}).compile();

service = module.get<TimezonesService>(TimezonesService);
timezoneRepository = module.get<Repository<Timezone>>(getRepositoryToken(Timezone));
});

it('should successfully delete a timezone', async () => {
const mockId = '123';

const mockTimezone: Timezone = {
id: '123',
timezone: 'UTC',
gmtOffset: '0',
description: 'Coordinated Universal Time',
created_at: new Date(),
updated_at: new Date(),
};
jest.spyOn(timezoneRepository, 'findOne').mockResolvedValue(mockTimezone);
jest.spyOn(timezoneRepository, 'delete').mockResolvedValue({ affected: 1 } as any);

const result = await service.deleteTimezone(mockId);

expect(result).toEqual({
status_code: 200,
message: 'Timezone deleted successfully',
});
});

it('should throw NotFoundException if timezone is not found', async () => {
jest.spyOn(timezoneRepository, 'findOne').mockResolvedValue(null);

await expect(service.deleteTimezone('invalid-id')).rejects.toThrow(NotFoundException);
});
});
Loading

0 comments on commit 39098ee

Please sign in to comment.