diff --git a/package-lock.json b/package-lock.json index 2d36451..eb15198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "handlebars": "^4.7.8", "jwks-rsa": "^3.1.0", "mailgun.js": "^9.3.0", - "mongoose": "^7.6.13", + "mongoose": "^7.8.2", "nodemailer": "^6.9.14", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", @@ -3526,6 +3526,7 @@ } }, "node_modules/@nestjs/mongoose": { + "version": "11.0.3", "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-11.0.3.tgz", "integrity": "sha512-tg7bbKD4MnNMPaiDLXK/JUyTNQxIn3rNnI+oYU1HorLpNiR2E8vPraWVvfptpIj+zferpT6LkrHMvtqvuIKNPw==", diff --git a/package.json b/package.json index 7aa907e..e5c3af0 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "handlebars": "^4.7.8", "jwks-rsa": "^3.1.0", "mailgun.js": "^9.3.0", - "mongoose": "^7.6.13", + "mongoose": "^7.8.2", "nodemailer": "^6.9.14", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", diff --git a/src/events/events.users.controller.spec.ts b/src/events/events.users.controller.spec.ts index df1b581..b9d9b19 100644 --- a/src/events/events.users.controller.spec.ts +++ b/src/events/events.users.controller.spec.ts @@ -1,10 +1,120 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EventService } from './events.users.service'; import { EventUserController } from './events.users.controller'; +import { EventAdminsController } from './events.admin.controller'; import { TestModule } from 'src/shared/testkits'; +import { Event } from 'src/shared/schema'; +import { EventDocument } from 'src/shared/schema/events.schema'; +import { Status } from 'src/shared/interfaces/event.type'; + +import { Types } from 'mongoose'; +import { NotFoundException } from '@nestjs/common'; + +describe('UsersAdminController', () => { + let adminController: EventAdminsController; + let eventService: EventService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [EventAdminsController], + providers: [ + { + provide: EventService, + useValue: { + approveEvent: jest.fn(), + softDeleteEvent: jest.fn(), + }, + }, + ], + }).compile(); + + adminController = module.get(EventAdminsController); + eventService = module.get(EventService); + }); + + it('should be defined', () => { + expect(adminController).toBeDefined(); + }); + + describe('approveEvent', () => { + const eventId = 'valid-event-id'; + + it('should successfully approve an event', async () => { + const mockEvent: EventDocument = { + id: eventId, + title: 'Tech Conference', + description: 'A conference about the latest in tech.', + date: new Date(), + status: 'APPROVED', + save: jest.fn(), + validate: jest.fn(), + remove: jest.fn(), + populate: jest.fn(), + } as unknown as EventDocument; + + jest.spyOn(eventService, 'approveEvent').mockResolvedValue(mockEvent); + + const result = await adminController.approveEvent(eventId); + + expect(result).toEqual(mockEvent); + expect(eventService.approveEvent).toHaveBeenCalledWith(eventId); // Verify the service method was called with the correct ID + }); + + it('should throw a NotFoundException if the event is not found', async () => { + jest + .spyOn(eventService, 'approveEvent') + .mockRejectedValue( + new NotFoundException(`Event with ID ${eventId} not found`), + ); + + await expect(adminController.approveEvent(eventId)).rejects.toThrow( + `Event with ID ${eventId} not found`, + ); + expect(eventService.approveEvent).toHaveBeenCalledWith(eventId); // Verify the service method was called with the correct ID + }); + }); + + describe('softDeleteEvent', () => { + const eventId = new Types.ObjectId().toString(); + + it('should successfully soft delete an event', async () => { + const mockEvent: EventDocument = { + _id: new Types.ObjectId(eventId), + title: 'Tech Conference', + description: 'A conference about the latest in tech.', + date: new Date(), + status: Status.DELETED, + save: jest.fn(), + validate: jest.fn(), + remove: jest.fn(), + populate: jest.fn(), + } as unknown as EventDocument; + + jest.spyOn(eventService, 'softDeleteEvent').mockResolvedValue(mockEvent); + + const result = await adminController.softDeleteEvent(eventId); + + expect(result).toEqual(mockEvent); + expect(eventService.softDeleteEvent).toHaveBeenCalledWith(eventId); + }); + + it('should throw a NotFoundException if the event is not found', async () => { + jest + .spyOn(eventService, 'softDeleteEvent') + .mockRejectedValue( + new NotFoundException(`Event with ID ${eventId} not found`), + ); + + await expect(adminController.softDeleteEvent(eventId)).rejects.toThrow( + `Event with ID ${eventId} not found`, + ); + expect(eventService.softDeleteEvent).toHaveBeenCalledWith(eventId); + }); + }); +}); describe('UsersController', () => { - let controller: EventUserController + let controller: EventUserController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [TestModule], diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index 9521053..d16fd9e 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,24 +1,34 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + ForbiddenException, + InternalServerErrorException, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; + +import { Types, Model, Document } from 'mongoose'; + import { UsersController } from './users.controller'; +import { UsersAdminsController } from './users.admin.controller'; import { UsersService } from './users.service'; + +import { UpdateUserDto } from './dto/update-user.dto'; +import { UserAddPhotoDto } from './dto/user-add-photo.dto'; +import { TempLeadDto } from './dto/temp-lead.dto'; +import { RequestReactivationDto } from './dto/request-reactivation.dto'; +import { DeactivateAccountDto } from './dto/deactivate-account.dto'; + +import { CreateUserDto } from 'src/shared/dtos/create-user.dto'; import { TestModule } from 'src/shared/testkits'; -import { UsersAdminsController } from './users.admin.controller'; import { DBModule, User } from 'src/shared/schema'; import { + ApiReq, ApplicationStatus, RegistrationMethod, UserRole, UserStatus, - ApiReq, } from 'src/shared/interfaces'; -// imported to handle the paginated response from findAll -import { Model, Document, Types } from 'mongoose'; -import { IPageable } from 'src/shared/utils'; -import { - BadRequestException, - NotFoundException, - UnauthorizedException, -} from '@nestjs/common'; type UserDocument = Document & User & { _id: Types.ObjectId }; @@ -94,6 +104,7 @@ describe('UsersAdminController', () => { const userMock = createUserMock({ email: 'test@example.com', }); + // redirecting the dbquery to use the userMock jest.spyOn(usersService, 'findByEmail').mockResolvedValue(userMock); const result = await adminController.findByUsername(requestMock.email); @@ -110,675 +121,765 @@ describe('UsersAdminController', () => { }); }); - describe('findAll', () => { - // clearing mock data between test to prevent data leaking - beforeEach(() => { - jest.clearAllMocks(); - }); - - // create a diffenent request for api - const createRequestMock = (query = {}): ApiReq => ({ - query: { - page: '1', - limit: '10', - order: 'DESC', - ...query, - }, - }); - - it('should return paginated users with default parameters', async () => { - // Create mock request - const requestMock = createRequestMock(); + describe('RequestVerificationToken', () => { + const mockReq: ApiReq = { user: { id: 'adminId' } }; // Mock request object + const userId = 'validUserId'; - // mongoose record style and spreads a user data onto it - const mockRecords = [ - { - _id: new Types.ObjectId(), - ...createUserMock({ email: 'user1@example.com' }), - }, - { - _id: new Types.ObjectId(), - ...createUserMock({ email: 'user2@example.com' }), - }, - ]; - // expected findAll service response - const mockPaginatedResponse: IPageable = { - results: mockRecords, - totalRecords: 2, - perPageLimit: 10, - totalPages: 1, - currentPage: 1, - previousPage: null, - nextPage: null, - }; + it('should successfully request verification', async () => { jest - .spyOn(usersService, 'findAll') - .mockResolvedValue(mockPaginatedResponse); - const result = await adminController.findAll(requestMock); - expect(result).toEqual(mockPaginatedResponse); - expect(usersService.findAll).toHaveBeenCalledWith(requestMock); - }); - it('should handle filtering by user status', async () => { - const requestMock = createRequestMock({ - userByStatuses: `${UserStatus.ACTIVE},${UserStatus.DISABLE}`, - }); + .spyOn(usersService, 'requestVerification') + .mockResolvedValue(undefined); - const mockRecords = [ - { - _id: new Types.ObjectId(), - ...createUserMock({ - email: 'active@example.com', - status: UserStatus.ACTIVE, - }), - }, - ]; + const result = await adminController.RequestVerificationToken( + mockReq, + userId, + ); - const mockPaginatedResponse: IPageable = { - results: mockRecords, - totalRecords: 1, - perPageLimit: 10, - totalPages: 1, - currentPage: 1, - previousPage: null, - nextPage: null, - }; + expect(result).toBeUndefined(); // Updated to match the method behavior + expect(usersService.requestVerification).toHaveBeenCalledWith( + mockReq, + userId, + ); + }); + it('should throw an error if the user is not found', async () => { jest - .spyOn(usersService, 'findAll') - .mockResolvedValue(mockPaginatedResponse); - - const result = await adminController.findAll(requestMock); + .spyOn(usersService, 'requestVerification') + .mockRejectedValue(new Error('User not found')); - expect(result).toEqual(mockPaginatedResponse); - expect(usersService.findAll).toHaveBeenCalledWith(requestMock); + await expect( + adminController.RequestVerificationToken(mockReq, userId), + ).rejects.toThrow('User not found'); + expect(usersService.requestVerification).toHaveBeenCalledWith( + mockReq, + userId, + ); }); - it('should handle filtering by user roles', async () => { - const requestMock = createRequestMock({ - userByRoles: `${UserRole.USER},${UserRole.ADMIN}`, - }); - - const mockRecords = [ - { - _id: new Types.ObjectId(), - ...createUserMock({ - email: 'admin@example.com', - role: [UserRole.ADMIN], - }), - }, - ]; - - const mockPaginatedResponse: IPageable = { - results: mockRecords, - totalRecords: 1, - perPageLimit: 10, - totalPages: 1, - currentPage: 1, - previousPage: null, - nextPage: null, - }; - + it('should handle verification request not allowed error', async () => { jest - .spyOn(usersService, 'findAll') - .mockResolvedValue(mockPaginatedResponse); - - const result = await adminController.findAll(requestMock); + .spyOn(usersService, 'requestVerification') + .mockRejectedValue( + new Error('Verification request not allowed at this time'), + ); - expect(result).toEqual(mockPaginatedResponse); - expect(usersService.findAll).toHaveBeenCalledWith(requestMock); + await expect( + adminController.RequestVerificationToken(mockReq, userId), + ).rejects.toThrow('Verification request not allowed at this time'); + expect(usersService.requestVerification).toHaveBeenCalledWith( + mockReq, + userId, + ); }); - it('should handle multiple filters combined', async () => { - const requestMock = createRequestMock({ - userByStatuses: UserStatus.ACTIVE, - userByRoles: UserRole.USER, - userDateRange: '2023-01-01,2023-12-31', - page: '2', - limit: '5', - order: 'ASC', - }); - - const mockRecords = [ - { - _id: new Types.ObjectId(), - ...createUserMock(), - }, - ]; - - const mockPaginatedResponse: IPageable = { - results: mockRecords, - totalRecords: 6, - perPageLimit: 5, - totalPages: 2, - currentPage: 2, - previousPage: 1, - nextPage: null, - }; - + it('should handle user already verified error', async () => { jest - .spyOn(usersService, 'findAll') - .mockResolvedValue(mockPaginatedResponse); - - const result = await adminController.findAll(requestMock); + .spyOn(usersService, 'requestVerification') + .mockRejectedValue(new Error('User is already verified')); - expect(result).toEqual(mockPaginatedResponse); - expect(usersService.findAll).toHaveBeenCalledWith(requestMock); + await expect( + adminController.RequestVerificationToken(mockReq, userId), + ).rejects.toThrow('User is already verified'); + expect(usersService.requestVerification).toHaveBeenCalledWith( + mockReq, + userId, + ); }); + }); - it('should handle empty results', async () => { - const requestMock = createRequestMock({ - userByStatuses: UserStatus.DEACTIVATED, - }); - - const mockPaginatedResponse: IPageable = { - results: [], - totalRecords: 0, - perPageLimit: 10, - totalPages: 0, - currentPage: 1, - previousPage: null, - nextPage: null, - }; + describe('updateUserStatus', () => { + const mockReq: any = { user: { id: 'adminId' } }; // Mock request object + const userId = 'validUserId'; + const payload = { status: UserStatus.ACTIVE }; // Example payload - jest - .spyOn(usersService, 'findAll') - .mockResolvedValue(mockPaginatedResponse); + it('should successfully update user status', async () => { + const updatedUser = createUserMock({ status: UserStatus.ACTIVE }); // Mock updated user + jest.spyOn(usersService, 'updateStatus').mockResolvedValue(updatedUser); - const result = await adminController.findAll(requestMock); - - expect(result).toEqual(mockPaginatedResponse); - expect(usersService.findAll).toHaveBeenCalledWith(requestMock); - }); - }); - describe('change Password', () => { - const mockUser = createUserMock({ _id: new Types.ObjectId() }); - const mockRequest = { - user: { - _id: mockUser._id, - email: mockUser.email, - role: [UserRole.ADMIN], - }, - }; - const UserChangePasswordDto = { - oldPassword: 'oldPassword@123', - newPassword: 'newPassword@123', - confirmPassword: 'newPassword@123', - }; - - it('should change the user password', async () => { - const updatedUser = { ...mockUser, password: 'newHashedPassword' }; - jest.spyOn(usersService, 'changePassword').mockResolvedValue(updatedUser); - const result = await adminController.changePassword( - mockRequest, - mockUser._id.toString(), - UserChangePasswordDto, + const result = await adminController.updateUserStatus( + mockReq, + userId, + payload, ); - expect(result).toEqual(updatedUser); - expect(usersService.changePassword).toHaveBeenLastCalledWith( - mockRequest, - mockUser._id.toString(), - UserChangePasswordDto, - true, + + expect(result).toEqual(updatedUser); // Assert the updated user is returned + expect(usersService.updateStatus).toHaveBeenCalledWith( + userId, + UserStatus.ACTIVE, ); }); - }); - describe('userInvite', () => { - const inviteDto = { - firstName: 'John', - lastName: 'Doe', - email: 'test@example.com', - roles: [UserRole.USER], - joinMethod: RegistrationMethod.SIGN_UP, - }; - it('should successfully invite a user', async () => { - const expectedResult = createUserMock(inviteDto); - jest.spyOn(usersService, 'userInvite').mockResolvedValue(expectedResult); - const result = await adminController.userInvite(inviteDto); - expect(result).toEqual(expectedResult); - expect(usersService.userInvite).toHaveBeenCalledWith(inviteDto); - }); - it('should throw error when inviting existing user', async () => { + + it('should throw an error if user is not found', async () => { jest - .spyOn(usersService, 'userInvite') - .mockRejectedValue(new BadRequestException('User already exists')); + .spyOn(usersService, 'updateStatus') + .mockRejectedValue(new Error('User not found')); - await expect(adminController.userInvite(inviteDto)).rejects.toThrow( - BadRequestException, + await expect( + adminController.updateUserStatus(mockReq, userId, payload), + ).rejects.toThrow('User not found'); + expect(usersService.updateStatus).toHaveBeenCalledWith( + userId, + UserStatus.ACTIVE, ); }); - it('should throw error when email is invalid', async () => { - const invalidDto = { ...inviteDto, email: 'invalid-email' }; - + it('should throw an error for invalid status', async () => { jest - .spyOn(usersService, 'userInvite') - .mockRejectedValue(new BadRequestException('Invalid email format')); + .spyOn(usersService, 'updateStatus') + .mockRejectedValue(new Error('Invalid status')); - await expect(adminController.userInvite(invalidDto)).rejects.toThrow( - BadRequestException, + await expect( + adminController.updateUserStatus(mockReq, userId, { + status: 'INVALID_STATUS' as unknown as UserStatus, + }), + ).rejects.toThrow('Invalid status'); + expect(usersService.updateStatus).toHaveBeenCalledWith( + userId, + 'INVALID_STATUS', ); }); }); - describe('getUserProfile', () => { - it('should return the user profile', async () => { - const mockUser = createUserMock(); - const mockReq = { user: mockUser }; - jest.spyOn(usersService, 'findMe').mockResolvedValue(mockUser); + describe('getApplicationByEmail', () => { + const email = 'test@example.com'; - const result = await adminController.getUserProfile(mockReq); + it('should return a pending application', async () => { + const pendingApplication = createUserDocumentMock({ + email, + applicationStatus: ApplicationStatus.PENDING, + }); - expect(result).toEqual(mockUser); - expect(usersService.findMe).toHaveBeenCalledWith(mockReq); - }); + jest + .spyOn(usersService, 'viewOneApplication') + .mockResolvedValue(pendingApplication); - it('should throw unauthorized when user not in request', async () => { - const mockReq = {}; + const result = await adminController.getApplicationByEmail(email); + + expect(result).toEqual(pendingApplication); + expect(usersService.viewOneApplication).toHaveBeenCalledWith(email); + }); + it('should throw an error if no application is found', async () => { jest - .spyOn(usersService, 'findMe') - .mockRejectedValue(new UnauthorizedException()); + .spyOn(usersService, 'viewOneApplication') + .mockRejectedValue( + new NotFoundException(`Application with email ${email} not found`), + ); - await expect(adminController.getUserProfile(mockReq)).rejects.toThrow( - UnauthorizedException, - ); + await expect( + adminController.getApplicationByEmail(email), + ).rejects.toThrow(`Application with email ${email} not found`); + expect(usersService.viewOneApplication).toHaveBeenCalledWith(email); }); - it('should throw not found when user profile does not exist', async () => { - const mockReq = { user: { id: 'non-existent' } }; + it('should throw an error if the application is not pending', async () => { + const nonPendingApplication = createUserMock({ + email, + applicationStatus: ApplicationStatus.APPROVED, + }); jest - .spyOn(usersService, 'findMe') - .mockRejectedValue(new NotFoundException('User profile not found')); + .spyOn(usersService, 'viewOneApplication') + .mockRejectedValue( + new NotFoundException(`${email} has no pending application`), + ); - await expect(adminController.getUserProfile(mockReq)).rejects.toThrow( - NotFoundException, - ); + await expect( + adminController.getApplicationByEmail(email), + ).rejects.toThrow(`${email} has no pending application`); + expect(usersService.viewOneApplication).toHaveBeenCalledWith(email); }); }); - describe('remove', () => { - const userId = new Types.ObjectId().toString(); - const userMock = createUserMock({ - _id: new Types.ObjectId(), - firstName: 'John', - lastName: 'Doe', - email: 'test@example.com', - role: [UserRole.USER], - }); + describe('viewApplications', () => { + it('should return a list of pending applications', async () => { + const mockApplications = [ + createUserDocumentMock({ + email: 'applicant1@example.com', + applicationStatus: ApplicationStatus.PENDING, + leadPosition: 'Team Lead', + }), + createUserDocumentMock({ + email: 'applicant2@example.com', + applicationStatus: ApplicationStatus.PENDING, + leadPosition: 'Project Manager', + }), + ]; - it('should remove a user and return the user object', async () => { - // Mock the return value to be the user object - jest.spyOn(usersService, 'remove').mockResolvedValue(userMock); + jest + .spyOn(usersService, 'viewApplications') + .mockResolvedValue(mockApplications); - const result = await adminController.remove(userId); + const result = await adminController.viewApplications(); - expect(result).toEqual(userMock); - expect(usersService.remove).toHaveBeenCalledWith(userId); + expect(result).toEqual(mockApplications); // Verify the returned value matches the mock + expect(usersService.viewApplications).toHaveBeenCalled(); // Verify the service method was called }); - it('should throw error when user not found', async () => { + it('should throw a NotFoundException if no applications are found', async () => { jest - .spyOn(usersService, 'remove') - .mockRejectedValue( - new NotFoundException(`User with ID ${userId} not found`), - ); + .spyOn(usersService, 'viewApplications') + .mockRejectedValue(new NotFoundException('No application data found!')); - await expect(adminController.remove(userId)).rejects.toThrow( - NotFoundException, + await expect(adminController.viewApplications()).rejects.toThrow( + 'No application data found!', ); + expect(usersService.viewApplications).toHaveBeenCalled(); // Verify the service method was called }); + }); + + describe('approveApplication', () => { + const email = 'applicant@example.com'; + + it('should successfully approve a lead application', async () => { + const successMessage = + 'John Doe has been verified as a lead for TechCorp'; - it('should throw error when trying to remove admin user', async () => { - // Assuming you have logic in your service to prevent admin user deletion jest - .spyOn(usersService, 'remove') - .mockRejectedValue(new BadRequestException('Cannot remove admin user')); + .spyOn(usersService, 'approveTempApplication') + .mockResolvedValue(successMessage); - await expect(adminController.remove(userId)).rejects.toThrow( - BadRequestException, + const result = await adminController.approveApplication(email); + + expect(result).toBe(successMessage); // Verify the returned message matches the mock + expect(usersService.approveTempApplication).toHaveBeenCalledWith(email); // Verify the service method was called with the correct email + }); + + it('should throw a NotFoundException if the application is not found', async () => { + jest + .spyOn(usersService, 'approveTempApplication') + .mockRejectedValue( + new NotFoundException(`Application for ${email} not found`), + ); + + await expect(adminController.approveApplication(email)).rejects.toThrow( + `Application for ${email} not found`, ); + expect(usersService.approveTempApplication).toHaveBeenCalledWith(email); // Verify the service method was called with the correct email }); }); - describe('update', () => { - const userId = new Types.ObjectId().toString(); - const updateDto = { - userId: userId, - firstName: 'Updated', - lastName: 'Name', - }; + describe('reject', () => { + const email = 'applicant@example.com'; + const defaultMessage = 'Your application was rejected'; - it('should update a user', async () => { - const mockReq = { user: createUserMock() }; - const expectedResult = createUserMock(updateDto); + it('should successfully reject a lead application with a default message', async () => { + const successMessage = `${email} application has been rejected`; - jest.spyOn(usersService, 'update').mockResolvedValue(expectedResult); + jest + .spyOn(usersService, 'rejectTempApplication') + .mockResolvedValue(successMessage); - const result = await adminController.update(mockReq, userId, updateDto); + const result = await adminController.reject(email, { message: '' }); - expect(result).toEqual(expectedResult); - expect(usersService.update).toHaveBeenCalledWith(userId, updateDto); + expect(result).toBe(successMessage); // Verify the returned message matches the mock + expect(usersService.rejectTempApplication).toHaveBeenCalledWith( + email, + defaultMessage, + ); // Verify the service method was called with the correct arguments }); - it('should throw error when user not found', async () => { - const mockReq = { user: createUserMock() }; + it('should successfully reject a lead application with a custom message', async () => { + const customMessage = + 'We regret to inform you that your application did not meet our criteria.'; + const successMessage = `${email} application has been rejected`; jest - .spyOn(usersService, 'update') - .mockRejectedValue(new NotFoundException('User not found')); + .spyOn(usersService, 'rejectTempApplication') + .mockResolvedValue(successMessage); - await expect( - adminController.update(mockReq, userId, updateDto), - ).rejects.toThrow(NotFoundException); - }); + const result = await adminController.reject(email, { + message: customMessage, + }); - it('should throw error when invalid data provided', async () => { - const mockReq = { user: createUserMock() }; - const invalidDto = { userId, firstName: '' }; + expect(result).toBe(successMessage); // Verify the returned message matches the mock + expect(usersService.rejectTempApplication).toHaveBeenCalledWith( + email, + customMessage, + ); // Verify the service method was called with the correct arguments + }); + it('should throw a NotFoundException if the application is not found', async () => { jest - .spyOn(usersService, 'update') - .mockRejectedValue(new BadRequestException('Invalid user data')); + .spyOn(usersService, 'rejectTempApplication') + .mockRejectedValue( + new NotFoundException(`Application for ${email} not found`), + ); await expect( - adminController.update(mockReq, userId, invalidDto), - ).rejects.toThrow(BadRequestException); + adminController.reject(email, { message: '' }), + ).rejects.toThrow(`Application for ${email} not found`); + expect(usersService.rejectTempApplication).toHaveBeenCalledWith( + email, + defaultMessage, + ); // Verify the service method was called }); }); - describe('addPhoto', () => { - const userId = new Types.ObjectId().toString(); - const photoDto = { - userId, - photo: 'new-photo-url.jpg', - }; + describe('generateLink', () => { + const email = 'applicant@example.com'; - it('should add a photo to user profile', async () => { - const mockReq = { user: createUserMock() }; - const expectedResult = createUserMock({ photo: photoDto.photo }); + it('should generate an invite link successfully', async () => { + const mockLink = `https://example.com/invite-link?data=encryptedString`; - jest.spyOn(usersService, 'addPhoto').mockResolvedValue(expectedResult); + jest.spyOn(usersService, 'inviteLead').mockResolvedValue(mockLink); - const result = await adminController.addPhoto(mockReq, userId, photoDto); + const result = await adminController.generateLink(email); - expect(result).toEqual(expectedResult); - expect(usersService.addPhoto).toHaveBeenCalledWith( - photoDto.userId, - photoDto, - ); + expect(result).toEqual({ link: mockLink }); // Verify the returned link matches the mock + expect(usersService.inviteLead).toHaveBeenCalledWith(email); // Verify the service method was called with the correct email }); - it('should throw error when invalid photo format', async () => { - const mockReq = { user: createUserMock() }; - const invalidPhotoDto = { ...photoDto, photo: 'invalid-format' }; - + it('should throw a NotFoundException if the user is not found', async () => { jest - .spyOn(usersService, 'addPhoto') - .mockRejectedValue(new BadRequestException('Invalid photo format')); + .spyOn(usersService, 'inviteLead') + .mockRejectedValue( + new NotFoundException(`User with email ${email} not found`), + ); - await expect( - adminController.addPhoto(mockReq, userId, invalidPhotoDto), - ).rejects.toThrow(BadRequestException); + await expect(adminController.generateLink(email)).rejects.toThrow( + `User with email ${email} not found`, + ); + expect(usersService.inviteLead).toHaveBeenCalledWith(email); // Verify the service method was called with the correct email }); + }); +}); - it('should throw error when photo size exceeds limit', async () => { - const mockReq = { user: createUserMock() }; +describe('UsersController', () => { + let controller: UsersController; + let service: UsersService; - jest - .spyOn(usersService, 'addPhoto') - .mockRejectedValue(new BadRequestException('Photo size exceeds limit')); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: { + update: jest.fn().mockResolvedValue({ + _id: '123', + firstName: 'John', + lastName: 'Doe', + userHandle: 'johndoe', + roles: [UserRole.ADMIN], + photo: 'https://example.com/avatar.jpg', + age: 30, + phone: '1234567890', + gender: 'Male', + deviceId: 'abc123', + deviceToken: 'xyz456', + }), + addPhoto: jest.fn().mockResolvedValue({ + _id: '123', + photo: 'https://cloudinary.com/photo.jpg', + }), + createTempRegistration: jest.fn(), + createUser: jest.fn(), + requestVerification: jest.fn(), + deactivateAccount: jest.fn(), + requestReactivation: jest.fn(), + paraseEncryptedParams: jest.fn(), + findById: jest.fn(), + }, + }, + ], + }).compile(); - await expect( - adminController.addPhoto(mockReq, userId, photoDto), - ).rejects.toThrow(BadRequestException); - }); + controller = module.get(UsersController); + service = module.get(UsersService); }); - describe('RequestVerificationToken', () => { - const userId = new Types.ObjectId().toString(); + describe('addPhoto', () => { + it('should add a photo and return the updated user profile', async () => { + const userId = '123'; + const photoUrl = 'https://cloudinary.com/photo.jpg'; + const payload: UserAddPhotoDto = { photo: photoUrl, userId }; - it('should request new verification token', async () => { - const mockReq = { user: createUserMock() }; + const req = { user: { _id: userId } } as any; - // Mocking the requestVerification method to resolve successfully - jest - .spyOn(usersService, 'requestVerification') - .mockResolvedValue(undefined); // or just don't mock return value + const result = await controller.addPhoto(req, payload); - await adminController.RequestVerificationToken(mockReq, userId); + expect(service.addPhoto).toHaveBeenCalledWith(userId, payload); + expect(result).toEqual({ + _id: userId, + photo: photoUrl, + }); + }); - expect(usersService.requestVerification).toHaveBeenCalledWith( - mockReq, - userId, + it('should throw ForbiddenException if user ID in payload does not match', async () => { + const userId = '123'; + const photoUrl = 'https://cloudinary.com/photo.jpg'; + const payload: UserAddPhotoDto = { + photo: photoUrl, + userId: 'differentUserId', + }; + + const req = { user: { _id: userId } } as any; + + await expect(controller.addPhoto(req, payload)).rejects.toThrowError( + ForbiddenException, ); }); + }); - it('should throw error when user not found', async () => { - const mockReq = { user: createUserMock() }; + describe('createLead', () => { + it('should successfully create a lead registration', async () => { + const tempLeadDto: TempLeadDto = { + email: 'test@example.com', + leadPosition: 'Tech Lead', + firstName: 'John', + lastName: 'Doe', + createdAt: new Date(), + }; jest - .spyOn(usersService, 'requestVerification') - .mockRejectedValue(new Error('User not found')); + .spyOn(service, 'createTempRegistration') + .mockResolvedValue('Application sent'); - await expect( - adminController.RequestVerificationToken(mockReq, userId), - ).rejects.toThrow('User not found'); + const result = await controller.createLead(tempLeadDto); + + expect(service.createTempRegistration).toHaveBeenCalledWith( + tempLeadDto.email, + tempLeadDto.leadPosition, + ); + expect(result).toBe('Application sent'); }); - it('should throw error when verification request not allowed', async () => { - const mockReq = { user: createUserMock() }; + it('should throw BadRequestException if next application time is in the future', async () => { + const tempLeadDto: TempLeadDto = { + email: 'test@example.com', + leadPosition: 'Tech Lead', + firstName: 'John', + lastName: 'Doe', + createdAt: new Date(), + }; jest - .spyOn(usersService, 'requestVerification') + .spyOn(service, 'createTempRegistration') .mockRejectedValue( - new Error('Verification request not allowed at this time'), + new BadRequestException( + 'The next time you can apply as a lead is in the future', + ), ); - await expect( - adminController.RequestVerificationToken(mockReq, userId), - ).rejects.toThrow('Verification request not allowed at this time'); + await expect(controller.createLead(tempLeadDto)).rejects.toThrowError( + BadRequestException, + ); }); + }); - it('should throw error when user already verified', async () => { - const mockReq = { - user: createUserMock({ applicationStatus: ApplicationStatus.APPROVED }), + describe('register (invite-link)', () => { + it('should return the correct URL if user exists', async () => { + const encryptedData = 'someEncryptedData'; + const parsedData = { + userId: '507f1f77bcf86cd799439011', + email: 'user@example.com', + }; // Valid ObjectId string + const userExists = { + _id: new Types.ObjectId('507f1f77bcf86cd799439011'), // Using a valid 24-character hex string + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + // Add any additional fields as required by your schema }; - jest - .spyOn(usersService, 'requestVerification') - .mockRejectedValue(new Error('User is already verified')); + jest.spyOn(service, 'paraseEncryptedParams').mockReturnValue(parsedData); + jest.spyOn(service, 'findById').mockResolvedValue(userExists as any); - await expect( - adminController.RequestVerificationToken(mockReq, userId), - ).rejects.toThrow('User is already verified'); + const result = await controller.register(encryptedData); + + expect(service.paraseEncryptedParams).toHaveBeenCalledWith(encryptedData); + expect(service.findById).toHaveBeenCalledWith(parsedData.userId); + expect(result).toEqual({ + url: `/leads/createLead?email=${userExists.email}`, + }); }); + }); - it('should throw error when too many requests', async () => { - const mockReq = { user: createUserMock() }; + it('should return the new user form URL if userId is missing', async () => { + const encryptedData = 'someEncryptedData'; + const parsedData = { userId: '', email: 'newuser@example.com' }; - jest - .spyOn(usersService, 'requestVerification') - .mockRejectedValue(new Error('Too many verification requests')); + jest.spyOn(service, 'paraseEncryptedParams').mockReturnValue(parsedData); - await expect( - adminController.RequestVerificationToken(mockReq, userId), - ).rejects.toThrow('Too many verification requests'); + const result = await controller.register(encryptedData); + + expect(service.paraseEncryptedParams).toHaveBeenCalledWith(encryptedData); + expect(result).toEqual({ + url: `/leads/new-user-form?${new URLSearchParams({ + email: parsedData.email, + }).toString()}`, }); }); - describe('updateUser Status', () => { - const userId = new Types.ObjectId().toString(); + it('should throw NotFoundException if user does not exist', async () => { + const encryptedData = 'someEncryptedData'; + const parsedData = { userId: '123', email: 'user@example.com' }; - it('should update user status to active', async () => { - const statusDto = { status: UserStatus.ACTIVE }; - const mockReq = { user: createUserMock() }; - const expectedResult = createUserMock({ status: UserStatus.ACTIVE }); + jest.spyOn(service, 'paraseEncryptedParams').mockReturnValue(parsedData); + jest + .spyOn(service, 'findById') + .mockRejectedValue(new NotFoundException('User Not found')); - jest - .spyOn(usersService, 'updateStatus') - .mockResolvedValue(expectedResult); + await expect(controller.register(encryptedData)).rejects.toThrowError( + NotFoundException, + ); + expect(service.paraseEncryptedParams).toHaveBeenCalledWith(encryptedData); + expect(service.findById).toHaveBeenCalledWith(parsedData.userId); + }); - const result = await adminController.updateUserStatus( - mockReq, - userId, - statusDto, - ); + it('should throw NotFoundException if the link is invalid', async () => { + const encryptedData = 'invalidEncryptedData'; - expect(result).toEqual(expectedResult); - expect(usersService.updateStatus).toHaveBeenCalledWith( - userId, - statusDto.status, - ); + jest.spyOn(service, 'paraseEncryptedParams').mockImplementation(() => { + throw new Error('Invalid encryption'); }); - it('should throw error when updating to invalid status', async () => { - const invalidStatusDto = { status: 'INVALID_STATUS' as UserStatus }; - const mockReq = { user: createUserMock() }; + await expect(controller.register(encryptedData)).rejects.toThrowError( + NotFoundException, + ); + expect(service.paraseEncryptedParams).toHaveBeenCalledWith(encryptedData); + }); - jest - .spyOn(usersService, 'updateStatus') - .mockRejectedValue(new BadRequestException('Invalid user status')); + describe('update', () => { + it('should update a user', async () => { + const userId = '123'; + const updateUserDto: UpdateUserDto = { + firstName: 'John', + lastName: 'Doe', + userHandle: 'johndoe', + roles: [UserRole.ADMIN], + photo: 'https://example.com/avatar.jpg', + age: 30, + phone: '1234567890', + gender: 'Male', + deviceId: 'abc123', + deviceToken: 'xyz456', + userId: '123', + }; - await expect( - adminController.updateUserStatus(mockReq, userId, invalidStatusDto), - ).rejects.toThrow(BadRequestException); + const result = await controller.update( + { user: { _id: '123' } }, + userId, + updateUserDto, + ); + + expect(service.update).toHaveBeenCalledWith('123', updateUserDto); + expect(result).toEqual({ + _id: '123', + firstName: 'John', + lastName: 'Doe', + userHandle: 'johndoe', + roles: [UserRole.ADMIN], + photo: 'https://example.com/avatar.jpg', + age: 30, + phone: '1234567890', + gender: 'Male', + deviceId: 'abc123', + deviceToken: 'xyz456', + }); }); - it('should throw error when updating non-existent user', async () => { - const statusDto = { status: UserStatus.ACTIVE }; - const mockReq = { user: createUserMock() }; + it('should throw NotFoundException if user not found', async () => { + const userId = '123'; + const updateUserDto: UpdateUserDto = { + firstName: 'John', + lastName: 'Doe', + userId: '123', + }; - // Mocking the service to throw an error when the user is not found - jest - .spyOn(usersService, 'updateStatus') - .mockRejectedValue(new NotFoundException('User not found')); + jest.spyOn(service, 'update').mockRejectedValue(new NotFoundException()); await expect( - adminController.updateUserStatus(mockReq, userId, statusDto), - ).rejects.toThrow(NotFoundException); + controller.update({ user: { _id: '123' } }, userId, updateUserDto), + ).rejects.toThrowError(NotFoundException); }); }); + describe('newUserForm', () => { + it('should successfully create a user and return the URL', async () => { + const userData: CreateUserDto = { + email: 'newuser@example.com', + password: 'strongPassword123', + firstName: 'Jane', + lastName: 'Doe', + joinMethod: RegistrationMethod.SIGN_UP, + location: { + coordinates: [40.7128, -74.006], + type: 'Point', + }, + deviceId: 'device123', + deviceToken: 'token123', + }; - describe('Lead application endpoints', () => { - describe('getApplicationByEmail', () => { - it('should find an application by email', async () => { - const email = 'lead@example.com'; - const expectedResult = createUserMock({ email }); + const createdUser = { email: userData.email }; - jest - .spyOn(usersService, 'viewOneApplication') - .mockResolvedValue(expectedResult); + jest.spyOn(service, 'createUser').mockResolvedValue(createdUser); - const result = await adminController.getApplicationByEmail(email); + const result = await controller.newUserForm(userData); - expect(result).toEqual(expectedResult); - expect(usersService.viewOneApplication).toHaveBeenCalledWith(email); + expect(service.createUser).toHaveBeenCalledWith(userData); + expect(result).toEqual({ + url: `/leads/create?email=${createdUser.email}`, }); }); - describe('viewApplications', () => { - it('should return all lead applications', async () => { - const expectedResults = [ - createUserMock({ email: 'lead1@example.com' }), - createUserMock({ email: 'lead2@example.com' }), - ]; - - jest - .spyOn(usersService, 'viewApplications') - .mockResolvedValue(expectedResults); + it('should throw InternalServerErrorException if user creation fails', async () => { + const userData: CreateUserDto = { + email: 'newuser@example.com', + password: 'strongPassword123', + firstName: 'Jane', + lastName: 'Doe', + joinMethod: RegistrationMethod.SIGN_UP, // Using enum correctly + location: { + coordinates: [40.7128, -74.006], + type: 'Point', + }, + deviceId: 'device123', + deviceToken: 'token123', + }; - const result = await adminController.viewApplications(); + jest + .spyOn(service, 'createUser') + .mockRejectedValue(new Error('Failed to create user')); - expect(result).toEqual(expectedResults); - expect(usersService.viewApplications).toHaveBeenCalled(); - }); + await expect(controller.newUserForm(userData)).rejects.toThrowError( + InternalServerErrorException, + ); + expect(service.createUser).toHaveBeenCalledWith(userData); }); + }); + describe('RequestVerificationToken', () => { + it('should successfully request a verification token', async () => { + const userId = '123'; + const req = { user: { _id: userId } } as any; - describe('approveApplication', () => { - it('should approve a lead application', async () => { - const email = 'lead@example.com'; - const expectedResult = 'Application approved successfully'; - - jest - .spyOn(usersService, 'approveTempApplication') - .mockResolvedValue(expectedResult); + jest.spyOn(service, 'requestVerification').mockResolvedValue(undefined); - const result = await adminController.approveApplication(email); + const result = await controller.RequestVerificationToken(req, userId); - expect(result).toEqual(expectedResult); - expect(usersService.approveTempApplication).toHaveBeenCalledWith(email); - }); + expect(service.requestVerification).toHaveBeenCalledWith(req, userId); + expect(result).toBeUndefined(); }); - describe('reject', () => { - it('should reject a lead application with custom message', async () => { - const email = 'lead@example.com'; - const rejectDto = { message: 'Custom rejection message' }; - const expectedResult = 'Application rejected successfully'; + it('should throw an error if user is not found', async () => { + const userId = '123'; + const req = { user: { _id: userId } } as any; - jest - .spyOn(usersService, 'rejectTempApplication') - .mockResolvedValue(expectedResult); + jest + .spyOn(service, 'requestVerification') + .mockRejectedValue(new NotFoundException('User not found')); + + await expect( + controller.RequestVerificationToken(req, userId), + ).rejects.toThrowError(NotFoundException); + expect(service.requestVerification).toHaveBeenCalledWith(req, userId); + }); + }); + describe('deactivateAccount', () => { + it('should successfully deactivate the user account', async () => { + const userId = '123'; + const payload: DeactivateAccountDto = { reason: 'No longer needed' }; + const req = { user: { _id: userId } } as any; + + type UserWithOptionalId = Partial & { _id?: string }; + const deactivatedUser: UserWithOptionalId = { + _id: userId, + status: UserStatus.DEACTIVATED, + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + password: 'hashedPassword123', + profileSummary: 'Sample profile summary', + }; - const result = await adminController.reject(email, rejectDto); + jest + .spyOn(service, 'deactivateAccount') + .mockResolvedValue(deactivatedUser as User); - expect(result).toEqual(expectedResult); - expect(usersService.rejectTempApplication).toHaveBeenCalledWith( - email, - rejectDto.message, - ); - }); + const result = await controller.deactivateAccount(req, userId, payload); - it('should reject with default message when no message provided', async () => { - const email = 'lead@example.com'; - const rejectDto = {}; - const expectedResult = 'Application rejected successfully'; + expect(service.deactivateAccount).toHaveBeenCalledWith(userId); + expect(result).toEqual(deactivatedUser); + }); - jest - .spyOn(usersService, 'rejectTempApplication') - .mockResolvedValue(expectedResult); + it('should throw NotFoundException if the user is not found', async () => { + const userId = '123'; + const payload: DeactivateAccountDto = { reason: 'No longer needed' }; + const req = { user: { _id: userId } } as any; - const result = await adminController.reject(email, rejectDto); + jest + .spyOn(service, 'deactivateAccount') + .mockRejectedValue(new NotFoundException('User not found')); - expect(result).toEqual(expectedResult); - expect(usersService.rejectTempApplication).toHaveBeenCalledWith( - email, - 'Your application was rejected', - ); - }); + await expect( + controller.deactivateAccount(req, userId, payload), + ).rejects.toThrowError(NotFoundException); + expect(service.deactivateAccount).toHaveBeenCalledWith(userId); }); + }); + describe('requestReactivation', () => { + it('should successfully reactivate the user account', async () => { + const userId = '123'; + const payload: RequestReactivationDto = { + message: 'Please reactivate my account', + }; - describe('generateLink', () => { - it('should generate registration link for lead', async () => { - const email = 'lead@example.com'; - const expectedLink = 'http://example.com/register/token123'; + type UserWithOptionalId = Partial & { _id?: string }; - jest.spyOn(usersService, 'inviteLead').mockResolvedValue(expectedLink); + const reactivatedUser: UserWithOptionalId = { + _id: userId, + status: UserStatus.ACTIVE, + email: 'user@example.com', + firstName: 'John', + lastName: 'Doe', + }; - const result = await adminController.generateLink(email); + jest + .spyOn(service, 'requestReactivation') + .mockResolvedValue(reactivatedUser as User); - expect(result).toEqual({ link: expectedLink }); - expect(usersService.inviteLead).toHaveBeenCalledWith(email); - }); - }); + const result = await controller.requestReactivation(userId, payload); - describe('getUsersWithLeadRole', () => { - it('should return all users with lead role', async () => { - const expectedResults = [ - createUserMock({ role: [UserRole.LEAD] }), - createUserMock({ role: [UserRole.LEAD] }), - ]; + expect(service.requestReactivation).toHaveBeenCalledWith(userId); + expect(result).toEqual(reactivatedUser); + }); - jest - .spyOn(usersService, 'getUsersWithLeadRole') - .mockResolvedValue(expectedResults); + it('should throw NotFoundException if the user is not found', async () => { + const userId = '123'; + const payload: RequestReactivationDto = { + message: 'Please reactivate my account', + }; - const result = await adminController.getUsersWithLeadRole(); + jest + .spyOn(service, 'requestReactivation') + .mockRejectedValue(new NotFoundException('User not found')); - expect(result).toEqual(expectedResults); - expect(usersService.getUsersWithLeadRole).toHaveBeenCalled(); - }); + await expect( + controller.requestReactivation(userId, payload), + ).rejects.toThrowError(NotFoundException); + expect(service.requestReactivation).toHaveBeenCalledWith(userId); }); }); }); + +/* +'*generates a mock user object that can be used across multiple tests + *choose which part you want to override using createMock({parameter:new_value}) +*/ + +const createUserDocumentMock = ( + overrides: Partial = {}, +): UserDocument => { + const baseUser = createUserMock(overrides); // Use your existing createUserMock function + + return { + ...baseUser, // Include all the fields from createUserMock + _id: new Types.ObjectId(), // Add Mongoose ObjectId + save: jest.fn(), // Mock Mongoose Document methods + validate: jest.fn(), + remove: jest.fn(), + populate: jest.fn(), + } as unknown as UserDocument; // Cast to UserDocument type +}; + + diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c3165eb..d04579a 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -32,6 +32,8 @@ import { UserAddPhotoDto } from './dto/user-add-photo.dto'; import { DeactivateAccountDto } from './dto/deactivate-account.dto'; import { RequestReactivationDto } from './dto/request-reactivation.dto'; import { TempLeadDto } from './dto/temp-lead.dto'; +import { ForbiddenException } from '@nestjs/common'; + @ApiTags('users') @Controller('users') @@ -129,6 +131,12 @@ export class UsersController { @UseGuards(JwtUsersGuard) @Put('/profile-photo') async addPhoto(@Request() req: ApiReq, @Body() payload: UserAddPhotoDto) { + // added a check to confirm if the userId in the request matches the userId in the payload + if (req.user._id.toString() !== payload.userId) { + throw new ForbiddenException('User ID does not match the authenticated user.'); + } + + // if it matches then proceed with adding the photo if the IDs match return this.usersService.addPhoto(req.user._id.toString(), payload); } @@ -202,7 +210,7 @@ export class UsersController { @Param('userId') userId: string, @Body() payload: DeactivateAccountDto, ) { - // Optional: Use payload.reason if needed + return this.usersService.deactivateAccount(userId); }