Skip to content

Commit

Permalink
Merge branch 'main' into THR-35-library-management-service
Browse files Browse the repository at this point in the history
  • Loading branch information
casparneumann-cap authored Nov 23, 2023
2 parents 0aa5a91 + e563729 commit 8ad11d2
Show file tree
Hide file tree
Showing 11 changed files with 377 additions and 71 deletions.
1 change: 1 addition & 0 deletions apps/server/src/infra/preview-generator/interface/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './preview';
export * from './preview-input-mime-types.enum';
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export enum PreviewInputMimeTypes {
IMAGE_HEIF = 'image/heif',
IMAGE_TIFF = 'image/tiff',
IMAGE_WEBP = 'image/webp',
APPLICATION_PDF = 'application/pdf',
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';
import { GetFile, S3ClientAdapter } from '@infra/s3-client';
import { UnprocessableEntityException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@src/core/logger';
import { Readable } from 'node:stream';
import { PreviewGeneratorService } from './preview-generator.service';

const streamMock = jest.fn();
const resizeMock = jest.fn();
const coalesceMock = jest.fn();
const selectFrameMock = jest.fn();
const imageMagickMock = () => {
return { stream: streamMock, resize: resizeMock, data: Readable.from('text') };
return {
stream: streamMock,
resize: resizeMock,
selectFrame: selectFrameMock,
coalesce: coalesceMock,
data: Readable.from('text'),
};
};
jest.mock('gm', () => {
return {
subClass: () => imageMagickMock,
};
});

const createFile = (contentRange?: string): GetFile => {
const createFile = (contentRange?: string, contentType?: string): GetFile => {
const text = 'testText';
const readable = Readable.from(text);

const fileResponse = {
data: readable,
contentType: 'image/jpeg',
contentType,
contentLength: text.length,
contentRange,
etag: 'testTag',
Expand Down Expand Up @@ -68,76 +77,206 @@ describe('PreviewGeneratorService', () => {
});

describe('generatePreview', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile();
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};

describe('WHEN download of original and preview file is successful', () => {
it('should call storageClient get method with originFilePath', async () => {
const { params } = setup();
describe('WHEN preview is possible', () => {
describe('WHEN mime type is jpeg', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};

it('should call storageClient get method with originFilePath', async () => {
const { params } = setup();

await service.generatePreview(params);

expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath);
});

it('should call imagemagicks resize method', async () => {
const { params } = setup();

await service.generatePreview(params);

expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>');
expect(resizeMock).toHaveBeenCalledTimes(1);
});

it('should call imagemagicks stream method', async () => {
const { params } = setup();

await service.generatePreview(params);

expect(streamMock).toHaveBeenCalledWith(params.previewOptions.format);
expect(streamMock).toHaveBeenCalledTimes(1);
});

it('should call S3ClientAdapters create method', async () => {
const { params, expectedFileData } = setup();

await service.generatePreview(params);

expect(s3ClientAdapter.create).toHaveBeenCalledWith(params.previewFilePath, expectedFileData);
expect(s3ClientAdapter.create).toHaveBeenCalledTimes(1);
});

it('should should return values', async () => {
const { params } = setup();
const expectedValue = { previewFilePath: params.previewFilePath, status: true };

const result = await service.generatePreview(params);

expect(result).toEqual(expectedValue);
});
});

describe('WHEN mime type is pdf', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.pdf',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'application/pdf');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};

it('should call imagemagicks selectFrameMock method', async () => {
const { params } = setup();

await service.generatePreview(params);

expect(selectFrameMock).toHaveBeenCalledWith(0);
expect(resizeMock).toHaveBeenCalledTimes(1);
});
});

describe('WHEN mime type is gif', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.gif',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'image/gif');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

await service.generatePreview(params);
const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath);
});
const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

it('should call imagemagicks resize method', async () => {
const { params } = setup();
return { params, originFile, expectedFileData };
};

await service.generatePreview(params);
it('should call imagemagicks coalesce method', async () => {
const { params } = setup();

expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>');
expect(resizeMock).toHaveBeenCalledTimes(1);
});

it('should call imagemagicks stream method', async () => {
const { params } = setup();

await service.generatePreview(params);
await service.generatePreview(params);

expect(streamMock).toHaveBeenCalledWith(params.previewOptions.format);
expect(streamMock).toHaveBeenCalledTimes(1);
expect(coalesceMock).toHaveBeenCalledTimes(1);
expect(resizeMock).toHaveBeenCalledTimes(1);
});
});
});

it('should call S3ClientAdapters create method', async () => {
const { params, expectedFileData } = setup();

await service.generatePreview(params);

expect(s3ClientAdapter.create).toHaveBeenCalledWith(params.previewFilePath, expectedFileData);
expect(s3ClientAdapter.create).toHaveBeenCalledTimes(1);
});

it('should should return values', async () => {
const { params } = setup();
const expectedValue = { previewFilePath: params.previewFilePath, status: true };

const result = await service.generatePreview(params);

expect(result).toEqual(expectedValue);
describe('WHEN preview is not possible', () => {
const setup = (mimeType?: string, width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, mimeType);
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

return { params, originFile };
};

describe('WHEN mimeType is undefined', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup();

const error = new UnprocessableEntityException();
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});

describe('WHEN mimeType is text/plain ', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup('text/plain');

const error = new UnprocessableEntityException();
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});
});
});

