add basic tests

This commit is contained in:
Nystik
2026-04-04 17:04:55 +02:00
parent fade3c30c5
commit 2c8344022b
5 changed files with 1403 additions and 5 deletions

1230
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,9 @@
"scripts": {
"build": "node build.js",
"dev:server": "node server/index.js",
"dev": "npm run build && npm run dev:server"
"dev": "npm run build && npm run dev:server",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"archiver": "^7.0.1",
@@ -22,6 +24,7 @@
"esbuild-svelte": "^0.9.4",
"lucide-svelte": "^0.577.0",
"path-browserify": "^1.0.1",
"svelte": "^4.2.20"
"svelte": "^4.2.20",
"vitest": "^4.1.2"
}
}

View File

@@ -552,3 +552,5 @@ router.get("/download-zip", async (req, res) => {
});
module.exports = router;
module.exports.resolveVaultPath = resolveVaultPath;
module.exports.encodeContentDispositionFilename = encodeContentDispositionFilename;

109
server/routes/fs.test.mjs Normal file
View File

@@ -0,0 +1,109 @@
import path from "path";
import { describe, it, expect } from "vitest";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const {
resolveVaultPath,
encodeContentDispositionFilename,
} = require("./fs.js");
// -- encodeContentDispositionFilename --------------------------------
describe("encodeContentDispositionFilename", () => {
it("handles a plain ASCII filename", () => {
expect(encodeContentDispositionFilename("report.pdf")).toBe(
'attachment; filename="report.pdf"',
);
});
it("preserves spaces in quotes", () => {
expect(encodeContentDispositionFilename("my report.pdf")).toBe(
'attachment; filename="my report.pdf"',
);
});
it("escapes double quotes", () => {
const result = encodeContentDispositionFilename('file"name.txt');
expect(result).toBe('attachment; filename="file\\"name.txt"');
});
it("escapes backslashes", () => {
const result = encodeContentDispositionFilename("path\\to\\file.txt");
expect(result).toBe('attachment; filename="path\\\\to\\\\file.txt"');
});
it("produces ASCII fallback and filename* for unicode", () => {
const result = encodeContentDispositionFilename(
"\u65E5\u672C\u8A9Enotes.md",
);
expect(result).toContain('filename="___notes.md"');
expect(result).toContain("filename*=UTF-8''");
expect(result).toContain("%E6%97%A5");
});
it("replaces only non-ASCII in the fallback for mixed filenames", () => {
const result = encodeContentDispositionFilename("report_2024\u5E74.pdf");
expect(result).toContain('filename="report_2024_.pdf"');
expect(result).toContain("filename*=UTF-8''");
});
it("strips control characters", () => {
const result = encodeContentDispositionFilename("bad\x00file\x1F.txt");
expect(result).toBe('attachment; filename="badfile.txt"');
});
it("does not crash on empty string", () => {
const result = encodeContentDispositionFilename("");
expect(result).toBe('attachment; filename=""');
});
});
// -- resolveVaultPath ------------------------------------------------
describe("resolveVaultPath", () => {
const root = "/vaults/test";
it("resolves a simple relative path", () => {
const result = resolveVaultPath(root, "notes/daily.md");
expect(result).toBe(path.resolve(root, "notes/daily.md"));
});
it("resolves empty string to vault root", () => {
expect(resolveVaultPath(root, "")).toBe(path.resolve(root));
});
it("allows a path that equals the vault root exactly", () => {
expect(resolveVaultPath(root, "")).toBe(path.resolve(root));
});
it("treats null input as vault root", () => {
expect(resolveVaultPath(root, null)).toBe(path.resolve(root));
});
it("treats undefined input as vault root", () => {
expect(resolveVaultPath(root, undefined)).toBe(path.resolve(root));
});
it("strips leading slashes", () => {
const result = resolveVaultPath(root, "///notes/daily.md");
expect(result).toBe(path.resolve(root, "notes/daily.md"));
});
it("resolves ./ segments correctly", () => {
const result = resolveVaultPath(root, "./notes/../notes/daily.md");
expect(result).toBe(path.resolve(root, "notes/daily.md"));
});
it("rejects ../ that escapes vault root", () => {
expect(resolveVaultPath(root, "../")).toBe(null);
});
it("rejects deep traversal", () => {
expect(resolveVaultPath(root, "a/b/c/../../../../etc/passwd")).toBe(null);
});
it("rejects traversal to a sibling vault with a shared prefix", () => {
expect(resolveVaultPath(root, "../testing/foo")).toBe(null);
});
});

View File

@@ -0,0 +1,60 @@
import { describe, it, expect } from "vitest";
import { MetadataCache } from "./metadata-cache.js";
// -- Path normalization ----------------------------------------------
describe("MetadataCache path normalization", () => {
it("converts backslashes to forward slashes", () => {
const cache = new MetadataCache();
cache.set("foo\\bar\\baz.md", { type: "file", size: 10 });
expect(cache.has("foo/bar/baz.md")).toBe(true);
});
it("strips leading and trailing slashes", () => {
const cache = new MetadataCache();
cache.set("/foo/bar/", { type: "file", size: 10 });
expect(cache.has("foo/bar")).toBe(true);
});
it("handles null and undefined as empty string", () => {
const cache = new MetadataCache();
cache.set(null, { type: "directory", size: 0 });
expect(cache.has("")).toBe(true);
expect(cache.has(undefined)).toBe(true);
});
it("normalizes //foo\\\\bar// to foo/bar", () => {
const cache = new MetadataCache();
cache.set("//foo\\bar//", { type: "file", size: 5 });
expect(cache.has("foo/bar")).toBe(true);
});
});
// -- Operations ------------------------------------------------------
describe("MetadataCache populate and merge", () => {
it.todo("populate() clears existing entries");
it.todo("merge() preserves existing entries");
it.todo("populate then merge -- pre-existing entries survive merge");
});
describe("MetadataCache toStat", () => {
it.todo("returns correct shape with all expected fields and methods");
it.todo("returns null for missing paths");
it.todo("constructs dates from zero when mtime/ctime are missing");
});
describe("MetadataCache readdir", () => {
it.todo("root readdir returns top-level entries");
it.todo("nested dir returns only direct children, not grandchildren");
it.todo(
"readdir of foo does not include foobar entries (prefix false-match)",
);
it.todo("infers directory type for paths with no direct map entry");
it.todo("returns empty array for path with no children");
it.todo("returns empty array for nonexistent path");
});
describe("MetadataCache rename", () => {
it.todo("rename file: old path gone, new path present with same metadata");
});