515763bc72
Build Docker Image / build (push) Failing after 1m22s
MCP (Model Context Protocol) server for reading and writing an Obsidian vault, gated by OAuth-issued JWT bearer tokens. See README.md for setup.
140 lines
5.0 KiB
C#
140 lines
5.0 KiB
C#
using Microsoft.Extensions.Options;
|
|
using ObsidianMcp.Config;
|
|
using ObsidianMcp.Services;
|
|
|
|
namespace ObsidianMcp.Tests;
|
|
|
|
/// <summary>
|
|
/// VaultWriteGuard 单测。
|
|
/// 核心场景:保护文件名永禁写、白名单允许、非白名单拒绝、空白名单全拒、前缀 vs 精确匹配。
|
|
/// </summary>
|
|
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<UnauthorizedAccessException>(
|
|
() => 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<UnauthorizedAccessException>(() => 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<UnauthorizedAccessException>(() => 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<UnauthorizedAccessException>(() => 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 */ }
|
|
}
|
|
}
|