@bitbonsai/mcpvault 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -641
- package/dist/src/createServer.d.ts +11 -0
- package/dist/src/createServer.d.ts.map +1 -0
- package/dist/src/createServer.js +383 -0
- package/dist/src/filesystem.d.ts +26 -0
- package/dist/src/filesystem.d.ts.map +1 -0
- package/dist/src/filesystem.js +73 -5
- package/dist/src/frontmatter.d.ts +16 -0
- package/dist/src/frontmatter.d.ts.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/pathfilter.d.ts +13 -0
- package/dist/src/pathfilter.d.ts.map +1 -0
- package/dist/src/search.d.ts +11 -0
- package/dist/src/search.d.ts.map +1 -0
- package/dist/src/types.d.ts +134 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/uri.d.ts +10 -0
- package/dist/src/uri.d.ts.map +1 -0
- package/package.json +10 -3
- package/dist/src/filesystem.test.js +0 -946
- package/dist/src/frontmatter.test.js +0 -118
- package/dist/src/integration.test.js +0 -168
- package/dist/src/pathfilter.test.js +0 -264
- package/dist/src/search.test.js +0 -212
- package/dist/src/uri.test.js +0 -46
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "vitest";
|
|
2
|
-
import { FrontmatterHandler, parseFrontmatter } from "./frontmatter.js";
|
|
3
|
-
const handler = new FrontmatterHandler();
|
|
4
|
-
test("parse note with frontmatter", () => {
|
|
5
|
-
const content = `---
|
|
6
|
-
title: Test Note
|
|
7
|
-
tags: [test, example]
|
|
8
|
-
created: 2023-01-01
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Test Note
|
|
12
|
-
|
|
13
|
-
This is a test note with frontmatter.`;
|
|
14
|
-
const result = handler.parse(content);
|
|
15
|
-
expect(result.frontmatter.title).toBe("Test Note");
|
|
16
|
-
expect(result.frontmatter.tags).toEqual(["test", "example"]);
|
|
17
|
-
expect(result.frontmatter.created).toEqual(new Date("2023-01-01"));
|
|
18
|
-
expect(result.content.trim()).toBe("# Test Note\n\nThis is a test note with frontmatter.");
|
|
19
|
-
});
|
|
20
|
-
test("parse note without frontmatter", () => {
|
|
21
|
-
const content = `# Test Note
|
|
22
|
-
|
|
23
|
-
This is a test note without frontmatter.`;
|
|
24
|
-
const result = handler.parse(content);
|
|
25
|
-
expect(result.frontmatter).toEqual({});
|
|
26
|
-
expect(result.content).toBe(content);
|
|
27
|
-
});
|
|
28
|
-
test("stringify with frontmatter", () => {
|
|
29
|
-
const frontmatter = {
|
|
30
|
-
title: "Test Note",
|
|
31
|
-
tags: ["test", "example"]
|
|
32
|
-
};
|
|
33
|
-
const content = "# Test Note\n\nContent here.";
|
|
34
|
-
const result = handler.stringify(frontmatter, content);
|
|
35
|
-
expect(result).toContain("---");
|
|
36
|
-
expect(result).toContain("title: Test Note");
|
|
37
|
-
expect(result).toContain("tags:");
|
|
38
|
-
expect(result).toContain("# Test Note");
|
|
39
|
-
});
|
|
40
|
-
test("stringify without frontmatter", () => {
|
|
41
|
-
const content = "# Test Note\n\nContent here.";
|
|
42
|
-
const result = handler.stringify({}, content);
|
|
43
|
-
expect(result).toBe(content);
|
|
44
|
-
});
|
|
45
|
-
test("validate valid frontmatter", () => {
|
|
46
|
-
const frontmatter = {
|
|
47
|
-
title: "Valid Title",
|
|
48
|
-
tags: ["tag1", "tag2"],
|
|
49
|
-
date: new Date("2023-01-01"),
|
|
50
|
-
count: 42,
|
|
51
|
-
enabled: true
|
|
52
|
-
};
|
|
53
|
-
const result = handler.validate(frontmatter);
|
|
54
|
-
expect(result.isValid).toBe(true);
|
|
55
|
-
expect(result.errors).toHaveLength(0);
|
|
56
|
-
});
|
|
57
|
-
test("validate invalid frontmatter with function", () => {
|
|
58
|
-
const frontmatter = {
|
|
59
|
-
title: "Invalid",
|
|
60
|
-
badFunction: () => "not allowed"
|
|
61
|
-
};
|
|
62
|
-
const result = handler.validate(frontmatter);
|
|
63
|
-
expect(result.isValid).toBe(false);
|
|
64
|
-
expect(result.errors.length).toBeGreaterThan(0);
|
|
65
|
-
// The specific error message may vary between YAML libraries
|
|
66
|
-
expect(result.errors[0]).toMatch(/Functions are not allowed|Invalid YAML structure/);
|
|
67
|
-
});
|
|
68
|
-
test("update frontmatter in existing content", () => {
|
|
69
|
-
const content = `---
|
|
70
|
-
title: Old Title
|
|
71
|
-
tags: [old]
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
# Content
|
|
75
|
-
|
|
76
|
-
Some content here.`;
|
|
77
|
-
const updates = {
|
|
78
|
-
title: "New Title",
|
|
79
|
-
modified: "2023-12-01"
|
|
80
|
-
};
|
|
81
|
-
const result = handler.updateFrontmatter(content, updates);
|
|
82
|
-
expect(result).toContain("title: New Title");
|
|
83
|
-
expect(result).toContain("modified: '2023-12-01'");
|
|
84
|
-
expect(result).toContain("tags:");
|
|
85
|
-
expect(result).toContain("# Content");
|
|
86
|
-
});
|
|
87
|
-
describe("parseFrontmatter", () => {
|
|
88
|
-
test("returns undefined for null and undefined", () => {
|
|
89
|
-
expect(parseFrontmatter(null)).toBeUndefined();
|
|
90
|
-
expect(parseFrontmatter(undefined)).toBeUndefined();
|
|
91
|
-
});
|
|
92
|
-
test("passes through a plain object", () => {
|
|
93
|
-
const obj = { tags: ["test"], title: "Hello" };
|
|
94
|
-
expect(parseFrontmatter(obj)).toBe(obj);
|
|
95
|
-
});
|
|
96
|
-
test("parses a JSON string into an object", () => {
|
|
97
|
-
const input = '{"tags": ["test"], "title": "Hello"}';
|
|
98
|
-
expect(parseFrontmatter(input)).toEqual({ tags: ["test"], title: "Hello" });
|
|
99
|
-
});
|
|
100
|
-
test("parses an empty JSON object string", () => {
|
|
101
|
-
expect(parseFrontmatter("{}")).toEqual({});
|
|
102
|
-
});
|
|
103
|
-
test("throws for a non-JSON string", () => {
|
|
104
|
-
expect(() => parseFrontmatter("not json")).toThrow("frontmatter must be a JSON object");
|
|
105
|
-
});
|
|
106
|
-
test("throws for a JSON array string", () => {
|
|
107
|
-
expect(() => parseFrontmatter('[1, 2, 3]')).toThrow("frontmatter must be a JSON object");
|
|
108
|
-
});
|
|
109
|
-
test("throws for a JSON primitive string", () => {
|
|
110
|
-
expect(() => parseFrontmatter('"just a string"')).toThrow("frontmatter must be a JSON object");
|
|
111
|
-
});
|
|
112
|
-
test("throws for an array value", () => {
|
|
113
|
-
expect(() => parseFrontmatter([1, 2, 3])).toThrow("frontmatter must be a JSON object");
|
|
114
|
-
});
|
|
115
|
-
test("throws for a number value", () => {
|
|
116
|
-
expect(() => parseFrontmatter(42)).toThrow("frontmatter must be a JSON object");
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { test, expect, beforeEach, afterEach, describe } from "vitest";
|
|
2
|
-
import { FileSystemService } from "./filesystem.js";
|
|
3
|
-
import { FrontmatterHandler } from "./frontmatter.js";
|
|
4
|
-
import { PathFilter } from "./pathfilter.js";
|
|
5
|
-
import { SearchService } from "./search.js";
|
|
6
|
-
import { writeFile, mkdir, mkdtemp, rm } from "fs/promises";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
import { tmpdir } from "os";
|
|
9
|
-
let testVaultPath;
|
|
10
|
-
let pathFilter;
|
|
11
|
-
let frontmatterHandler;
|
|
12
|
-
let fileSystem;
|
|
13
|
-
let searchService;
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
testVaultPath = await mkdtemp(join(tmpdir(), "mcpvault-integration-"));
|
|
16
|
-
// Initialize services (same as server.ts)
|
|
17
|
-
pathFilter = new PathFilter();
|
|
18
|
-
frontmatterHandler = new FrontmatterHandler();
|
|
19
|
-
fileSystem = new FileSystemService(testVaultPath, pathFilter, frontmatterHandler);
|
|
20
|
-
searchService = new SearchService(testVaultPath, pathFilter);
|
|
21
|
-
});
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
try {
|
|
24
|
-
await rm(testVaultPath, { recursive: true });
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// Ignore cleanup errors
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
// ============================================================================
|
|
31
|
-
// INTEGRATION TESTS - END-TO-END WORKFLOW
|
|
32
|
-
// ============================================================================
|
|
33
|
-
describe("Integration: Service Layer Workflows", () => {
|
|
34
|
-
test("write, read, and delete note workflow", async () => {
|
|
35
|
-
// 1. Write a note with frontmatter
|
|
36
|
-
await fileSystem.writeNote({
|
|
37
|
-
path: "test-note.md",
|
|
38
|
-
content: "# Test Note\n\nThis is a test.",
|
|
39
|
-
frontmatter: { tags: ["test"], status: "draft" }
|
|
40
|
-
});
|
|
41
|
-
// 2. Read the note back
|
|
42
|
-
const note = await fileSystem.readNote("test-note.md");
|
|
43
|
-
expect(note.content).toContain("This is a test");
|
|
44
|
-
expect(note.frontmatter?.tags).toEqual(["test"]);
|
|
45
|
-
expect(note.frontmatter?.status).toBe("draft");
|
|
46
|
-
// 3. Delete the note
|
|
47
|
-
const deleteResult = await fileSystem.deleteNote({
|
|
48
|
-
path: "test-note.md",
|
|
49
|
-
confirmPath: "test-note.md"
|
|
50
|
-
});
|
|
51
|
-
expect(deleteResult.success).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
test("search notes with special characters in filenames", async () => {
|
|
54
|
-
// Create notes with special characters in paths
|
|
55
|
-
const testCases = [
|
|
56
|
-
{ path: "folder (archive)/note [old].md", content: "# Old Note\n\nArchived keyword." },
|
|
57
|
-
{ path: "C++/notes.md", content: "# C++ Notes\n\nProgramming keyword." },
|
|
58
|
-
{ path: "backup.2024/important.md", content: "# Important\n\nBackup keyword." },
|
|
59
|
-
{ path: "price$100.md", content: "# Pricing\n\nCost keyword." }
|
|
60
|
-
];
|
|
61
|
-
// Write all test notes
|
|
62
|
-
for (const { path, content } of testCases) {
|
|
63
|
-
if (path.includes('/')) {
|
|
64
|
-
const dirName = path.split('/')[0];
|
|
65
|
-
if (dirName) {
|
|
66
|
-
await mkdir(join(testVaultPath, dirName), { recursive: true });
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
await writeFile(join(testVaultPath, path), content);
|
|
70
|
-
}
|
|
71
|
-
// Search for keyword
|
|
72
|
-
const results = await searchService.search({
|
|
73
|
-
query: "keyword",
|
|
74
|
-
limit: 10
|
|
75
|
-
});
|
|
76
|
-
expect(results.length).toBe(4);
|
|
77
|
-
// Verify paths with special characters are returned correctly
|
|
78
|
-
const paths = results.map((r) => r.p);
|
|
79
|
-
expect(paths).toContain("folder (archive)/note [old].md");
|
|
80
|
-
expect(paths).toContain("C++/notes.md");
|
|
81
|
-
});
|
|
82
|
-
test("write note with regex special chars in content", async () => {
|
|
83
|
-
const content = `# Price List
|
|
84
|
-
|
|
85
|
-
Item: Widget ($10.50)
|
|
86
|
-
Regex: [a-z]+ matches lowercase
|
|
87
|
-
Math: 2 + 2 = 4
|
|
88
|
-
Pattern: backup.2024/**/*.md`;
|
|
89
|
-
await fileSystem.writeNote({
|
|
90
|
-
path: "special-chars.md",
|
|
91
|
-
content
|
|
92
|
-
});
|
|
93
|
-
// Read back and verify exact content
|
|
94
|
-
const note = await fileSystem.readNote("special-chars.md");
|
|
95
|
-
expect(note.content).toContain("($10.50)");
|
|
96
|
-
expect(note.content).toContain("[a-z]+");
|
|
97
|
-
expect(note.content).toContain("2 + 2 = 4");
|
|
98
|
-
expect(note.content).toContain("backup.2024/**/*.md");
|
|
99
|
-
});
|
|
100
|
-
test("search matches note filename even without content match", async () => {
|
|
101
|
-
// Issue #30: notes without a heading that rely on filename for discovery
|
|
102
|
-
await fileSystem.writeNote({
|
|
103
|
-
path: "Yard.md",
|
|
104
|
-
content: "Some info about lawn care and gardening tips."
|
|
105
|
-
});
|
|
106
|
-
await fileSystem.writeNote({
|
|
107
|
-
path: "Kitchen.md",
|
|
108
|
-
content: "Recipes and kitchen organization."
|
|
109
|
-
});
|
|
110
|
-
// Search for "yard" — should match Yard.md by filename
|
|
111
|
-
const results = await searchService.search({
|
|
112
|
-
query: "yard",
|
|
113
|
-
searchContent: true,
|
|
114
|
-
limit: 10
|
|
115
|
-
});
|
|
116
|
-
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
117
|
-
expect(results.some(r => r.p === "Yard.md")).toBe(true);
|
|
118
|
-
// Verify filename-only match has reasonable fields
|
|
119
|
-
const yardResult = results.find(r => r.p === "Yard.md");
|
|
120
|
-
expect(yardResult.t).toBe("Yard");
|
|
121
|
-
expect(yardResult.mc).toBeGreaterThanOrEqual(1);
|
|
122
|
-
// Search for "kitchen" — should match Kitchen.md by filename
|
|
123
|
-
const kitchenResults = await searchService.search({
|
|
124
|
-
query: "kitchen",
|
|
125
|
-
searchContent: true,
|
|
126
|
-
limit: 10
|
|
127
|
-
});
|
|
128
|
-
// Should match both filename AND content (content contains "kitchen")
|
|
129
|
-
expect(kitchenResults.some(r => r.p === "Kitchen.md")).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
test("multi-step workflow: search, read multiple, update frontmatter", async () => {
|
|
132
|
-
// Create several notes
|
|
133
|
-
for (let i = 1; i <= 3; i++) {
|
|
134
|
-
await fileSystem.writeNote({
|
|
135
|
-
path: `note-${i}.md`,
|
|
136
|
-
content: `# Note ${i}\n\nThis contains searchterm.`,
|
|
137
|
-
frontmatter: { id: i, processed: false }
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
// Search for notes
|
|
141
|
-
const searchResults = await searchService.search({
|
|
142
|
-
query: "searchterm",
|
|
143
|
-
limit: 10
|
|
144
|
-
});
|
|
145
|
-
expect(searchResults.length).toBe(3);
|
|
146
|
-
// Read multiple notes
|
|
147
|
-
const paths = searchResults.map(r => r.p);
|
|
148
|
-
const readResult = await fileSystem.readMultipleNotes({
|
|
149
|
-
paths,
|
|
150
|
-
includeContent: true,
|
|
151
|
-
includeFrontmatter: true
|
|
152
|
-
});
|
|
153
|
-
expect(readResult.successful.length).toBe(3);
|
|
154
|
-
// Update frontmatter on all notes
|
|
155
|
-
for (const path of paths) {
|
|
156
|
-
await fileSystem.updateFrontmatter({
|
|
157
|
-
path,
|
|
158
|
-
frontmatter: { processed: true },
|
|
159
|
-
merge: true
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
// Verify updates
|
|
163
|
-
for (const path of paths) {
|
|
164
|
-
const note = await fileSystem.readNote(path);
|
|
165
|
-
expect(note.frontmatter?.processed).toBe(true);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe } from "vitest";
|
|
2
|
-
import { PathFilter } from "./pathfilter.js";
|
|
3
|
-
describe("PathFilter", () => {
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// BASIC FUNCTIONALITY
|
|
6
|
-
// ============================================================================
|
|
7
|
-
test("allows markdown files by default", () => {
|
|
8
|
-
const filter = new PathFilter();
|
|
9
|
-
expect(filter.isAllowed("notes/test.md")).toBe(true);
|
|
10
|
-
expect(filter.isAllowed("test.markdown")).toBe(true);
|
|
11
|
-
expect(filter.isAllowed("folder/subfolder/note.txt")).toBe(true);
|
|
12
|
-
});
|
|
13
|
-
test("blocks .obsidian directory", () => {
|
|
14
|
-
const filter = new PathFilter();
|
|
15
|
-
expect(filter.isAllowed(".obsidian")).toBe(false);
|
|
16
|
-
expect(filter.isAllowed(".obsidian/app.json")).toBe(false);
|
|
17
|
-
expect(filter.isAllowed(".obsidian/plugins/plugin/main.js")).toBe(false);
|
|
18
|
-
});
|
|
19
|
-
test("blocks .git directory", () => {
|
|
20
|
-
const filter = new PathFilter();
|
|
21
|
-
expect(filter.isAllowed(".git")).toBe(false);
|
|
22
|
-
expect(filter.isAllowed(".git/config")).toBe(false);
|
|
23
|
-
expect(filter.isAllowed(".git/objects/abc123")).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
test("blocks node_modules", () => {
|
|
26
|
-
const filter = new PathFilter();
|
|
27
|
-
expect(filter.isAllowed("node_modules")).toBe(false);
|
|
28
|
-
expect(filter.isAllowed("node_modules/package/index.js")).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
test("blocks system files", () => {
|
|
31
|
-
const filter = new PathFilter();
|
|
32
|
-
expect(filter.isAllowed(".DS_Store")).toBe(false);
|
|
33
|
-
expect(filter.isAllowed("Thumbs.db")).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
test("blocks non-allowed extensions", () => {
|
|
36
|
-
const filter = new PathFilter();
|
|
37
|
-
expect(filter.isAllowed("script.js")).toBe(false);
|
|
38
|
-
expect(filter.isAllowed("data.json")).toBe(false);
|
|
39
|
-
expect(filter.isAllowed("image.png")).toBe(false);
|
|
40
|
-
});
|
|
41
|
-
test("allows non-note files for directory listing", () => {
|
|
42
|
-
const filter = new PathFilter();
|
|
43
|
-
expect(filter.isAllowedForListing("image.png")).toBe(true);
|
|
44
|
-
expect(filter.isAllowedForListing("docs/report.pdf")).toBe(true);
|
|
45
|
-
expect(filter.isAllowedForListing("archive/data.json")).toBe(true);
|
|
46
|
-
});
|
|
47
|
-
test("blocks restricted paths in directory listing", () => {
|
|
48
|
-
const filter = new PathFilter();
|
|
49
|
-
expect(filter.isAllowedForListing(".obsidian/app.json")).toBe(false);
|
|
50
|
-
expect(filter.isAllowedForListing(".git/config")).toBe(false);
|
|
51
|
-
expect(filter.isAllowedForListing("node_modules/pkg/index.js")).toBe(false);
|
|
52
|
-
expect(filter.isAllowedForListing(".DS_Store")).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// REGEX SPECIAL CHARACTERS - SECURITY TESTS
|
|
56
|
-
// ============================================================================
|
|
57
|
-
describe("regex special characters in paths", () => {
|
|
58
|
-
test("handles dots in filenames literally", () => {
|
|
59
|
-
const filter = new PathFilter();
|
|
60
|
-
// Dots should be literal, not regex wildcards
|
|
61
|
-
expect(filter.isAllowed("file.name.md")).toBe(true);
|
|
62
|
-
expect(filter.isAllowed("v1.0.0-notes.md")).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
test("handles parentheses in paths", () => {
|
|
65
|
-
const filter = new PathFilter();
|
|
66
|
-
expect(filter.isAllowed("notes/(archived)/old.md")).toBe(true);
|
|
67
|
-
expect(filter.isAllowed("project (copy).md")).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
test("handles square brackets in paths", () => {
|
|
70
|
-
const filter = new PathFilter();
|
|
71
|
-
expect(filter.isAllowed("notes/[2024]/january.md")).toBe(true);
|
|
72
|
-
expect(filter.isAllowed("[inbox]/task.md")).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
test("handles curly braces in paths", () => {
|
|
75
|
-
const filter = new PathFilter();
|
|
76
|
-
expect(filter.isAllowed("templates/{daily}.md")).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
test("handles plus signs in paths", () => {
|
|
79
|
-
const filter = new PathFilter();
|
|
80
|
-
expect(filter.isAllowed("C++/notes.md")).toBe(true);
|
|
81
|
-
expect(filter.isAllowed("topic+subtopic.md")).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
test("handles question marks in paths", () => {
|
|
84
|
-
const filter = new PathFilter();
|
|
85
|
-
// Question mark is a glob wildcard, but in actual filenames should work
|
|
86
|
-
expect(filter.isAllowed("FAQ?.md")).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
test("handles asterisks in filenames", () => {
|
|
89
|
-
const filter = new PathFilter();
|
|
90
|
-
// Asterisk in filename (rare but valid on Unix)
|
|
91
|
-
expect(filter.isAllowed("important*.md")).toBe(true);
|
|
92
|
-
expect(filter.isAllowed("file*name.md")).toBe(true);
|
|
93
|
-
expect(filter.isAllowed("notes/todo*.md")).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
test("asterisk in custom ignored pattern works as glob", () => {
|
|
96
|
-
const filter = new PathFilter({
|
|
97
|
-
ignoredPatterns: ["temp*/**"]
|
|
98
|
-
});
|
|
99
|
-
// Pattern uses * as wildcard - should match temp, temp1, temporary, etc.
|
|
100
|
-
expect(filter.isAllowed("temp/file.md")).toBe(false);
|
|
101
|
-
expect(filter.isAllowed("temp1/file.md")).toBe(false);
|
|
102
|
-
expect(filter.isAllowed("temporary/file.md")).toBe(false);
|
|
103
|
-
// Should NOT match "atemp" (pattern starts with temp)
|
|
104
|
-
expect(filter.isAllowed("atemp/file.md")).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
test("double asterisk ** matches nested paths", () => {
|
|
107
|
-
const filter = new PathFilter({
|
|
108
|
-
ignoredPatterns: ["archive/**"]
|
|
109
|
-
});
|
|
110
|
-
expect(filter.isAllowed("archive/old.md")).toBe(false);
|
|
111
|
-
expect(filter.isAllowed("archive/2024/jan/note.md")).toBe(false);
|
|
112
|
-
expect(filter.isAllowed("other/archive/note.md")).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
test("handles pipe character in paths", () => {
|
|
115
|
-
const filter = new PathFilter();
|
|
116
|
-
expect(filter.isAllowed("option|choice.md")).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
test("handles caret in paths", () => {
|
|
119
|
-
const filter = new PathFilter();
|
|
120
|
-
expect(filter.isAllowed("version^2.md")).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
test("handles dollar sign in paths", () => {
|
|
123
|
-
const filter = new PathFilter();
|
|
124
|
-
expect(filter.isAllowed("price$100.md")).toBe(true);
|
|
125
|
-
expect(filter.isAllowed("$HOME/notes.md")).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
test("handles backslash (Windows paths)", () => {
|
|
128
|
-
const filter = new PathFilter();
|
|
129
|
-
// Backslashes should be normalized to forward slashes
|
|
130
|
-
expect(filter.isAllowed("folder\\subfolder\\note.md")).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
// ============================================================================
|
|
134
|
-
// CUSTOM IGNORED PATTERNS WITH SPECIAL CHARS
|
|
135
|
-
// ============================================================================
|
|
136
|
-
describe("custom patterns with special characters", () => {
|
|
137
|
-
test("custom pattern with dots is treated literally", () => {
|
|
138
|
-
const filter = new PathFilter({
|
|
139
|
-
ignoredPatterns: ["backup.2024/**"]
|
|
140
|
-
});
|
|
141
|
-
expect(filter.isAllowed("backup.2024/notes.md")).toBe(false);
|
|
142
|
-
// "backup_2024" should NOT match "backup.2024" pattern
|
|
143
|
-
expect(filter.isAllowed("backup_2024/notes.md")).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
test("custom pattern with parentheses works", () => {
|
|
146
|
-
const filter = new PathFilter({
|
|
147
|
-
ignoredPatterns: ["(archive)/**"]
|
|
148
|
-
});
|
|
149
|
-
expect(filter.isAllowed("(archive)/old.md")).toBe(false);
|
|
150
|
-
expect(filter.isAllowed("archive/old.md")).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
test("custom pattern with brackets works", () => {
|
|
153
|
-
const filter = new PathFilter({
|
|
154
|
-
ignoredPatterns: ["[trash]/**"]
|
|
155
|
-
});
|
|
156
|
-
expect(filter.isAllowed("[trash]/deleted.md")).toBe(false);
|
|
157
|
-
expect(filter.isAllowed("trash/deleted.md")).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
// ============================================================================
|
|
161
|
-
// PATH TRAVERSAL ATTEMPTS
|
|
162
|
-
// ============================================================================
|
|
163
|
-
describe("path traversal prevention", () => {
|
|
164
|
-
test("blocks obvious traversal patterns", () => {
|
|
165
|
-
const filter = new PathFilter({
|
|
166
|
-
ignoredPatterns: ["../**"]
|
|
167
|
-
});
|
|
168
|
-
expect(filter.isAllowed("../secret.md")).toBe(false);
|
|
169
|
-
expect(filter.isAllowed("../../etc/passwd")).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
test("handles encoded traversal attempts", () => {
|
|
172
|
-
const filter = new PathFilter();
|
|
173
|
-
// These should be allowed by PathFilter (path validation is in FileSystem)
|
|
174
|
-
// but filter shouldn't crash on unusual characters
|
|
175
|
-
expect(() => filter.isAllowed("%2e%2e/secret.md")).not.toThrow();
|
|
176
|
-
expect(() => filter.isAllowed("..%2fnotes.md")).not.toThrow();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
test("allows Obsidian first-party file types", () => {
|
|
180
|
-
const filter = new PathFilter();
|
|
181
|
-
expect(filter.isAllowed("_Bases/daily-notes.base")).toBe(true);
|
|
182
|
-
expect(filter.isAllowed("canvas/mindmap.canvas")).toBe(true);
|
|
183
|
-
});
|
|
184
|
-
// ============================================================================
|
|
185
|
-
// FILTER PATHS BATCH OPERATION
|
|
186
|
-
// ============================================================================
|
|
187
|
-
describe("filterPaths", () => {
|
|
188
|
-
test("filters array of paths correctly", () => {
|
|
189
|
-
const filter = new PathFilter();
|
|
190
|
-
const paths = [
|
|
191
|
-
"notes/valid.md",
|
|
192
|
-
".obsidian/config.json",
|
|
193
|
-
"archive/old.md",
|
|
194
|
-
".git/HEAD",
|
|
195
|
-
"readme.txt"
|
|
196
|
-
];
|
|
197
|
-
const allowed = filter.filterPaths(paths);
|
|
198
|
-
expect(allowed).toEqual([
|
|
199
|
-
"notes/valid.md",
|
|
200
|
-
"archive/old.md",
|
|
201
|
-
"readme.txt"
|
|
202
|
-
]);
|
|
203
|
-
});
|
|
204
|
-
test("handles empty array", () => {
|
|
205
|
-
const filter = new PathFilter();
|
|
206
|
-
expect(filter.filterPaths([])).toEqual([]);
|
|
207
|
-
});
|
|
208
|
-
test("handles array with all blocked paths", () => {
|
|
209
|
-
const filter = new PathFilter();
|
|
210
|
-
const paths = [
|
|
211
|
-
".obsidian/app.json",
|
|
212
|
-
".git/config",
|
|
213
|
-
"node_modules/pkg/index.js"
|
|
214
|
-
];
|
|
215
|
-
expect(filter.filterPaths(paths)).toEqual([]);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
// ============================================================================
|
|
219
|
-
// EDGE CASES
|
|
220
|
-
// ============================================================================
|
|
221
|
-
describe("edge cases", () => {
|
|
222
|
-
test("handles empty path", () => {
|
|
223
|
-
const filter = new PathFilter();
|
|
224
|
-
expect(() => filter.isAllowed("")).not.toThrow();
|
|
225
|
-
});
|
|
226
|
-
test("handles path with only extension", () => {
|
|
227
|
-
const filter = new PathFilter();
|
|
228
|
-
expect(filter.isAllowed(".md")).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
test("handles very long paths", () => {
|
|
231
|
-
const filter = new PathFilter();
|
|
232
|
-
const longPath = "a/".repeat(100) + "note.md";
|
|
233
|
-
expect(() => filter.isAllowed(longPath)).not.toThrow();
|
|
234
|
-
expect(filter.isAllowed(longPath)).toBe(true);
|
|
235
|
-
});
|
|
236
|
-
test("handles unicode characters in paths", () => {
|
|
237
|
-
const filter = new PathFilter();
|
|
238
|
-
expect(filter.isAllowed("notes/日本語.md")).toBe(true);
|
|
239
|
-
expect(filter.isAllowed("émojis/🎉.md")).toBe(true);
|
|
240
|
-
expect(filter.isAllowed("中文/笔记.md")).toBe(true);
|
|
241
|
-
});
|
|
242
|
-
test("handles spaces in paths", () => {
|
|
243
|
-
const filter = new PathFilter();
|
|
244
|
-
expect(filter.isAllowed("my notes/important file.md")).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
test("handles directories (no extension)", () => {
|
|
247
|
-
const filter = new PathFilter();
|
|
248
|
-
// Directories should be allowed (no extension check)
|
|
249
|
-
expect(filter.isAllowed("folder/subfolder/")).toBe(true);
|
|
250
|
-
expect(filter.isAllowed("notes")).toBe(true);
|
|
251
|
-
});
|
|
252
|
-
test("handles directories with dots in their names", () => {
|
|
253
|
-
const filter = new PathFilter();
|
|
254
|
-
// Folders with dots should be allowed (common pattern: "1. Project", "2.5 Notes")
|
|
255
|
-
expect(filter.isAllowed("1. Project")).toBe(true);
|
|
256
|
-
expect(filter.isAllowed("2. Archive")).toBe(true);
|
|
257
|
-
expect(filter.isAllowed("3.5 Research")).toBe(true);
|
|
258
|
-
expect(filter.isAllowed("1. Project/subfolder")).toBe(true);
|
|
259
|
-
expect(filter.isAllowed("1. Project/note.md")).toBe(true);
|
|
260
|
-
// But files in those folders should still need proper extensions
|
|
261
|
-
expect(filter.isAllowed("1. Project/file.js")).toBe(false);
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
});
|