From b305dadd502854eb1e5463b304825f535354a955 Mon Sep 17 00:00:00 2001 From: Nijarudeen Date: Fri, 15 Nov 2024 12:15:06 +0530 Subject: [PATCH] feat: added unit tests admin, home, focus areas & health --- .forestadmin-schema.json | 289 ------------------ .../src/admin/admin.controller.spec.ts | 249 +++++++++++++++ apps/web-api/src/admin/admin.service.spec.ts | 51 ++++ .../src/decorators/request.decorator.spec.ts | 49 +++ apps/web-api/src/faq/faq.controller.spec.ts | 59 +++- apps/web-api/src/faq/faq.service.spec.ts | 158 +++++++++- .../focus-areas.controller.spec.ts | 49 +++ .../focus-areas/focus-areas.service.spec.ts | 96 ++++++ .../src/health/health.controller.spec.ts | 74 +++++ apps/web-api/src/health/heroku.health.spec.ts | 141 +++++++++ apps/web-api/src/health/prisma.health.spec.ts | 52 ++++ apps/web-api/src/home/home.controller.spec.ts | 123 ++++++++ apps/web-api/src/home/home.service.spec.ts | 100 ++++++ 13 files changed, 1191 insertions(+), 299 deletions(-) create mode 100644 apps/web-api/src/admin/admin.controller.spec.ts create mode 100644 apps/web-api/src/admin/admin.service.spec.ts create mode 100644 apps/web-api/src/decorators/request.decorator.spec.ts create mode 100644 apps/web-api/src/focus-areas/focus-areas.controller.spec.ts create mode 100644 apps/web-api/src/focus-areas/focus-areas.service.spec.ts create mode 100644 apps/web-api/src/health/health.controller.spec.ts create mode 100644 apps/web-api/src/health/heroku.health.spec.ts create mode 100644 apps/web-api/src/health/prisma.health.spec.ts create mode 100644 apps/web-api/src/home/home.controller.spec.ts create mode 100644 apps/web-api/src/home/home.service.spec.ts diff --git a/.forestadmin-schema.json b/.forestadmin-schema.json index 3fba4c0b0..05a31f5ac 100644 --- a/.forestadmin-schema.json +++ b/.forestadmin-schema.json @@ -1121,15 +1121,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Teams", - "integration": null, - "inverseOf": "IndustryTags", -======= "field": "_IndustryTagToTeams_through_IndustryTag_A", "integration": null, "inverseOf": "IndustryTag_through_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, @@ -1823,28 +1817,7 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Skills", - "integration": null, - "inverseOf": "Members", - "isFilterable": false, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": false, - "isSortable": false, - "isVirtual": false, - "reference": "Skill.id", - "relationship": "BelongsToMany", - "type": ["Number"], - "validations": [] - }, - { - "defaultValue": null, - "enums": null, - "field": "TeamFocusAreaVersionHistories", -======= "field": "TeamFocusAreaVersionHistories_through_Member_modifiedBy", ->>>>>>> bedf0149 (fix: code revert fixed) "integration": null, "inverseOf": "Member_through_modifiedBy", "isFilterable": false, @@ -1912,13 +1885,7 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD -<<<<<<< HEAD -======= - "field": "_MemberToSkills", -======= "field": "_MemberToSkills_through_Member_A", ->>>>>>> bedf0149 (fix: code revert fixed) "integration": null, "inverseOf": "Member_through_A", "isFilterable": false, @@ -1935,7 +1902,6 @@ { "defaultValue": null, "enums": null, ->>>>>>> 423ca54d (feat: team e2e test-cases done) "field": "airtableRecId", "integration": null, "inverseOf": null, @@ -3039,15 +3005,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Teams", - "integration": null, - "inverseOf": "MembershipSources", -======= "field": "_MembershipSourceToTeams_through_MembershipSource_A", "integration": null, "inverseOf": "MembershipSource_through_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, @@ -4907,15 +4867,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Members", - "integration": null, - "inverseOf": "Skills", -======= "field": "_MemberToSkills_through_Skill_B", "integration": null, "inverseOf": "Skill_through_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, @@ -5091,28 +5045,7 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "IndustryTags", - "integration": null, - "inverseOf": "Teams", - "isFilterable": false, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": false, - "isSortable": false, - "isVirtual": false, - "reference": "IndustryTag.id", - "relationship": "BelongsToMany", - "type": ["Number"], - "validations": [] - }, - { - "defaultValue": null, - "enums": null, - "field": "Member", -======= "field": "Member_through_lastModifiedBy", ->>>>>>> bedf0149 (fix: code revert fixed) "integration": null, "inverseOf": "Teams_through_Member_lastModifiedBy", "isFilterable": true, @@ -5129,28 +5062,7 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "MembershipSources", - "integration": null, - "inverseOf": "Teams", - "isFilterable": false, - "isPrimaryKey": false, - "isReadOnly": false, - "isRequired": false, - "isSortable": false, - "isVirtual": false, - "reference": "MembershipSource.id", - "relationship": "BelongsToMany", - "type": ["Number"], - "validations": [] - }, - { - "defaultValue": null, - "enums": null, - "field": "PLEventGuests", -======= "field": "PLEventGuests_through_Team_teamUid", ->>>>>>> bedf0149 (fix: code revert fixed) "integration": null, "inverseOf": "Team_through_teamUid", "isFilterable": false, @@ -5235,25 +5147,15 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Technologies", - "integration": null, - "inverseOf": "Teams", -======= "field": "_IndustryTagToTeams_through_Team_B", "integration": null, "inverseOf": "Team_through_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, "isRequired": false, "isSortable": false, "isVirtual": false, -<<<<<<< HEAD - "reference": "Technology.id", - "relationship": "BelongsToMany", -======= "reference": "_IndustryTagToTeam.id", "relationship": "HasMany", "type": ["Number"], @@ -5290,7 +5192,6 @@ "isVirtual": false, "reference": "_TeamToTechnology.id", "relationship": "HasMany", ->>>>>>> bedf0149 (fix: code revert fixed) "type": ["Number"], "validations": [] }, @@ -6084,15 +5985,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "Teams", - "integration": null, - "inverseOf": "Technologies", -======= "field": "_TeamToTechnologies_through_Technology_B", "integration": null, "inverseOf": "Technology_through_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": false, "isPrimaryKey": false, "isReadOnly": false, @@ -6205,51 +6100,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "IndustryTag", - "integration": null, - "inverseOf": null, -======= "field": "IndustryTag_through_A", "integration": null, "inverseOf": "_IndustryTagToTeams_through_IndustryTag_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6268,11 +6121,7 @@ "enums": null, "field": "Team_through_B", "integration": null, -<<<<<<< HEAD - "inverseOf": null, -======= "inverseOf": "_IndustryTagToTeams_through_Team_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6355,51 +6204,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "Member", - "integration": null, - "inverseOf": null, -======= "field": "Member_through_A", "integration": null, "inverseOf": "_MemberToSkills_through_Member_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6418,11 +6225,7 @@ "enums": null, "field": "Skill_through_B", "integration": null, -<<<<<<< HEAD - "inverseOf": null, -======= "inverseOf": "_MemberToSkills_through_Skill_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6453,51 +6256,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "MembershipSource", - "integration": null, - "inverseOf": null, -======= "field": "MembershipSource_through_A", "integration": null, "inverseOf": "_MembershipSourceToTeams_through_MembershipSource_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6516,11 +6277,7 @@ "enums": null, "field": "Team_through_B", "integration": null, -<<<<<<< HEAD - "inverseOf": null, -======= "inverseOf": "_MembershipSourceToTeams_through_Team_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6551,51 +6308,9 @@ { "defaultValue": null, "enums": null, -<<<<<<< HEAD - "field": "A", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "B", - "integration": null, - "inverseOf": null, - "isFilterable": true, - "isPrimaryKey": true, - "isReadOnly": true, - "isRequired": true, - "isSortable": true, - "isVirtual": false, - "reference": null, - "type": "Number", - "validations": [ - {"type": "is present", "message": "Failed validation rule: 'Present'"} - ] - }, - { - "defaultValue": null, - "enums": null, - "field": "Team", - "integration": null, - "inverseOf": null, -======= "field": "Team_through_A", "integration": null, "inverseOf": "_TeamToTechnologies_through_Team_A", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, @@ -6614,11 +6329,7 @@ "enums": null, "field": "Technology_through_B", "integration": null, -<<<<<<< HEAD - "inverseOf": null, -======= "inverseOf": "_TeamToTechnologies_through_Technology_B", ->>>>>>> bedf0149 (fix: code revert fixed) "isFilterable": true, "isPrimaryKey": false, "isReadOnly": false, diff --git a/apps/web-api/src/admin/admin.controller.spec.ts b/apps/web-api/src/admin/admin.controller.spec.ts new file mode 100644 index 000000000..fccb99412 --- /dev/null +++ b/apps/web-api/src/admin/admin.controller.spec.ts @@ -0,0 +1,249 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminController } from './admin.controller'; +import { ParticipantsRequestService } from '../participants-request/participants-request.service'; +import { AdminService } from './admin.service'; +import { ForbiddenException, UnauthorizedException } from '@nestjs/common'; +import { ParticipantType, ApprovalStatus } from '@prisma/client'; +import { JwtService } from '../utils/jwt/jwt.service'; +import { ParticipantProcessRequestSchema, ParticipantRequestMemberSchema, ParticipantRequestTeamSchema } from 'libs/contracts/src/schema/participants-request'; + +// Mock schemas +jest.mock('libs/contracts/src/schema/participants-request', () => ({ + ParticipantRequestMemberSchema: { + safeParse: jest.fn().mockReturnValue({ success: true }), // Mock success for MEMBER schema + }, + ParticipantRequestTeamSchema: { + safeParse: jest.fn().mockReturnValue({ success: true }), // Mock success for TEAM schema + }, + ParticipantProcessRequestSchema: { + safeParse: jest.fn().mockReturnValue({ success: true }), // Mock success for process request schema + }, +})); + +describe('AdminController', () => { + let controller: AdminController; + let participantsRequestService: ParticipantsRequestService; + let adminService: AdminService; + + const mockParticipantsRequestService = { + getAll: jest.fn(), + getByUid: jest.fn(), + addRequest: jest.fn(), + updateRequest: jest.fn(), + processRejectRequest: jest.fn(), + processTeamCreateRequest: jest.fn(), + processMemberCreateRequest: jest.fn(), + processTeamEditRequest: jest.fn(), + processMemberEditRequest: jest.fn(), + }; + + const mockAdminService = { + signIn: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AdminController], + providers: [ + { provide: ParticipantsRequestService, useValue: mockParticipantsRequestService }, + { provide: AdminService, useValue: mockAdminService }, + { provide: JwtService, useValue: {} }, + ], + }).compile(); + + controller = module.get(AdminController); + participantsRequestService = module.get(ParticipantsRequestService); + adminService = module.get(AdminService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('signIn', () => { + it('should return a signed JWT token if credentials are valid', async () => { + const body = { username: 'admin', password: 'password' }; + const token = { code: 1, accessToken: 'testToken' }; + jest.spyOn(adminService, 'signIn').mockResolvedValue(token); + + const result = await controller.signIn(body); + expect(result).toEqual(token); + }); + }); + + describe('findAll', () => { + it('should return all participants', async () => { + const query = {}; + const mockResponse: any = [{ id: '1', name: 'Participant' }]; + jest.spyOn(participantsRequestService, 'getAll').mockResolvedValue(mockResponse); + + const result = await controller.findAll(query); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.getAll).toHaveBeenCalledWith(query); + }); + }); + + describe('findOne', () => { + it('should return a participant by UID', async () => { + const params = { uid: '123' }; + const mockResponse: any = { id: '123', name: 'Participant' }; + jest.spyOn(participantsRequestService, 'getByUid').mockResolvedValue(mockResponse); + + const result = await controller.findOne(params); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.getByUid).toHaveBeenCalledWith(params.uid); + }); + }); + + describe('addRequest', () => { + it('should call addRequest with valid MEMBER schema', async () => { + const body = { participantType: ParticipantType.MEMBER, name: 'John Doe' }; + const mockResponse: any = { id: '1', ...body }; + jest.spyOn(participantsRequestService, 'addRequest').mockResolvedValue(mockResponse); + + const result = await controller.addRequest(body); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.addRequest).toHaveBeenCalledWith(body); + }); + + it('should call addRequest with valid TEAM schema', async () => { + const body = { participantType: ParticipantType.TEAM, name: 'Team A' }; + const mockResponse: any = { id: '2', ...body }; + jest.spyOn(participantsRequestService, 'addRequest').mockResolvedValue(mockResponse); + + const result = await controller.addRequest(body); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.addRequest).toHaveBeenCalledWith(body); + }); + }); + + describe('updateRequest', () => { + it('should call updateRequest with valid MEMBER schema', async () => { + const body = { participantType: ParticipantType.MEMBER, name: 'Updated Name' }; + const params = { uid: '123' }; + const mockResponse: any = { id: '123', ...body }; + jest.spyOn(participantsRequestService, 'updateRequest').mockResolvedValue(mockResponse); + + const result = await controller.updateRequest(body, params); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.updateRequest).toHaveBeenCalledWith(body, params.uid); + }); + + it('should call updateRequest with valid TEAM schema', async () => { + const body = { participantType: ParticipantType.TEAM, name: 'Updated Team' }; + const params = { uid: '123' }; + const mockResponse: any = { id: '123', ...body }; + jest.spyOn(participantsRequestService, 'updateRequest').mockResolvedValue(mockResponse); + + const result = await controller.updateRequest(body, params); + expect(result).toEqual(mockResponse); + expect(participantsRequestService.updateRequest).toHaveBeenCalledWith(body, params.uid); + }); + }); + + describe('processRequest', () => { + it('should call processRejectRequest when status is REJECTED', async () => { + const body = { participantType: 'MEMBER', status: ApprovalStatus.REJECTED }; + const params = { uid: '123' }; + jest.spyOn(participantsRequestService, 'processRejectRequest').mockResolvedValue('success' as any); + + const result = await controller.processRequest(body, params); + expect(result).toEqual('success'); + expect(participantsRequestService.processRejectRequest).toHaveBeenCalledWith(params.uid); + }); + + it('should call processTeamCreateRequest when status is APPROVED for TEAM without referenceUid', async () => { + const body = { participantType: 'TEAM', status: ApprovalStatus.APPROVED }; + const params = { uid: '123' }; + jest.spyOn(participantsRequestService, 'processTeamCreateRequest').mockResolvedValue('success' as any); + + const result = await controller.processRequest(body, params); + expect(result).toEqual('success'); + expect(participantsRequestService.processTeamCreateRequest).toHaveBeenCalledWith(params.uid); + }); + + it('should call processMemberCreateRequest when status is APPROVED for MEMBER without referenceUid', async () => { + const body = { participantType: 'MEMBER', status: ApprovalStatus.APPROVED }; + const params = { uid: '123' }; + jest.spyOn(participantsRequestService, 'processMemberCreateRequest').mockResolvedValue('success' as any); + + const result = await controller.processRequest(body, params); + expect(result).toEqual('success'); + expect(participantsRequestService.processMemberCreateRequest).toHaveBeenCalledWith(params.uid); + }); + + it('should call processTeamEditRequest when status is APPROVED for TEAM with referenceUid', async () => { + const body = { participantType: 'TEAM', status: ApprovalStatus.APPROVED, referenceUid: '456' }; + const params = { uid: '123' }; + jest.spyOn(participantsRequestService, 'processTeamEditRequest').mockResolvedValue('success' as any); + + const result = await controller.processRequest(body, params); + expect(result).toEqual('success'); + expect(participantsRequestService.processTeamEditRequest).toHaveBeenCalledWith(params.uid); + }); + + it('should call processMemberEditRequest when status is APPROVED for MEMBER with referenceUid', async () => { + const body = { participantType: 'MEMBER', status: ApprovalStatus.APPROVED, referenceUid: '456' }; + const params = { uid: '123' }; + jest.spyOn(participantsRequestService, 'processMemberEditRequest').mockResolvedValue('success' as any); + + const result = await controller.processRequest(body, params); + expect(result).toEqual('success'); + expect(participantsRequestService.processMemberEditRequest).toHaveBeenCalledWith(params.uid); + }); + }); + + describe('AdminController - ForbiddenException Scenarios', () => { + + + describe('addRequest - ForbiddenException cases', () => { + it('should throw ForbiddenException for invalid MEMBER schema', async () => { + const body = { participantType: ParticipantType.MEMBER, invalidField: 'error' }; + jest.spyOn(ParticipantRequestMemberSchema, 'safeParse').mockReturnValueOnce({ success: false } as any); + + await expect(controller.addRequest(body)).rejects.toThrow(ForbiddenException); + }); + + it('should throw ForbiddenException for invalid TEAM schema', async () => { + const body = { participantType: ParticipantType.TEAM, invalidField: 'error' }; + jest.spyOn(ParticipantRequestTeamSchema, 'safeParse').mockReturnValueOnce({ success: false } as any); + + await expect(controller.addRequest(body)).rejects.toThrow(ForbiddenException); + }); + + it('should throw ForbiddenException for unknown participant type', async () => { + const body = { participantType: 'UNKNOWN_TYPE' }; + + await expect(controller.addRequest(body)).rejects.toThrow(ForbiddenException); + }); + }); + + describe('updateRequest - ForbiddenException cases', () => { + it('should throw ForbiddenException for invalid MEMBER schema on update', async () => { + const body = { participantType: ParticipantType.MEMBER, invalidField: 'error' }; + const params = { uid: '123' }; + jest.spyOn(ParticipantRequestMemberSchema, 'safeParse').mockReturnValueOnce({ success: false } as any); + + await expect(controller.updateRequest(body, params)).rejects.toThrow(ForbiddenException); + }); + + it('should throw ForbiddenException for invalid TEAM schema on update', async () => { + const body = { participantType: ParticipantType.TEAM, invalidField: 'error' }; + const params = { uid: '123' }; + jest.spyOn(ParticipantRequestTeamSchema, 'safeParse').mockReturnValueOnce({ success: false } as any); + + await expect(controller.updateRequest(body, params)).rejects.toThrow(ForbiddenException); + }); + }); + + describe('processRequest - ForbiddenException case', () => { + it('should throw ForbiddenException for invalid process request schema', async () => { + const body = { status: 'INVALID_STATUS' }; + const params = { uid: '123' }; + jest.spyOn(ParticipantProcessRequestSchema, 'safeParse').mockReturnValueOnce({ success: false } as any); + + await expect(controller.processRequest(body, params)).rejects.toThrow(ForbiddenException); + }); + }); + }); +}); diff --git a/apps/web-api/src/admin/admin.service.spec.ts b/apps/web-api/src/admin/admin.service.spec.ts new file mode 100644 index 000000000..ca1fcf328 --- /dev/null +++ b/apps/web-api/src/admin/admin.service.spec.ts @@ -0,0 +1,51 @@ +import { AdminService } from './admin.service'; +import { UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '../utils/jwt/jwt.service'; + +describe('AdminService', () => { + let adminService: AdminService; + let jwtService: JwtService; + + beforeEach(() => { + // Mock JwtService + jwtService = { + getSignedToken: jest.fn().mockResolvedValue('mockedToken'), + } as unknown as JwtService; + + adminService = new AdminService(jwtService); + + // Mock environment variables + process.env.ADMIN_USERNAME = 'admin'; + process.env.ADMIN_PASSWORD = 'password'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return a signed JWT token if credentials are valid', async () => { + const username = 'admin'; + const password = 'password'; + + const result = await adminService.signIn(username, password); + + expect(result).toEqual({ code: 1, accessToken: 'mockedToken' }); + expect(jwtService.getSignedToken).toHaveBeenCalledWith(['DIRECTORYADMIN']); + }); + + it('should throw UnauthorizedException if username is invalid', async () => { + const username = 'invalidAdmin'; + const password = 'password'; + + await expect(adminService.signIn(username, password)).rejects.toThrow(UnauthorizedException); + expect(jwtService.getSignedToken).not.toHaveBeenCalled(); + }); + + it('should throw UnauthorizedException if password is invalid', async () => { + const username = 'admin'; + const password = 'wrongPassword'; + + await expect(adminService.signIn(username, password)).rejects.toThrow(UnauthorizedException); + expect(jwtService.getSignedToken).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/web-api/src/decorators/request.decorator.spec.ts b/apps/web-api/src/decorators/request.decorator.spec.ts new file mode 100644 index 000000000..51a0656ee --- /dev/null +++ b/apps/web-api/src/decorators/request.decorator.spec.ts @@ -0,0 +1,49 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +// Define the RequestIp decorator +export const RequestIp = createParamDecorator((data: unknown, context: ExecutionContext) => { + const req = context.switchToHttp().getRequest(); + return req.headers['x-forwarded-for'] || req.connection.remoteAddress; +}); + +describe('RequestIp Decorator', () => { + it('should return the x-forwarded-for header if it exists', () => { + const mockRequest = { + headers: { + 'x-forwarded-for': '123.45.67.89', + }, + connection: { + remoteAddress: '98.76.54.32', // This should not be returned + }, + }; + + const mockContext = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as unknown as ExecutionContext; // Type assertion to satisfy TypeScript + + // Call the decorator as it would be called in a real scenario + const result = RequestIp('', mockContext); // Provide an empty string + expect(result).toBe('123.45.67.89'); // Check the returned value + }); + + it('should return remoteAddress if x-forwarded-for header does not exist', () => { + const mockRequest = { + headers: {}, + connection: { + remoteAddress: '98.76.54.32', + }, + }; + + const mockContext = { + switchToHttp: () => ({ + getRequest: () => mockRequest, + }), + } as unknown as ExecutionContext; // Type assertion to satisfy TypeScript + + // Call the decorator as it would be called in a real scenario + const result = RequestIp('', mockContext); // Provide an empty string + expect(result).toBe('98.76.54.32'); // Check the returned value + }); +}); diff --git a/apps/web-api/src/faq/faq.controller.spec.ts b/apps/web-api/src/faq/faq.controller.spec.ts index 33a6d5f2a..0a871ed27 100644 --- a/apps/web-api/src/faq/faq.controller.spec.ts +++ b/apps/web-api/src/faq/faq.controller.spec.ts @@ -1,20 +1,69 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FaqController } from './faq.controller'; import { FaqService } from './faq.service'; +import { CustomQuestionSchemaDto, CustomQuestionResponseDto } from 'libs/contracts/src/schema'; +import { InternalServerErrorException } from '@nestjs/common'; +import { ZodValidationPipe } from 'nestjs-zod'; +import { RequestIp } from '../decorators/request.decorator'; describe('FaqController', () => { - let controller: FaqController; + let faqController: FaqController; + let faqService: FaqService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [FaqController], - providers: [FaqService], + providers: [ + { + provide: FaqService, + useValue: { + addQuestion: jest.fn(), + }, + }, + ], }).compile(); - controller = module.get(FaqController); + faqController = module.get(FaqController); + faqService = module.get(FaqService); }); - it('should be defined', () => { - expect(controller).toBeDefined(); + describe('create', () => { + it('should return success when addQuestion is successful', async () => { + const body: CustomQuestionSchemaDto = { + email: 'user@example.com', + question: 'How does this work?', + type: 'ASK_QUESTION', + }; + const requestIp = '127.0.0.1'; + + // Mock the addQuestion method to return true (successful operation) + jest.spyOn(faqService, 'addQuestion').mockResolvedValue(true); + + // Call the controller method + const result: CustomQuestionResponseDto = await faqController.create(body, requestIp); + + // Assertions + expect(result).toEqual({ success: true }); + expect(faqService.addQuestion).toHaveBeenCalledWith(body, requestIp); + }); + + it('should throw InternalServerErrorException when addQuestion fails', async () => { + const body: CustomQuestionSchemaDto = { + email: 'user@example.com', + question: 'How does this work?', + type: 'ASK_QUESTION', + }; + const requestIp = '127.0.0.1'; + + // Mock the addQuestion method to return false (unsuccessful operation) + jest.spyOn(faqService, 'addQuestion').mockResolvedValue(false); + + // Call the controller method and expect an exception + try { + await faqController.create(body, requestIp); + } catch (error) { + expect(error).toBeInstanceOf(InternalServerErrorException); + } + }); }); }); diff --git a/apps/web-api/src/faq/faq.service.spec.ts b/apps/web-api/src/faq/faq.service.spec.ts index 1459e6cec..48962cc53 100644 --- a/apps/web-api/src/faq/faq.service.spec.ts +++ b/apps/web-api/src/faq/faq.service.spec.ts @@ -1,18 +1,166 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { InternalServerErrorException } from '@nestjs/common'; import { FaqService } from './faq.service'; +import { PrismaService } from '../shared/prisma.service'; +import { LogService } from '../shared/log.service'; +import { AwsService } from '../utils/aws/aws.service'; +import { CustomQuestionSchemaDto } from 'libs/contracts/src/schema'; +import { + ASK_QUESTION, + ASK_QUESTION_SUBJECT, + FEEDBACK, + FEEDBACK_SUBJECT, + SHARE_IDEA, + SHARE_IDEA_SUBJECT, + SUPPORT, + SUPPORT_SUBJECT, +} from '../utils/constants'; +import path from 'path'; +import { SendRawEmailResponse } from 'aws-sdk/clients/ses'; describe('FaqService', () => { - let service: FaqService; + let faqService: FaqService; + let prismaService: PrismaService; + let logService: LogService; + let awsService: AwsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [FaqService], + providers: [ + FaqService, + { provide: PrismaService, useValue: { faq: { create: jest.fn() } } }, + { provide: LogService, useValue: { info: jest.fn(), error: jest.fn() } }, + { provide: AwsService, useValue: { sendEmailWithTemplate: jest.fn() } }, + ], }).compile(); - service = module.get(FaqService); + faqService = module.get(FaqService); + prismaService = module.get(PrismaService); + logService = module.get(LogService); + awsService = module.get(AwsService); + + process.env.IS_EMAIL_ENABLED = 'true'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('addQuestion', () => { + it('should save a new FAQ question and send notification', async () => { + const question: CustomQuestionSchemaDto = { + email: 'user@example.com', + question: 'What is the return policy?', + type: 'ASK_QUESTION', + }; + const requestIP = '127.0.0.1'; + const mockResult = { uid: 'faq_123' }; + + // Mock Prisma method + (prismaService.faq.create as jest.Mock).mockResolvedValue(mockResult); + jest.spyOn(faqService, 'notifyNewCustomQuestion').mockResolvedValueOnce(); + + const result = await faqService.addQuestion(question, requestIP); + + expect(prismaService.faq.create).toHaveBeenCalledWith({ + data: { + email: question.email, + question: question.question, + type: question.type, + requestIp: requestIP, + }, + }); + expect(faqService.notifyNewCustomQuestion).toHaveBeenCalledWith(mockResult); + expect(logService.info).toHaveBeenCalledWith( + `New faq question request created from ${question.email} with ref id ${mockResult.uid}` + ); + expect(result).toBe(true); + }); + + it('should throw an error if the FAQ question cannot be saved', async () => { + const question: CustomQuestionSchemaDto = { + email: 'user@example.com', + question: 'What is the return policy?', + type: 'ASK_QUESTION', + }; + const requestIP = '127.0.0.1'; + + // Mock the Prisma create method to throw an error + (prismaService.faq.create as jest.Mock).mockRejectedValue(new Error('Database error')); + + await expect(faqService.addQuestion(question, requestIP)).rejects.toThrow(InternalServerErrorException); + expect(logService.info).not.toHaveBeenCalled(); + }); }); - it('should be defined', () => { - expect(service).toBeDefined(); + describe('getEmailSubjectByType', () => { + it('should return ASK_QUESTION_SUBJECT for type ASK_QUESTION', () => { + const result = faqService.getEmailSubjectByType(ASK_QUESTION); + expect(result).toBe(ASK_QUESTION_SUBJECT); + }); + + it('should return SUPPORT_SUBJECT for type SUPPORT', () => { + const result = faqService.getEmailSubjectByType(SUPPORT); + expect(result).toBe(SUPPORT_SUBJECT); + }); + + it('should return FEEDBACK_SUBJECT for type FEEDBACK', () => { + const result = faqService.getEmailSubjectByType(FEEDBACK); + expect(result).toBe(FEEDBACK_SUBJECT); + }); + + it('should return SHARE_IDEA_SUBJECT for type SHARE_IDEA', () => { + const result = faqService.getEmailSubjectByType(SHARE_IDEA); + expect(result).toBe(SHARE_IDEA_SUBJECT); + }); + + it('should return null for an unknown type', () => { + const result = faqService.getEmailSubjectByType('UNKNOWN_TYPE'); + expect(result).toBeNull(); + }); + }); + + describe('FaqService - notifyNewCustomQuestion', () => { + it('should notify support team with a new question without errors', async () => { + const faq = { + type: 'ASK_QUESTION', + email: 'user@example.com', + question: 'How to use this feature?', + uid: 'unique-id', + }; + + // Mock getEmailSubjectByType to return a valid subject + jest.spyOn(faqService, 'getEmailSubjectByType').mockReturnValue('A new feedback received'); + + // Mock sendEmailWithTemplate to resolve successfully + const mockSendEmailResponse: any = { + MessageId: '12345', + $response: {}, // Adding a mock $response to satisfy the type + }; + jest.spyOn(awsService, 'sendEmailWithTemplate').mockResolvedValue(mockSendEmailResponse); + + // Mock logger methods + const loggerInfoSpy = jest.spyOn(logService, 'info'); + const loggerErrorSpy = jest.spyOn(logService, 'error'); + + // Call the method under test + await faqService.notifyNewCustomQuestion(faq); + + // Assertions + expect(faqService.getEmailSubjectByType).toHaveBeenCalledWith(faq.type); + expect(awsService.sendEmailWithTemplate).toHaveBeenCalledWith( + path.join(__dirname, '/shared/contactUs.hbs'), + faq, + '', + 'A new feedback received', + 'member-services@plnetwork.io', + ['navaneeth@ideas2it.com'], + [] + ); + expect(loggerInfoSpy).toHaveBeenCalledWith( + `New faq request from ${faq.email} - ${faq.uid} notified to support team ref: 12345` + ); + expect(loggerErrorSpy).not.toHaveBeenCalled(); + }); }); }); diff --git a/apps/web-api/src/focus-areas/focus-areas.controller.spec.ts b/apps/web-api/src/focus-areas/focus-areas.controller.spec.ts new file mode 100644 index 000000000..62efe5452 --- /dev/null +++ b/apps/web-api/src/focus-areas/focus-areas.controller.spec.ts @@ -0,0 +1,49 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FocusAreasService } from './focus-areas.service'; +import { Request } from 'express'; +import { of } from 'rxjs'; +import { apiFocusAreas } from 'libs/contracts/src/lib/contract-focus-areas'; +import { initNestServer } from '@ts-rest/nest'; +import { FocusAreaController } from './focus-areas.controller'; + +const server = initNestServer(apiFocusAreas); + +describe('FocusAreaController', () => { + let controller: FocusAreaController; + let service: FocusAreasService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FocusAreaController], + providers: [ + { + provide: FocusAreasService, + useValue: { + findAll: jest.fn(), + }, + }, + ], + }).compile(); + + controller = module.get(FocusAreaController); + service = module.get(FocusAreasService); + }); + + describe('findAll', () => { + it('should return the expected focus areas', async () => { + const mockQuery = { type: 'PROJECT' }; + const mockResponse: any = [{ id: 1, name: 'Focus Area 1' }]; + + // Mocking the service call + jest.spyOn(service, 'findAll').mockResolvedValue(mockResponse); + + // Simulating the request object + const req = { query: mockQuery } as unknown as Request; + + const result = await controller.findAll(req); + + expect(service.findAll).toHaveBeenCalledWith(mockQuery); + expect(result).toEqual(mockResponse); + }); + }); +}); diff --git a/apps/web-api/src/focus-areas/focus-areas.service.spec.ts b/apps/web-api/src/focus-areas/focus-areas.service.spec.ts new file mode 100644 index 000000000..38bace184 --- /dev/null +++ b/apps/web-api/src/focus-areas/focus-areas.service.spec.ts @@ -0,0 +1,96 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FocusAreasService } from './focus-areas.service'; +import { PrismaService } from '../shared/prisma.service'; +import { TeamsService } from '../teams/teams.service'; +import { ProjectsService } from '../projects/projects.service'; +import { TEAM, PROJECT } from '../utils/constants'; + +describe('FocusAreasService', () => { + let service: FocusAreasService; + let prismaService: PrismaService; + let teamsService: TeamsService; + let projectsService: ProjectsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FocusAreasService, + { + provide: PrismaService, + useValue: { + focusArea: { + findMany: jest.fn(), + }, + }, + }, + { + provide: TeamsService, + useValue: { + buildTeamFilter: jest.fn(), + }, + }, + { + provide: ProjectsService, + useValue: { + buildProjectFilter: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(FocusAreasService); + prismaService = module.get(PrismaService); + teamsService = module.get(TeamsService); + projectsService = module.get(ProjectsService); + }); + + describe('findAll', () => { + it('should retrieve all focus areas with TEAM type filter applied', async () => { + const query = { type: TEAM }; + const mockFocusAreas: any = [{ id: 1, name: 'Focus Area 1' }]; + jest.spyOn(prismaService.focusArea, 'findMany').mockResolvedValue(mockFocusAreas); + + const result = await service.findAll(query); + + expect(prismaService.focusArea.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + include: expect.any(Object), + orderBy: { createdAt: 'desc' }, + }) + ); + expect(result).toEqual(mockFocusAreas); + }); + + it('should retrieve all focus areas with PROJECT type filter applied', async () => { + const query = { type: PROJECT }; + const mockFocusAreas: any = [{ id: 2, name: 'Focus Area 2' }]; + jest.spyOn(prismaService.focusArea, 'findMany').mockResolvedValue(mockFocusAreas); + + const result = await service.findAll(query); + + expect(prismaService.focusArea.findMany).toHaveBeenCalledWith( + expect.objectContaining({ + include: expect.any(Object), + orderBy: { createdAt: 'desc' }, + }) + ); + expect(result).toEqual(mockFocusAreas); + }); + + it('should call buildTeamFilter when TEAM type is specified', async () => { + const query = { type: TEAM }; + jest.spyOn(teamsService, 'buildTeamFilter').mockReturnValue({} as any); + await service.findAll(query); + + expect(teamsService.buildTeamFilter).toHaveBeenCalledWith(query); + }); + + it('should call buildProjectFilter when PROJECT type is specified', async () => { + const query = { type: PROJECT }; + jest.spyOn(projectsService, 'buildProjectFilter').mockReturnValue({} as any); + await service.findAll(query); + + expect(projectsService.buildProjectFilter).toHaveBeenCalledWith(query); + }); + }); +}); diff --git a/apps/web-api/src/health/health.controller.spec.ts b/apps/web-api/src/health/health.controller.spec.ts new file mode 100644 index 000000000..e079ca9bd --- /dev/null +++ b/apps/web-api/src/health/health.controller.spec.ts @@ -0,0 +1,74 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HealthController } from './health.controller'; +import { HealthCheckService, HealthCheckResult, HttpHealthIndicator } from '@nestjs/terminus'; +import { PrismaHealthIndicator } from './prisma.health'; +import { HerokuHealthIndicator } from './heroku.health'; + +describe('HealthController', () => { + let controller: HealthController; + let healthCheckService: HealthCheckService; + let prismaHealthIndicator: PrismaHealthIndicator; + let herokuHealthIndicator: HerokuHealthIndicator; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [HealthController], + providers: [ + HealthCheckService, + HttpHealthIndicator, + PrismaHealthIndicator, + HerokuHealthIndicator, + ], + }) + .overrideProvider(HealthCheckService) + .useValue({ + check: jest.fn(), + }) + .overrideProvider(PrismaHealthIndicator) + .useValue({ + isHealthy: jest.fn(), + }) + .overrideProvider(HerokuHealthIndicator) + .useValue({ + isHealthy: jest.fn(), + }) + .compile(); + + controller = module.get(HealthController); + healthCheckService = module.get(HealthCheckService); + prismaHealthIndicator = module.get(PrismaHealthIndicator); + herokuHealthIndicator = module.get(HerokuHealthIndicator); + }); + + it('should return healthy status when all indicators are healthy', async () => { + const mockHealthResult: HealthCheckResult = { + status: 'ok', + details: { + heroku: { status: 'up' }, + prisma: { status: 'up' }, + }, + }; + + // Mock each health indicator to return a healthy status + jest.spyOn(healthCheckService, 'check').mockResolvedValue(mockHealthResult); + + const result = await controller.healthCheck(); + expect(result).toEqual(mockHealthResult); + }); + + it('should return unhealthy status if any indicator fails', async () => { + const mockUnhealthyResult: HealthCheckResult = { + status: 'error', + details: { + heroku: { status: 'down', message: 'Heroku is down' }, + prisma: { status: 'up' }, + }, + }; + + // Mock the health check service to return an unhealthy result + jest.spyOn(healthCheckService, 'check').mockResolvedValue(mockUnhealthyResult); + + const result = await controller.healthCheck(); + expect(result).toEqual(mockUnhealthyResult); + }); +}); diff --git a/apps/web-api/src/health/heroku.health.spec.ts b/apps/web-api/src/health/heroku.health.spec.ts new file mode 100644 index 000000000..f16c5a1a5 --- /dev/null +++ b/apps/web-api/src/health/heroku.health.spec.ts @@ -0,0 +1,141 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HttpHealthIndicator } from '@nestjs/terminus'; +import { HealthCheckError } from '@nestjs/terminus'; +import { AxiosResponse } from 'axios'; +import { HerokuHealthIndicator } from './heroku.health'; + +describe('HerokuHealthIndicator', () => { + let herokuHealthIndicator: HerokuHealthIndicator; + let httpHealthIndicator: HttpHealthIndicator; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + HerokuHealthIndicator, + { + provide: HttpHealthIndicator, + useValue: { + responseCheck: jest.fn(), + }, + }, + ], + }).compile(); + + herokuHealthIndicator = module.get(HerokuHealthIndicator); + httpHealthIndicator = module.get(HttpHealthIndicator); + }); + + describe('isHealthy', () => { + it('should return healthy status when Heroku status is green', async () => { + const mockResponse: any = { + 'heroku-status': { status: 'up' }, + }; + + // Mocking the successful response + (httpHealthIndicator.responseCheck as jest.Mock).mockResolvedValue(mockResponse); + + const result = await herokuHealthIndicator.isHealthy(); + + // Ensure the expected response structure + expect(result).toEqual({ + 'heroku-status': { status: 'up' }, + }); + }); + + it('should throw a HealthCheckError when any Heroku system is not green', async () => { + const mockResponse: any = { + status: 200, + data: { + status: [ + { + system: 'system1', + status: 'green', + }, + { + system: 'system2', + status: 'red', // Simulating a "red" status + }, + ], + }, + headers: {}, + config: {}, + request: {}, + }; + + // Mocking the responseCheck to resolve with the mocked response + (httpHealthIndicator.responseCheck as jest.Mock).mockImplementation((name, url, callback) => { + return callback(mockResponse).then(() => { + // Simulating the error being thrown when a system is not green + throw new HealthCheckError('Heroku status check failed', { + message: 'Heroku system system2 is not green', + status: 'red', + }); + }); + }); + + // Expecting the HealthCheckError to be thrown + await expect(herokuHealthIndicator.isHealthy()).rejects.toThrowError( + new HealthCheckError('Heroku status check failed', { + message: 'Heroku system system2 is not green', + status: 'red', + }) + ); + }); + + it('should throw a HealthCheckError when the request fails', async () => { + // Mocking the HttpHealthIndicator's responseCheck to simulate a failed request + const mockError = new Error('Heroku status check failed'); // You can use a custom error here + (httpHealthIndicator.responseCheck as jest.Mock).mockRejectedValue(mockError); + + // Expecting the HealthCheckError to be thrown when the request fails + await expect(herokuHealthIndicator.isHealthy()).rejects.toThrowError( + new HealthCheckError('Heroku status check failed', mockError) + ); + }); + + it('should throw a HealthCheckError with the correct message when system status is not green', async () => { + // Mocking the HttpHealthIndicator's responseCheck to return a "yellow" status + (httpHealthIndicator.responseCheck as jest.Mock).mockImplementation(async (_, __, checkFn) => { + // Mock response simulating a status with "yellow" status + const mockResponse = { + status: 200, + data: { + status: [ + { status: 'green', system: 'system1' }, + { status: 'yellow', system: 'system2' }, // Non-green status + ], + }, + config: {}, + headers: {}, + request: {}, + }; + + // Call the check function to trigger the HealthCheckError + return checkFn(mockResponse as AxiosResponse); + }); + + // Expecting the isHealthy method to throw a HealthCheckError + await expect(herokuHealthIndicator.isHealthy()).rejects.toThrowError( + new HealthCheckError('Heroku status check failed', { + message: `Heroku system system2 is not green`, + status: 'yellow', + }) + ); + }); + + it('should return healthy status if the response status is not 200 but the systems are green', async () => { + const mockResponse: any = { + 'heroku-status': { status: 'up' }, + }; + + // Mocking the response with a non-200 status + (httpHealthIndicator.responseCheck as jest.Mock).mockResolvedValue(mockResponse); + + // Ensure that the function still returns a healthy status + const result = await herokuHealthIndicator.isHealthy(); + expect(result).toEqual({ + 'heroku-status': { status: 'up' }, + }); + }); + }); +}); diff --git a/apps/web-api/src/health/prisma.health.spec.ts b/apps/web-api/src/health/prisma.health.spec.ts new file mode 100644 index 000000000..750d5ff58 --- /dev/null +++ b/apps/web-api/src/health/prisma.health.spec.ts @@ -0,0 +1,52 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaService } from '../shared/prisma.service'; +import { HealthCheckError } from '@nestjs/terminus'; +import { PrismaHealthIndicator } from './prisma.health'; + +describe('PrismaHealthIndicator', () => { + let prismaHealthIndicator: PrismaHealthIndicator; + let prismaService: PrismaService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PrismaHealthIndicator, + { + provide: PrismaService, + useValue: { + $queryRaw: jest.fn(), // Mocking the $queryRaw method + }, + }, + ], + }).compile(); + + prismaHealthIndicator = module.get(PrismaHealthIndicator); + prismaService = module.get(PrismaService); + }); + + describe('isHealthy', () => { + it('should return healthy status when Prisma query is successful', async () => { + // Mocking the successful query execution + (prismaService.$queryRaw as jest.Mock).mockResolvedValue(true); + + const result = await prismaHealthIndicator.isHealthy('prisma'); + + expect(result).toEqual({ + prisma: { status: 'up' }, // This is what getStatus(key, true) would return + }); + expect(prismaService.$queryRaw).toHaveBeenCalledWith(['SELECT 1']); + }); + + it('should throw a HealthCheckError when Prisma query fails', async () => { + // Mocking the failure of the query + (prismaService.$queryRaw as jest.Mock).mockRejectedValue(new Error('Database error')); + + await expect(prismaHealthIndicator.isHealthy('prisma')) + .rejects + .toThrowError(HealthCheckError); + await expect(prismaHealthIndicator.isHealthy('prisma')) + .rejects + .toThrowError('Prisma check failed'); + }); + }); +}); diff --git a/apps/web-api/src/home/home.controller.spec.ts b/apps/web-api/src/home/home.controller.spec.ts new file mode 100644 index 000000000..4150313fd --- /dev/null +++ b/apps/web-api/src/home/home.controller.spec.ts @@ -0,0 +1,123 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HomeController } from './home.controller'; +import { HomeService } from './home.service'; +import { MembersService } from '../members/members.service'; +import { HuskyService } from '../husky/husky.service'; +import { ForbiddenException, BadRequestException } from '@nestjs/common'; +import { UserTokenValidation } from '../guards/user-token-validation.guard'; +import { PrismaQueryBuilder } from '../utils/prisma-query-builder'; +import { ResponseDiscoveryQuestionSchema } from '@protocol-labs-network/contracts'; + +describe('HomeController', () => { + let homeController: HomeController; + let homeService: HomeService; + let membersService: MembersService; + let huskyService: HuskyService; + let prismaQueryBuilder: PrismaQueryBuilder; + + beforeEach(async () => { + const prismaQueryBuilderMock = { + build: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ + controllers: [HomeController], + providers: [ + { + provide: HomeService, + useValue: { fetchAllFeaturedData: jest.fn() }, + }, + { + provide: MembersService, + useValue: { + findMemberByEmail: jest.fn(), + checkIfAdminUser: jest.fn(), + }, + }, + { + provide: PrismaQueryBuilder, + useValue: prismaQueryBuilderMock, + }, + { + provide: HuskyService, + useValue: { + fetchDiscoverQuestions: jest.fn(), + fetchDiscoverQuestionBySlug: jest.fn(), + createDiscoverQuestion: jest.fn(), + updateDiscoveryQuestionBySlug: jest.fn(), + updateDiscoveryQuestionShareCount: jest.fn(), + updateDiscoveryQuestionViewCount: jest.fn(), + }, + }, + ], + }) + .overrideGuard(UserTokenValidation) + .useValue({ canActivate: jest.fn(() => true) }) + .compile(); + + homeController = module.get(HomeController); + homeService = module.get(HomeService); + membersService = module.get(MembersService); + huskyService = module.get(HuskyService); + prismaQueryBuilder = new PrismaQueryBuilder(ResponseDiscoveryQuestionSchema as any); + }); + + it('should return all featured data', async () => { + const mockData = { members: [], teams: [], events: [], projects: [] }; + jest.spyOn(homeService, 'fetchAllFeaturedData').mockResolvedValue(mockData); + + const result = await homeController.getAllFeaturedData(); + expect(result).toEqual(mockData); + expect(homeService.fetchAllFeaturedData).toHaveBeenCalled(); + }); + + it('should throw ForbiddenException if user is not admin in addDiscoveryQuestion', async () => { + jest.spyOn(membersService, 'findMemberByEmail').mockResolvedValue({ email: 'test@example.com' } as any); + jest.spyOn(membersService, 'checkIfAdminUser').mockReturnValue(false); + + await expect(homeController.addDiscoveryQuestion({} as any, { userEmail: 'test@example.com' })).rejects.toThrow( + ForbiddenException + ); + }); + + it('should create a discovery question if user is admin', async () => { + const mockMember = { email: 'test@example.com' }; + jest.spyOn(membersService, 'findMemberByEmail').mockResolvedValue(mockMember as any); + jest.spyOn(membersService, 'checkIfAdminUser').mockReturnValue(true); + + await homeController.addDiscoveryQuestion({} as any, { userEmail: 'test@example.com' }); + + expect(membersService.findMemberByEmail).toHaveBeenCalledWith('test@example.com'); + expect(membersService.checkIfAdminUser).toHaveBeenCalledWith(mockMember); + expect(huskyService.createDiscoverQuestion).toHaveBeenCalledWith({}, mockMember); + }); + + it('should throw BadRequestException for invalid attribute in modifyDiscoveryQuestionShareCountOrViewCount', async () => { + await expect( + homeController.modifyDiscoveryQuestionShareCountOrViewCount('slug1', { attribute: 'invalid' }) + ).rejects.toThrow(BadRequestException); + }); + + it('should update view count for valid attribute in modifyDiscoveryQuestionShareCountOrViewCount', async () => { + await homeController.modifyDiscoveryQuestionShareCountOrViewCount('slug1', { attribute: 'viewCount' }); + expect(huskyService.updateDiscoveryQuestionViewCount).toHaveBeenCalledWith('slug1'); + }); + + it('should call fetchDiscoverQuestions with the correct built query', async () => { + // Arrange + const mockRequest: any = { query: { field: 'value' } }; + const mockQueryResult: any = { someField: 'someValue' }; + const mockResponse: any = [{ id: 1, question: 'Sample question' }]; + + // Spy on the build method of PrismaQueryBuilder instance + jest.spyOn(prismaQueryBuilder, 'build').mockReturnValue(mockQueryResult); + jest.spyOn(huskyService, 'fetchDiscoverQuestions').mockResolvedValue(mockResponse); + + // Act + const result = await homeController.getDiscoveryQuestions(mockRequest); + + // Assert + expect(prismaQueryBuilder.build).toBeCalledTimes(1) + expect(huskyService.fetchDiscoverQuestions).toHaveBeenCalledWith(mockQueryResult); + expect(result).toEqual(mockResponse); + }); +}); diff --git a/apps/web-api/src/home/home.service.spec.ts b/apps/web-api/src/home/home.service.spec.ts new file mode 100644 index 000000000..162889f27 --- /dev/null +++ b/apps/web-api/src/home/home.service.spec.ts @@ -0,0 +1,100 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { HomeService } from './home.service'; +import { MembersService } from '../members/members.service'; +import { TeamsService } from '../teams/teams.service'; +import { PLEventsService } from '../pl-events/pl-events.service'; +import { ProjectsService } from '../projects/projects.service'; +import { InternalServerErrorException } from '@nestjs/common'; + +describe('HomeService', () => { + let homeService: HomeService; + let membersService: MembersService; + let teamsService: TeamsService; + let plEventsService: PLEventsService; + let projectsService: ProjectsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + HomeService, + { + provide: MembersService, + useValue: { findAll: jest.fn() }, + }, + { + provide: TeamsService, + useValue: { findAll: jest.fn() }, + }, + { + provide: PLEventsService, + useValue: { getPLEvents: jest.fn() }, + }, + { + provide: ProjectsService, + useValue: { getProjects: jest.fn() }, + }, + ], + }).compile(); + + homeService = module.get(HomeService); + membersService = module.get(MembersService); + teamsService = module.get(TeamsService); + plEventsService = module.get(PLEventsService); + projectsService = module.get(ProjectsService); + }); + + it('should return featured data successfully', async () => { + const mockMembers: any = [{ id: 1, name: 'Member1' }]; + const mockTeams: any = [{ id: 1, name: 'Team1' }]; + const mockEvents: any = [{ id: 1, name: 'Event1' }]; + const mockProjects: any = [{ id: 1, name: 'Project1' }]; + + jest.spyOn(membersService, 'findAll').mockResolvedValue(mockMembers); + jest.spyOn(teamsService, 'findAll').mockResolvedValue(mockTeams); + jest.spyOn(plEventsService, 'getPLEvents').mockResolvedValue(mockEvents); + jest.spyOn(projectsService, 'getProjects').mockResolvedValue(mockProjects); + + const result = await homeService.fetchAllFeaturedData(); + + expect(result).toEqual({ + members: mockMembers, + teams: mockTeams, + events: mockEvents, + projects: mockProjects, + }); + expect(membersService.findAll).toHaveBeenCalledWith({ + where: { isFeatured: true }, + include: { + image: true, + location: true, + skills: true, + teamMemberRoles: { + include: { + team: { + include: { logo: true }, + }, + }, + }, + }, + }); + expect(teamsService.findAll).toHaveBeenCalledWith({ + where: { isFeatured: true }, + include: { logo: true }, + }); + expect(plEventsService.getPLEvents).toHaveBeenCalledWith({ + where: { isFeatured: true }, + }); + expect(projectsService.getProjects).toHaveBeenCalledWith({ + where: { isFeatured: true }, + }); + }); + + it('should throw InternalServerErrorException when an error occurs', async () => { + jest.spyOn(membersService, 'findAll').mockRejectedValue(new Error('Database error')); + + await expect(homeService.fetchAllFeaturedData()).rejects.toThrow(InternalServerErrorException); + await expect(homeService.fetchAllFeaturedData()).rejects.toThrow( + 'Error occured while retrieving featured data: Database error' + ); + }); +});