describe('WHEN previewParams.width not set', () => {
const setup = (width = 500) => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
width,
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};

return { params, originFile, expectedFileData };
};

it('should not call imagemagicks resize method', async () => {
const { params } = setup(0);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { GetFile, S3ClientAdapter } from '@infra/s3-client';
import { Injectable } from '@nestjs/common';
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { Logger } from '@src/core/logger';
import { subClass } from 'gm';
import { PassThrough } from 'stream';
import { PreviewFileOptions, PreviewOptions, PreviewResponseMessage } from './interface';
import { PreviewFileOptions, PreviewInputMimeTypes, PreviewOptions, PreviewResponseMessage } from './interface';
import { PreviewActionsLoggable } from './loggable/preview-actions.loggable';
import { PreviewGeneratorBuilder } from './preview-generator.builder';

Expand All @@ -20,6 +20,9 @@ export class PreviewGeneratorService {
const { originFilePath, previewFilePath, previewOptions } = params;

const original = await this.downloadOriginFile(originFilePath);

this.checkIfPreviewPossible(original, params);

const preview = this.resizeAndConvert(original, previewOptions);

const file = PreviewGeneratorBuilder.buildFile(preview, params.previewOptions);
Expand All @@ -34,6 +37,16 @@ export class PreviewGeneratorService {
};
}

private checkIfPreviewPossible(original: GetFile, params: PreviewFileOptions): void | UnprocessableEntityException {
const isPreviewPossible =
original.contentType && Object.values<string>(PreviewInputMimeTypes).includes(original.contentType);

if (!isPreviewPossible) {
this.logger.warning(new PreviewActionsLoggable('PreviewGeneratorService.previewNotPossible', params));
throw new UnprocessableEntityException();
}
}

private async downloadOriginFile(pathToFile: string): Promise<GetFile> {
const file = await this.storageClient.get(pathToFile);

Expand All @@ -45,6 +58,14 @@ export class PreviewGeneratorService {

const preview = this.imageMagick(original.data);

if (original.contentType === PreviewInputMimeTypes.APPLICATION_PDF) {
preview.selectFrame(0);
}

if (original.contentType === PreviewInputMimeTypes.IMAGE_GIF) {
preview.coalesce();
}

if (width) {
preview.resize(width, undefined, '>');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { FileRecordParentType } from '@infra/rabbitmq';
import { EntityManager } from '@mikro-orm/mongodb';
import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client';
import { ContextExternalToolService } from '@modules/tool/context-external-tool/service';
import { Test, TestingModule } from '@nestjs/testing';
import { FileRecordParentType } from '@infra/rabbitmq';
import {
columnBoardFactory,
columnFactory,
Expand Down Expand Up @@ -171,6 +171,14 @@ describe(RecursiveDeleteVisitor.name, () => {
expect(em.remove).toHaveBeenCalledWith(em.getReference(linkElement.constructor, linkElement.id));
expect(em.remove).toHaveBeenCalledWith(em.getReference(childLinkElement.constructor, childLinkElement.id));
});

it('should call deleteFilesOfParent', async () => {
const { linkElement } = setup();

await service.visitLinkElementAsync(linkElement);

expect(filesStorageClientAdapterService.deleteFilesOfParent).toHaveBeenCalledWith(linkElement.id);
});
});

describe('visitSubmissionContainerElementAsync', () => {
Expand Down
Loading

0 comments on commit 8ad11d2

Please sign in to comment.