Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-6041 - catch errors from imagemagick #4675

Merged
merged 7 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum ErrorType {
CREATE_PREVIEW_NOT_POSSIBLE = 'CREATE_PREVIEW_NOT_POSSIBLE',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { InternalServerErrorException } from '@nestjs/common';
import { PreviewNotPossibleException } from './preview-exception';

describe(PreviewNotPossibleException.name, () => {
describe('WHEN getLogMessage is called', () => {
const setup = () => {
const payload = {
originFilePath: 'originFilePath',
previewFilePath: 'previewFilePath',
previewOptions: {
format: 'format',
width: 100,
},
};
const error = new Error('error');

return { payload, error };
};

it('should return error log message', () => {
const { payload, error } = setup();

const exception = new PreviewNotPossibleException(payload, error);

const result = exception.getLogMessage();

expect(result).toEqual({
type: InternalServerErrorException.name,
stack: exception.stack,
error,
data: {
originFilePath: 'originFilePath',
previewFilePath: 'previewFilePath',
format: 'format',
width: 100,
},
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { InternalServerErrorException } from '@nestjs/common';
import { ErrorLogMessage, Loggable } from '@src/core/logger';
import { PreviewFileOptions } from '../interface';
import { ErrorType } from '../interface/error-status.enum';

export class PreviewNotPossibleException extends InternalServerErrorException implements Loggable {
constructor(private readonly payload: PreviewFileOptions, private readonly error?: Error) {
super(ErrorType.CREATE_PREVIEW_NOT_POSSIBLE);
}

getLogMessage(): ErrorLogMessage {
const { originFilePath, previewFilePath, previewOptions } = this.payload;
const message: ErrorLogMessage = {
type: InternalServerErrorException.name,
stack: this.stack,
error: this.error,
data: {
originFilePath,
previewFilePath,
format: previewOptions.format,
width: previewOptions.width,
},
};

return message;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { DeepMocked, createMock } from '@golevelup/ts-jest';
import { GetFile, S3ClientAdapter } from '@infra/s3-client';
import { UnprocessableEntityException } from '@nestjs/common';
import { InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@src/core/logger';
import { Readable } from 'node:stream';
import { PassThrough, Readable } from 'node:stream';
import { ErrorType } from './interface/error-status.enum';
import { PreviewGeneratorService } from './preview-generator.service';

const streamMock = jest.fn();
let streamMock = jest.fn();
const resizeMock = jest.fn();
const coalesceMock = jest.fn();
const selectFrameMock = jest.fn();
Expand All @@ -16,7 +18,7 @@ const imageMagickMock = () => {
resize: resizeMock,
selectFrame: selectFrameMock,
coalesce: coalesceMock,
data: Readable.from('text'),
data: Buffer.from('text'),
};
};
jest.mock('gm', () => {
Expand All @@ -40,6 +42,21 @@ const createFile = (contentRange?: string, contentType?: string): GetFile => {
return fileResponse;
};

const createMockStream = (err: Error | null = null) => {
const stdout = new PassThrough();
const stderr = new PassThrough();

streamMock = jest
.fn()
.mockImplementation(
(_format: string, callback: (err: Error | null, stdout: PassThrough, stderr: PassThrough) => void) => {
callback(err, stdout, stderr);
}
);

return { stdout, stderr };
};

describe('PreviewGeneratorService', () => {
let module: TestingModule;
let service: PreviewGeneratorService;
Expand Down Expand Up @@ -92,15 +109,14 @@ describe('PreviewGeneratorService', () => {
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);
const data = Buffer.from('text');
const { stdout } = createMockStream();

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};
process.nextTick(() => {
stdout.write(data);
});

return { params, originFile, expectedFileData };
return { params, originFile };
};

it('should call storageClient get method with originFilePath', async () => {
Expand All @@ -125,12 +141,18 @@ describe('PreviewGeneratorService', () => {

await service.generatePreview(params);

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

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

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const expectedFileData = expect.objectContaining({
data: expect.any(PassThrough),
mimeType: params.previewOptions.format,
});

await service.generatePreview(params);

Expand Down Expand Up @@ -161,15 +183,14 @@ describe('PreviewGeneratorService', () => {
const originFile = createFile(undefined, 'application/pdf');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);
const data = Buffer.from('text');
const { stdout } = createMockStream();

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};
process.nextTick(() => {
stdout.write(data);
});

return { params, originFile, expectedFileData };
return { params, originFile };
};

it('should call imagemagicks selectFrameMock method', async () => {
Expand All @@ -195,15 +216,14 @@ describe('PreviewGeneratorService', () => {
const originFile = createFile(undefined, 'image/gif');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);
const data = Buffer.from('text');
const { stdout } = createMockStream();

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};
process.nextTick(() => {
stdout.write(data);
});

return { params, originFile, expectedFileData };
return { params, originFile };
};

it('should call imagemagicks coalesce method', async () => {
Expand Down Expand Up @@ -237,7 +257,7 @@ describe('PreviewGeneratorService', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup();

const error = new UnprocessableEntityException();
const error = new UnprocessableEntityException(ErrorType.CREATE_PREVIEW_NOT_POSSIBLE);
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});
Expand All @@ -246,7 +266,7 @@ describe('PreviewGeneratorService', () => {
it('should throw UnprocessableEntityException', async () => {
const { params } = setup('text/plain');

const error = new UnprocessableEntityException();
const error = new UnprocessableEntityException(ErrorType.CREATE_PREVIEW_NOT_POSSIBLE);
await expect(service.generatePreview(params)).rejects.toThrowError(error);
});
});
Expand All @@ -266,15 +286,14 @@ describe('PreviewGeneratorService', () => {
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data = Readable.from('text');
streamMock.mockReturnValueOnce(data);
const data = Buffer.from('text');
const { stdout } = createMockStream();

const expectedFileData = {
data,
mimeType: params.previewOptions.format,
};
process.nextTick(() => {
stdout.write(data);
});

return { params, originFile, expectedFileData };
return { params, originFile };
};

it('should not call imagemagicks resize method', async () => {
Expand All @@ -286,5 +305,75 @@ describe('PreviewGeneratorService', () => {
expect(resizeMock).not.toHaveBeenCalledTimes(1);
});
});

describe('WHEN STDERR stream has an error', () => {
const setup = () => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

const data1 = Buffer.from('imagemagick ');
const data2 = Buffer.from('is not found');
const { stderr } = createMockStream();

process.nextTick(() => {
stderr.write(data1);
stderr.write(data2);
stderr.end();
});

const expectedError = new InternalServerErrorException(ErrorType.CREATE_PREVIEW_NOT_POSSIBLE);

return { params, originFile, expectedError };
};

it('should throw error', async () => {
const { params, expectedError } = setup();

await expect(service.generatePreview(params)).rejects.toThrowError(expectedError);
});

it('should have external error in getLogMessage', async () => {
const { params } = setup();
try {
await service.generatePreview(params);
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(error.getLogMessage().error).toEqual(new Error('imagemagick is not found'));
}
});
});

describe('WHEN GM library has an error', () => {
const setup = () => {
const params = {
originFilePath: 'file/test.jpeg',
previewFilePath: 'preview/text.webp',
previewOptions: {
format: 'webp',
},
};
const originFile = createFile(undefined, 'image/jpeg');
s3ClientAdapter.get.mockResolvedValueOnce(originFile);

createMockStream(new Error('imagemagic is not found'));

const expectedError = new InternalServerErrorException(ErrorType.CREATE_PREVIEW_NOT_POSSIBLE);

return { params, originFile, expectedError };
};

it('should throw error', async () => {
const { params, expectedError } = setup();

await expect(service.generatePreview(params)).rejects.toThrowError(expectedError);
});
});
});
});
Loading
Loading