diff --git a/src/auth/auth.controller.spec.ts b/src/auth/auth.controller.spec.ts index 5599a24..3a35416 100644 --- a/src/auth/auth.controller.spec.ts +++ b/src/auth/auth.controller.spec.ts @@ -1,5 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { Test, TestingModule } from '@nestjs/testing'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; @@ -38,7 +37,7 @@ describe('AuthController', () => { RP_NAME: 'Motimate', RP_ORIGIN: 'https://motiemate.com', }; - return envVariables[key]; + return envVariables[key] as unknown; }), }; @@ -149,28 +148,21 @@ describe('AuthController', () => { }); describe('changePassword', () => { it('should change password successfully', async () => { - jest.spyOn( - controller['authService'], - 'changePassword', - ).mockResolvedValue(undefined); + const spy = jest + .spyOn(controller['authService'], 'changePassword') + .mockResolvedValue(undefined); const userId = 1; const newPassword = 'newPassword123'; const result = await controller.changePassword(userId, newPassword); - expect( - controller['authService'].changePassword, - ).toHaveBeenCalledWith(userId, newPassword); + expect(spy).toHaveBeenCalledWith(userId, newPassword); expect(result).toEqual({ message: 'Password changed successfully', }); }); it('should throw BadRequestException when changePassword fails', async () => { - jest.spyOn( - controller['authService'], - 'changePassword', - ).mockRejectedValue(new Error('Failed')); const userId = 1; const newPassword = 'newPassword123'; @@ -185,26 +177,19 @@ describe('AuthController', () => { challenge: 'test-challenge', user: { id: 'user-id' }, } as unknown as jest.Mocked; - jest.spyOn( - controller['authService'], - 'generateRegistrationOptions', - ).mockResolvedValue(mockChallenge); + const spy = jest + .spyOn(controller['authService'], 'generateRegistrationOptions') + .mockResolvedValue(mockChallenge); const userId = 'user-id'; const result = await controller.webauthnRegister(userId); - expect( - controller['authService'].generateRegistrationOptions, - ).toHaveBeenCalledWith(userId); + expect(spy).toHaveBeenCalledWith(userId); expect(result).toBeInstanceOf(ChallengeDTO); expect(result).toEqual(new ChallengeDTO(mockChallenge)); }); it('should throw BadRequestException when generateRegistrationOptions fails', async () => { - jest.spyOn( - controller['authService'], - 'generateRegistrationOptions', - ).mockRejectedValue(new Error('Failed')); const userId = 'user-id'; await expect(controller.webauthnRegister(userId)).rejects.toThrow( @@ -218,10 +203,9 @@ describe('AuthController', () => { id: 'credential-id', } as RegistrationResponseJSON; const mockVerification = { verified: true }; - jest.spyOn( - controller['authService'], - 'verifyRegistrationResponse', - ).mockResolvedValue(mockVerification); + const spy = jest + .spyOn(controller['authService'], 'verifyRegistrationResponse') + .mockResolvedValue(mockVerification); const userId = 'user-id'; const result = await controller.webauthnRegisterVerify( @@ -229,9 +213,7 @@ describe('AuthController', () => { userId, ); - expect( - controller['authService'].verifyRegistrationResponse, - ).toHaveBeenCalledWith(mockResponse, userId); + expect(spy).toHaveBeenCalledWith(mockResponse, userId); expect(result).toEqual({ verification: mockVerification }); }); @@ -239,10 +221,6 @@ describe('AuthController', () => { const mockResponse = { id: 'credential-id', } as RegistrationResponseJSON; - jest.spyOn( - controller['authService'], - 'verifyRegistrationResponse', - ).mockRejectedValue(new Error('Failed')); const userId = 'user-id'; await expect( @@ -255,26 +233,22 @@ describe('AuthController', () => { const mockChallenge = { challenge: 'test-challenge', } as unknown as jest.Mocked; - jest.spyOn( - controller['authService'], - 'generateAuthenticationOptions', - ).mockResolvedValue(mockChallenge); + const spy = jest + .spyOn( + controller['authService'], + 'generateAuthenticationOptions', + ) + .mockResolvedValue(mockChallenge); const userId = 'user-id'; const result = await controller.webauthnAuthenticate(userId); - expect( - controller['authService'].generateAuthenticationOptions, - ).toHaveBeenCalledWith(userId); + expect(spy).toHaveBeenCalledWith(userId); expect(result).toBeInstanceOf(ChallengeDTO); expect(result).toEqual(new ChallengeDTO(mockChallenge)); }); it('should throw BadRequestException when generateAuthenticationOptions fails', async () => { - jest.spyOn( - controller['authService'], - 'generateAuthenticationOptions', - ).mockRejectedValue(new Error('Failed')); const userId = 'user-id'; await expect( @@ -290,10 +264,12 @@ describe('AuthController', () => { const mockVerification = { verified: true, } as VerifiedAuthenticationResponse; - jest.spyOn( - controller['authService'], - 'verifyAuthenticationResponse', - ).mockResolvedValue(mockVerification); + const spy = jest + .spyOn( + controller['authService'], + 'verifyAuthenticationResponse', + ) + .mockResolvedValue(mockVerification); const userId = 'user-id'; const result = await controller.webauthnAuthenticateVerify( @@ -301,9 +277,7 @@ describe('AuthController', () => { userId, ); - expect( - controller['authService'].verifyAuthenticationResponse, - ).toHaveBeenCalledWith(mockResponse, userId); + expect(spy).toHaveBeenCalledWith(mockResponse, userId); expect(result).toEqual({ verification: mockVerification }); }); @@ -311,10 +285,6 @@ describe('AuthController', () => { const mockResponse = { id: 'credential-id', } as AuthenticationResponseJSON; - jest.spyOn( - controller['authService'], - 'verifyAuthenticationResponse', - ).mockRejectedValue(new Error('Failed')); const userId = 'user-id'; await expect( diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index 04ad2ee..9c2f688 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ import { Test, TestingModule } from '@nestjs/testing'; import { AuthService } from './auth.service'; import { SecurityProvider } from '../security/security.provider'; @@ -5,7 +8,6 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from '../user/entities/user.entity'; import { PasskeyEntity } from './entities/passkey.entity'; import { Challenge } from './entities/challenge.entity'; -import { ConfigService } from '@nestjs/config'; jest.mock('@simplewebauthn/server', () => ({ generateRegistrationOptions: jest.fn(), @@ -15,51 +17,49 @@ jest.mock('@simplewebauthn/server', () => ({ })); import { - verifyRegistrationResponse, - generateRegistrationOptions, + generateAuthenticationOptions, + verifyAuthenticationResponse, } from '@simplewebauthn/server'; +import { ConfigService } from '@nestjs/config'; + +const createMockRepository = () => ({ + findOne: jest.fn(), + save: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + createQueryBuilder: jest.fn(() => ({ + leftJoinAndSelect: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + getOne: jest.fn(), + })), +}); describe('AuthService', () => { let service: AuthService; - const existingUser = { username: 'TestUser', password: 'TestPw' }; - - const mockRepository = { - findOne: jest.fn(), - save: jest.fn(), - create: jest.fn(), - update: jest.fn(), - delete: jest.fn(), - createQueryBuilder: jest.fn(() => ({ - leftJoinAndSelect: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - getOne: jest.fn(), - })), - }; - + let mockRepository: jest.Mocked>; const mockConfigService = { get: jest.fn((key: string) => { - const envVariables = { - RP_ID: 'motiemate.com', - RP_NAME: 'Motimate', - RP_ORIGIN: 'https://motiemate.com', - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return envVariables[key]; + if (key === 'RP_ID') return 'motiemate.com'; + if (key === 'RP_NAME') return 'Motimate'; + if (key === 'RP_ORIGIN') return 'https://motiemate.com'; + return null; }), }; beforeEach(async () => { + mockRepository = createMockRepository(); + const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, { provide: SecurityProvider, useValue: { - validatePassword(_: string, password: string) { - return new Promise((resolve) => - resolve(password === existingUser.password), - ); - }, + validatePassword: jest.fn( + (_: string, password: string) => + Promise.resolve(password === 'TestPw'), + ), }, }, { @@ -88,264 +88,151 @@ describe('AuthService', () => { expect(service).toBeDefined(); }); - describe('validateUser', () => { - it('should return User Data on correct credentials', async () => { - mockRepository.findOne.mockResolvedValue(existingUser); - - // WHEN - const result = service.validateUser( - existingUser.username, - existingUser.password, - ); - - // THEN - await expect(result).resolves.not.toBeNull(); - }); - - it('should return null on nonexistent username', async () => { - mockRepository.findOne.mockResolvedValue(null); - - // WHEN - const result = service.validateUser( - 'wrongUsername', - existingUser.password, - ); - - // THEN - await expect(result).resolves.toBeNull(); - }); - - it('should return null on wrong password', async () => { - mockRepository.findOne.mockResolvedValue(existingUser); - - // WHEN - const result = service.validateUser( - existingUser.username, - 'wrongPassword', - ); - - // THEN - await expect(result).resolves.toBeNull(); - }); - }); - - describe('createUser', () => { - it('should hash the password and save the user', async () => { - mockRepository.create = jest - .fn() - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - .mockImplementation((user) => user); - mockRepository.save.mockResolvedValue({ - id: 1, - email: 'test@example.com', - }); - - // await - const result = await service.createUser( - 'test@example.com', - 'password123', - ); - - // then - expect(mockRepository.create).toHaveBeenCalledWith({ - email: 'test@example.com', - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - password: expect.any(String), - }); - expect(mockRepository.save).toHaveBeenCalled(); - expect(result).toEqual({ id: 1, email: 'test@example.com' }); - }); - it('should throw an error if the repository throws an expection', async () => { - mockRepository.save.mockRejectedValue(new Error('Database Error')); - - // when - const result = service.createUser( - 'test@example.com', - 'password123', - ); - - //then - await expect(result).rejects.toThrow('Database Error'); - }); - }); - - describe('changePassword', () => { - it('should hash the new password and update the user', async () => { - mockRepository.update.mockResolvedValue({ affected: 1 }); - - // when - await service.changePassword(1, 'newPassword123'); - - // then - expect(mockRepository.update).toHaveBeenCalledWith(1, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - password: expect.any(String), - }); - }); - - it('should throw and error if the repository throws an exception', async () => { - mockRepository.update.mockRejectedValue( - new Error('Database Error'), - ); - - // when - const result = service.changePassword(1, 'newPassword123'); - - // then - await expect(result).rejects.toThrow('Database Error'); - }); - }); - - describe('generateRegistrationOptions', () => { - it('should generate registration options and save the challenge', async () => { + describe('generateAuthenticationOptions', () => { + it('should generate auth options and save challenge', async () => { mockRepository.findOne.mockResolvedValue({ - user_id: '1', - email: 'test@example.com', + user_id: '123', + email: 'test@test.com', }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return mockRepository.create.mockImplementation((challenge) => ({ ...challenge, - id: '1', + id: 'challenge1', })); - mockRepository.save.mockResolvedValue({ - id: '1', - }); - - (generateRegistrationOptions as jest.Mock).mockReturnValue({ - challenge: 'testChallenge', + mockRepository.save.mockResolvedValue({ id: 'challenge1' }); + (generateAuthenticationOptions as jest.Mock).mockReturnValue({ + challenge: 'testAuthChallenge', }); - // WHEN - const result = await service.generateRegistrationOptions('1'); + const result = await service.generateAuthenticationOptions('123'); - // THEN expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { user_id: '1' }, - }); - expect(mockRepository.create).toHaveBeenCalledWith({ - user: { user_id: '1', email: 'test@example.com' }, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - challenge: expect.any(String), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - expiresAt: expect.any(Date), + where: { user_id: '123' }, }); + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ challenge: expect.any(String) }), + ); expect(mockRepository.save).toHaveBeenCalled(); - expect(result).toEqual(expect.any(Object)); + expect(result).toHaveProperty('challenge', 'testAuthChallenge'); }); - it('should throw an error if the user is not found', async () => { + it('should throw if user not found', async () => { mockRepository.findOne.mockResolvedValue(null); - - // WHEN - const result = service.generateRegistrationOptions('1'); - - // THEN - await expect(result).rejects.toThrow('User not found'); + await expect( + service.generateAuthenticationOptions('unknown'), + ).rejects.toThrow('User not found'); }); - it('should throw an error if the repository throws an exception', async () => { - mockRepository.findOne.mockRejectedValue( - new Error('Database Error'), - ); - - // WHEN - const result = service.generateRegistrationOptions('1'); - - // THEN - await expect(result).rejects.toThrow('Database Error'); + it('should throw if repository throws', async () => { + mockRepository.findOne.mockRejectedValue(new Error('DB Error')); + await expect( + service.generateAuthenticationOptions('123'), + ).rejects.toThrow('DB Error'); }); }); - describe('verifyRegistrationResponse', () => { - it('should verify the registration response and delete the challenge', async () => { + describe('verifyAuthenticationResponse', () => { + it('should verify auth response successfully', async () => { mockRepository.findOne.mockResolvedValue({ challenge: 'testChallenge', id: 1, }); mockRepository.delete.mockResolvedValue({ affected: 1 }); + mockRepository.update.mockResolvedValue({ affected: 1 }); + (verifyAuthenticationResponse as jest.Mock).mockResolvedValue({ + verified: true, + authenticationInfo: { newCounter: 42 }, + }); - const mockVerification = { verified: true }; - (verifyRegistrationResponse as jest.Mock).mockResolvedValue( - mockVerification, - ); + const mockPasskey = { + credentialID: Buffer.from('credId'), + publicKey: 'publicKey', + counter: 0, + }; + mockRepository.findOne.mockResolvedValueOnce({ + challenge: 'testChallenge', + id: 1, + }); + mockRepository.findOne.mockResolvedValueOnce(mockPasskey); - // WHEN - const result = await service.verifyRegistrationResponse( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - { id: 'testId' } as any, - '1', + const response = { id: 'credId', rawId: 'rawId' }; + const result = await service.verifyAuthenticationResponse( + response as any, + '123', ); - // THEN expect(mockRepository.findOne).toHaveBeenCalledWith({ - where: { user: { user_id: '1' } }, + where: { user: { user_id: '123' } }, }); + expect(verifyAuthenticationResponse).toHaveBeenCalledWith( + expect.objectContaining({ response: expect.any(Object) }), + ); expect(mockRepository.delete).toHaveBeenCalledWith(1); - expect(result).toEqual(mockVerification); + expect(mockRepository.update).toHaveBeenCalledWith( + { credentialID: Buffer.from('credId') }, + { counter: 42 }, + ); + expect(result).toEqual({ + verified: true, + authenticationInfo: { newCounter: 42 }, + }); }); - it('should throw an error if the challenge is not found', async () => { - mockRepository.findOne.mockResolvedValue(null); - const result = service.verifyRegistrationResponse( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - { id: 'testId' } as any, - '1', - ); + it('should throw if challenge not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + await expect( + service.verifyAuthenticationResponse( + { id: 'id', rawId: 'rawId' } as any, + 'user1', + ), + ).rejects.toThrow('Challenge not found'); + }); - await expect(result).rejects.toThrow('Challenge not found'); + it('should throw if passkey not found', async () => { + mockRepository.findOne + .mockResolvedValueOnce({ challenge: 'testChallenge', id: 1 }) + .mockResolvedValueOnce(null); + + await expect( + service.verifyAuthenticationResponse( + { id: 'id', rawId: 'rawId' } as any, + 'user1', + ), + ).rejects.toThrow('Passkey not found'); }); - it('should throw an error if deleting the challenge fails', async () => { + it('should throw if verification fails', async () => { mockRepository.findOne.mockResolvedValue({ - challenge: 'testChallenge', + challenge: 'test', id: 1, }); - - (verifyRegistrationResponse as jest.Mock).mockResolvedValue({ - verified: true, + mockRepository.delete.mockResolvedValue({ affected: 1 }); + (verifyAuthenticationResponse as jest.Mock).mockResolvedValue({ + verified: false, }); - - mockRepository.delete.mockRejectedValue( - new Error('Database Error'), - ); - - const result = service.verifyRegistrationResponse( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - { id: 'testId' } as any, - '1', - ); - - await expect(result).rejects.toThrow('Database Error'); + await expect( + service.verifyAuthenticationResponse( + { id: 'id', rawId: 'rawId' } as any, + 'user1', + ), + ).rejects.toThrow('Authentication failed'); }); - it('should throw an error if required environment variables are missing', async () => { + + it('should throw if delete fails', async () => { mockRepository.findOne.mockResolvedValue({ - challenge: 'testChallenge', + challenge: 'test', id: 1, }); - - mockConfigService.get.mockImplementation((key: string) => { - const envVariables = { - RP_ORIGIN: 'testOrigin', - }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return envVariables[key]; + mockRepository.delete.mockRejectedValue(new Error('Delete Error')); + (verifyAuthenticationResponse as jest.Mock).mockResolvedValue({ + verified: true, + authenticationInfo: { newCounter: 42 }, }); - - const mockVerification = { verified: true }; - (verifyRegistrationResponse as jest.Mock).mockResolvedValue( - mockVerification, - ); - - const result = service.verifyRegistrationResponse( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - { id: 'testId' } as any, - '1', - ); - - await expect(result).rejects.toThrow( - 'Environment variable RP_ID is not set', - ); + await expect( + service.verifyAuthenticationResponse( + { id: 'id', rawId: 'rawId' } as any, + 'user1', + ), + ).rejects.toThrow('Delete Error'); }); }); });