فهرست منبع

test(server): all the tests (#911)

Jason Rasmussen 2 سال پیش
والد
کامیت
296a5e786e

+ 28 - 0
server/apps/immich/src/api-v1/asset/dto/check-existing-assets.dto.spec.ts

@@ -0,0 +1,28 @@
+import { plainToInstance } from 'class-transformer';
+import { validateSync } from 'class-validator';
+import { CheckExistingAssetsDto } from './check-existing-assets.dto';
+
+describe('CheckExistingAssetsDto', () => {
+  it('should fail with an empty list', () => {
+    const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [], deviceId: 'test-device' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('deviceAssetIds');
+  });
+
+  it('should fail with an empty string', () => {
+    const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [''], deviceId: 'test-device' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('deviceAssetIds');
+  });
+
+  it('should work with valid asset ids', () => {
+    const dto = plainToInstance(CheckExistingAssetsDto, {
+      deviceAssetIds: ['asset-1', 'asset-2'],
+      deviceId: 'test-device',
+    });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(0);
+  });
+});

+ 5 - 3
server/apps/immich/src/api-v1/asset/dto/check-existing-assets.dto.ts

@@ -1,9 +1,11 @@
-import { IsNotEmpty } from 'class-validator';
+import { ArrayNotEmpty, IsNotEmpty, IsString } from 'class-validator';
 
 export class CheckExistingAssetsDto {
-  @IsNotEmpty()
+  @ArrayNotEmpty()
+  @IsString({ each: true })
+  @IsNotEmpty({ each: true })
   deviceAssetIds!: string[];
 
   @IsNotEmpty()
   deviceId!: string;
-}
+}

+ 33 - 0
server/apps/immich/src/api-v1/auth/dto/login-credential.dto.spec.ts

