From ba553f28da8b0c909ec4b46bf959c423d340ec00 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 6 Feb 2021 13:14:26 +0100 Subject: [PATCH] Tests: Rewrote AuthService unit test The unit test now uses per test mocking of the necessary functions instead of one mock in the beforeEach call. Also some tests got expanded to cover more error cases. Signed-off-by: Philip Molares --- src/auth/auth.service.spec.ts | 248 +++++++++++++++++++++++----------- 1 file changed, 169 insertions(+), 79 deletions(-) diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index a8ca9ffcb..8506b5070 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -13,86 +13,47 @@ import { UsersModule } from '../users/users.module'; import { Identity } from '../users/identity.entity'; import { LoggerModule } from '../logger/logger.module'; import { AuthToken } from './auth-token.entity'; -import { TokenNotValidError } from '../errors/errors'; +import { NotInDBError, TokenNotValidError } from '../errors/errors'; +import { Repository } from 'typeorm'; describe('AuthService', () => { let service: AuthService; let user: User; let authToken: AuthToken; + let userRepo: Repository; + let authTokenRepo: Repository; beforeEach(async () => { - user = { - authTokens: [], - createdAt: new Date(), - displayName: 'hardcoded', - id: '1', - identities: [], - ownedNotes: [], - historyEntries: [], - updatedAt: new Date(), - userName: 'Testy', - }; - - authToken = { - accessTokenHash: '', - createdAt: new Date(), - id: 1, - label: 'testIdentifier', - keyId: 'abc', - lastUsed: null, - user: null, - validUntil: null, - }; - const module: TestingModule = await Test.createTestingModule({ providers: [ AuthService, { provide: getRepositoryToken(AuthToken), - useValue: {}, + useClass: Repository, }, ], imports: [PassportModule, UsersModule, LoggerModule], }) - .overrideProvider(getRepositoryToken(AuthToken)) - .useValue({ - findOne: (): AuthToken => { - return { - ...authToken, - user: user, - }; - }, - save: async (entity: AuthToken) => { - if (entity.lastUsed === undefined) { - expect(entity.lastUsed).toBeUndefined(); - } else { - expect(entity.lastUsed.getTime()).toBeLessThanOrEqual( - new Date().getTime(), - ); - } - return entity; - }, - remove: async (entity: AuthToken) => { - expect(entity).toEqual({ - ...authToken, - user: user, - }); - }, - }) .overrideProvider(getRepositoryToken(Identity)) .useValue({}) .overrideProvider(getRepositoryToken(User)) - .useValue({ - findOne: (): User => { - return { - ...user, - authTokens: [authToken], - }; - }, - }) + .useClass(Repository) .compile(); service = module.get(AuthService); + userRepo = module.get>(getRepositoryToken(User)); + authTokenRepo = module.get>( + getRepositoryToken(AuthToken), + ); + + user = User.create('hardcoded', 'Testy') as User; + authToken = AuthToken.create( + user, + 'testToken', + 'testKeyId', + 'abc', + new Date(new Date().getTime() + 60000), // make this AuthToken valid for 1min + ) as AuthToken; }); it('should be defined', () => { @@ -121,6 +82,9 @@ describe('AuthService', () => { describe('getTokensByUsername', () => { it('works', async () => { + jest + .spyOn(userRepo, 'findOne') + .mockResolvedValueOnce({ ...user, authTokens: [authToken] }); const tokens = await service.getTokensByUsername(user.userName); expect(tokens).toHaveLength(1); expect(tokens).toEqual([authToken]); @@ -128,9 +92,14 @@ describe('AuthService', () => { }); describe('getAuthToken', () => { + const token = 'testToken'; it('works', async () => { - const token = 'testToken'; - authToken.accessTokenHash = await service.hashPassword(token); + const accessTokenHash = await service.hashPassword(token); + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ + ...authToken, + user: user, + accessTokenHash: accessTokenHash, + }); const authTokenFromCall = await service.getAuthTokenAndValidate( authToken.keyId, token, @@ -138,12 +107,61 @@ describe('AuthService', () => { expect(authTokenFromCall).toEqual({ ...authToken, user: user, + accessTokenHash: accessTokenHash, + }); + }); + describe('fails:', () => { + it('AuthToken could not be found', async () => { + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce(undefined); + try { + await service.getAuthTokenAndValidate(authToken.keyId, token); + } catch (e) { + expect(e).toBeInstanceOf(NotInDBError); + } + }); + it('AuthToken has wrong hash', async () => { + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ + ...authToken, + user: user, + accessTokenHash: 'the wrong hash', + }); + try { + await service.getAuthTokenAndValidate(authToken.keyId, token); + } catch (e) { + expect(e).toBeInstanceOf(TokenNotValidError); + } + }); + it('AuthToken has wrong validUntil Date', async () => { + const accessTokenHash = await service.hashPassword(token); + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ + ...authToken, + user: user, + accessTokenHash: accessTokenHash, + validUntil: new Date(1549312452000), + }); + try { + await service.getAuthTokenAndValidate(authToken.keyId, token); + } catch (e) { + expect(e).toBeInstanceOf(TokenNotValidError); + } }); }); }); describe('setLastUsedToken', () => { it('works', async () => { + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValueOnce({ + ...authToken, + user: user, + lastUsed: new Date(1549312452000), + }); + jest + .spyOn(authTokenRepo, 'save') + .mockImplementationOnce(async (authTokenSaved, _) => { + expect(authTokenSaved.keyId).toEqual(authToken.keyId); + expect(authTokenSaved.lastUsed).not.toEqual(1549312452000); + return authToken; + }); await service.setLastUsedToken(authToken.keyId); }); }); @@ -151,7 +169,21 @@ describe('AuthService', () => { describe('validateToken', () => { it('works', async () => { const token = 'testToken'; - authToken.accessTokenHash = await service.hashPassword(token); + const accessTokenHash = await service.hashPassword(token); + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({ + ...user, + authTokens: [authToken], + }); + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({ + ...authToken, + user: user, + accessTokenHash: accessTokenHash, + }); + jest + .spyOn(authTokenRepo, 'save') + .mockImplementationOnce(async (_, __) => { + return authToken; + }); const userByToken = await service.validateToken( `${authToken.keyId}.${token}`, ); @@ -160,34 +192,88 @@ describe('AuthService', () => { authTokens: [authToken], }); }); - it('fails on too long token', () => { - expect( - service.validateToken(`${authToken.keyId}.${'a'.repeat(73)}`), - ).rejects.toBeInstanceOf(TokenNotValidError); + describe('fails:', () => { + it('the secret is missing', () => { + expect( + service.validateToken(`${authToken.keyId}`), + ).rejects.toBeInstanceOf(TokenNotValidError); + }); + it('the secret is too long', () => { + expect( + service.validateToken(`${authToken.keyId}.${'a'.repeat(73)}`), + ).rejects.toBeInstanceOf(TokenNotValidError); + }); }); }); describe('removeToken', () => { it('works', async () => { + jest.spyOn(authTokenRepo, 'findOne').mockResolvedValue({ + ...authToken, + user: user, + }); + jest + .spyOn(authTokenRepo, 'remove') + .mockImplementationOnce(async (token, __) => { + expect(token).toEqual({ + ...authToken, + user: user, + }); + return authToken; + }); await service.removeToken(authToken.keyId); }); }); describe('createTokenForUser', () => { - it('works', async () => { - const identifier = 'identifier2'; - const token = await service.createTokenForUser( - user.userName, - identifier, - 0, - ); - expect(token.label).toEqual(identifier); - expect( - token.validUntil.getTime() - - (new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000), - ).toBeLessThanOrEqual(10000); - expect(token.lastUsed).toBeNull(); - expect(token.secret.startsWith(token.keyId)).toBeTruthy(); + describe('works', () => { + const identifier = 'testIdentifier'; + it('with validUntil 0', async () => { + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({ + ...user, + authTokens: [authToken], + }); + jest + .spyOn(authTokenRepo, 'save') + .mockImplementationOnce(async (authTokenSaved: AuthToken, _) => { + expect(authTokenSaved.lastUsed).toBeUndefined(); + return authTokenSaved; + }); + const token = await service.createTokenForUser( + user.userName, + identifier, + 0, + ); + expect(token.label).toEqual(identifier); + expect( + token.validUntil.getTime() - + (new Date().getTime() + 2 * 365 * 24 * 60 * 60 * 1000), + ).toBeLessThanOrEqual(10000); + expect(token.lastUsed).toBeNull(); + expect(token.secret.startsWith(token.keyId)).toBeTruthy(); + }); + it('with validUntil not 0', async () => { + jest.spyOn(userRepo, 'findOne').mockResolvedValueOnce({ + ...user, + authTokens: [authToken], + }); + jest + .spyOn(authTokenRepo, 'save') + .mockImplementationOnce(async (authTokenSaved: AuthToken, _) => { + expect(authTokenSaved.lastUsed).toBeUndefined(); + return authTokenSaved; + }); + const validUntil = new Date().getTime() + 30000; + const token = await service.createTokenForUser( + user.userName, + identifier, + validUntil, + ); + expect(token.label).toEqual(identifier); + expect(token.validUntil.getTime()).toEqual(validUntil); + expect(token.lastUsed).toBeNull(); + expect(token.secret.startsWith(token.keyId)).toBeTruthy(); + }); }); }); @@ -203,6 +289,10 @@ describe('AuthService', () => { describe('toAuthTokenDto', () => { it('works', async () => { + const authToken = new AuthToken(); + authToken.keyId = 'testKeyId'; + authToken.label = 'testLabel'; + authToken.createdAt = new Date(); const tokenDto = await service.toAuthTokenDto(authToken); expect(tokenDto.keyId).toEqual(authToken.keyId); expect(tokenDto.lastUsed).toBeNull();