mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => "/home/testuser"));
|
||||
|
||||
vi.mock("node:os", () => ({
|
||||
homedir: mockHomedir,
|
||||
default: { homedir: mockHomedir },
|
||||
}));
|
||||
|
||||
const FAKE_HOME = "/home/testuser";
|
||||
|
||||
import {
|
||||
compileWildcardPattern,
|
||||
compileWildcardPatternEntries,
|
||||
findCompiledWildcardMatch,
|
||||
findCompiledWildcardMatchForNames,
|
||||
wildcardMatch,
|
||||
} from "#src/wildcard-matcher";
|
||||
|
||||
afterEach(() => {
|
||||
mockHomedir.mockClear();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("compileWildcardPatternEntries", () => {
|
||||
test("returns empty array for empty iterable", () => {
|
||||
const result = compileWildcardPatternEntries([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test("compiles a single exact pattern", () => {
|
||||
const result = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].pattern).toBe("read");
|
||||
expect(result[0].state).toBe("allow");
|
||||
});
|
||||
|
||||
test("compiles multiple patterns in order", () => {
|
||||
const entries: [string, string][] = [
|
||||
["read", "allow"],
|
||||
["write", "deny"],
|
||||
["bash *", "ask"],
|
||||
];
|
||||
const result = compileWildcardPatternEntries(entries);
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((r) => r.pattern)).toEqual(["read", "write", "bash *"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCompiledWildcardMatch", () => {
|
||||
test("returns null for empty patterns array", () => {
|
||||
const result = findCompiledWildcardMatch([], "read");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("matches exact pattern", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
const result = findCompiledWildcardMatch(patterns, "read");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.state).toBe("allow");
|
||||
expect(result?.matchedPattern).toBe("read");
|
||||
expect(result?.matchedName).toBe("read");
|
||||
});
|
||||
|
||||
test("returns null when no pattern matches", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
const result = findCompiledWildcardMatch(patterns, "write");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("matches glob * pattern", () => {
|
||||
const patterns = compileWildcardPatternEntries([["git *", "allow"]]);
|
||||
const result = findCompiledWildcardMatch(patterns, "git status");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.state).toBe("allow");
|
||||
expect(result?.matchedPattern).toBe("git *");
|
||||
});
|
||||
|
||||
test("glob * matches zero or more characters", () => {
|
||||
const patterns = compileWildcardPatternEntries([["git*", "allow"]]);
|
||||
expect(findCompiledWildcardMatch(patterns, "git")).not.toBeNull();
|
||||
expect(findCompiledWildcardMatch(patterns, "git status")).not.toBeNull();
|
||||
expect(findCompiledWildcardMatch(patterns, "npm install")).toBeNull();
|
||||
});
|
||||
|
||||
test("last-match-wins precedence: later pattern overrides earlier", () => {
|
||||
const patterns = compileWildcardPatternEntries([
|
||||
["git *", "allow"],
|
||||
["git push *", "deny"],
|
||||
]);
|
||||
const result = findCompiledWildcardMatch(patterns, "git push origin main");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.state).toBe("deny");
|
||||
expect(result?.matchedPattern).toBe("git push *");
|
||||
});
|
||||
|
||||
test("last-match-wins: specific deny before broad allow matches the later one", () => {
|
||||
const patterns = compileWildcardPatternEntries([
|
||||
["*", "deny"],
|
||||
["git status", "allow"],
|
||||
]);
|
||||
const result = findCompiledWildcardMatch(patterns, "git status");
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.state).toBe("allow");
|
||||
});
|
||||
|
||||
test("exact pattern does not match partial name", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
expect(findCompiledWildcardMatch(patterns, "read ")).toBeNull();
|
||||
expect(findCompiledWildcardMatch(patterns, "readonly")).toBeNull();
|
||||
});
|
||||
|
||||
test("regex special characters in pattern are escaped", () => {
|
||||
const patterns = compileWildcardPatternEntries([
|
||||
["tool.name", "allow"],
|
||||
["tool+extra", "deny"],
|
||||
]);
|
||||
// "tool.name" should not match "toolXname" (dot is escaped)
|
||||
expect(findCompiledWildcardMatch(patterns, "toolXname")).toBeNull();
|
||||
// Exact match works
|
||||
expect(findCompiledWildcardMatch(patterns, "tool.name")).not.toBeNull();
|
||||
expect(findCompiledWildcardMatch(patterns, "tool+extra")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCompiledWildcardMatchForNames", () => {
|
||||
test("returns null for empty names array", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
const result = findCompiledWildcardMatchForNames(patterns, []);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null when all names are whitespace", () => {
|
||||
const patterns = compileWildcardPatternEntries([[" ", "allow"]]);
|
||||
const result = findCompiledWildcardMatchForNames(patterns, [" ", "\t"]);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("matches first name that has a pattern match", () => {
|
||||
const patterns = compileWildcardPatternEntries([
|
||||
["read", "allow"],
|
||||
["write", "deny"],
|
||||
]);
|
||||
const result = findCompiledWildcardMatchForNames(patterns, [
|
||||
"grep",
|
||||
"write",
|
||||
]);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.matchedName).toBe("write");
|
||||
expect(result?.state).toBe("deny");
|
||||
});
|
||||
|
||||
test("trims whitespace from names before matching", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
const result = findCompiledWildcardMatchForNames(patterns, [" read "]);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.state).toBe("allow");
|
||||
});
|
||||
|
||||
test("returns null when no name matches any pattern", () => {
|
||||
const patterns = compileWildcardPatternEntries([["read", "allow"]]);
|
||||
const result = findCompiledWildcardMatchForNames(patterns, [
|
||||
"write",
|
||||
"grep",
|
||||
]);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("multi-name lookup: returns match for first matching name in order", () => {
|
||||
const patterns = compileWildcardPatternEntries([
|
||||
["read", "allow"],
|
||||
["write", "deny"],
|
||||
]);
|
||||
// "read" comes before "write" in names array, so "read" should match first
|
||||
const result = findCompiledWildcardMatchForNames(patterns, [
|
||||
"read",
|
||||
"write",
|
||||
]);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.matchedName).toBe("read");
|
||||
expect(result?.state).toBe("allow");
|
||||
});
|
||||
|
||||
test("compileWildcardPattern produces correct pattern metadata", () => {
|
||||
const compiled = compileWildcardPattern("bash *", "ask");
|
||||
expect(compiled.pattern).toBe("bash *");
|
||||
expect(compiled.state).toBe("ask");
|
||||
expect(compiled.matches("bash ls -la")).toBe(true);
|
||||
expect(compiled.matches("echo hello")).toBe(false);
|
||||
});
|
||||
|
||||
test("a compiled pattern folds the value it is handed (#653)", () => {
|
||||
const compiled = compileWildcardPattern("/dev/*", "allow", {
|
||||
windowsSeparators: true,
|
||||
});
|
||||
expect(compiled.matches("/dev/null")).toBe(true);
|
||||
expect(compiled.matches("\\dev\\null")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("wildcardMatch", () => {
|
||||
test("'*' pattern matches any value", () => {
|
||||
expect(wildcardMatch("*", "anything")).toBe(true);
|
||||
expect(wildcardMatch("*", "")).toBe(true);
|
||||
expect(wildcardMatch("*", "bash")).toBe(true);
|
||||
});
|
||||
|
||||
test("'*' pattern matches values containing newlines", () => {
|
||||
expect(wildcardMatch("*", "line1\nline2")).toBe(true);
|
||||
expect(wildcardMatch("*", "a\nb\nc")).toBe(true);
|
||||
});
|
||||
|
||||
test("prefix-wildcard pattern matches value with embedded newlines", () => {
|
||||
const command =
|
||||
"node -e \"\nimport('x').then(() => {\n console.log('done');\n});\n\"";
|
||||
expect(wildcardMatch("node *", command)).toBe(true);
|
||||
});
|
||||
|
||||
test("compileWildcardPattern matches a multiline string", () => {
|
||||
const compiled = compileWildcardPattern("*", "allow");
|
||||
expect(compiled.matches("a\nb")).toBe(true);
|
||||
});
|
||||
|
||||
test("exact pattern matches identical value", () => {
|
||||
expect(wildcardMatch("read", "read")).toBe(true);
|
||||
expect(wildcardMatch("external_directory", "external_directory")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("exact pattern does not match a different value", () => {
|
||||
expect(wildcardMatch("read", "write")).toBe(false);
|
||||
expect(wildcardMatch("read", "readonly")).toBe(false);
|
||||
expect(wildcardMatch("read", "read ")).toBe(false);
|
||||
});
|
||||
|
||||
test("glob pattern matches with wildcard", () => {
|
||||
expect(wildcardMatch("git *", "git status")).toBe(true);
|
||||
expect(wildcardMatch("git *", "git push origin main")).toBe(true);
|
||||
expect(wildcardMatch("git *", "npm install")).toBe(false);
|
||||
});
|
||||
|
||||
test("glob with no trailing space matches longer string", () => {
|
||||
expect(wildcardMatch("git*", "git")).toBe(true);
|
||||
expect(wildcardMatch("git*", "git status")).toBe(true);
|
||||
expect(wildcardMatch("git*", "npm")).toBe(false);
|
||||
});
|
||||
|
||||
test("regex special characters in pattern are treated as literals", () => {
|
||||
expect(wildcardMatch("tool.name", "tool.name")).toBe(true);
|
||||
expect(wildcardMatch("tool.name", "toolXname")).toBe(false);
|
||||
});
|
||||
|
||||
describe("trailing wildcard optionality", () => {
|
||||
test("'git *' matches bare 'git' (trailing space+wildcard is optional)", () => {
|
||||
expect(wildcardMatch("git *", "git")).toBe(true);
|
||||
});
|
||||
|
||||
test("'git *' still matches 'git status' (existing behaviour preserved)", () => {
|
||||
expect(wildcardMatch("git *", "git status")).toBe(true);
|
||||
});
|
||||
|
||||
test("'git *' still matches 'git status --short'", () => {
|
||||
expect(wildcardMatch("git *", "git status --short")).toBe(true);
|
||||
});
|
||||
|
||||
test("'git *' does not match an unrelated command", () => {
|
||||
expect(wildcardMatch("git *", "npm install")).toBe(false);
|
||||
});
|
||||
|
||||
test("'git status *' matches bare 'git status'", () => {
|
||||
expect(wildcardMatch("git status *", "git status")).toBe(true);
|
||||
});
|
||||
|
||||
test("'git status *' matches 'git status --short'", () => {
|
||||
expect(wildcardMatch("git status *", "git status --short")).toBe(true);
|
||||
});
|
||||
|
||||
test("non-trailing '*' is unaffected: 'g*t' does not match 'g' or 't'", () => {
|
||||
expect(wildcardMatch("g*t", "g")).toBe(false);
|
||||
expect(wildcardMatch("g*t", "t")).toBe(false);
|
||||
});
|
||||
|
||||
test("non-trailing '*' still matches when content is present: 'g*t' matches 'git'", () => {
|
||||
expect(wildcardMatch("g*t", "git")).toBe(true);
|
||||
});
|
||||
|
||||
test("'git*' (no space) still matches bare 'git' — unchanged behaviour", () => {
|
||||
expect(wildcardMatch("git*", "git")).toBe(true);
|
||||
});
|
||||
|
||||
test("'*' alone still matches everything", () => {
|
||||
expect(wildcardMatch("*", "git")).toBe(true);
|
||||
expect(wildcardMatch("*", "")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("match options (Windows path folding)", () => {
|
||||
test("caseInsensitive matches a value differing only in case", () => {
|
||||
expect(
|
||||
wildcardMatch("C:\\Users\\Foo\\*", "c:\\users\\foo\\bar.md", {
|
||||
caseInsensitive: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("case folding is off by default", () => {
|
||||
expect(wildcardMatch("C:\\Users\\Foo\\*", "c:\\users\\foo\\bar.md")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("windowsSeparators matches a backslash value against a forward-slash pattern", () => {
|
||||
expect(
|
||||
wildcardMatch("C:/Users/Foo/*", "C:\\Users\\Foo\\bar.md", {
|
||||
windowsSeparators: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("separator normalization is off by default", () => {
|
||||
expect(wildcardMatch("C:/Users/Foo/*", "C:\\Users\\Foo\\bar.md")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("both options fold a mixed-case forward-slash pattern onto a lowercased backslash value", () => {
|
||||
expect(
|
||||
wildcardMatch(
|
||||
"C:/Users/Foo/AppData/Roaming/*",
|
||||
"c:\\users\\foo\\appdata\\roaming\\npm\\x.md",
|
||||
{ caseInsensitive: true, windowsSeparators: true },
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("windowsSeparators folds both operands (#653)", () => {
|
||||
test("a forward-slash pattern matches a forward-slash value", () => {
|
||||
expect(
|
||||
wildcardMatch("/dev/null", "/dev/null", { windowsSeparators: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a forward-slash glob matches a forward-slash device value", () => {
|
||||
expect(
|
||||
wildcardMatch("/dev/*", "/dev/null", {
|
||||
caseInsensitive: true,
|
||||
windowsSeparators: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a forward-slash relative pattern matches a forward-slash value", () => {
|
||||
expect(
|
||||
wildcardMatch("src/*", "src/foo.ts", { windowsSeparators: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("a backslash pattern matches a forward-slash value", () => {
|
||||
expect(
|
||||
wildcardMatch("src\\*", "src/foo.ts", { windowsSeparators: true }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("the value fold is off by default", () => {
|
||||
expect(wildcardMatch("src\\*", "src/foo.ts")).toBe(false);
|
||||
expect(wildcardMatch("/dev/null", "\\dev\\null")).toBe(false);
|
||||
});
|
||||
|
||||
test("folding separators does not make unrelated values match", () => {
|
||||
expect(
|
||||
wildcardMatch("/dev/null", "/dev/stdout", { windowsSeparators: true }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("? single-character wildcard", () => {
|
||||
test("'?' matches exactly one character", () => {
|
||||
expect(wildcardMatch("?", "a")).toBe(true);
|
||||
expect(wildcardMatch("?", "Z")).toBe(true);
|
||||
expect(wildcardMatch("?", "5")).toBe(true);
|
||||
});
|
||||
|
||||
test("'?' does not match zero characters", () => {
|
||||
expect(wildcardMatch("?", "")).toBe(false);
|
||||
expect(wildcardMatch("a?", "a")).toBe(false);
|
||||
});
|
||||
|
||||
test("'?' does not match two or more characters", () => {
|
||||
expect(wildcardMatch("?", "ab")).toBe(false);
|
||||
expect(wildcardMatch("?", "abc")).toBe(false);
|
||||
});
|
||||
|
||||
test("multiple '?' match exactly that many characters", () => {
|
||||
expect(wildcardMatch("f??", "foo")).toBe(true);
|
||||
expect(wildcardMatch("f??", "fo")).toBe(false);
|
||||
expect(wildcardMatch("f??", "fooo")).toBe(false);
|
||||
});
|
||||
|
||||
test("'?' combined with '*'", () => {
|
||||
// git + one char + anything
|
||||
expect(wildcardMatch("git?*", "git status")).toBe(true);
|
||||
// git + zero chars — '?' requires one
|
||||
expect(wildcardMatch("git?*", "git")).toBe(false);
|
||||
});
|
||||
|
||||
test("'?' matches path separators and special characters", () => {
|
||||
expect(wildcardMatch("a?b", "a/b")).toBe(true);
|
||||
expect(wildcardMatch("a?b", "a.b")).toBe(true);
|
||||
expect(wildcardMatch("a?b", "a\nb")).toBe(true);
|
||||
});
|
||||
|
||||
test("'?' in pattern matches literal '?' in value", () => {
|
||||
expect(wildcardMatch("a?c", "a?c")).toBe(true);
|
||||
});
|
||||
|
||||
test("'?' in bash-style patterns", () => {
|
||||
expect(wildcardMatch("git statu?", "git status")).toBe(true);
|
||||
expect(wildcardMatch("git statu?", "git statux")).toBe(true);
|
||||
expect(wildcardMatch("git statu?", "git statu")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("home path expansion in patterns", () => {
|
||||
test("wildcardMatch expands ~ prefix in pattern before matching", () => {
|
||||
const expandedPath = join(FAKE_HOME, "dev/project");
|
||||
expect(wildcardMatch("~/dev/project", expandedPath)).toBe(true);
|
||||
});
|
||||
|
||||
test("wildcardMatch expands ~/glob in pattern", () => {
|
||||
const expandedFile = join(FAKE_HOME, "dev/project/file.ts");
|
||||
expect(wildcardMatch("~/dev/*", expandedFile)).toBe(true);
|
||||
});
|
||||
|
||||
test("wildcardMatch ~/glob does not match a different home directory", () => {
|
||||
expect(wildcardMatch("~/dev/*", "/other/user/dev/file.ts")).toBe(false);
|
||||
});
|
||||
|
||||
test("wildcardMatch expands $HOME prefix in pattern before matching", () => {
|
||||
const expandedPath = join(FAKE_HOME, "dev/project");
|
||||
expect(wildcardMatch("$HOME/dev/project", expandedPath)).toBe(true);
|
||||
});
|
||||
|
||||
test("wildcardMatch expands $HOME/glob in pattern", () => {
|
||||
const expandedFile = join(FAKE_HOME, "work/file.ts");
|
||||
expect(wildcardMatch("$HOME/work/*", expandedFile)).toBe(true);
|
||||
});
|
||||
|
||||
test("compileWildcardPattern retains original ~ pattern in .pattern field", () => {
|
||||
const compiled = compileWildcardPattern("~/dev/*", "allow");
|
||||
expect(compiled.pattern).toBe("~/dev/*");
|
||||
});
|
||||
|
||||
test("compileWildcardPattern retains original $HOME pattern in .pattern field", () => {
|
||||
const compiled = compileWildcardPattern("$HOME/dev/*", "allow");
|
||||
expect(compiled.pattern).toBe("$HOME/dev/*");
|
||||
});
|
||||
|
||||
test("compileWildcardPattern expanded pattern matches the expanded path", () => {
|
||||
const compiled = compileWildcardPattern("~/dev/*", "allow");
|
||||
const expandedFile = join(FAKE_HOME, "dev/file.ts");
|
||||
expect(compiled.matches(expandedFile)).toBe(true);
|
||||
});
|
||||
|
||||
test("non-home pattern is unaffected", () => {
|
||||
expect(wildcardMatch("/absolute/path/*", "/absolute/path/file")).toBe(true);
|
||||
expect(wildcardMatch("/absolute/path/*", "/other/file")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user