import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const FIXTURES = join(__dirname, "../../fixtures");
// ---------------------------------------------------------------------------
// 1. File validation
// ---------------------------------------------------------------------------
// We need to mock `../config.js` which file-validation.ts imports as `env`.
// Provide a minimal env object with the fields file-validation.ts uses.
vi.mock("../../../apps/api/src/config.js", () => ({
env: {
MAX_MEGAPIXELS: 100,
WORKSPACE_PATH: "/tmp/test-workspace",
},
}));
describe("validateImageBuffer", () => {
let validateImageBuffer: typeof import("../../../apps/api/src/lib/file-validation.js").validateImageBuffer;
beforeEach(async () => {
// Re-import to pick up mock
const mod = await import("../../../apps/api/src/lib/file-validation.js");
validateImageBuffer = mod.validateImageBuffer;
});
// -- Valid formats --------------------------------------------------------
it("accepts a valid PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("png");
expect(result.width).toBe(200);
expect(result.height).toBe(150);
}
});
it("accepts a valid JPEG file", async () => {
const buf = await readFile(join(FIXTURES, "test-100x100.jpg"));
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("jpeg");
expect(result.width).toBe(100);
expect(result.height).toBe(100);
}
});
it("accepts a valid WebP file", async () => {
const buf = await readFile(join(FIXTURES, "test-50x50.webp"));
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("webp");
expect(result.width).toBe(50);
expect(result.height).toBe(50);
}
});
it("accepts a tiny 1x1 PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-1x1.png"));
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("png");
expect(result.width).toBe(1);
expect(result.height).toBe(1);
}
});
// -- Synthetic valid buffers for formats without fixtures -----------------
it("accepts a GIF buffer with correct magic bytes", async () => {
// Minimal GIF89a: header + logical screen descriptor + terminator
const gif = Buffer.from("474946383961010001000000002c00000000010001000002024401003b", "hex");
const result = await validateImageBuffer(gif);
// sharp may or may not parse this minimal GIF; what matters is magic bytes pass
// If sharp fails metadata, that is "Failed to read image metadata"
expect(result).toBeDefined();
if (result.valid) {
expect(result.format).toBe("gif");
}
});
it("accepts a BMP buffer with correct magic bytes", async () => {
// Build a minimal 1x1 24-bit BMP (62 bytes header + 4 bytes pixel data)
const bmp = Buffer.alloc(66);
// BM signature
bmp[0] = 0x42;
bmp[1] = 0x4d;
// File size (66 bytes, little-endian)
bmp.writeUInt32LE(66, 2);
// Reserved
bmp.writeUInt32LE(0, 6);
// Pixel data offset
bmp.writeUInt32LE(62, 10);
// DIB header size (40 = BITMAPINFOHEADER)
bmp.writeUInt32LE(40, 14);
// Width = 1
bmp.writeInt32LE(1, 18);
// Height = 1
bmp.writeInt32LE(1, 22);
// Planes = 1
bmp.writeUInt16LE(1, 26);
// Bits per pixel = 24
bmp.writeUInt16LE(24, 28);
// Compression = 0 (BI_RGB)
bmp.writeUInt32LE(0, 30);
// Image size (can be 0 for BI_RGB)
bmp.writeUInt32LE(0, 34);
// X/Y pixels per meter
bmp.writeInt32LE(2835, 38);
bmp.writeInt32LE(2835, 42);
// Colors used / important
bmp.writeUInt32LE(0, 46);
bmp.writeUInt32LE(0, 50);
const result = await validateImageBuffer(bmp);
expect(result).toBeDefined();
if (result.valid) {
expect(result.format).toBe("bmp");
}
});
it("accepts a HEIC file with correct magic bytes", async () => {
const heicBuf = await readFile(join(FIXTURES, "test-200x150.heic"));
const result = await validateImageBuffer(heicBuf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("heif");
}
});
it("accepts a TIFF buffer (little-endian byte order)", async () => {
// Minimal TIFF is complex; just verify magic bytes detection works
// and sharp either parses or gives metadata error (not "unrecognized format")
const tiffLE = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(tiffLE);
// Magic bytes should be detected as tiff, but sharp may fail on a truncated TIFF
expect(result).toBeDefined();
if (!result.valid) {
expect(result.reason).toBe("Failed to read image metadata");
}
});
it("accepts a TIFF buffer (big-endian byte order)", async () => {
const tiffBE = Buffer.from([0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x08]);
const result = await validateImageBuffer(tiffBE);
expect(result).toBeDefined();
if (!result.valid) {
expect(result.reason).toBe("Failed to read image metadata");
}
});
// -- Empty / null / missing -----------------------------------------------
it("rejects an empty buffer", async () => {
const result = await validateImageBuffer(Buffer.alloc(0));
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("File is empty");
}
});
it("rejects null passed as buffer", async () => {
const result = await validateImageBuffer(null as unknown as Buffer);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("File is empty");
}
});
it("rejects undefined passed as buffer", async () => {
const result = await validateImageBuffer(undefined as unknown as Buffer);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("File is empty");
}
});
// -- Random / garbage data ------------------------------------------------
it("rejects a buffer of random bytes", async () => {
const garbage = Buffer.from(Array.from({ length: 256 }, () => Math.floor(Math.random() * 256)));
// Ensure the first bytes do not accidentally match any magic signature
garbage[0] = 0x00;
garbage[1] = 0x00;
const result = await validateImageBuffer(garbage);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Unrecognized image format");
}
});
it("rejects a text file disguised with no image magic bytes", async () => {
const text = Buffer.from("This is definitely not an image file. Lorem ipsum dolor sit amet.");
const result = await validateImageBuffer(text);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Unrecognized image format");
}
});
it("rejects an HTML file", async () => {
const html = Buffer.from("
Hello
");
const result = await validateImageBuffer(html);
expect(result.valid).toBe(false);
});
it("rejects a JSON file", async () => {
const json = Buffer.from(JSON.stringify({ image: "fake.png", width: 100 }));
const result = await validateImageBuffer(json);
expect(result.valid).toBe(false);
});
// -- Truncated headers ----------------------------------------------------
it("rejects a buffer with only the first byte of a PNG header", async () => {
const partial = Buffer.from([0x89]);
const result = await validateImageBuffer(partial);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Unrecognized image format");
}
});
it("rejects a truncated PNG header (3 of 4 magic bytes)", async () => {
const partial = Buffer.from([0x89, 0x50, 0x4e]);
const result = await validateImageBuffer(partial);
expect(result.valid).toBe(false);
});
it("rejects a buffer with PNG magic bytes but no real image data", async () => {
// Correct 4-byte PNG signature but nothing valid after
const fakePng = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(fakePng);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Failed to read image metadata");
}
});
it("rejects a buffer with JPEG magic bytes but truncated body", async () => {
const fakeJpeg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]);
const result = await validateImageBuffer(fakeJpeg);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Failed to read image metadata");
}
});
// -- RIFF container that is NOT WebP (e.g. WAV/AVI) ----------------------
it("rejects a RIFF container that is not WebP (e.g. WAVE)", async () => {
// RIFF....WAVE
const riffWave = Buffer.alloc(12);
riffWave.write("RIFF", 0, "ascii");
riffWave.writeUInt32LE(4, 4); // chunk size
riffWave.write("WAVE", 8, "ascii");
const result = await validateImageBuffer(riffWave);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Unrecognized image format");
}
});
it("rejects a RIFF container that is AVI", async () => {
const riffAvi = Buffer.alloc(12);
riffAvi.write("RIFF", 0, "ascii");
riffAvi.writeUInt32LE(4, 4);
riffAvi.write("AVI ", 8, "ascii");
const result = await validateImageBuffer(riffAvi);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("Unrecognized image format");
}
});
it("rejects a RIFF container too short to contain WEBP signature", async () => {
// Only 8 bytes of RIFF -- no room for the format tag at bytes 8-11
const shortRiff = Buffer.alloc(8);
shortRiff.write("RIFF", 0, "ascii");
shortRiff.writeUInt32LE(0, 4);
const result = await validateImageBuffer(shortRiff);
expect(result.valid).toBe(false);
});
// -- Partial magic bytes edge cases ---------------------------------------
it("rejects a single-byte buffer (0xFF -- JPEG first byte only)", async () => {
const result = await validateImageBuffer(Buffer.from([0xff]));
expect(result.valid).toBe(false);
});
it("rejects two JPEG magic bytes without the third", async () => {
const result = await validateImageBuffer(Buffer.from([0xff, 0xd8]));
expect(result.valid).toBe(false);
});
it("accepts BMP magic (decoded via CLI, not Sharp)", async () => {
const fakeBmp = Buffer.from([0x42, 0x4d, 0x00, 0x00]);
const result = await validateImageBuffer(fakeBmp);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("bmp");
}
});
// -- Oversized image (megapixel limit) ------------------------------------
it("rejects an image that exceeds MAX_MEGAPIXELS", async () => {
// We cannot easily build a 100MP+ image buffer in a test, so re-mock env
// with a very low limit (0.0001 MP = 100 pixels) and test with the 200x150 fixture
const origMod = await import("../../../apps/api/src/config.js");
const savedMax = origMod.env.MAX_MEGAPIXELS;
origMod.env.MAX_MEGAPIXELS = 0.0001; // 100 pixels -- 200x150 = 30000px >> 100
try {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toContain("exceeds maximum size");
expect(result.reason).toContain("MP");
}
} finally {
origMod.env.MAX_MEGAPIXELS = savedMax;
}
});
// -- Polyglot / tricky inputs ---------------------------------------------
it("rejects a ZIP file (PK magic 0x50 0x4B)", async () => {
const zip = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(zip);
expect(result.valid).toBe(false);
});
it("rejects a PDF file (%PDF-)", async () => {
const pdf = Buffer.from("%PDF-1.4 fake content");
const result = await validateImageBuffer(pdf);
expect(result.valid).toBe(false);
});
it("rejects an EXE file (MZ header)", async () => {
const exe = Buffer.from([0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(exe);
expect(result.valid).toBe(false);
});
// -- Null-byte buffers ----------------------------------------------------
it("rejects a buffer that is entirely null bytes (small)", async () => {
const nullBuf = Buffer.alloc(32); // all zeros
const result = await validateImageBuffer(nullBuf);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("File contains no image data");
}
});
it("rejects a large buffer that is entirely null bytes", async () => {
const nullBuf = Buffer.alloc(1024); // all zeros, > 64 bytes
const result = await validateImageBuffer(nullBuf);
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.reason).toBe("File contains no image data");
}
});
// -- SVG detection --------------------------------------------------------
it("accepts an SVG buffer with valid XML", async () => {
const svg = Buffer.from(
'',
);
const result = await validateImageBuffer(svg);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("svg");
}
});
// -- HDR text detection ---------------------------------------------------
it("accepts an HDR buffer with #?RADIANCE header", async () => {
// Build a minimal buffer that starts with the HDR magic text
const hdrHeader = Buffer.from("#?RADIANCE\n");
const padding = Buffer.alloc(100);
const hdrBuf = Buffer.concat([hdrHeader, padding]);
const result = await validateImageBuffer(hdrBuf);
// HDR is a CLI_DECODED_FORMAT, so it bypasses sharp metadata
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("hdr");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
it("accepts an HDR buffer with #?RGBE header", async () => {
const hdrHeader = Buffer.from("#?RGBE\n");
const padding = Buffer.alloc(100);
const hdrBuf = Buffer.concat([hdrHeader, padding]);
const result = await validateImageBuffer(hdrBuf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("hdr");
}
});
it("does not detect HDR for a buffer shorter than 10 bytes", async () => {
const shortBuf = Buffer.from("#?RADIAN"); // 8 bytes, too short
const result = await validateImageBuffer(shortBuf);
expect(result.valid).toBe(false);
});
// -- RAW extension differentiation ----------------------------------------
it("detects RAW format when TIFF magic bytes + RAW extension (DNG)", async () => {
const tiffLE = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(tiffLE, "photo.dng");
// RAW is a CLI_DECODED_FORMAT, so width/height are 0
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("raw");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
it("detects RAW format with CR2 extension", async () => {
const tiffLE = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(tiffLE, "photo.cr2");
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("raw");
}
});
it("detects RAW format with NEF extension", async () => {
const tiffLE = Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x08, 0x00, 0x00, 0x00]);
const result = await validateImageBuffer(tiffLE, "photo.nef");
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("raw");
}
});
// -- TGA extension-only detection -----------------------------------------
it("detects TGA format by extension (no magic bytes)", async () => {
// TGA has no magic bytes, so detection is extension-only
const randomBytes = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
const result = await validateImageBuffer(randomBytes, "image.tga");
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("tga");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
// -- CLI decoded format bypass (PSD, EXR) ---------------------------------
it("detects PSD format and bypasses sharp dimension check", async () => {
// PSD magic: "8BPS"
const psdBuf = Buffer.alloc(64);
psdBuf[0] = 0x38;
psdBuf[1] = 0x42;
psdBuf[2] = 0x50;
psdBuf[3] = 0x53;
const result = await validateImageBuffer(psdBuf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("psd");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
it("detects EXR format and bypasses sharp dimension check", async () => {
// OpenEXR magic bytes
const exrBuf = Buffer.alloc(64);
exrBuf[0] = 0x76;
exrBuf[1] = 0x2f;
exrBuf[2] = 0x31;
exrBuf[3] = 0x01;
const result = await validateImageBuffer(exrBuf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("exr");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
// -- ICO detection --------------------------------------------------------
it("detects ICO format via magic bytes", async () => {
const icoBuf = Buffer.alloc(64);
icoBuf[0] = 0x00;
icoBuf[1] = 0x00;
icoBuf[2] = 0x01;
icoBuf[3] = 0x00;
// Need non-zero bytes past first 64 positions to avoid null-byte rejection
icoBuf[4] = 0x01;
const result = await validateImageBuffer(icoBuf);
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("ico");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
// -- AVIF/HEIF ftyp brand verification -----------------------------------
it("rejects ftyp box with unrecognized brand (not avif/heic)", async () => {
// Build a buffer with ftyp at offset 4 but brand "mp41" (not avif or heif)
const buf = Buffer.alloc(16);
buf.write("ftyp", 4, "ascii");
buf.write("mp41", 8, "ascii");
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(false);
});
// -- JXL detection --------------------------------------------------------
it("detects JXL ISOBMFF container format", async () => {
const jxlBuf = Buffer.alloc(64);
// JXL ISOBMFF magic: 00 00 00 0C 4A 58 4C 20
jxlBuf[0] = 0x00;
jxlBuf[1] = 0x00;
jxlBuf[2] = 0x00;
jxlBuf[3] = 0x0c;
jxlBuf[4] = 0x4a;
jxlBuf[5] = 0x58;
jxlBuf[6] = 0x4c;
jxlBuf[7] = 0x20;
// Need non-null bytes to avoid null-byte rejection
jxlBuf[8] = 0x01;
const result = await validateImageBuffer(jxlBuf);
// JXL is not in CLI_DECODED_FORMATS, so sharp metadata may fail
expect(result).toBeDefined();
if (result.valid) {
expect(result.format).toBe("jxl");
}
});
it("detects JXL raw codestream format", async () => {
const jxlRaw = Buffer.alloc(64);
jxlRaw[0] = 0xff;
jxlRaw[1] = 0x0a;
// Need more non-zero data
jxlRaw[2] = 0x01;
const result = await validateImageBuffer(jxlRaw);
expect(result).toBeDefined();
if (result.valid) {
expect(result.format).toBe("jxl");
}
});
// -- Exotic formats from fixture files ------------------------------------
describe("validates exotic formats", () => {
const { readFileSync } = require("node:fs");
const FORMATS_DIR = join(FIXTURES, "formats");
it("accepts PBM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pbm"));
const result = await validateImageBuffer(buf, "sample.pbm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pbm");
});
it("accepts PGM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pgm"));
const result = await validateImageBuffer(buf, "sample.pgm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pgm");
});
it("accepts PPM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.ppm"));
const result = await validateImageBuffer(buf, "sample.ppm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("ppm");
});
it("accepts DDS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dds"));
const result = await validateImageBuffer(buf, "sample.dds");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dds");
});
it("accepts DPX file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dpx"));
const result = await validateImageBuffer(buf, "sample.dpx");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dpx");
});
it("accepts FITS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.fits"));
const result = await validateImageBuffer(buf, "sample.fits");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("fits");
});
it("accepts JP2 file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.jp2"));
const result = await validateImageBuffer(buf, "sample.jp2");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("jp2");
});
it("accepts QOI file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.qoi"));
const result = await validateImageBuffer(buf, "sample.qoi");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("qoi");
});
it("accepts SVGZ file (detected as svg)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.svgz"));
const result = await validateImageBuffer(buf, "sample.svgz");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("svg");
});
it("accepts EPS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.eps"));
const result = await validateImageBuffer(buf, "sample.eps");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("eps");
});
it("accepts CUR file with CUR magic bytes", async () => {
// Build a buffer with proper CUR magic (00 00 02 00) since the fixture
// has ICO magic bytes (00 00 01 00) which gets detected as ico instead
const curBuf = Buffer.alloc(64);
curBuf[0] = 0x00;
curBuf[1] = 0x00;
curBuf[2] = 0x02;
curBuf[3] = 0x00;
curBuf[4] = 0x01;
const result = await validateImageBuffer(curBuf, "sample.cur");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("cur");
});
it("accepts HEIF file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.heif"));
const result = await validateImageBuffer(buf, "sample.heif");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("heif");
});
it("accepts APNG file (detected as png)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.apng"));
const result = await validateImageBuffer(buf, "sample.apng");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("png");
});
it("accepts DNG file (detected as raw)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dng"));
const result = await validateImageBuffer(buf, "sample.dng");
expect(result.valid).toBe(true);
if (result.valid) {
expect(result.format).toBe("raw");
expect(result.width).toBe(0);
expect(result.height).toBe(0);
}
});
it("rejects a PDF file", async () => {
const pdfBuffer = Buffer.from("%PDF-1.4 fake content");
const result = await validateImageBuffer(pdfBuffer, "test.pdf");
expect(result.valid).toBe(false);
});
});
});
// ---------------------------------------------------------------------------
// 1b. isRawExtension
// ---------------------------------------------------------------------------
describe("isRawExtension", () => {
let isRawExtension: typeof import("../../../apps/api/src/lib/file-validation.js").isRawExtension;
beforeEach(async () => {
const mod = await import("../../../apps/api/src/lib/file-validation.js");
isRawExtension = mod.isRawExtension;
});
it("returns true for DNG extension", () => {
expect(isRawExtension("dng")).toBe(true);
});
it("returns true for CR2 extension", () => {
expect(isRawExtension("cr2")).toBe(true);
});
it("returns true for NEF extension", () => {
expect(isRawExtension("nef")).toBe(true);
});
it("returns true for ARW extension", () => {
expect(isRawExtension("arw")).toBe(true);
});
it("returns true for ORF extension", () => {
expect(isRawExtension("orf")).toBe(true);
});
it("returns true for RW2 extension", () => {
expect(isRawExtension("rw2")).toBe(true);
});
it("returns true for uppercase extensions", () => {
expect(isRawExtension("DNG")).toBe(true);
expect(isRawExtension("CR2")).toBe(true);
});
it("returns true with leading dot", () => {
expect(isRawExtension(".dng")).toBe(true);
expect(isRawExtension(".CR2")).toBe(true);
});
it("returns false for non-RAW extensions", () => {
expect(isRawExtension("png")).toBe(false);
expect(isRawExtension("jpg")).toBe(false);
expect(isRawExtension("tiff")).toBe(false);
expect(isRawExtension("webp")).toBe(false);
});
it("returns false for empty string", () => {
expect(isRawExtension("")).toBe(false);
});
});
// ---------------------------------------------------------------------------
// 2. Object storage capacity guard (replaces former workspace tests)
// ---------------------------------------------------------------------------
describe("assertLocalCapacity / isBelowCapacity", () => {
it("isBelowCapacity returns true when free space is below 0.5 GB", async () => {
const { isBelowCapacity, CAPACITY_CRITICAL_GB } = await import(
"../../../apps/api/src/lib/object-storage.js"
);
expect(CAPACITY_CRITICAL_GB).toBe(0.5);
// 0.4 GB free should be below threshold
expect(isBelowCapacity(0.4 * 1024 ** 3)).toBe(true);
// 0.0 bytes free
expect(isBelowCapacity(0)).toBe(true);
});
it("isBelowCapacity returns false when free space is above 0.5 GB", async () => {
const { isBelowCapacity } = await import("../../../apps/api/src/lib/object-storage.js");
// 1 GB free
expect(isBelowCapacity(1 * 1024 ** 3)).toBe(false);
// Exactly at threshold (0.5 GB) should be false (not strictly less)
expect(isBelowCapacity(0.5 * 1024 ** 3)).toBe(false);
});
it("isBelowCapacity boundary: just below 0.5 GB is true", async () => {
const { isBelowCapacity } = await import("../../../apps/api/src/lib/object-storage.js");
// One byte below 0.5 GB
expect(isBelowCapacity(0.5 * 1024 ** 3 - 1)).toBe(true);
});
});
// ---------------------------------------------------------------------------
// 3. Env loading
// ---------------------------------------------------------------------------
describe("loadEnv", () => {
const originalEnv = { ...process.env };
beforeEach(() => {
// Reset to a clean slate before each test --
// wipe all keys that our schema cares about so defaults kick in
const keysToClean = [
"PORT",
"AUTH_ENABLED",
"DEFAULT_USERNAME",
"DEFAULT_PASSWORD",
"STORAGE_MODE",
"FILE_MAX_AGE_HOURS",
"CLEANUP_INTERVAL_MINUTES",
"MAX_UPLOAD_SIZE_MB",
"MAX_BATCH_SIZE",
"CONCURRENT_JOBS",
"MAX_MEGAPIXELS",
"RATE_LIMIT_PER_MIN",
"SKIP_MUST_CHANGE_PASSWORD",
"DATABASE_URL",
"WORKSPACE_PATH",
"DEFAULT_THEME",
"DEFAULT_LOCALE",
"DEFAULT_TOOL_VIEW",
];
for (const key of keysToClean) {
delete process.env[key];
}
});
afterAll(() => {
// Restore original process.env
for (const key of Object.keys(process.env)) {
if (!(key in originalEnv)) delete process.env[key];
}
Object.assign(process.env, originalEnv);
});
// We import the raw loadEnv function (not the cached `env` from config.ts)
// so each call reads the current process.env.
it("parses env vars and applies Zod schema correctly", async () => {
// loadEnv reads process.env; vitest.config.ts injects test env vars.
// We verify the schema parses them correctly rather than testing defaults
// (which requires a clean env that vitest's env injection prevents).
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
const env = loadEnv();
// Verify types are correct after Zod parsing
expect(typeof env.PORT).toBe("number");
expect(typeof env.AUTH_ENABLED).toBe("boolean");
expect(typeof env.DEFAULT_USERNAME).toBe("string");
expect(typeof env.DEFAULT_PASSWORD).toBe("string");
expect(["local", "s3"]).toContain(env.STORAGE_MODE);
expect(typeof env.FILE_MAX_AGE_HOURS).toBe("number");
expect(typeof env.CLEANUP_INTERVAL_MINUTES).toBe("number");
expect(typeof env.MAX_UPLOAD_SIZE_MB).toBe("number");
expect(typeof env.MAX_BATCH_SIZE).toBe("number");
expect(typeof env.CONCURRENT_JOBS).toBe("number");
expect(typeof env.MAX_MEGAPIXELS).toBe("number");
expect(typeof env.RATE_LIMIT_PER_MIN).toBe("number");
expect(typeof env.SKIP_MUST_CHANGE_PASSWORD).toBe("boolean");
expect(typeof env.DATABASE_URL).toBe("string");
expect(typeof env.WORKSPACE_PATH).toBe("string");
expect(["light", "dark"]).toContain(env.DEFAULT_THEME);
expect(typeof env.DEFAULT_LOCALE).toBe("string");
expect(["sidebar", "fullscreen"]).toContain(env.DEFAULT_TOOL_VIEW);
});
it("parses custom PORT as a number via coercion", async () => {
process.env.PORT = "8080";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().PORT).toBe(8080);
});
it("coerces string 'true' for AUTH_ENABLED to boolean true", async () => {
process.env.AUTH_ENABLED = "true";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().AUTH_ENABLED).toBe(true);
});
it("coerces string 'false' for AUTH_ENABLED to boolean false", async () => {
process.env.AUTH_ENABLED = "false";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().AUTH_ENABLED).toBe(false);
});
it("rejects AUTH_ENABLED with a non-enum value", async () => {
process.env.AUTH_ENABLED = "yes";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(() => loadEnv()).toThrow();
});
it("rejects STORAGE_MODE with an invalid enum value", async () => {
process.env.STORAGE_MODE = "gcs";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(() => loadEnv()).toThrow();
});
it("rejects DEFAULT_THEME with an invalid enum value", async () => {
process.env.DEFAULT_THEME = "solarized";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(() => loadEnv()).toThrow();
});
it("accepts DEFAULT_TOOL_VIEW=fullscreen", async () => {
process.env.DEFAULT_TOOL_VIEW = "fullscreen";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().DEFAULT_TOOL_VIEW).toBe("fullscreen");
});
it("accepts DEFAULT_TOOL_VIEW=sidebar", async () => {
process.env.DEFAULT_TOOL_VIEW = "sidebar";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().DEFAULT_TOOL_VIEW).toBe("sidebar");
});
it("defaults DEFAULT_TOOL_VIEW to sidebar when not set", async () => {
delete process.env.DEFAULT_TOOL_VIEW;
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().DEFAULT_TOOL_VIEW).toBe("sidebar");
});
it("rejects DEFAULT_TOOL_VIEW with an invalid value", async () => {
process.env.DEFAULT_TOOL_VIEW = "grid";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(() => loadEnv()).toThrow();
});
it("coerces numeric strings for MAX_MEGAPIXELS", async () => {
process.env.MAX_MEGAPIXELS = "50";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().MAX_MEGAPIXELS).toBe(50);
});
it("coerces floating point string for FILE_MAX_AGE_HOURS", async () => {
process.env.FILE_MAX_AGE_HOURS = "2.5";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
expect(loadEnv().FILE_MAX_AGE_HOURS).toBe(2.5);
});
it("accepts 0 for numeric fields", async () => {
process.env.PORT = "0";
process.env.CONCURRENT_JOBS = "0";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
const env = loadEnv();
expect(env.PORT).toBe(0);
expect(env.CONCURRENT_JOBS).toBe(0);
});
it("coerces negative numbers for numeric fields", async () => {
process.env.PORT = "-1";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
// zod coerce will parse to -1; there is no positive-only constraint
expect(loadEnv().PORT).toBe(-1);
});
it("accepts string values for string fields", async () => {
process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/testdb";
process.env.DEFAULT_LOCALE = "fr";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
const env = loadEnv();
expect(env.DATABASE_URL).toBe("postgres://user:pass@localhost:5432/testdb");
expect(env.DEFAULT_LOCALE).toBe("fr");
});
it("rejects non-numeric strings for number fields", async () => {
process.env.PORT = "not_a_number";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
// z.coerce.number() produces NaN for non-numeric strings, and zod rejects NaN
expect(() => loadEnv()).toThrow();
});
it("accepts all custom values at once", async () => {
process.env.PORT = "9999";
process.env.AUTH_ENABLED = "true";
process.env.DEFAULT_USERNAME = "root";
process.env.DEFAULT_PASSWORD = "s3cret!";
process.env.STORAGE_MODE = "s3";
process.env.S3_BUCKET = "my-bucket";
process.env.S3_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE";
process.env.S3_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
process.env.DEFAULT_THEME = "dark";
process.env.MAX_BATCH_SIZE = "500";
const { loadEnv } = await import("../../../apps/api/src/lib/env.js");
const env = loadEnv();
expect(env.PORT).toBe(9999);
expect(env.AUTH_ENABLED).toBe(true);
expect(env.DEFAULT_USERNAME).toBe("root");
expect(env.DEFAULT_PASSWORD).toBe("s3cret!");
expect(env.STORAGE_MODE).toBe("s3");
expect(env.S3_BUCKET).toBe("my-bucket");
expect(env.S3_ACCESS_KEY_ID).toBe("AKIAIOSFODNN7EXAMPLE");
expect(env.S3_SECRET_ACCESS_KEY).toBe("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
expect(env.DEFAULT_THEME).toBe("dark");
expect(env.MAX_BATCH_SIZE).toBe(500);
});
});
// ---------------------------------------------------------------------------
// 4. Auth helpers (hashPassword / verifyPassword)
// ---------------------------------------------------------------------------
// These functions are pure crypto -- they do not import db or env at the module
// top level, so we can import them directly without mocking the database.
// However auth.ts does import db and env at the top level, so we need to mock those.
vi.mock("../../../apps/api/src/db/index.js", () => ({
db: {},
pool: {},
closeDb: async () => {},
schema: { users: {}, sessions: {} },
}));
describe("hashPassword", () => {
let hashPassword: typeof import("../../../apps/api/src/plugins/auth.js").hashPassword;
beforeEach(async () => {
const mod = await import("../../../apps/api/src/plugins/auth.js");
hashPassword = mod.hashPassword;
});
it("produces a string in salt:hash format", async () => {
const result = await hashPassword("mypassword");
const parts = result.split(":");
expect(parts).toHaveLength(2);
expect(parts[0].length).toBeGreaterThan(0); // salt
expect(parts[1].length).toBeGreaterThan(0); // hash
});
it("salt is 32 bytes (64 hex chars)", async () => {
const result = await hashPassword("test");
const salt = result.split(":")[0];
expect(salt.length).toBe(64); // 32 bytes * 2 hex chars
// Ensure it's valid hex
expect(/^[0-9a-f]+$/.test(salt)).toBe(true);
});
it("hash (derived key) is 64 bytes (128 hex chars)", async () => {
const result = await hashPassword("test");
const hash = result.split(":")[1];
expect(hash.length).toBe(128); // 64 bytes * 2 hex chars
expect(/^[0-9a-f]+$/.test(hash)).toBe(true);
});
it("different passwords produce different hashes", async () => {
const hash1 = await hashPassword("password1");
const hash2 = await hashPassword("password2");
expect(hash1).not.toBe(hash2);
});
it("same password produces different salts (non-deterministic)", async () => {
const hash1 = await hashPassword("samepassword");
const hash2 = await hashPassword("samepassword");
const salt1 = hash1.split(":")[0];
const salt2 = hash2.split(":")[0];
expect(salt1).not.toBe(salt2);
});
it("same password produces different derived keys due to different salts", async () => {
const hash1 = await hashPassword("samepassword");
const hash2 = await hashPassword("samepassword");
const derived1 = hash1.split(":")[1];
const derived2 = hash2.split(":")[1];
expect(derived1).not.toBe(derived2);
});
it("handles empty string password", async () => {
const result = await hashPassword("");
expect(result).toContain(":");
const parts = result.split(":");
expect(parts[0].length).toBe(64);
expect(parts[1].length).toBe(128);
});
it("handles very long password", async () => {
const longPw = "a".repeat(10_000);
const result = await hashPassword(longPw);
expect(result).toContain(":");
const parts = result.split(":");
expect(parts[0].length).toBe(64);
expect(parts[1].length).toBe(128);
});
it("handles unicode password", async () => {
const result = await hashPassword("\u{1F600}\u{1F680}\u4F60\u597D");
expect(result).toContain(":");
const parts = result.split(":");
expect(parts).toHaveLength(2);
});
it("handles password with colons (does not break salt:hash parsing)", async () => {
const result = await hashPassword("pass:with:colons");
// The output format is salt:hash -- salt and hash are hex so they cannot contain colons
const parts = result.split(":");
expect(parts).toHaveLength(2);
});
});
describe("verifyPassword", () => {
let hashPassword: typeof import("../../../apps/api/src/plugins/auth.js").hashPassword;
let verifyPassword: typeof import("../../../apps/api/src/plugins/auth.js").verifyPassword;
beforeEach(async () => {
const mod = await import("../../../apps/api/src/plugins/auth.js");
hashPassword = mod.hashPassword;
verifyPassword = mod.verifyPassword;
});
it("returns true for the correct password", async () => {
const stored = await hashPassword("correcthorse");
expect(await verifyPassword("correcthorse", stored)).toBe(true);
});
it("returns false for the wrong password", async () => {
const stored = await hashPassword("correcthorse");
expect(await verifyPassword("wronghorse", stored)).toBe(false);
});
it("returns false for empty password when hash was non-empty", async () => {
const stored = await hashPassword("realpassword");
expect(await verifyPassword("", stored)).toBe(false);
});
it("returns true for empty password when hash was from empty password", async () => {
const stored = await hashPassword("");
expect(await verifyPassword("", stored)).toBe(true);
});
it("returns false for a stored hash with no colon separator", async () => {
expect(await verifyPassword("anything", "nocolonhere")).toBe(false);
});
it("returns false for an empty stored hash string", async () => {
expect(await verifyPassword("anything", "")).toBe(false);
});
it("returns false for stored hash that is just a colon", async () => {
expect(await verifyPassword("anything", ":")).toBe(false);
});
it("returns false when salt is present but hash part is empty", async () => {
expect(await verifyPassword("test", "abcd1234:")).toBe(false);
});
it("returns false when hash part is present but salt is empty", async () => {
expect(await verifyPassword("test", ":abcd1234")).toBe(false);
});
it("returns false for a corrupted (truncated) hash", async () => {
const stored = await hashPassword("mypass");
// Truncate the hash part
const truncated = stored.substring(0, stored.indexOf(":") + 5);
expect(await verifyPassword("mypass", truncated)).toBe(false);
});
it("returns false for a valid salt with wrong hash bytes", async () => {
const stored = await hashPassword("mypass");
const salt = stored.split(":")[0];
// Replace the hash with a different valid hex string of the same length
const fakeHash = "ff".repeat(64);
expect(await verifyPassword("mypass", `${salt}:${fakeHash}`)).toBe(false);
});
it("handles unicode passwords in verification", async () => {
const pw = "\u00E9\u00E8\u00EA\u00EB"; // accented characters
const stored = await hashPassword(pw);
expect(await verifyPassword(pw, stored)).toBe(true);
expect(await verifyPassword("eeee", stored)).toBe(false);
});
it("is case-sensitive", async () => {
const stored = await hashPassword("Password");
expect(await verifyPassword("Password", stored)).toBe(true);
expect(await verifyPassword("password", stored)).toBe(false);
expect(await verifyPassword("PASSWORD", stored)).toBe(false);
});
});
// ---------------------------------------------------------------------------
// 5. sanitizeFilename
// ---------------------------------------------------------------------------
// The function is duplicated across multiple route files. Since it is not exported,
// we replicate the exact logic here and test it directly. This tests the BEHAVIOR
// so if any of the copies drift, these tests document the expected contract.
function sanitizeFilename(raw: string): string {
let name = basename(raw);
name = name.replace(/\.\./g, "");
name = name.replace(/\0/g, "");
if (!name || name === "." || name === "..") {
name = "upload";
}
// Guard against double-extension attacks (e.g. "image.png.php").
const dotIndex = name.indexOf(".");
if (dotIndex !== -1) {
const parts = name.split(".");
const SAFE_IMAGE_EXTENSIONS = new Set([
".jpg",
".jpeg",
".png",
".webp",
".gif",
".bmp",
".tiff",
".tif",
".avif",
".svg",
".pdf",
]);
for (let i = 1; i < parts.length; i++) {
const ext = `.${parts[i].toLowerCase()}`;
if (SAFE_IMAGE_EXTENSIONS.has(ext)) {
name = parts.slice(0, i + 1).join(".");
break;
}
}
}
// Truncate to filesystem-safe length (255 byte NAME_MAX minus margin for _toolId suffix)
const MAX_NAME_BYTES = 200;
const enc = new TextEncoder();
if (enc.encode(name).length > MAX_NAME_BYTES) {
const dotIdx = name.lastIndexOf(".");
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
let base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
while (enc.encode(base + ext).length > MAX_NAME_BYTES) {
base = base.slice(0, -1);
}
name = base + ext;
}
return name;
}
describe("sanitizeFilename", () => {
// -- Path traversal attacks -----------------------------------------------
it("strips directory traversal ../../etc/passwd", () => {
const result = sanitizeFilename("../../etc/passwd");
expect(result).toBe("passwd");
expect(result).not.toContain("..");
expect(result).not.toContain("/");
});
it("strips more deeply nested traversal", () => {
const result = sanitizeFilename("../../../../../../../etc/shadow");
expect(result).toBe("shadow");
});
it("strips Windows-style backslash traversal", () => {
const result = sanitizeFilename("..\\..\\Windows\\System32\\config\\SAM");
// basename on POSIX treats backslashes literally; on Windows it would strip
// Either way the result should not contain path separators
expect(result).not.toContain("/");
});
it("strips traversal with URL encoding (literal %2e%2e)", () => {
// basename sees this as a literal filename with percent signs
const result = sanitizeFilename("%2e%2e/%2e%2e/etc/passwd");
expect(result).toBe("passwd");
});
// -- Null bytes -----------------------------------------------------------
it("removes null bytes from filename", () => {
const result = sanitizeFilename("image\0.png");
expect(result).toBe("image.png");
expect(result).not.toContain("\0");
});
it("removes multiple null bytes", () => {
const result = sanitizeFilename("\0\0evil\0\0.exe\0");
expect(result).not.toContain("\0");
});
it("falls back when filename is only null bytes", () => {
const result = sanitizeFilename("\0\0\0");
expect(result).toBe("upload");
});
// -- Empty / degenerate inputs --------------------------------------------
it("returns fallback for empty string", () => {
const result = sanitizeFilename("");
expect(result).toBe("upload");
});
it("returns fallback for single dot", () => {
const result = sanitizeFilename(".");
expect(result).toBe("upload");
});
it("returns fallback for double dot", () => {
const result = sanitizeFilename("..");
expect(result).toBe("upload");
});
it("returns fallback for triple dots (.. removal leaves .)", () => {
// "..." -> remove ".." -> "." -> fallback
const result = sanitizeFilename("...");
expect(result).toBe("upload");
});
it("returns fallback for four dots (.. removal leaves empty)", () => {
// "...." -> remove all ".." occurrences -> "" -> fallback
const result = sanitizeFilename("....");
expect(result).toBe("upload");
});
it("returns fallback for slash only", () => {
const result = sanitizeFilename("/");
expect(result).toBe("upload");
});
// -- Normal filenames preserved -------------------------------------------
it("preserves a normal filename", () => {
expect(sanitizeFilename("photo.png")).toBe("photo.png");
});
it("preserves a filename with spaces", () => {
expect(sanitizeFilename("my photo 2024.jpg")).toBe("my photo 2024.jpg");
});
it("preserves a filename with dashes and underscores", () => {
expect(sanitizeFilename("my-photo_v2.webp")).toBe("my-photo_v2.webp");
});
it("preserves a dotfile (starts with single dot)", () => {
expect(sanitizeFilename(".gitignore")).toBe(".gitignore");
});
it("preserves filename with multiple extensions", () => {
expect(sanitizeFilename("archive.tar.gz")).toBe("archive.tar.gz");
});
// -- Unicode filenames ----------------------------------------------------
it("preserves CJK characters", () => {
expect(sanitizeFilename("\u5199\u771F.png")).toBe("\u5199\u771F.png");
});
it("preserves emoji in filenames", () => {
expect(sanitizeFilename("\u{1F600}photo.jpg")).toBe("\u{1F600}photo.jpg");
});
it("preserves Arabic script", () => {
expect(sanitizeFilename("\u0635\u0648\u0631\u0629.png")).toBe("\u0635\u0648\u0631\u0629.png");
});
it("preserves accented Latin characters", () => {
expect(sanitizeFilename("caf\u00E9.png")).toBe("caf\u00E9.png");
});
// -- Tricky edge cases ----------------------------------------------------
it("extracts basename from a full path", () => {
expect(sanitizeFilename("/usr/local/bin/image.png")).toBe("image.png");
});
it("handles filename consisting only of spaces", () => {
const result = sanitizeFilename(" ");
// basename(" ") returns " " which is truthy and not "." or ".."
expect(result).toBe(" ");
});
it("strips double dots embedded in a filename but keeps the rest", () => {
// "my..file..name.png" -> remove ".." -> "myfilename.png"
expect(sanitizeFilename("my..file..name.png")).toBe("myfilename.png");
});
it("handles very long filenames by truncating to filesystem-safe length", () => {
const longName = `${"a".repeat(500)}.png`;
const result = sanitizeFilename(longName);
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
expect(result).toMatch(/\.png$/);
});
it("truncates very long filenames to filesystem-safe length", () => {
const longName = `${"a".repeat(300)}.jpg`;
const result = sanitizeFilename(longName);
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
expect(result).toMatch(/\.jpg$/);
});
it("handles filename with only extension", () => {
expect(sanitizeFilename(".png")).toBe(".png");
});
it("handles directory path with trailing slash", () => {
// basename("/foo/bar/") returns "bar" on POSIX
expect(sanitizeFilename("/foo/bar/")).toBe("bar");
});
});