From 96ed2f21197f6821d49dd0ba8a5f41b35a86741c Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Mon, 23 Jan 2023 05:07:59 +0530 Subject: [PATCH] test: wrote tests for auth service file --- .../src/auth/auth.service.spec.ts | 336 +++++++++++++++++- 1 file changed, 331 insertions(+), 5 deletions(-) diff --git a/packages/hoppscotch-backend/src/auth/auth.service.spec.ts b/packages/hoppscotch-backend/src/auth/auth.service.spec.ts index 3a396b674..b9341a561 100644 --- a/packages/hoppscotch-backend/src/auth/auth.service.spec.ts +++ b/packages/hoppscotch-backend/src/auth/auth.service.spec.ts @@ -1,10 +1,24 @@ +import { HttpStatus } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { mockDeep } from 'jest-mock-extended'; +import { Account, PasswordlessVerification } from '@prisma/client'; +import { mockDeep, mockFn } from 'jest-mock-extended'; +import { + INVALID_EMAIL, + INVALID_MAGIC_LINK_DATA, + INVALID_REFRESH_TOKEN, + MAGIC_LINK_EXPIRED, + PASSWORDLESS_DATA_NOT_FOUND, + USER_NOT_FOUND, +} from 'src/errors'; import { MailerService } from 'src/mailer/mailer.service'; import { PrismaService } from 'src/prisma/prisma.service'; import { AuthUser } from 'src/types/AuthUser'; import { UserService } from 'src/user/user.service'; import { AuthService } from './auth.service'; +import * as O from 'fp-ts/Option'; +import { verifyMagicDto } from './dto/verify-magic.dto'; +import { DateTime } from 'luxon'; +import * as argon2 from 'argon2'; const mockPrisma = mockDeep(); const mockUser = mockDeep(); @@ -27,9 +41,321 @@ const user: AuthUser = { createdOn: currentTime, }; +const passwordlessData: PasswordlessVerification = { + deviceIdentifier: 'k23hb7u7gdcujhb', + token: 'jhhj24sdjvl', + userUid: user.uid, + expiresOn: new Date(), +}; + +const magicLinkVerify: verifyMagicDto = { + deviceIdentifier: 'Dscdc', + token: 'SDcsdc', +}; + +const accountDetails: Account = { + id: '123dcdc', + userId: user.uid, + provider: 'email', + providerAccountId: user.uid, + providerRefreshToken: 'dscsdc', + providerAccessToken: 'sdcsdcsdc', + providerScope: 'user.email', + loggedIn: currentTime, +}; + +let nowPlus30 = new Date(); +nowPlus30.setMinutes(nowPlus30.getMinutes() + 30); +nowPlus30 = new Date(nowPlus30); + +const encodedRefreshToken = + '$argon2id$v=19$m=65536,t=3,p=4$JTP8yZ8YXMHdafb5pB9Rfg$tdZrILUxMb9dQbu0uuyeReLgKxsgYnyUNbc5ZxQmy5I'; + describe('signIn', () => { - // should throw error if email is not in valid format - // should successfully create a new user account and return the passwordless details - // should successfully return the passwordless details for a existing user account - + test('should throw error if email is not in valid format', async () => { + const result = await authService.signIn('bbbgmail.com'); + expect(result).toEqualLeft({ + message: INVALID_EMAIL, + statusCode: HttpStatus.BAD_REQUEST, + }); + }); + + test('should successfully create a new user account and return the passwordless details', async () => { + // check to see if user exists, return error + mockUser.findUserByEmail.mockResolvedValue(O.none); + // create new user + mockUser.createUserMagic.mockResolvedValue(user); + // create new entry in passwordlessVerification table + mockPrisma.passwordlessVerification.create.mockResolvedValueOnce( + passwordlessData, + ); + + const result = await authService.signIn('dwight@dundermifflin.com'); + expect(result).toEqualRight({ + deviceIdentifier: passwordlessData.deviceIdentifier, + }); + }); + + test('should successfully return the passwordless details for a pre-existing user account', async () => { + // check to see if user exists, return error + mockUser.findUserByEmail.mockResolvedValueOnce(O.some(user)); + // create new entry in passwordlessVerification table + mockPrisma.passwordlessVerification.create.mockResolvedValueOnce( + passwordlessData, + ); + + const result = await authService.signIn('dwight@dundermifflin.com'); + expect(result).toEqualRight({ + deviceIdentifier: passwordlessData.deviceIdentifier, + }); + }); +}); + +describe('verifyPasswordlessTokens', () => { + test('should throw INVALID_MAGIC_LINK_DATA if data is invalid', async () => { + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualLeft({ + message: INVALID_MAGIC_LINK_DATA, + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + test('should throw USER_NOT_FOUND if user is invalid', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + passwordlessData, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.none); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualLeft({ + message: USER_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + test('should successfully return auth token pair with provider account existing', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + { + ...passwordlessData, + expiresOn: nowPlus30, + }, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.some(user)); + // checkIfProviderAccountExists + mockPrisma.account.findUnique.mockResolvedValueOnce(accountDetails); + // mockPrisma.account.findUnique.mockResolvedValueOnce(null); + // generateAuthTokens + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockResolvedValueOnce(user); + // deletePasswordlessVerificationToken + mockPrisma.passwordlessVerification.delete.mockResolvedValueOnce( + passwordlessData, + ); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualRight({ + access_token: user.refreshToken, + refresh_token: user.refreshToken, + }); + }); + + test('should successfully return auth token pair with provider account not existing', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + { + ...passwordlessData, + expiresOn: nowPlus30, + }, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.some(user)); + // checkIfProviderAccountExists + mockPrisma.account.findUnique.mockResolvedValueOnce(null); + mockUser.createUserSSO.mockResolvedValueOnce(user); + // generateAuthTokens + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockResolvedValueOnce(user); + // deletePasswordlessVerificationToken + mockPrisma.passwordlessVerification.delete.mockResolvedValueOnce( + passwordlessData, + ); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualRight({ + access_token: user.refreshToken, + refresh_token: user.refreshToken, + }); + }); + + test('should throw MAGIC_LINK_EXPIRED if passwordless token is expired', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + passwordlessData, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.some(user)); + // checkIfProviderAccountExists + mockPrisma.account.findUnique.mockResolvedValueOnce(accountDetails); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualLeft({ + message: MAGIC_LINK_EXPIRED, + statusCode: HttpStatus.UNAUTHORIZED, + }); + }); + + test('should throw USER_NOT_FOUND when updating refresh tokens fails', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + { + ...passwordlessData, + expiresOn: nowPlus30, + }, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.some(user)); + // checkIfProviderAccountExists + mockPrisma.account.findUnique.mockResolvedValueOnce(accountDetails); + // mockPrisma.account.findUnique.mockResolvedValueOnce(null); + // generateAuthTokens + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualLeft({ + message: USER_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + test('should throw PASSWORDLESS_DATA_NOT_FOUND when deleting passwordlessVerification entry from DB', async () => { + // validatePasswordlessTokens + mockPrisma.passwordlessVerification.findUniqueOrThrow.mockResolvedValueOnce( + { + ...passwordlessData, + expiresOn: nowPlus30, + }, + ); + // findUserById + mockUser.findUserById.mockResolvedValue(O.some(user)); + // checkIfProviderAccountExists + mockPrisma.account.findUnique.mockResolvedValueOnce(accountDetails); + // mockPrisma.account.findUnique.mockResolvedValueOnce(null); + // generateAuthTokens + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockResolvedValueOnce(user); + // deletePasswordlessVerificationToken + mockPrisma.passwordlessVerification.delete.mockRejectedValueOnce( + 'RecordNotFound', + ); + + const result = await authService.verifyPasswordlessTokens(magicLinkVerify); + expect(result).toEqualLeft({ + message: PASSWORDLESS_DATA_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); +}); + +describe('generateAuthTokens', () => { + test('should successfully generate tokens with valid inputs', async () => { + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockResolvedValueOnce(user); + + const result = await authService.generateAuthTokens(user.uid); + expect(result).toEqualRight({ + access_token: 'hbfvdkhjbvkdvdfjvbnkhjb', + refresh_token: 'hbfvdkhjbvkdvdfjvbnkhjb', + }); + }); + + test('should throw USER_NOT_FOUND when updating refresh tokens fails', async () => { + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await authService.generateAuthTokens(user.uid); + expect(result).toEqualLeft({ + message: USER_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); +}); + +jest.mock('argon2', () => { + return { + verify: jest.fn((x, y) => { + if (y === null) return false; + return true; + }), + hash: jest.fn(), + }; +}); + +describe('refreshAuthTokens', () => { + test('should throw USER_NOT_FOUND when updating refresh tokens fails', async () => { + // generateAuthTokens + mockJWT.sign.mockReturnValue(user.refreshToken); + mockPrisma.user.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await authService.refreshAuthTokens( + '$argon2id$v=19$m=65536,t=3,p=4$MvVOam2clCOLtJFGEE26ZA$czvA5ez9hz+A/LML8QRgqgaFuWa5JcbwkH6r+imTQbs', + user, + ); + expect(result).toEqualLeft({ + message: USER_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + test('should throw USER_NOT_FOUND when user is invalid', async () => { + const result = await authService.refreshAuthTokens( + 'jshdcbjsdhcbshdbc', + null, + ); + expect(result).toEqualLeft({ + message: USER_NOT_FOUND, + statusCode: HttpStatus.NOT_FOUND, + }); + }); + + test('should successfully refresh the tokens and generate a new auth token pair', async () => { + // generateAuthTokens + mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb'); + mockPrisma.user.update.mockResolvedValueOnce({ + ...user, + refreshToken: 'sdhjcbjsdhcbshjdcb', + }); + + const result = await authService.refreshAuthTokens( + '$argon2id$v=19$m=65536,t=3,p=4$MvVOam2clCOLtJFGEE26ZA$czvA5ez9hz+A/LML8QRgqgaFuWa5JcbwkH6r+imTQbs', + user, + ); + expect(result).toEqualRight({ + access_token: 'sdhjcbjsdhcbshjdcb', + refresh_token: 'sdhjcbjsdhcbshjdcb', + }); + }); + + test('should throw INVALID_REFRESH_TOKEN when the refresh token is invalid', async () => { + // generateAuthTokens + mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb'); + mockPrisma.user.update.mockResolvedValueOnce({ + ...user, + refreshToken: 'sdhjcbjsdhcbshjdcb', + }); + + const result = await authService.refreshAuthTokens(null, user); + expect(result).toEqualLeft({ + message: INVALID_REFRESH_TOKEN, + statusCode: HttpStatus.NOT_FOUND, + }); + }); });