Files
gerbeur/api/test/lib/upload.test.ts

121 lines
4.0 KiB
TypeScript

import "../setup.ts";
import { PNG } from "../helpers.ts";
import { assertEquals, assertThrows } from "@std/assert";
import {
detectImageMime,
scalableImage,
validateImageUpload,
} from "../../lib/upload.ts";
import { APIException } from "../../lib/api-exception.ts";
Deno.test("detectImageMime: sniffs magic bytes, not names", () => {
assertEquals(detectImageMime(PNG), "image/png");
assertEquals(
detectImageMime(new Uint8Array([0xff, 0xd8, 0xff, 0xe0])),
"image/jpeg",
);
assertEquals(
detectImageMime(new TextEncoder().encode("GIF89a")),
"image/gif",
);
assertEquals(
detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")),
"image/webp",
);
assertEquals(
detectImageMime(new TextEncoder().encode("RIFF\0\0\0\0WAVEfmt ")),
null,
);
assertEquals(
detectImageMime(new TextEncoder().encode("<svg xmlns=...>")),
null,
);
assertEquals(detectImageMime(new Uint8Array()), null);
});
Deno.test("validateImageUpload: rejects non-images and anything over 5 MB", () => {
assertEquals(validateImageUpload(PNG), "image/png");
assertThrows(
() => validateImageUpload(new TextEncoder().encode("<html>")),
APIException,
"not a recognised image",
);
const big = new Uint8Array(5 * 1024 * 1024 + 1);
big.set(PNG);
assertThrows(() => validateImageUpload(big), APIException, "too large");
});
/** A JPEG's first bytes: SOI, then an EXIF segment stating `orientation`. */
function jpegWithOrientation(orientation: number, little: boolean): Uint8Array {
const u16 = (n: number) => little ? [n & 0xff, n >> 8] : [n >> 8, n & 0xff];
const u32 = (n: number) => little ? [...u16(n), 0, 0] : [0, 0, ...u16(n)];
const tiff = [
...(little ? [0x49, 0x49] : [0x4d, 0x4d]),
...u16(42),
...u32(8),
...u16(1),
...u16(0x0112),
...u16(3),
...u32(1),
...u16(orientation),
...[0, 0],
...u32(0),
];
const exif = [0x45, 0x78, 0x69, 0x66, 0, 0, ...tiff];
const length = exif.length + 2;
return new Uint8Array([
...[0xff, 0xd8],
// A JFIF segment first, as a camera's file may have.
...[0xff, 0xe0, 0x00, 0x04, 0x00, 0x00],
...[0xff, 0xe1, length >> 8, length & 0xff],
...exif,
...[0xff, 0xda],
]);
}
Deno.test("scalableImage: a JPEG's EXIF orientation, upright without one", () => {
const turn = (data: Uint8Array) => scalableImage(data)?.orientation;
assertEquals(turn(jpegWithOrientation(6, false)), 6);
assertEquals(turn(jpegWithOrientation(8, true)), 8);
assertEquals(turn(jpegWithOrientation(42, true)), 1);
assertEquals(turn(new Uint8Array([0xff, 0xd8, 0xff, 0xda, 0, 2])), 1);
// Cut short of what it announces: upright, not an error.
assertEquals(turn(jpegWithOrientation(6, false).subarray(0, 20)), 1);
assertEquals(turn(PNG), 1);
});
Deno.test("scalableImage: an animated WebP and what isn't an image are served as they are", () => {
const webp = (flags: number) =>
new Uint8Array([
...new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8X"),
...[10, 0, 0, 0],
flags,
]);
assertEquals(scalableImage(webp(0x02)), null);
// A reader without WebP gets a PNG of what may be transparent, a JPEG of
// the rest.
const still = { orientation: 1, animated: false };
assertEquals(scalableImage(webp(0x10)), { ...still, compat: "png" });
assertEquals(
scalableImage(new TextEncoder().encode("RIFF\0\0\0\0WEBPVP8 ")),
{ ...still, compat: "png" },
);
assertEquals(scalableImage(PNG), { ...still, compat: "png" });
assertEquals(scalableImage(jpegWithOrientation(1, true)), {
...still,
compat: "jpg",
});
assertEquals(scalableImage(new TextEncoder().encode("BM-a-bitmap")), {
...still,
compat: "jpg",
});
// Only a GIF's frames are an animation to keep.
assertEquals(scalableImage(new TextEncoder().encode("GIF89a")), {
orientation: 1,
animated: true,
compat: "png",
});
assertEquals(scalableImage(new TextEncoder().encode("<svg/>")), null);
assertEquals(scalableImage(new Uint8Array()), null);
});