diff --git a/server/apps/immich/src/api-v1/auth/auth.service.spec.ts b/server/apps/immich/src/api-v1/auth/auth.service.spec.ts index 0fbd9c1ae1..1a3a5a70ea 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.spec.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.spec.ts @@ -1,11 +1,10 @@ import { UserEntity } from '@app/database/entities/user.entity'; -import { BadRequestException } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; import { AuthType } from '../../constants/jwt.constant'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; import { OAuthService } from '../oauth/oauth.service'; -import { IUserRepository, USER_REPOSITORY } from '../user/user-repository'; +import { IUserRepository } from '../user/user-repository'; import { AuthService } from './auth.service'; import { SignUpDto } from './dto/sign-up.dto'; import { LoginResponseDto } from './response-dto/login-response.dto'; @@ -20,6 +19,17 @@ const fixtures = { const CLIENT_IP = '127.0.0.1'; jest.mock('bcrypt'); +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + Logger: jest.fn().mockReturnValue({ + verbose: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }), +})); describe('AuthService', () => { let sut: AuthService; @@ -61,19 +71,7 @@ describe('AuthService', () => { getLogoutEndpoint: jest.fn(), } as unknown as jest.Mocked; - const moduleRef = await Test.createTestingModule({ - providers: [ - AuthService, - { provide: ImmichJwtService, useValue: immichJwtServiceMock }, - { provide: OAuthService, useValue: oauthServiceMock }, - { - provide: USER_REPOSITORY, - useValue: userRepositoryMock, - }, - ], - }).compile(); - - sut = moduleRef.get(AuthService); + sut = new AuthService(oauthServiceMock, immichJwtServiceMock, userRepositoryMock); }); it('should be defined', () => { @@ -104,6 +102,62 @@ describe('AuthService', () => { }); }); + describe('changePassword', () => { + it('should change the password', async () => { + const authUser = { email: 'test@imimch.com' } as UserEntity; + const dto = { password: 'old-password', newPassword: 'new-password' }; + + compare.mockResolvedValue(true); + + userRepositoryMock.getByEmail.mockResolvedValue({ + email: 'test@immich.com', + password: 'hash-password', + } as UserEntity); + + await sut.changePassword(authUser, dto); + + expect(userRepositoryMock.getByEmail).toHaveBeenCalledWith(authUser.email, true); + expect(compare).toHaveBeenCalledWith('old-password', 'hash-password'); + }); + + it('should throw when auth user email is not found', async () => { + const authUser = { email: 'test@imimch.com' } as UserEntity; + const dto = { password: 'old-password', newPassword: 'new-password' }; + + userRepositoryMock.getByEmail.mockResolvedValue(null); + + await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('should throw when password does not match existing password', async () => { + const authUser = { email: 'test@imimch.com' } as UserEntity; + const dto = { password: 'old-password', newPassword: 'new-password' }; + + compare.mockResolvedValue(false); + + userRepositoryMock.getByEmail.mockResolvedValue({ + email: 'test@immich.com', + password: 'hash-password', + } as UserEntity); + + await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should throw when user does not have a password', async () => { + const authUser = { email: 'test@imimch.com' } as UserEntity; + const dto = { password: 'old-password', newPassword: 'new-password' }; + + compare.mockResolvedValue(false); + + userRepositoryMock.getByEmail.mockResolvedValue({ + email: 'test@immich.com', + password: '', + } as UserEntity); + + await expect(sut.changePassword(authUser, dto)).rejects.toBeInstanceOf(BadRequestException); + }); + }); + describe('logout', () => { it('should return the end session endpoint', async () => { oauthServiceMock.getLogoutEndpoint.mockResolvedValue('end-session-endpoint'); diff --git a/server/apps/immich/src/api-v1/auth/auth.service.ts b/server/apps/immich/src/api-v1/auth/auth.service.ts index 539283a050..307913257a 100644 --- a/server/apps/immich/src/api-v1/auth/auth.service.ts +++ b/server/apps/immich/src/api-v1/auth/auth.service.ts @@ -24,6 +24,7 @@ import { UserCore } from '../user/user.core'; @Injectable() export class AuthService { private userCore: UserCore; + private logger = new Logger(AuthService.name); constructor( private oauthService: OAuthService, @@ -44,7 +45,7 @@ export class AuthService { } if (!user) { - Logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`); + this.logger.warn(`Failed login attempt for user ${loginCredential.email} from ip address ${clientIp}`); throw new BadRequestException('Incorrect email or password'); } @@ -94,8 +95,8 @@ export class AuthService { }); return mapAdminSignupResponse(admin); - } catch (e) { - Logger.error('e', 'signUp'); + } catch (error) { + this.logger.error(`Unable to register admin user: ${error}`, (error as Error).stack); throw new InternalServerErrorException('Failed to register new admin user'); } } diff --git a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts index 29af0fdd9d..ecc7290e24 100644 --- a/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts +++ b/server/apps/immich/src/api-v1/oauth/oauth.service.spec.ts @@ -1,7 +1,7 @@ import { SystemConfig } from '@app/database/entities/system-config.entity'; import { UserEntity } from '@app/database/entities/user.entity'; import { ImmichConfigService } from '@app/immich-config'; -import { BadRequestException, Logger } from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { generators, Issuer } from 'openid-client'; import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; @@ -32,20 +32,17 @@ const loginResponse = { userEmail: 'user@immich.com,', } as LoginResponseDto; -jest.mock('@nestjs/common', () => { - return { - ...jest.requireActual('@nestjs/common'), - Logger: function MockLogger() { - Object.assign(this as Logger, { - verbose: jest.fn(), - debug: jest.fn(), - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }); - }, - }; -}); +jest.mock('@nestjs/common', () => ({ + ...jest.requireActual('@nestjs/common'), + Logger: jest.fn().mockReturnValue({ + verbose: jest.fn(), + debug: jest.fn(), + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }), +})); describe('OAuthService', () => { let sut: OAuthService;