mirror of
https://github.com/immich-app/immich.git
synced 2024-11-24 08:52:28 +02:00
test(server): all the tests (#911)
This commit is contained in:
parent
db0a55cd65
commit
296a5e786e
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
const errors = validateSync(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toEqual('email');
|
||||
});
|
||||
|
||||
params.email = 'invalid email';
|
||||
dto = plainToInstance(SignUpDto, params);
|
||||
errors = await validate(dto);
|
||||
expect(errors).toHaveLength(1);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -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
server/apps/immich/src/config/asset-upload.config.spec.ts
Normal file
141
server/apps/immich/src/config/asset-upload.config.spec.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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)));
|
||||
}
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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([
|
||||
|
Loading…
Reference in New Issue
Block a user