Files
kandev/apps/web/lib/utils/upload-selection.test.ts
Corey Fogg d4742089fc feat(web): add manual file upload and download to the Files panel (#3292)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-03 08:07:13 +00:00

86 lines
3.0 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { joinDestination, normalizeUploadSelection } from "./upload-selection";
/** Build a File carrying an optional webkitRelativePath, as a folder pick does. */
function makeFile(name: string, relativePath?: string): File {
const file = new File(["x"], name);
if (relativePath !== undefined) {
Object.defineProperty(file, "webkitRelativePath", { value: relativePath });
}
return file;
}
describe("normalizeUploadSelection", () => {
it("uses bare names for a flat multi-file pick", () => {
const { entries, skipped } = normalizeUploadSelection([makeFile("a.txt"), makeFile("b.json")]);
expect(entries.map((e) => e.relativePath)).toEqual(["a.txt", "b.json"]);
expect(skipped).toEqual([]);
});
it("preserves directory structure from a folder pick", () => {
const { entries } = normalizeUploadSelection([
makeFile("root.txt", "fixtures/root.txt"),
makeFile("leaf.txt", "fixtures/deep/nested/leaf.txt"),
]);
expect(entries.map((e) => e.relativePath)).toEqual([
"fixtures/root.txt",
"fixtures/deep/nested/leaf.txt",
]);
});
it("produces the same shape whichever picker was used", () => {
const flat = normalizeUploadSelection([makeFile("a.txt")]);
const folder = normalizeUploadSelection([makeFile("a.txt", "a.txt")]);
expect(flat.entries[0].relativePath).toBe(folder.entries[0].relativePath);
});
it("skips a traversing path instead of failing the batch", () => {
const { entries, skipped } = normalizeUploadSelection([
makeFile("good.txt"),
makeFile("evil.txt", "../evil.txt"),
makeFile("also-good.txt"),
]);
expect(entries.map((e) => e.relativePath)).toEqual(["good.txt", "also-good.txt"]);
expect(skipped).toEqual(["../evil.txt"]);
});
it("skips an absolute path", () => {
const { entries, skipped } = normalizeUploadSelection([makeFile("p", "/etc/passwd")]);
expect(entries).toEqual([]);
expect(skipped).toEqual(["/etc/passwd"]);
});
it("normalizes backslashes and redundant segments", () => {
const { entries } = normalizeUploadSelection([makeFile("x.txt", "dir\\.\\sub//x.txt")]);
expect(entries[0].relativePath).toBe("dir/sub/x.txt");
});
it("skips an interior traversal segment", () => {
const { entries, skipped } = normalizeUploadSelection([makeFile("x.txt", "dir/../../x.txt")]);
expect(entries).toEqual([]);
expect(skipped).toEqual(["dir/../../x.txt"]);
});
});
describe("joinDestination", () => {
it("joins a folder and a relative path", () => {
expect(joinDestination("fixtures", "a.txt")).toBe("fixtures/a.txt");
expect(joinDestination("fixtures", "deep/a.txt")).toBe("fixtures/deep/a.txt");
});
it("treats an empty folder as the workspace root", () => {
expect(joinDestination("", "a.txt")).toBe("a.txt");
});
it("tolerates surrounding slashes on the folder", () => {
expect(joinDestination("/fixtures/", "a.txt")).toBe("fixtures/a.txt");
});
});