diff --git a/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts b/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts index 016c261b122..6c5a3eb036d 100644 --- a/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.service.spec.ts @@ -1,6 +1,7 @@ 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'; @@ -16,13 +17,13 @@ jest.mock('gm', () => { }; }); -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', @@ -68,76 +69,136 @@ 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); + describe('WHEN download of original and preview file is successful', () => { + describe('WHEN preview is possible', () => { + 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 data = Readable.from('text'); + streamMock.mockReturnValueOnce(data); - const expectedFileData = { - data, - mimeType: params.previewOptions.format, - }; + const expectedFileData = { + data, + mimeType: params.previewOptions.format, + }; - return { params, originFile, expectedFileData }; - }; + 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(); + it('should call storageClient get method with originFilePath', async () => { + const { params } = setup(); - await service.generatePreview(params); + await service.generatePreview(params); - expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath); - }); + expect(s3ClientAdapter.get).toBeCalledWith(params.originFilePath); + }); - it('should call imagemagicks resize method', async () => { - const { params } = setup(); + it('should call imagemagicks resize method', async () => { + const { params } = setup(); - await service.generatePreview(params); + await service.generatePreview(params); - expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>'); - expect(resizeMock).toHaveBeenCalledTimes(1); - }); + expect(resizeMock).toHaveBeenCalledWith(params.previewOptions.width, undefined, '>'); + expect(resizeMock).toHaveBeenCalledTimes(1); + }); - it('should call imagemagicks stream method', async () => { - const { params } = setup(); + 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(streamMock).toHaveBeenCalledWith(params.previewOptions.format); + expect(streamMock).toHaveBeenCalledTimes(1); + }); - it('should call S3ClientAdapters create method', async () => { - const { params, expectedFileData } = setup(); + it('should call S3ClientAdapters create method', async () => { + const { params, expectedFileData } = setup(); - await service.generatePreview(params); + await service.generatePreview(params); - expect(s3ClientAdapter.create).toHaveBeenCalledWith(params.previewFilePath, expectedFileData); - expect(s3ClientAdapter.create).toHaveBeenCalledTimes(1); - }); + 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 }; + it('should should return values', async () => { + const { params } = setup(); + const expectedValue = { previewFilePath: params.previewFilePath, status: true }; - const result = await service.generatePreview(params); + const result = await service.generatePreview(params); - expect(result).toEqual(expectedValue); + 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); diff --git a/apps/server/src/infra/preview-generator/preview-generator.service.ts b/apps/server/src/infra/preview-generator/preview-generator.service.ts index 795fd0f4ddf..d9c2135ceb0 100644 --- a/apps/server/src/infra/preview-generator/preview-generator.service.ts +++ b/apps/server/src/infra/preview-generator/preview-generator.service.ts @@ -1,5 +1,5 @@ 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'; @@ -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); @@ -34,6 +37,16 @@ export class PreviewGeneratorService { }; } + private checkIfPreviewPossible(original: GetFile, params: PreviewFileOptions): void | UnprocessableEntityException { + const isPreviewPossible = + original.contentType && Object.values(PreviewInputMimeTypes).includes(original.contentType); + + if (!isPreviewPossible) { + this.logger.warning(new PreviewActionsLoggable('PreviewGeneratorService.previewNotPossible', params)); + throw new UnprocessableEntityException(); + } + } + private async downloadOriginFile(pathToFile: string): Promise { const file = await this.storageClient.get(pathToFile);