diff --git a/package-lock.json b/package-lock.json index 2d36451..3b244a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,9 @@ "@faker-js/faker": "^8.4.1", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^11.0.15", + "@types/bcryptjs": "^2.4.4", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", @@ -3721,6 +3723,7 @@ } }, "node_modules/@nestjs/testing": { + "version": "11.0.15", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.15.tgz", "integrity": "sha512-IMeDGWuzcmEVClOC+jYVJFtyjIG4clzllndhp6ECNiWHNdKR55PU6ugjKBB8kZ5JszME8OaIXUYFTdiR5dcXXA==", @@ -3728,6 +3731,7 @@ "license": "MIT", "dependencies": { "tslib": "2.8.1" + }, "funding": { "type": "opencollective", @@ -3748,6 +3752,13 @@ } } }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, "node_modules/@nestjs/throttler": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-6.4.0.tgz", diff --git a/package.json b/package.json index 7aa907e..16cd0c2 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "@faker-js/faker": "^8.4.1", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", - "@nestjs/testing": "^11.0.15", + + "@types/bcryptjs": "^2.4.4", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", diff --git a/src/shared/auth/guards/jwt.users.guard.ts b/src/shared/auth/guards/jwt.users.guard.ts index 6275608..2ce8b1b 100644 --- a/src/shared/auth/guards/jwt.users.guard.ts +++ b/src/shared/auth/guards/jwt.users.guard.ts @@ -7,8 +7,10 @@ import { import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; import { hasRequiredRoles } from 'src/shared/utils'; + import { UserRole } from '../../interfaces'; + @Injectable() export class JwtUsersGuard extends AuthGuard('jwt-user') diff --git a/src/shared/datalogs/data.logs.controller.spec.ts b/src/shared/datalogs/data.logs.controller.spec.ts index 505aed0..5e968ec 100644 --- a/src/shared/datalogs/data.logs.controller.spec.ts +++ b/src/shared/datalogs/data.logs.controller.spec.ts @@ -1,30 +1,94 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { JwtService } from '@nestjs/jwt'; -import { TestModule } from '../testkits'; import { DataLogsController } from './data.logs.controller'; import { DataLogsService } from './data.logs.service'; +import { JwtAdminsGuard } from '../auth/guards/jwt.admins.guard'; +import { JwtUsersGuard } from '../auth/guards/jwt.users.guard'; +import { NotFoundException } from '@nestjs/common'; describe('DataLogsController', () => { - let dataLogsController: DataLogsController; + let controller: DataLogsController; + let logsService: DataLogsService; + + const mockLogsService = { + findAll: jest.fn(), + publicLog: jest.fn(), + remove: jest.fn(), + }; beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - imports: [TestModule], + const module: TestingModule = await Test.createTestingModule({ controllers: [DataLogsController], - providers: [DataLogsService], + providers: [ + { + provide: DataLogsService, + useValue: mockLogsService, + }, + ], }).compile(); - dataLogsController = app.get( - DataLogsController, - ) as DataLogsController; + controller = module.get(DataLogsController); + logsService = module.get(DataLogsService); + }); + + describe('findAll', () => { + it('should return all logs for admins', async () => { + const mockResponse = [{ id: '1', message: 'Log entry' }]; + mockLogsService.findAll.mockResolvedValue(mockResponse); + + const result = await controller.findAll({ query: {} } as any); + expect(result).toEqual(mockResponse); + expect(logsService.findAll).toHaveBeenCalledWith({ query: {} }); + }); + }); + + describe('logUser', () => { + it('should log a user activity', async () => { + const mockRequest = { body: { message: 'User action' } }; + const mockSource = 'user-app'; + mockLogsService.publicLog.mockResolvedValue({ success: true }); + + const result = await controller.logUser(mockRequest as any, mockSource); + expect(result).toEqual({ success: true }); + expect(logsService.publicLog).toHaveBeenCalledWith( + mockRequest, + mockSource, + ); + }); + }); - global.dataLogsService = app.get(DataLogsService); - global.jwtService = app.get(JwtService); + describe('logAdmin', () => { + it('should log an admin activity', async () => { + const mockRequest = { body: { message: 'Admin action' } }; + const mockSource = 'admin-dashboard'; + mockLogsService.publicLog.mockResolvedValue({ success: true }); + + const result = await controller.logAdmin(mockRequest as any, mockSource); + expect(result).toEqual({ success: true }); + expect(logsService.publicLog).toHaveBeenCalledWith( + mockRequest, + mockSource, + ); + }); }); - describe('dataLogsController', () => { - it('should return true for dataLogsController"', () => { - expect(!!dataLogsController).toBe(true); + describe('remove', () => { + it('should remove a data log by ID', async () => { + const mockLogId = 'log123'; + mockLogsService.remove.mockResolvedValue({ success: true }); + + const result = await controller.remove(mockLogId); + expect(result).toEqual({ success: true }); + expect(logsService.remove).toHaveBeenCalledWith(mockLogId); + }); + + it('should throw NotFoundException if log is not found', async () => { + mockLogsService.remove.mockImplementationOnce(() => { + throw new NotFoundException('Log not found'); + }); + + await expect(controller.remove('invalid-log-id')).rejects.toThrow( + NotFoundException, + ); }); }); }); diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts index a44f5a0..1dae38e 100644 --- a/src/users/users.controller.spec.ts +++ b/src/users/users.controller.spec.ts @@ -1,4 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; + +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { JwtUsersGuard } from 'src/shared/auth/guards/jwt.users.guard'; + import { ApiReq, ApplicationStatus, @@ -67,49 +72,472 @@ const createUserMock = ( } as UserDocument; }; -describe('UsersAdminController', () => { +import { RequestReactivationDto } from './dto/request-reactivation.dto'; +import { + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { UserChangePasswordDto } from './dto/user-change-password.dto'; +import { TempLeadDto } from './dto/temp-lead.dto'; +import { CreateUserDto } from 'src/shared/dtos/create-user.dto'; +import { User } from 'src/shared/schema'; + +describe('UsersController', () => { let controller: UsersController; - let adminController: UsersAdminsController; - let usersService: UsersService; + let service: UsersService; + + const mockUser = { _id: '123', email: 'tdennis.developer@gmail.com' }; + + const mockUsersService = { + findMe: jest.fn().mockResolvedValue(mockUser), + changePassword: jest.fn(), + findAll: jest.fn().mockResolvedValue([mockUser]), + findById: jest.fn().mockResolvedValue(mockUser), + findByEmail: jest.fn().mockResolvedValue(mockUser), + update: jest.fn(), + addPhoto: jest.fn(), + createTempRegistration: jest.fn().mockResolvedValue('temp-registration-id'), + createUser: jest.fn().mockResolvedValue(mockUser), + requestVerification: jest.fn(), + deactivateAccount: jest.fn(), + requestReactivation: jest.fn(), + paraseEncryptedParams: jest.fn().mockReturnValue({ + userId: '6706619dbee933e796f61484', + email: 'tdennis.developer@gmail.com', + }), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [TestModule, DBModule], - controllers: [UsersController, UsersAdminsController], - providers: [UsersService], - }).compile(); + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: mockUsersService, + }, + ], + }) + .overrideGuard(JwtUsersGuard) + .useValue({ canActivate: () => true }) + .compile(); controller = module.get(UsersController); - adminController = module.get(UsersAdminsController); - usersService = module.get(UsersService); + service = module.get(UsersService); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear mock calls between tests }); it('should be defined', () => { - expect(adminController).toBeDefined(); + expect(controller).toBeDefined(); + }); + + describe('myProfile', () => { + it('should return the user profile', async () => { + const req = { user: { _id: '123' } }; + const result = await controller.myProfile(req); + expect(result).toEqual(mockUser); + expect(service.findMe).toHaveBeenCalled(); + }); + }); + + describe('findAll', () => { + it('should return an array of users', async () => { + const req = {}; + const result = await controller.findAll(req); + expect(result).toEqual([mockUser]); + expect(service.findAll).toHaveBeenCalled(); + }); + }); + + describe('findOne', () => { + it('should return a user by ID', async () => { + const result = await controller.findOne('6706619dbee933e796f61484'); + expect(result).toEqual(mockUser); + expect(service.findById).toHaveBeenCalledWith('6706619dbee933e796f61484'); + }); + + it('should throw NotFoundException if user is not found', async () => { + jest.spyOn(service, 'findById').mockResolvedValueOnce(null); + + await expect(controller.findOne('invalid-id')).rejects.toThrow( + NotFoundException, + ); + }); }); describe('findByUsername', () => { - const requestMock = { email: 'test@example.com' }; - it('should return true', async () => { - const userMock = createUserMock({ - email: 'test@example.com', + it('should return true if user exists', async () => { + const result = await controller.findByUsername( + 'tdennis.developer@gmail.com', + ); + expect(result).toBe(true); + expect(service.findByEmail).toHaveBeenCalledWith( + 'tdennis.developer@gmail.com', + ); + }); + + it('should return false if user does not exist', async () => { + jest.spyOn(service, 'findByEmail').mockResolvedValueOnce(null); + const result = await controller.findByUsername('nonexistent@example.com'); + expect(result).toBe(false); + }); + }); + + describe('changePassword', () => { + it('should call changePassword with correct arguments', async () => { + const req = { user: { _id: '6706619dbee933e796f61484' } }; + const payload: UserChangePasswordDto = { + oldPassword: 'old', + newPassword: 'new', + confirmPassword: 'new', + }; + + await controller.changePassword(req, payload); + expect(service.changePassword).toHaveBeenCalledWith( + req, + '6706619dbee933e796f61484', + payload, + ); + }); + }); + + describe('createLead', () => { + it('should create a temporary lead registration', async () => { + const payload: TempLeadDto = { + email: 'lead@gmail.com', + leadPosition: 'manager', + firstName: 'Dennis', + lastName: 'Dennis', + createdAt: new Date(), + }; + const result = await controller.createLead(payload); + expect(result).toBe('temp-registration-id'); + }); + }); + + describe('register', () => { + it('should redirect to new user form if userId is not found', async () => { + mockUsersService.paraseEncryptedParams.mockReturnValueOnce({ + email: 'tdennis.developer@gmail.com', + }); + const result = await controller.register('encrypted-data'); + expect(result).toEqual({ + url: `/leads/new-user-form?email=tdennis.developer%40gmail.com`, + }); + }); + + it('should throw NotFoundException for invalid link', async () => { + mockUsersService.paraseEncryptedParams.mockImplementationOnce(() => { + throw new Error(); }); + + await expect(controller.register('invalid-data')).rejects.toThrow( + NotFoundException, + ); + }); + }); + // redirecting the dbquery to use the userMock jest.spyOn(usersService, 'findByEmail').mockResolvedValue(userMock); const result = await adminController.findByUsername(requestMock.email); - expect(result).toEqual(true); - expect(usersService.findByEmail).toHaveBeenCalledWith('test@example.com'); + + describe('newUserForm', () => { + it('should create a new user and return redirect URL', async () => { + const payload: CreateUserDto = { + email: 'tdennis.developer@gmail.com', + password: 'password', + joinMethod: RegistrationMethod.SIGN_UP, + firstName: 'Dennis', + lastName: 'Dennis', + }; + const result = await controller.newUserForm(payload); + expect(result).toEqual({ + url: `/leads/create?email=${payload.email}`, + }); }); - it('should return false', async () => { - jest.spyOn(usersService, 'findByEmail').mockResolvedValue(null); - const result = await adminController.findByUsername(requestMock.email); - expect(result).toEqual(false); - expect(usersService.findByEmail).toHaveBeenCalledWith('test@example.com'); + it('should throw InternalServerErrorException on failure', async () => { + jest.spyOn(service, 'createUser').mockImplementationOnce(() => { + throw new Error(); + }); + await expect( + controller.newUserForm({ + email: '', + password: '', + lastName: '', + firstName: '', + joinMethod: RegistrationMethod.SIGN_UP, + }), + ).rejects.toThrow(InternalServerErrorException); + }); + }); + + describe('deactivateAccount', () => { + it('should call deactivateAccount with correct arguments', async () => { + const req = { user: { _id: '123' } }; + const payload = { reason: 'No longer needed' }; + + await controller.deactivateAccount(req, '123', payload); + expect(service.deactivateAccount).toHaveBeenCalledWith('123'); + }); + }); + + describe('requestReactivation', () => { + it('should call requestReactivation with correct arguments', async () => { + const payload: RequestReactivationDto = { message: 'Please reactivate' }; + await controller.requestReactivation('123', payload); + expect(service.requestReactivation).toHaveBeenCalledWith('123'); + }); + }); + + +/* +'*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 createUserMock = (overrides: Partial = {}): User => { + return { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + password: 'hashedpassword123', + profileSummary: 'Experienced software engineer', + jobTitle: 'Senior Developer', + currentCompany: 'TechCorp', + photo: 'profilephoto.jpg', + age: 30, + phone: '123-456-7890', + userHandle: 'johnDoe123', + gender: 'male', + location: { + type: 'Point', + coordinates: [40.7128, -74.006], + }, + deviceId: 'device12345', + deviceToken: 'deviceToken12345', + role: [UserRole.USER], + leadPosition: 'Tech Lead', + applicationStatus: ApplicationStatus.PENDING, + nextApplicationTime: new Date(), + joinMethod: RegistrationMethod.SIGN_UP, + status: UserStatus.ACTIVE, + emailVerification: true, + pendingInvitation: false, + socials: { + phoneNumber: '24242424', + email: 'balbal', + }, + nextVerificationRequestDate: new Date(), + ...overrides, // Overrides will allow you to customize the mock as needed + }; +}; +describe('UsersController', () => { + let controller: UsersController; + let service: UsersService; + + const mockUser = { _id: '123', email: 'tdennis.developer@gmail.com' }; + + const mockUsersService = { + findMe: jest.fn().mockResolvedValue(mockUser), + changePassword: jest.fn(), + findAll: jest.fn().mockResolvedValue([mockUser]), + findById: jest.fn().mockResolvedValue(mockUser), + findByEmail: jest.fn().mockResolvedValue(mockUser), + update: jest.fn(), + addPhoto: jest.fn(), + createTempRegistration: jest.fn().mockResolvedValue('temp-registration-id'), + createUser: jest.fn().mockResolvedValue(mockUser), + requestVerification: jest.fn(), + deactivateAccount: jest.fn(), + requestReactivation: jest.fn(), + paraseEncryptedParams: jest.fn().mockReturnValue({ + userId: '6706619dbee933e796f61484', + email: 'tdennis.developer@gmail.com', + }), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [UsersController], + providers: [ + { + provide: UsersService, + useValue: mockUsersService, + }, + ], + }) + .overrideGuard(JwtUsersGuard) + .useValue({ canActivate: () => true }) + .compile(); + + controller = module.get(UsersController); + service = module.get(UsersService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + // Add the additional describe blocks from the second code here, as-is. + describe('myProfile', () => { + it('should return the user profile', async () => { + const req = { user: { _id: '123' } }; + const result = await controller.myProfile(req); + expect(result).toEqual(mockUser); + expect(service.findMe).toHaveBeenCalled(); }); }); + describe('findAll', () => { + it('should return an array of users', async () => { + const req = {}; + const result = await controller.findAll(req); + expect(result).toEqual([mockUser]); + expect(service.findAll).toHaveBeenCalled(); + }); + }); + + describe('findOne', () => { + it('should return a user by ID', async () => { + const result = await controller.findOne('6706619dbee933e796f61484'); + expect(result).toEqual(mockUser); + expect(service.findById).toHaveBeenCalledWith('6706619dbee933e796f61484'); + }); + + it('should throw NotFoundException if user is not found', async () => { + jest.spyOn(service, 'findById').mockResolvedValueOnce(null); + + await expect(controller.findOne('invalid-id')).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('findByUsername', () => { + it('should return true if user exists', async () => { + const result = await controller.findByUsername( + 'tdennis.developer@gmail.com', + ); + expect(result).toBe(true); + expect(service.findByEmail).toHaveBeenCalledWith( + 'tdennis.developer@gmail.com', + ); + }); + + it('should return false if user does not exist', async () => { + jest.spyOn(service, 'findByEmail').mockResolvedValueOnce(null); + const result = await controller.findByUsername('nonexistent@example.com'); + expect(result).toBe(false); + }); + }); + + describe('changePassword', () => { + it('should call changePassword with correct arguments', async () => { + const req = { user: { _id: '6706619dbee933e796f61484' } }; + const payload: UserChangePasswordDto = { + oldPassword: 'old', + newPassword: 'new', + confirmPassword: 'new', + }; + + await controller.changePassword(req, payload); + expect(service.changePassword).toHaveBeenCalledWith( + req, + '6706619dbee933e796f61484', + payload, + ); + }); + }); + + describe('createLead', () => { + it('should create a temporary lead registration', async () => { + const payload: TempLeadDto = { + email: 'lead@gmail.com', + leadPosition: 'manager', + firstName: 'Dennis', + lastName: 'Dennis', + createdAt: new Date(), + }; + const result = await controller.createLead(payload); + expect(result).toBe('temp-registration-id'); + }); + }); + + describe('register', () => { + it('should redirect to new user form if userId is not found', async () => { + mockUsersService.paraseEncryptedParams.mockReturnValueOnce({ + email: 'tdennis.developer@gmail.com', + }); + const result = await controller.register('encrypted-data'); + expect(result).toEqual({ + url: `/leads/new-user-form?email=tdennis.developer%40gmail.com`, + }); + }); + + it('should throw NotFoundException for invalid link', async () => { + mockUsersService.paraseEncryptedParams.mockImplementationOnce(() => { + throw new Error(); + }); + await expect(controller.register('invalid-data')).rejects.toThrow( + NotFoundException, + ); + }); + }); + + describe('newUserForm', () => { + it('should create a new user and return redirect URL', async () => { + const payload: CreateUserDto = { + email: 'tdennis.developer@gmail.com', + password: 'password', + joinMethod: RegistrationMethod.SIGN_UP, + firstName: 'Dennis', + lastName: 'Dennis', + }; + const result = await controller.newUserForm(payload); + expect(result).toEqual({ + url: `/leads/create?email=${payload.email}`, + }); + }); + + it('should throw InternalServerErrorException on failure', async () => { + jest.spyOn(service, 'createUser').mockImplementationOnce(() => { + throw new Error(); + }); + await expect( + controller.newUserForm({ + email: '', + password: '', + lastName: '', + firstName: '', + joinMethod: RegistrationMethod.SIGN_UP, + }), + ).rejects.toThrow(InternalServerErrorException); + }); + }); + + describe('deactivateAccount', () => { + it('should call deactivateAccount with correct arguments', async () => { + const req = { user: { _id: '123' } }; + const payload = { reason: 'No longer needed' }; + + await controller.deactivateAccount(req, '123', payload); + expect(service.deactivateAccount).toHaveBeenCalledWith('123'); + }); + }); + + describe('requestReactivation', () => { + it('should call requestReactivation with correct arguments', async () => { + const payload: RequestReactivationDto = { message: 'Please reactivate' }; + await controller.requestReactivation('123', payload); + expect(service.requestReactivation).toHaveBeenCalledWith('123'); +======= describe('findAll', () => { // clearing mock data between test to prevent data leaking beforeEach(() => { @@ -783,6 +1211,7 @@ describe('UsersAdminController', () => { expect(result).toEqual(expectedResults); expect(usersService.getUsersWithLeadRole).toHaveBeenCalled(); }); + }); }); }); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index c3165eb..035c1e6 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -105,8 +105,12 @@ export class UsersController { } @Get(':id') - findOne(@Param('id') id: string) { - return this.usersService.findById(id); + async findOne(@Param('id') id: string) { + const user = await this.usersService.findById(id); + if (!user) { + throw new NotFoundException(`User with ID ${id} not found`); + } + return user; } @ApiBearerAuth() diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 867dbd1..fedb076 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -6,7 +6,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "moduleNameMapper": { + "moduleNameMapper": { "^src/(.*)$": "/$1", "testTimeout": 10000 } diff --git a/tsconfig.json b/tsconfig.json index c032553..b99a83e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,10 @@ "sourceMap": true, "outDir": "./dist", "baseUrl": "./", + "paths": { + "src/*": ["src/*"], + "src/shared/*": ["src/shared/*"] + }, "incremental": true, "skipLibCheck": true, "strictNullChecks": false,