Files
SnapOtter/tests/unit/api/filename.test.ts
T
SnapOtter 649ad5db9e test: massive test coverage expansion (+1,437 tests, 22 new files)
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
2026-05-09 09:02:29 +08:00

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}`);
}
});
});