Files
menu/test/pipeline.test.ts
T
tanshu a635c5a214 Answer an oversized photo with the reason, not a 500
A file past the size limit makes the multipart stream itself throw, which
escaped the per-file catch and reached the error handler: the phone that just
sent a 35 MB photo got 'Something went wrong'. Catch the limit errors around the
part loop, say which limit was hit, and report it once rather than twice. Also
asserts that derivatives carry no EXIF while the Original keeps the uploaded
bytes.
2026-09-22 21:41:12 +05:30

130 lines
4.9 KiB
TypeScript

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import sharp from 'sharp';
import {
ACCEPTED_MIME_TYPES,
UnsupportedImageError,
createImagePipeline,
} from '../src/images/pipeline.ts';
const pipeline = createImagePipeline();
async function photo(width: number, height: number, format: 'jpeg' | 'png' | 'webp' = 'jpeg') {
const base = sharp({
create: { width, height, channels: 3, background: { r: 20, g: 120, b: 170 } },
});
if (format === 'png') return base.png().toBuffer();
if (format === 'webp') return base.webp().toBuffer();
return base.jpeg().toBuffer();
}
describe('createImagePipeline', () => {
it('produces a Viewing image, a JPEG fallback and a Thumbnail', async () => {
const derivatives = await pipeline.process(await photo(3000, 2000), 'IMG_0001.JPG');
assert.equal(derivatives.sourceExt, 'jpg');
assert.match(derivatives.hash, /^[0-9a-f]{12}$/);
assert.equal(derivatives.webp.subarray(8, 12).toString(), 'WEBP');
const view = await sharp(derivatives.webp).metadata();
assert.equal(view.width, 1600);
assert.equal(view.height, 1067);
assert.equal(view.format, 'webp');
assert.equal(derivatives.width, 1600);
assert.equal(derivatives.height, 1067);
const jpeg = await sharp(derivatives.jpeg).metadata();
assert.equal(jpeg.width, 1600);
assert.equal(jpeg.format, 'jpeg');
const thumb = await sharp(derivatives.thumb).metadata();
assert.equal(thumb.width, 480);
assert.equal(thumb.format, 'webp');
assert.equal(derivatives.original.length > 0, true);
});
it('keeps the Original bytes untouched and hashes them', async () => {
const input = await photo(2400, 1600);
const derivatives = await pipeline.process(input, 'a.jpg');
assert.equal(Buffer.compare(derivatives.original, input), 0);
const again = await pipeline.process(input, 'a.jpg');
assert.equal(again.hash, derivatives.hash);
const different = await pipeline.process(await photo(2400, 1600, 'png'), 'b.png');
assert.notEqual(different.hash, derivatives.hash);
});
it('accepts PNG and WebP as well as JPEG', async () => {
assert.equal((await pipeline.process(await photo(800, 600, 'png'), 'a.png')).sourceExt, 'png');
assert.equal((await pipeline.process(await photo(800, 600, 'webp'), 'a.webp')).sourceExt, 'webp');
assert.deepEqual([...ACCEPTED_MIME_TYPES], ['image/jpeg', 'image/png', 'image/webp']);
});
it('does not enlarge an image smaller than the Viewing width', async () => {
const derivatives = await pipeline.process(await photo(200, 150), 'small.jpg');
assert.equal(derivatives.width, 200);
assert.equal(derivatives.height, 150);
const thumb = await sharp(derivatives.thumb).metadata();
assert.equal(thumb.width, 200);
});
it('applies EXIF orientation so the pixels are upright', async () => {
// 1200x600 with orientation 6 (rotate 90 CW) is 600x1200 once upright.
const rotated = await sharp({
create: { width: 1200, height: 600, channels: 3, background: { r: 200, g: 30, b: 30 } },
})
.withMetadata({ orientation: 6 })
.jpeg()
.toBuffer();
const derivatives = await pipeline.process(rotated, 'rotated.jpg');
assert.equal(derivatives.width, 600);
assert.equal(derivatives.height, 1200);
const view = await sharp(derivatives.webp).metadata();
assert.equal(view.width, 600);
assert.equal(view.height, 1200);
assert.equal(view.orientation, undefined);
});
it('rejects a file that is not an accepted image, naming the file', async () => {
await assert.rejects(
() => pipeline.process(Buffer.from('not an image at all'), 'notes.pdf'),
(error: unknown) =>
error instanceof UnsupportedImageError && error.message.includes('notes.pdf'),
);
});
it('rejects a GIF, which sharp can read but we do not accept', async () => {
const gif = await sharp({
create: { width: 40, height: 40, channels: 3, background: '#fff' },
})
.gif()
.toBuffer();
await assert.rejects(
() => pipeline.process(gif, 'anim.gif'),
(error: unknown) =>
error instanceof UnsupportedImageError && error.message.includes('gif'),
);
});
it('strips metadata from the derivatives but keeps the Original as uploaded', async () => {
const tagged = await sharp({
create: { width: 1200, height: 900, channels: 3, background: { r: 10, g: 90, b: 120 } },
})
.withExif({ IFD0: { Copyright: 'Test Kitchen', Artist: 'Nobody' } })
.jpeg()
.toBuffer();
assert.notEqual((await sharp(tagged).metadata()).exif, undefined);
const derivatives = await pipeline.process(tagged, 'tagged.jpg');
assert.equal((await sharp(derivatives.webp).metadata()).exif, undefined);
assert.equal((await sharp(derivatives.jpeg).metadata()).exif, undefined);
assert.equal((await sharp(derivatives.thumb).metadata()).exif, undefined);
assert.equal(Buffer.compare(derivatives.original, tagged), 0);
});
});