From bd5d4b76acda0c59afa214f173bd745335ede403 Mon Sep 17 00:00:00 2001 From: Akanni Emmanuel Date: Tue, 6 Aug 2024 11:22:15 +0100 Subject: [PATCH 1/3] feat: implement endpoint for refreshing an invitation --- src/modules/invite/invite.controller.ts | 6 +++ src/modules/invite/invite.service.ts | 68 ++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/modules/invite/invite.controller.ts b/src/modules/invite/invite.controller.ts index 96eaadfa4..e60cade9c 100644 --- a/src/modules/invite/invite.controller.ts +++ b/src/modules/invite/invite.controller.ts @@ -6,6 +6,7 @@ import { HttpException, HttpStatus, NotFoundException, + Post, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { InviteService } from './invite.service'; @@ -41,4 +42,9 @@ export class InviteController { } } } + + @Post(':orgId/invite/:inviteId/refresh') + async refreshInvitation(@Param('orgId') orgId: string, @Param('inviteId') inviteId: string) { + return await this.inviteService.refreshInvite(orgId, inviteId); + } } diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 676d6f989..41df24063 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -1,8 +1,14 @@ -import { HttpStatus, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { + ForbiddenException, + HttpStatus, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; import { InviteDto } from './dto/invite.dto'; import { Invite } from './entities/invite.entity'; import { Organisation } from '../../modules/organisations/entities/organisations.entity'; -import { Repository } from 'typeorm'; +import { Not, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { v4 as uuidv4 } from 'uuid'; @@ -66,4 +72,62 @@ export class InviteService { return responseData; } + + async refreshInvite(orgId: string, inviteId) { + try { + const organisation = await this.organisationRepository.findOne({ where: { id: orgId } }); + + if (!organisation) { + throw new NotFoundException({ + status_code: 404, + error: 'Not Found', + message: `The organization with ID ${orgId} does not exist`, + }); + } + + const invitation = await this.inviteRepository.findOne({ + where: { + id: inviteId, + organisation: organisation, + }, + }); + + if (!invitation) { + throw new NotFoundException({ + status_code: 404, + error: 'Not Found', + message: `The invitation with ID ${inviteId} does not exist in this organization`, + }); + } + + if (invitation.isAccepted) { + throw new ForbiddenException({ + status_code: 403, + message: `The invitation cannot be refreshed because it has been accepted`, + }); + } + + const newToken = uuidv4(); + + const updatedInvitation = this.inviteRepository.save({ + ...invitation, + token: newToken, + }); + const link = `${process.env.FRONTEND_URL}/invite?token=${newToken}`; + + return { + status_code: 200, + data: { + ...updatedInvitation, + link, + }, + message: 'Invitation link refreshed successfully', + }; + } catch (err) { + throw new InternalServerErrorException({ + message: `Something went wrong: ${err.message}`, + status_code: 500, + }); + } + } } From 047c4c44a32e92bc106eb137b37ab986862cbc57 Mon Sep 17 00:00:00 2001 From: Akanni Emmanuel Date: Wed, 7 Aug 2024 09:08:56 +0100 Subject: [PATCH 2/3] test: add testcases for invitation refresh service - refactored invite services --- src/modules/invite/invite.service.ts | 85 ++++++++++--------- .../invite/tests/invite.service.spec.ts | 54 +++++++++++- 2 files changed, 97 insertions(+), 42 deletions(-) diff --git a/src/modules/invite/invite.service.ts b/src/modules/invite/invite.service.ts index 41df24063..10f9bc0ec 100644 --- a/src/modules/invite/invite.service.ts +++ b/src/modules/invite/invite.service.ts @@ -11,6 +11,7 @@ import { Organisation } from '../../modules/organisations/entities/organisations import { Not, Repository } from 'typeorm'; import { InjectRepository } from '@nestjs/typeorm'; import { v4 as uuidv4 } from 'uuid'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; @Injectable() export class InviteService { @@ -74,60 +75,62 @@ export class InviteService { } async refreshInvite(orgId: string, inviteId) { - try { - const organisation = await this.organisationRepository.findOne({ where: { id: orgId } }); + const organisation = await this.organisationRepository.findOne({ where: { id: orgId } }); - if (!organisation) { - throw new NotFoundException({ + if (!organisation) { + throw new CustomHttpException( + { status_code: 404, error: 'Not Found', message: `The organization with ID ${orgId} does not exist`, - }); - } - - const invitation = await this.inviteRepository.findOne({ - where: { - id: inviteId, - organisation: organisation, }, - }); + HttpStatus.NOT_FOUND + ); + } - if (!invitation) { - throw new NotFoundException({ + const invitation = await this.inviteRepository.findOne({ + where: { + id: inviteId, + organisation: organisation, + }, + }); + + if (!invitation) { + throw new CustomHttpException( + { status_code: 404, error: 'Not Found', message: `The invitation with ID ${inviteId} does not exist in this organization`, - }); - } + }, + HttpStatus.NOT_FOUND + ); + } - if (invitation.isAccepted) { - throw new ForbiddenException({ + if (invitation.isAccepted) { + throw new CustomHttpException( + { status_code: 403, message: `The invitation cannot be refreshed because it has been accepted`, - }); - } - - const newToken = uuidv4(); - - const updatedInvitation = this.inviteRepository.save({ - ...invitation, - token: newToken, - }); - const link = `${process.env.FRONTEND_URL}/invite?token=${newToken}`; - - return { - status_code: 200, - data: { - ...updatedInvitation, - link, }, - message: 'Invitation link refreshed successfully', - }; - } catch (err) { - throw new InternalServerErrorException({ - message: `Something went wrong: ${err.message}`, - status_code: 500, - }); + HttpStatus.FORBIDDEN + ); } + + const newToken = uuidv4(); + + const updatedInvitation = this.inviteRepository.save({ + ...invitation, + token: newToken, + }); + const link = `${process.env.FRONTEND_URL}/invite?token=${newToken}`; + + return { + status_code: 200, + data: { + ...updatedInvitation, + link, + }, + message: 'Invitation link refreshed successfully', + }; } } diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 36fff73f9..76db7d675 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { HttpException, HttpStatus, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -92,4 +92,56 @@ describe('InviteService', () => { await expect(service.createInvite('1')).rejects.toThrow(NotFoundException); }); }); + + describe('refreshInvite', () => { + it('should refresh an invitation link successfully', async () => { + const orgId = uuidv4(); + const inviteId = uuidv4(); + const organisation = { id: orgId }; + const invitation = { id: inviteId, isAccepted: false }; + const newToken = uuidv4(); + const updatedInvitation = { ...invitation, token: newToken }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any); + jest.spyOn(repository, 'findOne').mockResolvedValue(invitation as any); + jest.spyOn(repository, 'save').mockResolvedValue(updatedInvitation as any); + (uuidv4 as jest.Mock).mockReturnValue(newToken); + + const result = await service.refreshInvite(orgId, inviteId); + + expect(result).toHaveProperty('data'); + }); + + it('should throw an exception if the organisation does not exist', async () => { + const orgId = uuidv4(); + const inviteId = uuidv4(); + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(null); + + await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException); + }); + + it('should throw an exception if the invitation does not exist', async () => { + const orgId = 'some-org-id'; + const inviteId = 'some-invite-id'; + const organisation = { id: orgId }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any); + jest.spyOn(repository, 'findOne').mockResolvedValue(null); + + await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException); + }); + + it('should throw an exception if the invitation has already been accepted', async () => { + const orgId = 'some-org-id'; + const inviteId = 'some-invite-id'; + const organisation = { id: orgId }; + const invitation = { id: inviteId, isAccepted: true }; + + jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any); + jest.spyOn(repository, 'findOne').mockResolvedValue(invitation as any); + + await expect(service.refreshInvite(orgId, inviteId)).rejects.toThrow(HttpException); + }); + }); }); From e792ffba777071a6161267b4104491dc9a5ef3e9 Mon Sep 17 00:00:00 2001 From: Akanni Emmanuel Date: Wed, 7 Aug 2024 09:57:14 +0100 Subject: [PATCH 3/3] refactor: added validation for organisation and invite id --- src/modules/invite/invite.controller.ts | 22 +++++++++++++++++++ .../invite/tests/invite.service.spec.ts | 8 +++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/modules/invite/invite.controller.ts b/src/modules/invite/invite.controller.ts index e60cade9c..e3866a2ae 100644 --- a/src/modules/invite/invite.controller.ts +++ b/src/modules/invite/invite.controller.ts @@ -10,6 +10,8 @@ import { } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { InviteService } from './invite.service'; +import { isUUID } from 'class-validator'; +import { CustomHttpException } from '../../helpers/custom-http-filter'; @ApiBearerAuth() @ApiTags('Organisation Invites') @@ -45,6 +47,26 @@ export class InviteController { @Post(':orgId/invite/:inviteId/refresh') async refreshInvitation(@Param('orgId') orgId: string, @Param('inviteId') inviteId: string) { + if (isUUID(orgId, 4)) { + throw new CustomHttpException( + { + status_code: 400, + error: 'Bad Request', + message: 'Invalid organisation ID format', + }, + 400 + ); + } else if (isUUID(inviteId, 4)) { + throw new CustomHttpException( + { + status_code: 400, + error: 'Bad Request', + message: 'Invalid organisation ID format', + }, + 400 + ); + } + return await this.inviteService.refreshInvite(orgId, inviteId); } } diff --git a/src/modules/invite/tests/invite.service.spec.ts b/src/modules/invite/tests/invite.service.spec.ts index 76db7d675..6eaa08d0e 100644 --- a/src/modules/invite/tests/invite.service.spec.ts +++ b/src/modules/invite/tests/invite.service.spec.ts @@ -122,8 +122,8 @@ describe('InviteService', () => { }); it('should throw an exception if the invitation does not exist', async () => { - const orgId = 'some-org-id'; - const inviteId = 'some-invite-id'; + const orgId = uuidv4(); + const inviteId = uuidv4(); const organisation = { id: orgId }; jest.spyOn(organisationRepo, 'findOne').mockResolvedValue(organisation as any); @@ -133,8 +133,8 @@ describe('InviteService', () => { }); it('should throw an exception if the invitation has already been accepted', async () => { - const orgId = 'some-org-id'; - const inviteId = 'some-invite-id'; + const orgId = uuidv4(); + const inviteId = uuidv4(); const organisation = { id: orgId }; const invitation = { id: inviteId, isAccepted: true };