From 2e1a3bcdc00662da19fb8e5d617b3d14bae57be0 Mon Sep 17 00:00:00 2001 From: Thiago Ramalho Date: Fri, 26 Apr 2024 16:17:40 -0300 Subject: [PATCH 1/3] feat: improve unit tests --- .../src/auth-jwt.module-definition.ts | 4 +- packages/nestjs-auth-jwt/src/index.spec.ts | 24 ++ .../__fixtures__/user/user.entity.fixture.ts | 13 + .../src/auth-local.module-definition.spec.ts | 235 ++++++++++++++ .../src/auth-local.module-definition.ts | 20 +- .../src/auth-local.strategy.spec.ts | 25 +- packages/nestjs-auth-local/src/index.spec.ts | 29 ++ .../auth-local-validate-user.service.spec.ts | 100 ++++++ .../src/auth-recovery.controller.spec.ts | 104 ++++++ .../auth-recovery.module-definition.spec.ts | 299 ++++++++++++++++++ .../src/auth-recovery.module-definition.ts | 34 +- .../src/auth-recovery.utils.spec.ts | 13 + .../nestjs-auth-recovery/src/index.spec.ts | 44 +++ ...auth-recovery-notification.service.spec.ts | 19 ++ .../src/auth-refresh.module-definition.ts | 6 +- .../nestjs-auth-refresh/src/index.spec.ts | 10 + .../decorators/auth-public.decorator.spec.ts | 20 ++ .../decorators/auth-user.decorator.spec.ts | 7 + .../authentication.exception.spec.ts | 23 ++ .../passport-strategy.factory.spec.ts | 38 +++ .../src/guards/auth.guard.spec.ts | 50 ++- .../services/validate-user.service.spec.ts | 35 ++ 22 files changed, 1116 insertions(+), 36 deletions(-) create mode 100644 packages/nestjs-auth-jwt/src/index.spec.ts create mode 100644 packages/nestjs-auth-local/src/auth-local.module-definition.spec.ts create mode 100644 packages/nestjs-auth-local/src/index.spec.ts create mode 100644 packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts create mode 100644 packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts create mode 100644 packages/nestjs-auth-recovery/src/auth-recovery.module-definition.spec.ts create mode 100644 packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts create mode 100644 packages/nestjs-auth-recovery/src/index.spec.ts create mode 100644 packages/nestjs-auth-refresh/src/index.spec.ts create mode 100644 packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts create mode 100644 packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts create mode 100644 packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts create mode 100644 packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts create mode 100644 packages/nestjs-authentication/src/services/validate-user.service.spec.ts diff --git a/packages/nestjs-auth-jwt/src/auth-jwt.module-definition.ts b/packages/nestjs-auth-jwt/src/auth-jwt.module-definition.ts index 5b7c8df00..8de1756ba 100644 --- a/packages/nestjs-auth-jwt/src/auth-jwt.module-definition.ts +++ b/packages/nestjs-auth-jwt/src/auth-jwt.module-definition.ts @@ -50,8 +50,8 @@ function definitionTransform( definition: DynamicModule, extras: AuthJwtOptionsExtrasInterface, ): DynamicModule { - const { providers = [] } = definition; - const { global = false } = extras; + const { providers } = definition; + const { global } = extras; return { ...definition, diff --git a/packages/nestjs-auth-jwt/src/index.spec.ts b/packages/nestjs-auth-jwt/src/index.spec.ts new file mode 100644 index 000000000..bf4fa2c65 --- /dev/null +++ b/packages/nestjs-auth-jwt/src/index.spec.ts @@ -0,0 +1,24 @@ +import { + AuthJwtModule, + AuthJwtStrategy, + AuthJwtGuard, + JwtAuthGuard, +} from './index'; + +describe('Index', () => { + it('AuthJwtModule should be imported', () => { + expect(AuthJwtModule).toBeInstanceOf(Function); + }); + + it('AuthJwtStrategy should be imported', () => { + expect(AuthJwtStrategy).toBeInstanceOf(Function); + }); + + it('AuthJwtGuard should be imported', () => { + expect(AuthJwtGuard).toBeInstanceOf(Function); + }); + + it('JwtAuthGuard should be imported', () => { + expect(JwtAuthGuard).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts index d6ef80693..eaf18be53 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts @@ -1,12 +1,25 @@ import { ReferenceIdInterface } from '@concepta/ts-core'; import { AuthLocalCredentialsInterface } from '../../interfaces/auth-local-credentials.interface'; +import { Allow, IsOptional, MinLength } from 'class-validator'; +import { allowedNodeEnvironmentFlags } from 'process'; export class UserFixture implements ReferenceIdInterface, AuthLocalCredentialsInterface { + @Allow() id!: string; + + @MinLength(3) username!: string; + + @Allow() active!: boolean; + + @Allow() password!: string; + + @IsOptional() passwordHash!: string | null; + + @IsOptional() passwordSalt!: string | null; } diff --git a/packages/nestjs-auth-local/src/auth-local.module-definition.spec.ts b/packages/nestjs-auth-local/src/auth-local.module-definition.spec.ts new file mode 100644 index 000000000..2333c99d9 --- /dev/null +++ b/packages/nestjs-auth-local/src/auth-local.module-definition.spec.ts @@ -0,0 +1,235 @@ +import { IssueTokenService } from '@concepta/nestjs-authentication'; +import { PasswordValidationService } from '@concepta/nestjs-password'; +import { FactoryProvider } from '@nestjs/common'; +import { mock } from 'jest-mock-extended'; +import { UserLookupServiceFixture } from './__fixtures__/user/user-lookup.service.fixture'; +import { + AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_SETTINGS_TOKEN, + AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, +} from './auth-local.constants'; +import { AuthLocalController } from './auth-local.controller'; +import { + createAuthLocalControllers, + createAuthLocalExports, + createAuthLocalIssueTokenServiceProvider, + createAuthLocalPasswordValidationServiceProvider, + createAuthLocalUserLookupServiceProvider, + createAuthLocalValidateUserServiceProvider, +} from './auth-local.module-definition'; +import { AuthLocalValidateUserService } from './services/auth-local-validate-user.service'; +import { JwtIssueService } from '@concepta/nestjs-jwt'; + +describe('Auth-local.module-definition', () => { + describe(createAuthLocalExports.name, () => { + it('should return an array with the expected tokens', () => { + const result = createAuthLocalExports(); + expect(result).toEqual([ + AUTH_LOCAL_MODULE_SETTINGS_TOKEN, + AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, + AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN, + ]); + }); + }); + + describe(createAuthLocalControllers.name, () => { + it('should return a default AuthLocalController', () => { + const result = createAuthLocalControllers(); + expect(result).toEqual([AuthLocalController]); + }); + + it('should return a default AuthLocalController', () => { + const result = createAuthLocalControllers({}); + expect(result).toEqual([AuthLocalController]); + }); + + it('should return a default AuthLocalController', () => { + const result = createAuthLocalControllers({ controllers: undefined }); + expect(result).toEqual([AuthLocalController]); + }); + + it('should return the provided controllers', () => { + const customController = class CustomController {}; + const result = createAuthLocalControllers({ + controllers: [customController], + }); + expect(result).toEqual([customController]); + }); + + it('should return an empty array if the controllers option is an empty array', () => { + const result = createAuthLocalControllers({ controllers: [] }); + expect(result).toEqual([]); + }); + + it('should return an array with AuthLocalController if the controllers option includes AuthLocalController', () => { + const result = createAuthLocalControllers({ + controllers: [AuthLocalController], + }); + expect(result).toEqual([AuthLocalController]); + }); + + it('should return an array with AuthLocalController and the provided controllers if the controllers option includes AuthLocalController and other controllers', () => { + const customController = class CustomController {}; + const result = createAuthLocalControllers({ + controllers: [AuthLocalController, customController], + }); + expect(result).toEqual([AuthLocalController, customController]); + }); + }); + + describe(createAuthLocalValidateUserServiceProvider.name, () => { + class TestUserLookupService extends UserLookupServiceFixture {} + class TestPasswordValidationService extends PasswordValidationService {} + class TestAuthLocalValidateUserService extends AuthLocalValidateUserService {} + + const testUserLookupService = mock(); + const testPasswordValidationService = mock(); + const testAuthLocalValidateUserService = + new TestAuthLocalValidateUserService( + testUserLookupService, + testPasswordValidationService, + ); + + it('should return a default validateUserService', async () => { + const provider = + createAuthLocalValidateUserServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(AuthLocalValidateUserService); + }); + + it('should return a validateUserService from initialization', async () => { + const provider = + createAuthLocalValidateUserServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + validateUserService: testAuthLocalValidateUserService, + }); + + expect(useFactoryResult).toBeInstanceOf(TestAuthLocalValidateUserService); + }); + + it('should return a override validateUserService', async () => { + const provider = createAuthLocalValidateUserServiceProvider({ + validateUserService: testAuthLocalValidateUserService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(TestAuthLocalValidateUserService); + }); + }); + + describe(createAuthLocalIssueTokenServiceProvider.name, () => { + class TestIssueTokenService extends IssueTokenService {} + + const jwtIssuer = mock(); + const testIssueTokenService = new TestIssueTokenService(jwtIssuer); + + it('should return an issueTokenService', async () => { + const provider = + createAuthLocalIssueTokenServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory( + {}, + testIssueTokenService, + ); + + expect(useFactoryResult).toBeInstanceOf(TestIssueTokenService); + }); + + it('should return an issueTokenService from initialization', async () => { + const provider = + createAuthLocalIssueTokenServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + issueTokenService: testIssueTokenService, + }); + + expect(useFactoryResult).toBeInstanceOf(TestIssueTokenService); + }); + + it('should return an overridden issueTokenService', async () => { + const provider = createAuthLocalIssueTokenServiceProvider({ + issueTokenService: testIssueTokenService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(TestIssueTokenService); + }); + }); + + describe(createAuthLocalUserLookupServiceProvider.name, () => { + class LookupService extends UserLookupServiceFixture {} + class OverrideLookupService extends UserLookupServiceFixture {} + + it('should return a default userLookupService', async () => { + const provider: FactoryProvider = + createAuthLocalUserLookupServiceProvider({ + userLookupService: new OverrideLookupService(), + }) as FactoryProvider; + + // useFactory is for when class was initially defined + const useFactoryResult = await provider.useFactory({ + userLookupService: new LookupService(), + }); + expect(useFactoryResult).toBeInstanceOf(OverrideLookupService); + }); + + it('should return a userLookupService from initialization', async () => { + const provider: FactoryProvider = + createAuthLocalUserLookupServiceProvider() as FactoryProvider; + + // useFactory is for when class was initially defined + const useFactoryResult = await provider.useFactory({ + userLookupService: new LookupService(), + }); + expect(useFactoryResult).toBeInstanceOf(LookupService); + }); + }); + + describe(createAuthLocalPasswordValidationServiceProvider.name, () => { + class TestPasswordValidationService extends PasswordValidationService {} + + const testPasswordValidationService = new TestPasswordValidationService(); + + it('should return an issueTokenService', async () => { + const provider = + createAuthLocalPasswordValidationServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory( + {}, + testPasswordValidationService, + ); + + expect(useFactoryResult).toBeInstanceOf(TestPasswordValidationService); + }); + + it('should return an issueTokenService from initialization', async () => { + const provider = + createAuthLocalPasswordValidationServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + passwordValidationService: testPasswordValidationService, + }); + + expect(useFactoryResult).toBeInstanceOf(TestPasswordValidationService); + }); + + it('should return an overridden issueTokenService', async () => { + const provider = createAuthLocalPasswordValidationServiceProvider({ + passwordValidationService: testPasswordValidationService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(TestPasswordValidationService); + }); + }); +}); diff --git a/packages/nestjs-auth-local/src/auth-local.module-definition.ts b/packages/nestjs-auth-local/src/auth-local.module-definition.ts index e0aca869c..9500402ea 100644 --- a/packages/nestjs-auth-local/src/auth-local.module-definition.ts +++ b/packages/nestjs-auth-local/src/auth-local.module-definition.ts @@ -58,8 +58,8 @@ function definitionTransform( definition: DynamicModule, extras: AuthLocalOptionsExtrasInterface, ): DynamicModule { - const { providers = [] } = definition; - const { controllers, global = false } = extras; + const { providers } = definition; + const { controllers, global } = extras; return { ...definition, @@ -104,7 +104,7 @@ export function createAuthLocalProviders(options: { } export function createAuthLocalControllers( - overrides: Pick = {}, + overrides?: Pick, ): DynamicModule['controllers'] { return overrides?.controllers !== undefined ? overrides.controllers @@ -126,7 +126,7 @@ export function createAuthLocalOptionsProvider( } export function createAuthLocalValidateUserServiceProvider( - optionsOverrides?: AuthLocalOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_LOCAL_MODULE_VALIDATE_USER_SERVICE_TOKEN, @@ -136,7 +136,7 @@ export function createAuthLocalValidateUserServiceProvider( AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN, ], useFactory: async ( - options: AuthLocalOptionsInterface, + options: Pick, userLookupService: AuthLocalUserLookupServiceInterface, passwordValidationService: PasswordValidationServiceInterface, ) => @@ -150,13 +150,13 @@ export function createAuthLocalValidateUserServiceProvider( } export function createAuthLocalIssueTokenServiceProvider( - optionsOverrides?: AuthLocalOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_LOCAL_MODULE_ISSUE_TOKEN_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN, IssueTokenService], useFactory: async ( - options: AuthLocalOptionsInterface, + options: Pick, defaultService: IssueTokenServiceInterface, ) => optionsOverrides?.issueTokenService ?? @@ -166,13 +166,13 @@ export function createAuthLocalIssueTokenServiceProvider( } export function createAuthLocalPasswordValidationServiceProvider( - optionsOverrides?: AuthLocalOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_LOCAL_MODULE_PASSWORD_VALIDATION_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN, PasswordValidationService], useFactory: async ( - options: AuthLocalOptionsInterface, + options: Pick, defaultService: PasswordValidationServiceInterface, ) => optionsOverrides?.passwordValidationService ?? @@ -182,7 +182,7 @@ export function createAuthLocalPasswordValidationServiceProvider( } export function createAuthLocalUserLookupServiceProvider( - optionsOverrides?: AuthLocalOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_LOCAL_MODULE_USER_LOOKUP_SERVICE_TOKEN, diff --git a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts index 32e20f1f4..2511128d8 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts @@ -1,5 +1,5 @@ import { PasswordValidationService } from '@concepta/nestjs-password'; -import { UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { randomUUID } from 'crypto'; import { mock } from 'jest-mock-extended'; import { AuthLocalStrategy } from './auth-local.strategy'; @@ -9,6 +9,8 @@ import { AuthLocalValidateUserServiceInterface } from './interfaces/auth-local-v import { AuthLocalValidateUserService } from './services/auth-local-validate-user.service'; import { UserFixture } from './__fixtures__/user/user.entity.fixture'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { AuthLocalValidateUserInterface } from './interfaces/auth-local-validate-user.interface'; describe(AuthLocalStrategy, () => { const USERNAME = 'username'; @@ -60,11 +62,32 @@ describe(AuthLocalStrategy, () => { expect(result.id).toBe(user.id); }); + it('should return user', async () => { + jest + .spyOn(validateUserService, 'validateUser') + .mockImplementationOnce((_dto: AuthLocalValidateUserInterface) => { + return null as unknown as Promise>; + }); + + const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); + await expect(t).rejects.toThrow(UnauthorizedException); + }); + it('should throw error on validateOrReject', async () => { const t = () => authLocalStrategy.validate(USERNAME, ''); await expect(t).rejects.toThrow(); }); + it('should throw BadRequest on validateOrReject', async () => { + const classValidator = require('class-validator'); + jest + .spyOn(classValidator, 'validateOrReject') + .mockRejectedValueOnce(BadRequestException); + + const t = () => authLocalStrategy.validate(USERNAME, PASSWORD); + await expect(t).rejects.toThrow(BadRequestException); + }); + it('should return no user on userLookupService.byUsername', async () => { jest.spyOn(userLookUpService, 'byUsername').mockResolvedValue(null); diff --git a/packages/nestjs-auth-local/src/index.spec.ts b/packages/nestjs-auth-local/src/index.spec.ts new file mode 100644 index 000000000..32d116772 --- /dev/null +++ b/packages/nestjs-auth-local/src/index.spec.ts @@ -0,0 +1,29 @@ +import { + AuthLocalModule, + AuthLocalController, + AuthLocalLoginDto, + AuthLocalGuard, + LocalAuthGuard, +} from './index'; + +describe('Index', () => { + it('AuthLocalModule should be imported', () => { + expect(AuthLocalModule).toBeInstanceOf(Function); + }); + + it('AuthLocalController should be imported', () => { + expect(AuthLocalController).toBeInstanceOf(Function); + }); + + it('AuthLocalLoginDto should be imported', () => { + expect(AuthLocalLoginDto).toBeInstanceOf(Function); + }); + + it('AuthLocalGuard should be imported', () => { + expect(AuthLocalGuard).toBeInstanceOf(Function); + }); + + it('LocalAuthGuard should be imported', () => { + expect(LocalAuthGuard).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts new file mode 100644 index 000000000..6ac8de7c5 --- /dev/null +++ b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts @@ -0,0 +1,100 @@ +import { AuthLocalValidateUserService } from './auth-local-validate-user.service'; +import { AuthLocalUserLookupServiceInterface } from '../interfaces/auth-local-user-lookup-service.interface'; +import { PasswordValidationServiceInterface } from '@concepta/nestjs-password'; +import { AuthLocalValidateUserInterface } from '../interfaces/auth-local-validate-user.interface'; +import { ReferenceIdInterface } from '@concepta/ts-core'; +import { UnauthorizedException } from '@nestjs/common'; +import { AuthLocalCredentialsInterface } from '../interfaces/auth-local-credentials.interface'; + +describe('AuthLocalValidateUserService', () => { + const USERNAME = 'test'; + const PASSWORD = 'test'; + + let service: AuthLocalValidateUserService; + let userLookupService: AuthLocalUserLookupServiceInterface; + let passwordValidationService: PasswordValidationServiceInterface; + + beforeEach(() => { + userLookupService = { + byUsername: jest.fn(), + } as unknown as AuthLocalUserLookupServiceInterface; + + passwordValidationService = { + validateObject: jest.fn(), + } as unknown as PasswordValidationServiceInterface; + + service = new AuthLocalValidateUserService( + userLookupService, + passwordValidationService, + ); + }); + + describe('validateUser', () => { + const USER = { + active: false, + id: 'uuid', + username: 'username', + password: 'password', + passwordHash: 'hash', + passwordSalt: 'salt', + }; + it('should throw an error if no user is found for the given username', async () => { + jest.spyOn(userLookupService, 'byUsername').mockResolvedValue(null); + + const t = () => + service.validateUser({ + username: USERNAME, + password: PASSWORD, + } as AuthLocalValidateUserInterface); + await expect(t).rejects.toThrowError( + 'No user found for username: ' + USERNAME, + ); + }); + + it('should throw an error if the user is inactive', async () => { + jest.spyOn(userLookupService, 'byUsername').mockResolvedValue(USER); + jest.spyOn(service, 'isActive').mockResolvedValue(false); + + const t = () => + service.validateUser({ + username: USERNAME, + password: PASSWORD, + } as AuthLocalValidateUserInterface); + await expect(t).rejects.toThrowError( + "User with username '" + USERNAME + "' is inactive", + ); + }); + + it('should throw an error if the password is invalid', async () => { + jest.spyOn(userLookupService, 'byUsername').mockResolvedValue(USER); + jest.spyOn(service, 'isActive').mockResolvedValue(true); + jest + .spyOn(passwordValidationService, 'validateObject') + .mockResolvedValue(false); + + const t = () => + service.validateUser({ + username: USER.username, + password: USER.password, + } as AuthLocalValidateUserInterface); + await expect(t).rejects.toThrowError( + 'Invalid password for username: ' + USER.username, + ); + }); + + it('should return the user if the user is found, active, and the password is valid', async () => { + jest.spyOn(userLookupService, 'byUsername').mockResolvedValue(USER); + jest.spyOn(service, 'isActive').mockResolvedValue(true); + jest + .spyOn(passwordValidationService, 'validateObject') + .mockResolvedValue(true); + + const result = await service.validateUser({ + username: USER.username, + password: USER.password, + } as AuthLocalValidateUserInterface); + + expect(result).toEqual(USER); + }); + }); +}); diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts new file mode 100644 index 000000000..a1c9bd829 --- /dev/null +++ b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts @@ -0,0 +1,104 @@ +import { AuthRecoveryController } from './auth-recovery.controller'; +import { AuthRecoveryService } from './services/auth-recovery.service'; +import { AuthRecoveryRecoverLoginDto } from './dto/auth-recovery-recover-login.dto'; +import { AuthRecoveryRecoverPasswordDto } from './dto/auth-recovery-recover-password.dto'; +import { AuthRecoveryUpdatePasswordDto } from './dto/auth-recovery-update-password.dto'; +import { mock } from 'jest-mock-extended'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; + +describe('AuthRecoveryController', () => { + let controller: AuthRecoveryController; + let authRecoveryService: AuthRecoveryService; + const dto: AuthRecoveryRecoverLoginDto = { + email: 'test@example.com', + }; + const passwordDto: AuthRecoveryUpdatePasswordDto = { + passcode: '123456', + newPassword: 'newPassword', + }; + beforeEach(() => { + authRecoveryService = mock(); + controller = new AuthRecoveryController(authRecoveryService); + }); + + describe('recoverLogin', () => { + it('should call recoverLogin method of AuthRecoveryService', async () => { + const recoverLoginSpy = jest.spyOn(authRecoveryService, 'recoverLogin'); + + await controller.recoverLogin(dto); + + expect(recoverLoginSpy).toHaveBeenCalledWith(dto.email); + }); + }); + + describe('recoverPassword', () => { + it('should call recoverPassword method of AuthRecoveryService', async () => { + const recoverPasswordSpy = jest.spyOn( + authRecoveryService, + 'recoverPassword', + ); + + await controller.recoverPassword(dto); + + expect(recoverPasswordSpy).toHaveBeenCalledWith(dto.email); + }); + }); + + describe('validatePasscode', () => { + it('should call validatePasscode method of AuthRecoveryService', async () => { + const validatePasscodeSpy = jest + .spyOn(authRecoveryService, 'validatePasscode') + .mockResolvedValue(null); + + const t = () => controller.validatePasscode(passwordDto.passcode); + await expect(t).rejects.toThrow(NotFoundException); + + expect(validatePasscodeSpy).toHaveBeenCalledWith(passwordDto.passcode); + }); + + it('should call validatePasscode method of AuthRecoveryService', async () => { + const validatePasscodeSpy = jest + .spyOn(authRecoveryService, 'validatePasscode') + .mockResolvedValue({ + assignee: { + id: '1', + }, + }); + + await controller.validatePasscode(passwordDto.passcode); + + expect(validatePasscodeSpy).toHaveBeenCalledWith(passwordDto.passcode); + }); + }); + + describe('updatePassword', () => { + it('should call updatePassword method of AuthRecoveryService', async () => { + const updatePasswordSpy = jest + .spyOn(authRecoveryService, 'updatePassword') + .mockResolvedValue(null); + + const t = () => controller.updatePassword(passwordDto); + await expect(t).rejects.toThrow(BadRequestException); + + expect(updatePasswordSpy).toHaveBeenCalledWith( + passwordDto.passcode, + passwordDto.newPassword, + ); + }); + + it('should call updatePassword method of AuthRecoveryService', async () => { + const updatePasswordSpy = jest + .spyOn(authRecoveryService, 'updatePassword') + .mockResolvedValue({ + id: '1', + }); + + await controller.updatePassword(passwordDto); + + expect(updatePasswordSpy).toHaveBeenCalledWith( + passwordDto.passcode, + passwordDto.newPassword, + ); + }); + }); +}); diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.spec.ts new file mode 100644 index 000000000..0756cfa32 --- /dev/null +++ b/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.spec.ts @@ -0,0 +1,299 @@ +import { FactoryProvider } from '@nestjs/common'; +import { mock } from 'jest-mock-extended'; +import { OtpServiceFixture } from './__fixtures__/otp/otp.service.fixture'; +import { UserLookupServiceFixture } from './__fixtures__/user/services/user-lookup.service.fixture'; +import { UserMutateServiceFixture } from './__fixtures__/user/services/user-mutate.service.fixture'; +import { + AUTH_RECOVERY_MODULE_EMAIL_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_OTP_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_SETTINGS_TOKEN, + AUTH_RECOVERY_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_USER_MUTATE_SERVICE_TOKEN, +} from './auth-recovery.constants'; +import { AuthRecoveryController } from './auth-recovery.controller'; +import { + createAuthRecoveryControllers, + createAuthRecoveryEmailServiceProvider, + createAuthRecoveryEntityManagerProxyProvider, + createAuthRecoveryExports, + createAuthRecoveryNotificationServiceProvider, + createAuthRecoveryOtpServiceProvider, + createAuthRecoveryUserLookupServiceProvider, + createAuthRecoveryUserMutateServiceProvider, +} from './auth-recovery.module-definition'; +import { AuthRecoveryEmailServiceInterface } from './interfaces/auth-recovery-email.service.interface'; +import { AuthRecoveryService } from './services/auth-recovery.service'; +import { AuthRecoveryUserLookupServiceInterface } from './interfaces/auth-recovery-user-lookup.service.interface'; +import { AuthRecoveryUserMutateServiceInterface } from './interfaces/auth-recovery-user-mutate.service.interface'; +import { AuthRecoveryNotificationServiceInterface } from './interfaces/auth-recovery-notification.service.interface'; +import { AuthRecoveryNotificationService } from './services/auth-recovery-notification.service'; +import { EntityManagerProxy } from '@concepta/typeorm-common'; + +describe('AuthRecoveryModuleDefinition', () => { + const mockEmailService = mock(); + const mockAuthRecoveryNotification = + mock(); + const mockEntityManagerProxy = mock(); + const mockAuthRecoveryOptions = { + emailService: mockEmailService, + otpService: new OtpServiceFixture(), + userLookupService: new UserLookupServiceFixture(), + userMutateService: new UserMutateServiceFixture(), + }; + describe(createAuthRecoveryExports.name, () => { + it('should return an array with the expected tokens', () => { + const result = createAuthRecoveryExports(); + expect(result).toEqual([ + AUTH_RECOVERY_MODULE_SETTINGS_TOKEN, + AUTH_RECOVERY_MODULE_OTP_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_EMAIL_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_USER_LOOKUP_SERVICE_TOKEN, + AUTH_RECOVERY_MODULE_USER_MUTATE_SERVICE_TOKEN, + AuthRecoveryService, + ]); + }); + }); + + describe(createAuthRecoveryControllers.name, () => { + it('should return a default AuthRecoveryController', () => { + const result = createAuthRecoveryControllers(); + expect(result).toEqual([AuthRecoveryController]); + }); + + it('should return a default AuthRecoveryController', () => { + const result = createAuthRecoveryControllers({}); + expect(result).toEqual([AuthRecoveryController]); + }); + + it('should return a default AuthRecoveryController', () => { + const result = createAuthRecoveryControllers({ controllers: undefined }); + expect(result).toEqual([AuthRecoveryController]); + }); + + it('should return the provided controllers', () => { + const customController = class CustomController {}; + const result = createAuthRecoveryControllers({ + controllers: [customController], + }); + expect(result).toEqual([customController]); + }); + + it('should return an empty array if the controllers option is an empty array', () => { + const result = createAuthRecoveryControllers({ controllers: [] }); + expect(result).toEqual([]); + }); + + it('should return an array with AuthRecoveryController if the controllers option includes AuthRecoveryController', () => { + const result = createAuthRecoveryControllers({ + controllers: [AuthRecoveryController], + }); + expect(result).toEqual([AuthRecoveryController]); + }); + + it('should return an array with AuthRecoveryController and the provided controllers if the controllers option includes AuthRecoveryController and other controllers', () => { + const customController = class CustomController {}; + const result = createAuthRecoveryControllers({ + controllers: [AuthRecoveryController, customController], + }); + expect(result).toEqual([AuthRecoveryController, customController]); + }); + }); + + describe(createAuthRecoveryOtpServiceProvider.name, () => { + class TestOtpService extends OtpServiceFixture {} + + const testOtpService = mock(); + + it('should return a default otpService', async () => { + const provider = + createAuthRecoveryOtpServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBe(undefined); + }); + + it('should return an otpService from initialization', async () => { + const provider = + createAuthRecoveryOtpServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + otpService: testOtpService, + }); + + expect(useFactoryResult).toBe(testOtpService); + }); + + it('should return an overridden otpService', async () => { + const provider = createAuthRecoveryOtpServiceProvider({ + otpService: mockAuthRecoveryOptions.otpService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(OtpServiceFixture); + }); + }); + + describe(createAuthRecoveryEmailServiceProvider.name, () => { + it('should return a have no default', async () => { + const provider = + createAuthRecoveryEmailServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBe(undefined); + }); + + it('should override an emailService', async () => { + const provider = createAuthRecoveryEmailServiceProvider({ + emailService: mockAuthRecoveryOptions.emailService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory(); + + expect(useFactoryResult).toBe(mockAuthRecoveryOptions.emailService); + }); + + it('should return an emailService from initialization', async () => { + const provider = + createAuthRecoveryEmailServiceProvider() as FactoryProvider; + + const testMockEmailService = mock(); + const useFactoryResult = await provider.useFactory({ + emailService: testMockEmailService, + }); + + expect(useFactoryResult).toBe(testMockEmailService); + }); + }); + + describe(createAuthRecoveryUserLookupServiceProvider.name, () => { + it('should return a have no default', async () => { + const provider = + createAuthRecoveryUserLookupServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBe(undefined); + }); + + it('should override userLookupService', async () => { + const provider = createAuthRecoveryUserLookupServiceProvider({ + userLookupService: mockAuthRecoveryOptions.userLookupService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory(); + + expect(useFactoryResult).toBe(mockAuthRecoveryOptions.userLookupService); + }); + + it('should return an userLookupService from initialization', async () => { + const provider = + createAuthRecoveryUserLookupServiceProvider() as FactoryProvider; + + const mockService = mock(); + const useFactoryResult = await provider.useFactory({ + userLookupService: mockService, + }); + + expect(useFactoryResult).toBe(mockService); + }); + }); + + describe(createAuthRecoveryUserMutateServiceProvider.name, () => { + it('should return a have no default', async () => { + const provider = + createAuthRecoveryUserMutateServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBe(undefined); + }); + + it('should override userMutateService', async () => { + const provider = createAuthRecoveryUserMutateServiceProvider({ + userMutateService: mockAuthRecoveryOptions.userMutateService, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory(); + + expect(useFactoryResult).toBe(mockAuthRecoveryOptions.userMutateService); + }); + + it('should return an userMutateService from initialization', async () => { + const provider = + createAuthRecoveryUserMutateServiceProvider() as FactoryProvider; + + const mockService = mock(); + const useFactoryResult = await provider.useFactory({ + userMutateService: mockService, + }); + + expect(useFactoryResult).toBe(mockService); + }); + }); + + describe(createAuthRecoveryNotificationServiceProvider.name, () => { + it('should return a default AuthRecoveryNotificationService', async () => { + const provider = + createAuthRecoveryNotificationServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(AuthRecoveryNotificationService); + }); + + it('should override notificationService', async () => { + const provider = createAuthRecoveryNotificationServiceProvider({ + notificationService: mockAuthRecoveryNotification, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory(); + + expect(useFactoryResult).toBe(mockAuthRecoveryNotification); + }); + + it('should return an notificationService from initialization', async () => { + const provider = + createAuthRecoveryNotificationServiceProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + notificationService: mockAuthRecoveryNotification, + }); + + expect(useFactoryResult).toBe(mockAuthRecoveryNotification); + }); + }); + describe(createAuthRecoveryEntityManagerProxyProvider.name, () => { + it('should return a default AuthRecoveryNotificationService', async () => { + const provider = + createAuthRecoveryEntityManagerProxyProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({}); + + expect(useFactoryResult).toBeInstanceOf(EntityManagerProxy); + }); + + it('should override notificationService', async () => { + const provider = createAuthRecoveryEntityManagerProxyProvider({ + entityManagerProxy: mockEntityManagerProxy, + }) as FactoryProvider; + + const useFactoryResult = await provider.useFactory(); + + expect(useFactoryResult).toBe(mockEntityManagerProxy); + }); + + it('should return an notificationService from initialization', async () => { + const provider = + createAuthRecoveryEntityManagerProxyProvider() as FactoryProvider; + + const useFactoryResult = await provider.useFactory({ + entityManagerProxy: mockEntityManagerProxy, + }); + + expect(useFactoryResult).toBe(mockEntityManagerProxy); + }); + }); +}); diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.ts b/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.ts index 894626c9f..5859d2650 100644 --- a/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.ts +++ b/packages/nestjs-auth-recovery/src/auth-recovery.module-definition.ts @@ -57,8 +57,8 @@ function definitionTransform( definition: DynamicModule, extras: AuthRecoveryOptionsExtrasInterface, ): DynamicModule { - const { providers = [] } = definition; - const { controllers, global = false } = extras; + const { providers } = definition; + const { controllers, global } = extras; return { ...definition, @@ -125,51 +125,53 @@ export function createAuthRecoverySettingsProvider( } export function createAuthRecoveryOtpServiceProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_RECOVERY_MODULE_OTP_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN], - useFactory: async (options: AuthRecoveryOptionsInterface) => + useFactory: async (options: Pick) => optionsOverrides?.otpService ?? options.otpService, }; } export function createAuthRecoveryEmailServiceProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_RECOVERY_MODULE_EMAIL_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN], - useFactory: async (options: AuthRecoveryOptionsInterface) => + useFactory: async (options: Pick) => optionsOverrides?.emailService ?? options.emailService, }; } export function createAuthRecoveryUserLookupServiceProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_RECOVERY_MODULE_USER_LOOKUP_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN], - useFactory: async (options: AuthRecoveryOptionsInterface) => - optionsOverrides?.userLookupService ?? options.userLookupService, + useFactory: async ( + options: Pick, + ) => optionsOverrides?.userLookupService ?? options.userLookupService, }; } export function createAuthRecoveryUserMutateServiceProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_RECOVERY_MODULE_USER_MUTATE_SERVICE_TOKEN, inject: [RAW_OPTIONS_TOKEN], - useFactory: async (options: AuthRecoveryOptionsInterface) => - optionsOverrides?.userMutateService ?? options.userMutateService, + useFactory: async ( + options: Pick, + ) => optionsOverrides?.userMutateService ?? options.userMutateService, }; } export function createAuthRecoveryNotificationServiceProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AuthRecoveryNotificationService, @@ -179,7 +181,7 @@ export function createAuthRecoveryNotificationServiceProvider( AUTH_RECOVERY_MODULE_EMAIL_SERVICE_TOKEN, ], useFactory: async ( - options: AuthRecoveryOptionsInterface, + options: Pick, settings: AuthRecoverySettingsInterface, emailService: AuthRecoveryEmailServiceInterface, ) => @@ -190,13 +192,13 @@ export function createAuthRecoveryNotificationServiceProvider( } export function createAuthRecoveryEntityManagerProxyProvider( - optionsOverrides?: AuthRecoveryOptions, + optionsOverrides?: Pick, ): Provider { return { provide: AUTH_RECOVERY_MODULE_ENTITY_MANAGER_PROXY_TOKEN, inject: [RAW_OPTIONS_TOKEN, getEntityManagerToken()], useFactory: async ( - options: AuthRecoveryOptionsInterface, + options: Pick, defaultEntityManager: EntityManager, ) => optionsOverrides?.entityManagerProxy ?? diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts new file mode 100644 index 000000000..3db6b0bbc --- /dev/null +++ b/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts @@ -0,0 +1,13 @@ +import { formatTokenUrl } from './auth-recovery.utils'; + +describe('formatTokenUrl', () => { + it('should return the correct URL', () => { + const baseUrl = 'https://example.com'; + const passcode = '123456'; + const expectedUrl = 'https://example.com/123456'; + + const result = formatTokenUrl(baseUrl, passcode); + + expect(result).toBe(expectedUrl); + }); +}); \ No newline at end of file diff --git a/packages/nestjs-auth-recovery/src/index.spec.ts b/packages/nestjs-auth-recovery/src/index.spec.ts new file mode 100644 index 000000000..7c3039c1f --- /dev/null +++ b/packages/nestjs-auth-recovery/src/index.spec.ts @@ -0,0 +1,44 @@ +import { + AuthRecoveryModule, + AuthRecoveryController, + AuthRecoveryService, + AuthRecoveryNotificationService, + AuthRecoveryRecoverLoginDto, + AuthRecoveryRecoverPasswordDto, + AuthRecoveryUpdatePasswordDto, + AuthRecoveryValidatePasscodeDto, +} from './index'; + +describe('Index', () => { + it('AuthRecoveryModule should be a function', () => { + expect(AuthRecoveryModule).toBeInstanceOf(Function); + }); + + it('AuthRecoveryController should be a function', () => { + expect(AuthRecoveryController).toBeInstanceOf(Function); + }); + + it('AuthRecoveryService should be a function', () => { + expect(AuthRecoveryService).toBeInstanceOf(Function); + }); + + it('AuthRecoveryNotificationService should be a function', () => { + expect(AuthRecoveryNotificationService).toBeInstanceOf(Function); + }); + + it('AuthRecoveryRecoverLoginDto should be a function', () => { + expect(AuthRecoveryRecoverLoginDto).toBeInstanceOf(Function); + }); + + it('AuthRecoveryRecoverPasswordDto should be a function', () => { + expect(AuthRecoveryRecoverPasswordDto).toBeInstanceOf(Function); + }); + + it('AuthRecoveryUpdatePasswordDto should be a function', () => { + expect(AuthRecoveryUpdatePasswordDto).toBeInstanceOf(Function); + }); + + it('AuthRecoveryValidatePasscodeDto should be a function', () => { + expect(AuthRecoveryValidatePasscodeDto).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-auth-recovery/src/services/auth-recovery-notification.service.spec.ts b/packages/nestjs-auth-recovery/src/services/auth-recovery-notification.service.spec.ts index f4c7c8dd7..cfedc49f3 100644 --- a/packages/nestjs-auth-recovery/src/services/auth-recovery-notification.service.spec.ts +++ b/packages/nestjs-auth-recovery/src/services/auth-recovery-notification.service.spec.ts @@ -61,4 +61,23 @@ describe('AuthRecoveryNotificationService', () => { ); expect(spyEmailService).toHaveBeenCalledTimes(1); }); + + it('Send recover email password', async () => { + authRecoveryNotificationService['settings'].email.tokenUrlFormatter = + undefined; + + await authRecoveryNotificationService.sendRecoverPasswordEmail( + 'me@mail.com', + 'me', + new Date(), + ); + expect(spyEmailService).toHaveBeenCalledTimes(1); + }); + + it('Send recover email password', async () => { + await authRecoveryNotificationService.sendPasswordUpdatedSuccefullyEmail( + 'me@mail.com', + ); + expect(emailService.sendMail).toHaveBeenCalled(); + }); }); diff --git a/packages/nestjs-auth-refresh/src/auth-refresh.module-definition.ts b/packages/nestjs-auth-refresh/src/auth-refresh.module-definition.ts index 7ef662425..5dc1b8b9a 100644 --- a/packages/nestjs-auth-refresh/src/auth-refresh.module-definition.ts +++ b/packages/nestjs-auth-refresh/src/auth-refresh.module-definition.ts @@ -56,8 +56,8 @@ function definitionTransform( definition: DynamicModule, extras: AuthRefreshOptionsExtrasInterface, ): DynamicModule { - const { providers = [] } = definition; - const { controllers, global = false } = extras; + const { providers } = definition; + const { controllers, global } = extras; return { ...definition, @@ -100,7 +100,7 @@ export function createAuthRefreshProviders(options: { } export function createAuthRefreshControllers( - overrides: Pick = {}, + overrides: Pick, ): DynamicModule['controllers'] { return overrides?.controllers !== undefined ? overrides.controllers diff --git a/packages/nestjs-auth-refresh/src/index.spec.ts b/packages/nestjs-auth-refresh/src/index.spec.ts new file mode 100644 index 000000000..3f410eb6f --- /dev/null +++ b/packages/nestjs-auth-refresh/src/index.spec.ts @@ -0,0 +1,10 @@ +import { AuthRefreshModule, RefreshAuthGuard } from './index'; + +describe('Index', () => { + it('should be defined', () => { + expect(AuthRefreshModule).toBeInstanceOf(Function); + }); + it('should be defined', () => { + expect(RefreshAuthGuard).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts new file mode 100644 index 000000000..49c4adc2c --- /dev/null +++ b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts @@ -0,0 +1,20 @@ +import * as common from '@nestjs/common'; +import { AUTHENTICATION_MODULE_DISABLE_GUARDS_TOKEN } from '../authentication.constants'; +import { AuthPublic } from './auth-public.decorator'; + +jest.mock('@nestjs/common', () => { + return { + SetMetadata: jest.fn().mockImplementation(() => 'mocked SetMetadata') // Mock SetMetadata + }; +}); + +describe('AuthPublic', () => { + it('should set metadata to disable guards', () => { + AuthPublic(); + // Assert that SetMetadata was called with specific arguments + expect(common.SetMetadata).toHaveBeenCalledWith( + AUTHENTICATION_MODULE_DISABLE_GUARDS_TOKEN, + true, + ); + }); +}); diff --git a/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts b/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts new file mode 100644 index 000000000..671951e88 --- /dev/null +++ b/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts @@ -0,0 +1,7 @@ +import { AuthUser } from './auth-user.decorator'; + +describe('Index', () => { + it('AuthUser should be imported', () => { + expect(AuthUser).toBeInstanceOf(Function); + }); +}); diff --git a/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts new file mode 100644 index 000000000..542113429 --- /dev/null +++ b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts @@ -0,0 +1,23 @@ + +import { BadRequestException } from '@nestjs/common'; +import { AuthenticationException } from './authentication.exception'; + +describe('AuthenticationException', () => { + it('should extend BadRequestException', () => { + const exception = new AuthenticationException(); + expect(exception).toBeInstanceOf(BadRequestException); + }); + + it('should have default message "Credentials are incorrect."', () => { + const exception = new AuthenticationException(); + const data = exception.getResponse() as { message: string }; + expect(data.message).toEqual('Credentials are incorrect.'); + }); + + it('should pass a string error message to the BadRequestException', () => { + const errorMessage = 'Specific error message'; + const exception = new AuthenticationException(errorMessage) + const data = exception.getResponse() as { message: string }; + expect(data.message).toContain(errorMessage); + }); +}); diff --git a/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts new file mode 100644 index 000000000..b5cca2d1c --- /dev/null +++ b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts @@ -0,0 +1,38 @@ +import { NotImplementedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-strategy'; +import { PassportStrategyFactory } from './passport-strategy.factory'; + +jest.mock('@nestjs/passport', () => ({ + PassportStrategy: jest.fn().mockImplementation((strategy, name) => ({ + strategy, + name, + })), +})); + +describe('PassportStrategyFactory', () => { + class MockStrategy extends Strategy { + authenticate() {} + } + + it('should return a strategy configured for Express', () => { + const strategyName = 'mockStrategy'; + const strategy = PassportStrategyFactory(MockStrategy, strategyName); + expect(strategy).toBeDefined(); + expect(strategy.name).toEqual(strategyName); + expect(PassportStrategy).toHaveBeenCalledWith(MockStrategy, strategyName); + }); + + it('should handle different strategy types and names correctly', () => { + const anotherStrategyName = 'anotherMockStrategy'; + const anotherStrategy = PassportStrategyFactory( + MockStrategy, + anotherStrategyName, + ); + expect(anotherStrategy.name).toEqual(anotherStrategyName); + expect(PassportStrategy).toHaveBeenCalledWith( + MockStrategy, + anotherStrategyName, + ); + }); +}); diff --git a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts index 35099789e..b2a270925 100644 --- a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts +++ b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts @@ -1,9 +1,51 @@ +import { ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { FastifyAuthGuard } from './fastify-auth.guard'; import { AuthGuard } from './auth.guard'; +import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; + +jest.mock('@nestjs/passport', () => ({ + AuthGuard: jest.fn().mockImplementation(() => jest.fn()), +})); +jest.mock('./fastify-auth.guard', () => ({ + FastifyAuthGuard: jest.fn().mockImplementation(() => jest.fn()), +})); describe('AuthGuard', () => { - it('should be success', async () => { - const guard = AuthGuard('jwt'); - // TODO: This need to be tested by e2e when we have Fastify integration - expect(guard).toBeDefined(); + let reflector: Reflector; + + beforeEach(() => { + reflector = new Reflector(); + }); + + it('should use PassportAuthGuard for Express', () => { + AuthGuard('local'); + expect(PassportAuthGuard).toHaveBeenCalledWith('local'); + }); + + it('should always activate if guards are disabled globally', () => { + const mockContext = { + getHandler: jest.fn(), + getClass: jest.fn(), + } as unknown as ExecutionContext; + const mockSettings = { enableGuards: false }; + jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(true); + + const Guard = AuthGuard('local', { canDisable: true }); + const guardInstance = new Guard(mockSettings, reflector); + expect(guardInstance.canActivate(mockContext)).toBeTruthy(); + }); + + it('should respect disableGuard callback', () => { + const mockContext = { + getHandler: jest.fn(), + getClass: jest.fn(), + } as unknown as ExecutionContext; + + const mockSettings = { enableGuards: false }; + + const Guard = AuthGuard('local', { canDisable: true }); + const guardInstance = new Guard(mockSettings, reflector); + expect(guardInstance.canActivate(mockContext)).toBeTruthy(); }); }); diff --git a/packages/nestjs-authentication/src/services/validate-user.service.spec.ts b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts new file mode 100644 index 000000000..8b4b18b50 --- /dev/null +++ b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts @@ -0,0 +1,35 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ValidateUserService } from './validate-user.service'; + +interface TestUser { + id: string; + active: boolean; +} +class ConcreteValidateUserService extends ValidateUserService { + async validateUser(...args: unknown[]): Promise { + // Implementation of the abstract method for testing purposes + return { id: 'user1', active: true } as TestUser; + } +} + +describe('ValidateUserService', () => { + let service: ConcreteValidateUserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConcreteValidateUserService], + }).compile(); + + service = module.get(ConcreteValidateUserService); + }); + + it('should return true if user is active', async () => { + const user = { id: 'user1', active: true }; + expect(await service.isActive(user)).toBe(true); + }); + + it('should return false if user is not active', async () => { + const user = { id: 'user1', active: false }; + expect(await service.isActive(user)).toBe(false); + }); +}); From b86fc8d74d8c4b027e390e1a21ca1d1c2cfc2825 Mon Sep 17 00:00:00 2001 From: Marshall Sorenson Date: Mon, 29 Apr 2024 11:15:58 -0400 Subject: [PATCH 2/3] chore: linting --- .../src/__fixtures__/user/user.entity.fixture.ts | 2 +- .../src/services/auth-local-validate-user.service.spec.ts | 3 --- .../nestjs-auth-recovery/src/auth-recovery.controller.spec.ts | 1 - packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts | 2 +- .../src/decorators/auth-public.decorator.spec.ts | 2 +- .../src/exceptions/authentication.exception.spec.ts | 3 +-- .../src/factories/passport-strategy.factory.spec.ts | 1 - packages/nestjs-authentication/src/guards/auth.guard.spec.ts | 1 - .../src/services/validate-user.service.spec.ts | 2 +- 9 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts index eaf18be53..ecd972db5 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts @@ -1,7 +1,7 @@ import { ReferenceIdInterface } from '@concepta/ts-core'; import { AuthLocalCredentialsInterface } from '../../interfaces/auth-local-credentials.interface'; import { Allow, IsOptional, MinLength } from 'class-validator'; -import { allowedNodeEnvironmentFlags } from 'process'; + export class UserFixture implements ReferenceIdInterface, AuthLocalCredentialsInterface { diff --git a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts index 6ac8de7c5..d89536b7c 100644 --- a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts +++ b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts @@ -2,9 +2,6 @@ import { AuthLocalValidateUserService } from './auth-local-validate-user.service import { AuthLocalUserLookupServiceInterface } from '../interfaces/auth-local-user-lookup-service.interface'; import { PasswordValidationServiceInterface } from '@concepta/nestjs-password'; import { AuthLocalValidateUserInterface } from '../interfaces/auth-local-validate-user.interface'; -import { ReferenceIdInterface } from '@concepta/ts-core'; -import { UnauthorizedException } from '@nestjs/common'; -import { AuthLocalCredentialsInterface } from '../interfaces/auth-local-credentials.interface'; describe('AuthLocalValidateUserService', () => { const USERNAME = 'test'; diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts index a1c9bd829..396ddab95 100644 --- a/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts +++ b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts @@ -1,7 +1,6 @@ import { AuthRecoveryController } from './auth-recovery.controller'; import { AuthRecoveryService } from './services/auth-recovery.service'; import { AuthRecoveryRecoverLoginDto } from './dto/auth-recovery-recover-login.dto'; -import { AuthRecoveryRecoverPasswordDto } from './dto/auth-recovery-recover-password.dto'; import { AuthRecoveryUpdatePasswordDto } from './dto/auth-recovery-update-password.dto'; import { mock } from 'jest-mock-extended'; import { BadRequestException, NotFoundException } from '@nestjs/common'; diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts index 3db6b0bbc..4de0e1c01 100644 --- a/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts +++ b/packages/nestjs-auth-recovery/src/auth-recovery.utils.spec.ts @@ -10,4 +10,4 @@ describe('formatTokenUrl', () => { expect(result).toBe(expectedUrl); }); -}); \ No newline at end of file +}); diff --git a/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts index 49c4adc2c..f4930cfd3 100644 --- a/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts +++ b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts @@ -4,7 +4,7 @@ import { AuthPublic } from './auth-public.decorator'; jest.mock('@nestjs/common', () => { return { - SetMetadata: jest.fn().mockImplementation(() => 'mocked SetMetadata') // Mock SetMetadata + SetMetadata: jest.fn().mockImplementation(() => 'mocked SetMetadata'), // Mock SetMetadata }; }); diff --git a/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts index 542113429..e4074f225 100644 --- a/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts +++ b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts @@ -1,4 +1,3 @@ - import { BadRequestException } from '@nestjs/common'; import { AuthenticationException } from './authentication.exception'; @@ -16,7 +15,7 @@ describe('AuthenticationException', () => { it('should pass a string error message to the BadRequestException', () => { const errorMessage = 'Specific error message'; - const exception = new AuthenticationException(errorMessage) + const exception = new AuthenticationException(errorMessage); const data = exception.getResponse() as { message: string }; expect(data.message).toContain(errorMessage); }); diff --git a/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts index b5cca2d1c..a1bdefe79 100644 --- a/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts +++ b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts @@ -1,4 +1,3 @@ -import { NotImplementedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-strategy'; import { PassportStrategyFactory } from './passport-strategy.factory'; diff --git a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts index b2a270925..0ed9e164a 100644 --- a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts +++ b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts @@ -1,6 +1,5 @@ import { ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; -import { FastifyAuthGuard } from './fastify-auth.guard'; import { AuthGuard } from './auth.guard'; import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; diff --git a/packages/nestjs-authentication/src/services/validate-user.service.spec.ts b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts index 8b4b18b50..4017c24cc 100644 --- a/packages/nestjs-authentication/src/services/validate-user.service.spec.ts +++ b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts @@ -6,7 +6,7 @@ interface TestUser { active: boolean; } class ConcreteValidateUserService extends ValidateUserService { - async validateUser(...args: unknown[]): Promise { + async validateUser(..._args: unknown[]): Promise { // Implementation of the abstract method for testing purposes return { id: 'user1', active: true } as TestUser; } From 22af7592bfd3243742709424d2a665aa82b87948 Mon Sep 17 00:00:00 2001 From: Marshall Sorenson Date: Mon, 29 Apr 2024 13:35:11 -0400 Subject: [PATCH 3/3] chore: code standards --- .../src/__fixtures__/user/user.entity.fixture.ts | 7 ------- .../nestjs-auth-local/src/auth-local.strategy.spec.ts | 6 +++--- .../services/auth-local-validate-user.service.spec.ts | 10 +++++----- .../src/auth-recovery.controller.spec.ts | 2 +- .../src/decorators/auth-public.decorator.spec.ts | 2 +- .../src/decorators/auth-user.decorator.spec.ts | 2 +- .../src/exceptions/authentication.exception.spec.ts | 2 +- .../src/factories/passport-strategy.factory.spec.ts | 4 ++-- .../src/guards/auth.guard.spec.ts | 2 +- .../src/services/validate-user.service.spec.ts | 2 +- 10 files changed, 16 insertions(+), 23 deletions(-) diff --git a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts index ecd972db5..021e347ae 100644 --- a/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts +++ b/packages/nestjs-auth-local/src/__fixtures__/user/user.entity.fixture.ts @@ -1,25 +1,18 @@ import { ReferenceIdInterface } from '@concepta/ts-core'; import { AuthLocalCredentialsInterface } from '../../interfaces/auth-local-credentials.interface'; -import { Allow, IsOptional, MinLength } from 'class-validator'; export class UserFixture implements ReferenceIdInterface, AuthLocalCredentialsInterface { - @Allow() id!: string; - @MinLength(3) username!: string; - @Allow() active!: boolean; - @Allow() password!: string; - @IsOptional() passwordHash!: string | null; - @IsOptional() passwordSalt!: string | null; } diff --git a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts index 2511128d8..f62dbac8e 100644 --- a/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts +++ b/packages/nestjs-auth-local/src/auth-local.strategy.spec.ts @@ -1,7 +1,7 @@ -import { PasswordValidationService } from '@concepta/nestjs-password'; -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { randomUUID } from 'crypto'; import { mock } from 'jest-mock-extended'; +import { PasswordValidationService } from '@concepta/nestjs-password'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { AuthLocalStrategy } from './auth-local.strategy'; import { AuthLocalSettingsInterface } from './interfaces/auth-local-settings.interface'; import { AuthLocalUserLookupServiceInterface } from './interfaces/auth-local-user-lookup-service.interface'; @@ -12,7 +12,7 @@ import { UserFixture } from './__fixtures__/user/user.entity.fixture'; import { ReferenceIdInterface } from '@concepta/ts-core'; import { AuthLocalValidateUserInterface } from './interfaces/auth-local-validate-user.interface'; -describe(AuthLocalStrategy, () => { +describe(AuthLocalStrategy.name, () => { const USERNAME = 'username'; const PASSWORD = 'password'; diff --git a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts index d89536b7c..f56126ec2 100644 --- a/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts +++ b/packages/nestjs-auth-local/src/services/auth-local-validate-user.service.spec.ts @@ -1,9 +1,9 @@ +import { PasswordValidationServiceInterface } from '@concepta/nestjs-password'; import { AuthLocalValidateUserService } from './auth-local-validate-user.service'; import { AuthLocalUserLookupServiceInterface } from '../interfaces/auth-local-user-lookup-service.interface'; -import { PasswordValidationServiceInterface } from '@concepta/nestjs-password'; import { AuthLocalValidateUserInterface } from '../interfaces/auth-local-validate-user.interface'; -describe('AuthLocalValidateUserService', () => { +describe(AuthLocalValidateUserService.name, () => { const USERNAME = 'test'; const PASSWORD = 'test'; @@ -44,7 +44,7 @@ describe('AuthLocalValidateUserService', () => { password: PASSWORD, } as AuthLocalValidateUserInterface); await expect(t).rejects.toThrowError( - 'No user found for username: ' + USERNAME, + `No user found for username: ${USERNAME}`, ); }); @@ -58,7 +58,7 @@ describe('AuthLocalValidateUserService', () => { password: PASSWORD, } as AuthLocalValidateUserInterface); await expect(t).rejects.toThrowError( - "User with username '" + USERNAME + "' is inactive", + `User with username '${USERNAME}' is inactive`, ); }); @@ -75,7 +75,7 @@ describe('AuthLocalValidateUserService', () => { password: USER.password, } as AuthLocalValidateUserInterface); await expect(t).rejects.toThrowError( - 'Invalid password for username: ' + USER.username, + `Invalid password for username: ${USER.username}`, ); }); diff --git a/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts index 396ddab95..e23f7974c 100644 --- a/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts +++ b/packages/nestjs-auth-recovery/src/auth-recovery.controller.spec.ts @@ -5,7 +5,7 @@ import { AuthRecoveryUpdatePasswordDto } from './dto/auth-recovery-update-passwo import { mock } from 'jest-mock-extended'; import { BadRequestException, NotFoundException } from '@nestjs/common'; -describe('AuthRecoveryController', () => { +describe(AuthRecoveryController.name, () => { let controller: AuthRecoveryController; let authRecoveryService: AuthRecoveryService; const dto: AuthRecoveryRecoverLoginDto = { diff --git a/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts index f4930cfd3..187202fac 100644 --- a/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts +++ b/packages/nestjs-authentication/src/decorators/auth-public.decorator.spec.ts @@ -8,7 +8,7 @@ jest.mock('@nestjs/common', () => { }; }); -describe('AuthPublic', () => { +describe(AuthPublic.name, () => { it('should set metadata to disable guards', () => { AuthPublic(); // Assert that SetMetadata was called with specific arguments diff --git a/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts b/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts index 671951e88..6e7b0ac2b 100644 --- a/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts +++ b/packages/nestjs-authentication/src/decorators/auth-user.decorator.spec.ts @@ -1,6 +1,6 @@ import { AuthUser } from './auth-user.decorator'; -describe('Index', () => { +describe(AuthUser.name, () => { it('AuthUser should be imported', () => { expect(AuthUser).toBeInstanceOf(Function); }); diff --git a/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts index e4074f225..8f2fcbf04 100644 --- a/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts +++ b/packages/nestjs-authentication/src/exceptions/authentication.exception.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { AuthenticationException } from './authentication.exception'; -describe('AuthenticationException', () => { +describe(AuthenticationException.name, () => { it('should extend BadRequestException', () => { const exception = new AuthenticationException(); expect(exception).toBeInstanceOf(BadRequestException); diff --git a/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts index a1bdefe79..161834ef6 100644 --- a/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts +++ b/packages/nestjs-authentication/src/factories/passport-strategy.factory.spec.ts @@ -1,5 +1,5 @@ -import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-strategy'; +import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategyFactory } from './passport-strategy.factory'; jest.mock('@nestjs/passport', () => ({ @@ -9,7 +9,7 @@ jest.mock('@nestjs/passport', () => ({ })), })); -describe('PassportStrategyFactory', () => { +describe(PassportStrategyFactory.name, () => { class MockStrategy extends Strategy { authenticate() {} } diff --git a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts index 0ed9e164a..fef85606d 100644 --- a/packages/nestjs-authentication/src/guards/auth.guard.spec.ts +++ b/packages/nestjs-authentication/src/guards/auth.guard.spec.ts @@ -10,7 +10,7 @@ jest.mock('./fastify-auth.guard', () => ({ FastifyAuthGuard: jest.fn().mockImplementation(() => jest.fn()), })); -describe('AuthGuard', () => { +describe(AuthGuard.name, () => { let reflector: Reflector; beforeEach(() => { diff --git a/packages/nestjs-authentication/src/services/validate-user.service.spec.ts b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts index 4017c24cc..5c23b3abe 100644 --- a/packages/nestjs-authentication/src/services/validate-user.service.spec.ts +++ b/packages/nestjs-authentication/src/services/validate-user.service.spec.ts @@ -12,7 +12,7 @@ class ConcreteValidateUserService extends ValidateUserService { } } -describe('ValidateUserService', () => { +describe(ValidateUserService.name, () => { let service: ConcreteValidateUserService; beforeEach(async () => {