using Microsoft.Extensions.Options;
using ObsidianMcp.Config;
using ObsidianMcp.Services;
namespace ObsidianMcp.Tests;
///
/// VaultWriteGuard 单测。
/// 核心场景:保护文件名永禁写、白名单允许、非白名单拒绝、空白名单全拒、前缀 vs 精确匹配。
///
public class VaultWriteGuardTests : IDisposable
{
private readonly string _tempRoot;
public VaultWriteGuardTests()
{
_tempRoot = Path.Combine(Path.GetTempPath(), "obsidian-mcp-guard-" + Guid.NewGuid());
Directory.CreateDirectory(_tempRoot);
}
private VaultWriteGuard MakeGuard(string[] whitelist)
{
var opts = Options.Create(new VaultOptions
{
Root = _tempRoot,
Blacklist = [],
WriteWhitelist = whitelist,
});
var resolver = new VaultPathResolver(opts);
return new VaultWriteGuard(resolver, opts);
}
private void EnsureDirFor(string relPath)
{
var abs = Path.Combine(_tempRoot, relPath.Replace('/', Path.DirectorySeparatorChar));
Directory.CreateDirectory(Path.GetDirectoryName(abs)!);
}
// ─── 白名单:前缀匹配 ────────────────────────────────────────────────────
[Theory]
[InlineData("Notes/foo.md")]
[InlineData("Notes/sub/bar.md")]
public void EnsureWritable_PrefixWhitelist_AllowsMatchingPaths(string path)
{
var guard = MakeGuard(["Notes/"]);
EnsureDirFor(path);
var result = guard.EnsureWritable(path);
Assert.True(result.StartsWith(_tempRoot, StringComparison.OrdinalIgnoreCase));
}
// ─── 白名单:精确匹配 ────────────────────────────────────────────────────
[Fact]
public void EnsureWritable_ExactWhitelist_AllowsOnlyExactPath()
{
var guard = MakeGuard(["todo.md"]);
EnsureDirFor("todo.md");
var result = guard.EnsureWritable("todo.md");
Assert.True(result.StartsWith(_tempRoot, StringComparison.OrdinalIgnoreCase));
}
[Fact]
public void EnsureWritable_ExactWhitelist_RejectsSiblingPath()
{
var guard = MakeGuard(["todo.md"]);
EnsureDirFor("other.md");
var ex = Assert.Throws(
() => guard.EnsureWritable("other.md"));
Assert.Contains("白名单", ex.Message);
}
// ─── 保护文件名永禁(即便父目录被白名单允许) ──────────────────────────
[Theory]
[InlineData("AGENTS.md")]
[InlineData("README.md")]
[InlineData("CLAUDE.md")]
[InlineData("Notes/AGENTS.md")]
[InlineData("Notes/README.md")]
[InlineData("Notes/CLAUDE.md")]
public void EnsureWritable_ProtectedFileName_ThrowsUnauthorized(string path)
{
var guard = MakeGuard(["Notes/", "AGENTS.md", "README.md", "CLAUDE.md"]);
EnsureDirFor(path);
var ex = Assert.Throws(() => guard.EnsureWritable(path));
Assert.Contains("禁止写入", ex.Message);
}
// ─── 空白名单:拒绝一切写入 ──────────────────────────────────────────────
[Theory]
[InlineData("Notes/foo.md")]
[InlineData("foo.md")]
public void EnsureWritable_EmptyWhitelist_RejectsEverything(string path)
{
var guard = MakeGuard([]);
EnsureDirFor(path);
var ex = Assert.Throws(() => guard.EnsureWritable(path));
Assert.Contains("白名单", ex.Message);
}
// ─── 非白名单路径拒绝 ────────────────────────────────────────────────────
[Theory]
[InlineData("Other/note.md")]
[InlineData("random.md")]
public void EnsureWritable_NonWhitelistedPath_ThrowsUnauthorized(string path)
{
var guard = MakeGuard(["Notes/"]);
EnsureDirFor(path);
var ex = Assert.Throws(() => guard.EnsureWritable(path));
Assert.Contains("白名单", ex.Message);
}
// ─── 反斜杠归一化 ────────────────────────────────────────────────────────
[Fact]
public void EnsureWritable_WhitelistAcceptsBackslashEntries()
{
var guard = MakeGuard(["Notes\\"]);
EnsureDirFor("Notes/foo.md");
var result = guard.EnsureWritable("Notes/foo.md");
Assert.True(result.StartsWith(_tempRoot, StringComparison.OrdinalIgnoreCase));
}
public void Dispose()
{
try { Directory.Delete(_tempRoot, recursive: true); }
catch { /* best effort */ }
}
}