@@ -0,0 +1,33 @@
+import { plainToInstance } from 'class-transformer';
+import { validateSync } from 'class-validator';
+import { LoginCredentialDto } from './login-credential.dto';
+
+describe('LoginCredentialDto', () => {
+  it('should fail without an email', () => {
+    const dto = plainToInstance(LoginCredentialDto, { password: 'password' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('email');
+  });
+
+  it('should fail with an invalid email', () => {
+    const dto = plainToInstance(LoginCredentialDto, { email: 'invalid.com', password: 'password' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('email');
+  });
+
+  it('should make the email all lowercase', () => {
+    const dto = plainToInstance(LoginCredentialDto, { email: 'TeSt@ImMiCh.com', password: 'password' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(0);
+    expect(dto.email).toEqual('test@immich.com');
+  });
+
+  it('should fail without a password', () => {
+    const dto = plainToInstance(LoginCredentialDto, { email: 'test@immich.com', password: '' });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('password');
+  });
+});

+ 4 - 3
server/apps/immich/src/api-v1/auth/dto/login-credential.dto.ts

@@ -1,13 +1,14 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
-import { IsNotEmpty } from 'class-validator';
+import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
 
 export class LoginCredentialDto {
-  @IsNotEmpty()
+  @IsEmail()
   @ApiProperty({ example: 'testuser@email.com' })
-  @Transform(({ value }) => value?.toLowerCase())
+  @Transform(({ value }) => value.toLowerCase())
   email!: string;
 
+  @IsString()
   @IsNotEmpty()
   @ApiProperty({ example: 'password' })
   password!: string;

+ 33 - 16
server/apps/immich/src/api-v1/auth/dto/sign-up.dto.spec.ts

@@ -1,27 +1,44 @@
 import { plainToInstance } from 'class-transformer';
-import { validate } from 'class-validator';
+import { validateSync } from 'class-validator';
 import { SignUpDto } from './sign-up.dto';
 
-describe('sign up DTO', () => {
-  it('validates the email', async () => {
-    const params: Partial<SignUpDto> = {
-      email: undefined,
+describe('SignUpDto', () => {
+  it('should require all fields', () => {
+    const dto = plainToInstance(SignUpDto, {
+      email: '',
+      password: '',
+      firstName: '',
+      lastName: '',
+    });
+    const errors = validateSync(dto);
+    expect(errors).toHaveLength(4);
+    expect(errors[0].property).toEqual('email');
+    expect(errors[1].property).toEqual('password');
+    expect(errors[2].property).toEqual('firstName');
+    expect(errors[3].property).toEqual('lastName');
+  });
+
+  it('should require a valid email', () => {
+    const dto = plainToInstance(SignUpDto, {
+      email: 'immich.com',
       password: 'password',
       firstName: 'first name',
       lastName: 'last name',
-    };
-    let dto: SignUpDto = plainToInstance(SignUpDto, params);
-    let errors = await validate(dto);
-    expect(errors).toHaveLength(1);
-
-    params.email = 'invalid email';
-    dto = plainToInstance(SignUpDto, params);
-    errors = await validate(dto);
+    });
+    const errors = validateSync(dto);
     expect(errors).toHaveLength(1);
+    expect(errors[0].property).toEqual('email');
+  });
 
-    params.email = 'valid@email.com';
-    dto = plainToInstance(SignUpDto, params);
-    errors = await validate(dto);
+  it('should make the email all lowercase', () => {
+    const dto = plainToInstance(SignUpDto, {
+      email: 'TeSt@ImMiCh.com',
+      password: 'password',
+      firstName: 'first name',
+      lastName: 'last name',
+    });
+    const errors = validateSync(dto);
     expect(errors).toHaveLength(0);
+    expect(dto.email).toEqual('test@immich.com');
   });
 });

+ 5 - 2
server/apps/immich/src/api-v1/auth/dto/sign-up.dto.ts

@@ -1,21 +1,24 @@
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
-import { IsNotEmpty, IsEmail } from 'class-validator';
+import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
 
 export class SignUpDto {
   @IsEmail()
   @ApiProperty({ example: 'testuser@email.com' })
-  @Transform(({ value }) => value?.toLowerCase())
+  @Transform(({ value }) => value.toLowerCase())
   email!: string;
 
+  @IsString()
   @IsNotEmpty()
   @ApiProperty({ example: 'password' })
   password!: string;
 
+  @IsString()
   @IsNotEmpty()
   @ApiProperty({ example: 'Admin' })
   firstName!: string;
 
+  @IsString()
   @IsNotEmpty()
   @ApiProperty({ example: 'Doe' })
   lastName!: string;

+ 141 - 0
server/apps/immich/src/config/asset-upload.config.spec.ts

@@ -0,0 +1,141 @@
+import { Request } from 'express';
+import * as fs from 'fs';
+import { multerUtils } from './asset-upload.config';
+
+const { fileFilter, destination, filename } = multerUtils;
+
+const mock = {
+  req: {} as Request,
+  userRequest: {
+    user: {
+      id: 'test-user',
+    },
+    body: {
+      deviceId: 'test-device',
+      fileExtension: '.jpg',
+    },
+  } as Request,
+  file: { originalname: 'test.jpg' } as Express.Multer.File,
+};
+
+jest.mock('fs');
+
+describe('assetUploadOption', () => {
+  let callback: jest.Mock;
+  let existsSync: jest.Mock;
+  let mkdirSync: jest.Mock;
+
+  beforeEach(() => {
+    jest.mock('fs');
+    mkdirSync = fs.mkdirSync as jest.Mock;
+    existsSync = fs.existsSync as jest.Mock;
+    callback = jest.fn();
+
+    existsSync.mockImplementation(() => true);
+  });
+
+  afterEach(() => {
+    jest.resetModules();
+  });
+
+  describe('fileFilter', () => {
+    it('should require a user', () => {
+      fileFilter(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should allow images', async () => {
+      const file = { mimetype: 'image/jpeg', originalname: 'test.jpg' } as any;
+      fileFilter(mock.userRequest, file, callback);
+      expect(callback).toHaveBeenCalledWith(null, true);
+    });
+
+    it('should allow videos', async () => {
+      const file = { mimetype: 'image/mp4', originalname: 'test.mp4' } as any;
+      fileFilter(mock.userRequest, file, callback);
+      expect(callback).toHaveBeenCalledWith(null, true);
+    });
+
+    it('should not allow unknown types', async () => {
+      const file = { mimetype: 'application/html', originalname: 'test.html' } as any;
+      const callback = jest.fn();
+      fileFilter(mock.userRequest, file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, accepted] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(accepted).toBe(false);
+    });
+  });
+
+  describe('destination', () => {
+    it('should require a user', () => {
+      destination(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should create non-existing directories', () => {
+      existsSync.mockImplementation(() => false);
+
+      destination(mock.userRequest, mock.file, callback);
+
+      expect(existsSync).toHaveBeenCalled();
+      expect(mkdirSync).toHaveBeenCalled();
+    });
+
+    it('should return the destination', () => {
+      destination(mock.userRequest, mock.file, callback);
+
+      expect(mkdirSync).not.toHaveBeenCalled();
+      expect(callback).toHaveBeenCalledWith(null, 'upload/test-user/original/test-device');
+    });
+
+    it('should sanitize the deviceId', () => {
+      const request = { ...mock.userRequest, body: { deviceId: 'test-devi\u0000ce' } } as Request;
+      destination(request, mock.file, callback);
+
+      const [folderName] = existsSync.mock.calls[0];
+      expect(folderName.endsWith('test-device')).toBeTruthy();
+      expect(callback).toHaveBeenCalledWith(null, 'upload/test-user/original/test-device');
+    });
+  });
+
+  describe('filename', () => {
+    it('should require a user', () => {
+      filename(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should return the filename', () => {
+      filename(mock.userRequest, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeNull();
+      expect(name.endsWith('.jpg')).toBeTruthy();
+    });
+
+    it('should sanitize the filename', () => {
+      const body = { ...mock.userRequest.body, fileExtension: '.jp\u0000g' };
+      const request = { ...mock.userRequest, body } as Request;
+      filename(request, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeNull();
+      expect(name.endsWith(mock.userRequest.body.fileExtension)).toBeTruthy();
+    });
+  });
+});

+ 49 - 37
server/apps/immich/src/config/asset-upload.config.ts

@@ -1,48 +1,60 @@
 import { APP_UPLOAD_LOCATION } from '@app/common/constants';
-import { HttpException, HttpStatus } from '@nestjs/common';
+import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
+import { randomUUID } from 'crypto';
+import { Request } from 'express';
 import { existsSync, mkdirSync } from 'fs';
 import { diskStorage } from 'multer';
 import { extname, join } from 'path';
-import { Request } from 'express';
-import { randomUUID } from 'crypto';
 import sanitize from 'sanitize-filename';
 
 export const assetUploadOption: MulterOptions = {
-  fileFilter: (req: Request, file: any, cb: any) => {
-    if (
-      file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef)$/)
-    ) {
-      cb(null, true);
-    } else {
-      cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
-    }
-  },
-
+  fileFilter,
   storage: diskStorage({
-    destination: (req: Request, file: Express.Multer.File, cb: any) => {
-      const basePath = APP_UPLOAD_LOCATION;
-
-      if (!req.user) {
-        return;
-      }
-
-      const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
-      const originalUploadFolder = join(basePath, req.user.id, 'original', sanitizedDeviceId);
-
-      if (!existsSync(originalUploadFolder)) {
-        mkdirSync(originalUploadFolder, { recursive: true });
-      }
-
-      // Save original to disk
-      cb(null, originalUploadFolder);
-    },
-
-    filename: (req: Request, file: Express.Multer.File, cb: any) => {
-      const fileNameUUID = randomUUID();
-      const fileName = `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`;
-      const sanitizedFileName = sanitize(fileName);
-      cb(null, sanitizedFileName);
-    },
+    destination,
+    filename,
   }),
 };
+
+export const multerUtils = { fileFilter, filename, destination };
+
+function fileFilter(req: Request, file: any, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+  if (
+    file.mimetype.match(/\/(jpg|jpeg|png|gif|mp4|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef)$/)
+  ) {
+    cb(null, true);
+  } else {
+    cb(new BadRequestException(`Unsupported file type ${extname(file.originalname)}`), false);
+  }
+}
+
+function destination(req: Request, file: Express.Multer.File, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+
+  const basePath = APP_UPLOAD_LOCATION;
+  const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
+  const originalUploadFolder = join(basePath, req.user.id, 'original', sanitizedDeviceId);
+
+  if (!existsSync(originalUploadFolder)) {
+    mkdirSync(originalUploadFolder, { recursive: true });
+  }
+
+  // Save original to disk
+  cb(null, originalUploadFolder);
+}
+
+function filename(req: Request, file: Express.Multer.File, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+
+  const fileNameUUID = randomUUID();
+  const fileName = `${fileNameUUID}${req.body['fileExtension'].toLowerCase()}`;
+  const sanitizedFileName = sanitize(fileName);
+  cb(null, sanitizedFileName);
+}

+ 114 - 0
server/apps/immich/src/config/profile-image-upload.config.spec.ts

@@ -0,0 +1,114 @@
+import { Request } from 'express';
+import * as fs from 'fs';
+import { multerUtils } from './profile-image-upload.config';
+
+const { fileFilter, destination, filename } = multerUtils;
+
+const mock = {
+  req: {} as Request,
+  userRequest: {
+    user: {
+      id: 'test-user',
+    },
+  } as Request,
+  file: { originalname: 'test.jpg' } as Express.Multer.File,
+};
+
+jest.mock('fs');
+
+describe('profileImageUploadOption', () => {
+  let callback: jest.Mock;
+  let existsSync: jest.Mock;
+  let mkdirSync: jest.Mock;
+
+  beforeEach(() => {
+    jest.mock('fs');
+    mkdirSync = fs.mkdirSync as jest.Mock;
+    existsSync = fs.existsSync as jest.Mock;
+    callback = jest.fn();
+
+    existsSync.mockImplementation(() => true);
+  });
+
+  afterEach(() => {
+    jest.resetModules();
+  });
+
+  describe('fileFilter', () => {
+    it('should require a user', () => {
+      fileFilter(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should allow images', async () => {
+      const file = { mimetype: 'image/jpeg', originalname: 'test.jpg' } as any;
+      fileFilter(mock.userRequest, file, callback);
+      expect(callback).toHaveBeenCalledWith(null, true);
+    });
+
+    it('should not allow gifs', async () => {
+      const file = { mimetype: 'image/gif', originalname: 'test.gif' } as any;
+      const callback = jest.fn();
+      fileFilter(mock.userRequest, file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, accepted] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(accepted).toBe(false);
+    });
+  });
+
+  describe('destination', () => {
+    it('should require a user', () => {
+      destination(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should create non-existing directories', () => {
+      existsSync.mockImplementation(() => false);
+
+      destination(mock.userRequest, mock.file, callback);
+
+      expect(existsSync).toHaveBeenCalled();
+      expect(mkdirSync).toHaveBeenCalled();
+    });
+
+    it('should return the destination', () => {
+      destination(mock.userRequest, mock.file, callback);
+
+      expect(mkdirSync).not.toHaveBeenCalled();
+      expect(callback).toHaveBeenCalledWith(null, './upload/test-user/profile');
+    });
+  });
+
+  describe('filename', () => {
+    it('should require a user', () => {
+      filename(mock.req, mock.file, callback);
+
+      expect(callback).toHaveBeenCalled();
+      const [error, name] = callback.mock.calls[0];
+      expect(error).toBeDefined();
+      expect(name).toBeUndefined();
+    });
+
+    it('should return the filename', () => {
+      filename(mock.userRequest, mock.file, callback);
+
+      expect(mkdirSync).not.toHaveBeenCalled();
+      expect(callback).toHaveBeenCalledWith(null, 'test-user.jpg');
+    });
+
+    it('should sanitize the filename', () => {
+      filename(mock.userRequest, { ...mock.file, originalname: 'test.j\u0000pg' }, callback);
+      expect(callback).toHaveBeenCalledWith(null, 'test-user.jpg');
+    });
+  });
+});

+ 45 - 33
server/apps/immich/src/config/profile-image-upload.config.ts

@@ -1,44 +1,56 @@
 import { APP_UPLOAD_LOCATION } from '@app/common/constants';
-import { HttpException, HttpStatus } from '@nestjs/common';
+import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
+import { Request } from 'express';
 import { existsSync, mkdirSync } from 'fs';
 import { diskStorage } from 'multer';
 import { extname } from 'path';
-import { Request } from 'express';
 import sanitize from 'sanitize-filename';
 
 export const profileImageUploadOption: MulterOptions = {
-  fileFilter: (req: Request, file: any, cb: any) => {
-    if (file.mimetype.match(/\/(jpg|jpeg|png|heic|heif|dng|webp)$/)) {
-      cb(null, true);
-    } else {
-      cb(new HttpException(`Unsupported file type ${extname(file.originalname)}`, HttpStatus.BAD_REQUEST), false);
-    }
-  },
-
+  fileFilter,
   storage: diskStorage({
-    destination: (req: Request, file: Express.Multer.File, cb: any) => {
-      if (!req.user) {
-        return;
-      }
-      const basePath = APP_UPLOAD_LOCATION;
-      const profileImageLocation = `${basePath}/${req.user.id}/profile`;
-
-      if (!existsSync(profileImageLocation)) {
-        mkdirSync(profileImageLocation, { recursive: true });
-      }
-
-      cb(null, profileImageLocation);
-    },
-
-    filename: (req: Request, file: Express.Multer.File, cb: any) => {
-      if (!req.user) {
-        return;
-      }
-      const userId = req.user.id;
-      const fileName = `${userId}${extname(file.originalname)}`;
-
-      cb(null, sanitize(String(fileName)));
-    },
+    destination,
+    filename,
   }),
 };
+
+export const multerUtils = { fileFilter, filename, destination };
+
+function fileFilter(req: Request, file: any, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+
+  if (file.mimetype.match(/\/(jpg|jpeg|png|heic|heif|dng|webp)$/)) {
+    cb(null, true);
+  } else {
+    cb(new BadRequestException(`Unsupported file type ${extname(file.originalname)}`), false);
+  }
+}
+
+function destination(req: Request, file: Express.Multer.File, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+
+  const basePath = APP_UPLOAD_LOCATION;
+  const profileImageLocation = `${basePath}/${req.user.id}/profile`;
+
+  if (!existsSync(profileImageLocation)) {
+    mkdirSync(profileImageLocation, { recursive: true });
+  }
+
+  cb(null, profileImageLocation);
+}
+
+function filename(req: Request, file: Express.Multer.File, cb: any) {
+  if (!req.user) {
+    return cb(new UnauthorizedException());
+  }
+
+  const userId = req.user.id;
+  const fileName = `${userId}${extname(file.originalname)}`;
+
+  cb(null, sanitize(String(fileName)));
+}

+ 100 - 0
server/apps/immich/src/modules/immich-jwt/immich-jwt.service.spec.ts

@@ -0,0 +1,100 @@
+import { Logger } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { Request } from 'express';
+import { ImmichJwtService } from './immich-jwt.service';
+
+describe('ImmichJwtService', () => {
+  let jwtService: JwtService;
+  let service: ImmichJwtService;
+
+  beforeEach(() => {
+    jwtService = new JwtService();
+    service = new ImmichJwtService(jwtService);
+  });
+
+  afterEach(() => {
+    jest.resetModules();
+  });
+
+  describe('generateToken', () => {
+    it('should generate the token', async () => {
+      const spy = jest.spyOn(jwtService, 'sign');
+      spy.mockImplementation((value) => value as string);
+      const dto = { userId: 'test-user', email: 'test-user@immich.com' };
+      const token = await service.generateToken(dto);
+      expect(token).toEqual(dto);
+    });
+  });
+
+  describe('validateToken', () => {
+    it('should validate the token', async () => {
+      const dto = { userId: 'test-user', email: 'test-user@immich.com' };
+      const spy = jest.spyOn(jwtService, 'verifyAsync');
+      spy.mockImplementation(() => dto as any);
+      const response = await service.validateToken('access-token');
+
+      expect(spy).toHaveBeenCalledTimes(1);
+      expect(response).toEqual({ userId: 'test-user', status: true });
+    });
+
+    it('should handle an invalid token', async () => {
+      const verifyAsync = jest.spyOn(jwtService, 'verifyAsync');
+      verifyAsync.mockImplementation(() => {
+        throw new Error('Invalid token!');
+      });
+
+      const error = jest.spyOn(Logger, 'error');
+      error.mockImplementation(() => null);
+      const response = await service.validateToken('access-token');
+
+      expect(verifyAsync).toHaveBeenCalledTimes(1);
+      expect(error).toHaveBeenCalledTimes(1);
+      expect(response).toEqual({ userId: null, status: false });
+    });
+  });
+
+  describe('extractJwtFromHeader', () => {
+    it('should handle no authorization header', () => {
+      const request = {
+        headers: {},
+      } as Request;
+      const token = service.extractJwtFromHeader(request);
+      expect(token).toBe(null);
+    });
+
+    it('should get the token from the authorization header', () => {
+      const upper = {
+        headers: {
+          authorization: 'Bearer token',
+        },
+      } as Request;
+
+      const lower = {
+        headers: {
+          authorization: 'bearer token',
+        },
+      } as Request;
+
+      expect(service.extractJwtFromHeader(upper)).toBe('token');
+      expect(service.extractJwtFromHeader(lower)).toBe('token');
+    });
+  });
+
+  describe('extracJwtFromCookie', () => {
+    it('should handle no cookie', () => {
+      const request = {} as Request;
+      const token = service.extractJwtFromCookie(request);
+      expect(token).toBe(null);
+    });
+
+    it('should get the token from the immich cookie', () => {
+      const request = {
+        cookies: {
+          immich_access_token: 'cookie',
+        },
+      } as Request;
+      const token = service.extractJwtFromCookie(request);
+      expect(token).toBe('cookie');
+    });
+  });
+});

+ 1 - 2
server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts

@@ -13,8 +13,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
   constructor(
     @InjectRepository(UserEntity)
     private usersRepository: Repository<UserEntity>,
-
-    private immichJwtService: ImmichJwtService,
+    immichJwtService: ImmichJwtService,
   ) {
     super({
       jwtFromRequest: ExtractJwt.fromExtractors([