mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Expand test coverage across all layers via 14 parallel agents: Unit tests (3,378 total, +534): - First-ever AI sidecar tests (157 tests covering bridge lifecycle, all 12 tool modules) - API route infrastructure (auth, pipeline, batch, settings, teams, roles, audit, api-keys, files, docs) - Lib coverage improvements (audit 7%->95%, worker-pool 33%->100%) - Web store/lib gap fills (features-store, tool-registry) Integration tests (4,403 total, +903): - Expanded 19 tool test files with parameter variations, format edge cases, boundary values - Cross-format matrix: 290 tests covering 14 tools x 17 formats - Adversarial/edge cases: 63 tests for extreme inputs, concurrent requests, corrupted files E2E-Docker (125 new tests): - Expanded 8 spec files + 1 new file covering all 49 tools - Added HEIC/format handling, auth failures, download verification GUI E2E (expanded 28 spec files): - Navigation, responsive layout, keyboard shortcuts - All 51 tool UIs with settings, processing, display modes - Batch/pipeline workflows, settings/RBAC, visual regression - Resilience, accessibility (ARIA, contrast, focus), performance budgets
143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { sanitizeFilename } from "../../../apps/api/src/lib/filename.js";
|
|
|
|
describe("sanitizeFilename", () => {
|
|
it("passes through a simple filename", () => {
|
|
expect(sanitizeFilename("photo.png")).toBe("photo.png");
|
|
});
|
|
|
|
it("strips directory path and returns basename only", () => {
|
|
expect(sanitizeFilename("/usr/local/bin/image.png")).toBe("image.png");
|
|
});
|
|
|
|
it("strips relative directory path", () => {
|
|
expect(sanitizeFilename("some/nested/dir/file.jpg")).toBe("file.jpg");
|
|
});
|
|
|
|
it("removes .. sequences", () => {
|
|
const result = sanitizeFilename("../../etc/passwd");
|
|
expect(result).toBe("passwd");
|
|
expect(result).not.toContain("..");
|
|
});
|
|
|
|
it("removes embedded .. sequences in filename", () => {
|
|
expect(sanitizeFilename("my..file..name.png")).toBe("myfilename.png");
|
|
});
|
|
|
|
it("removes null bytes", () => {
|
|
expect(sanitizeFilename("image\0.png")).toBe("image.png");
|
|
});
|
|
|
|
it("falls back to upload for empty string", () => {
|
|
expect(sanitizeFilename("")).toBe("upload");
|
|
});
|
|
|
|
it("falls back to upload for single dot", () => {
|
|
expect(sanitizeFilename(".")).toBe("upload");
|
|
});
|
|
|
|
it("falls back to upload for double dot", () => {
|
|
expect(sanitizeFilename("..")).toBe("upload");
|
|
});
|
|
|
|
it("falls back to upload for triple dots (.. removal leaves .)", () => {
|
|
expect(sanitizeFilename("...")).toBe("upload");
|
|
});
|
|
|
|
it("falls back to upload for four dots (.. removal leaves empty)", () => {
|
|
expect(sanitizeFilename("....")).toBe("upload");
|
|
});
|
|
|
|
it("truncates after first safe image extension (photo.png.php)", () => {
|
|
expect(sanitizeFilename("photo.png.php")).toBe("photo.png");
|
|
});
|
|
|
|
it("truncates after first safe image extension (report.jpg.exe)", () => {
|
|
expect(sanitizeFilename("report.jpg.exe")).toBe("report.jpg");
|
|
});
|
|
|
|
it("truncates after first safe image extension with multiple unsafe parts", () => {
|
|
expect(sanitizeFilename("evil.webp.php.sh")).toBe("evil.webp");
|
|
});
|
|
|
|
it("handles no extension", () => {
|
|
expect(sanitizeFilename("README")).toBe("README");
|
|
});
|
|
|
|
it("handles unknown extensions without truncation", () => {
|
|
expect(sanitizeFilename("archive.tar.gz")).toBe("archive.tar.gz");
|
|
});
|
|
|
|
it("handles filename with only unknown extensions", () => {
|
|
expect(sanitizeFilename("data.csv")).toBe("data.csv");
|
|
});
|
|
|
|
it("truncates very long filenames over 200 bytes", () => {
|
|
const longName = `${"a".repeat(300)}.png`;
|
|
const result = sanitizeFilename(longName);
|
|
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
|
|
expect(result).toMatch(/\.png$/);
|
|
});
|
|
|
|
it("truncates long filename without extension", () => {
|
|
const longName = "b".repeat(300);
|
|
const result = sanitizeFilename(longName);
|
|
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
|
|
});
|
|
|
|
it("handles unicode filenames", () => {
|
|
expect(sanitizeFilename("写真.png")).toBe("写真.png");
|
|
});
|
|
|
|
it("handles emoji filenames", () => {
|
|
expect(sanitizeFilename("\u{1F600}photo.jpg")).toBe("\u{1F600}photo.jpg");
|
|
});
|
|
|
|
it("handles filenames with spaces", () => {
|
|
expect(sanitizeFilename("my photo 2024.jpg")).toBe("my photo 2024.jpg");
|
|
});
|
|
|
|
it("handles filenames with dashes and underscores", () => {
|
|
expect(sanitizeFilename("my-photo_v2.webp")).toBe("my-photo_v2.webp");
|
|
});
|
|
|
|
it("preserves dotfiles", () => {
|
|
expect(sanitizeFilename(".gitignore")).toBe(".gitignore");
|
|
});
|
|
|
|
it("handles trailing slash in path", () => {
|
|
expect(sanitizeFilename("/foo/bar/")).toBe("bar");
|
|
});
|
|
|
|
it("handles only null bytes falling back to upload", () => {
|
|
expect(sanitizeFilename("\0\0\0")).toBe("upload");
|
|
});
|
|
|
|
it("truncates long unicode filenames correctly", () => {
|
|
const longUnicode = `${"\u{1F600}".repeat(100)}.png`;
|
|
const result = sanitizeFilename(longUnicode);
|
|
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
|
|
expect(result).toMatch(/\.png$/);
|
|
});
|
|
|
|
it("recognizes all safe image extensions", () => {
|
|
const extensions = [
|
|
"jpg",
|
|
"jpeg",
|
|
"png",
|
|
"webp",
|
|
"gif",
|
|
"bmp",
|
|
"tiff",
|
|
"tif",
|
|
"avif",
|
|
"svg",
|
|
"pdf",
|
|
];
|
|
for (const ext of extensions) {
|
|
const result = sanitizeFilename(`file.${ext}.evil`);
|
|
expect(result).toBe(`file.${ext}`);
|
|
}
|
|
});
|
|
});
|