build-flow.api/tests/BuildFlow.Rag.Tests/ObsidianSyncSecurityTests.cs
2026-07-24 12:29:06 +08:00

97 lines
3.5 KiB
C#

using BuildFlow.Application.Obsidian.Commands;
using BuildFlow.Application.Obsidian.Handlers;
using BuildFlow.Domain.Entities.KnowledgeBase;
using BuildFlow.Infrastructure.Obsidian;
using BuildFlow.Infrastructure.Persistence;
using BuildFlow.Rag.Tests.Stub;
using BuildFlow.Shared.Domain.Errors;
using BuildFlow.Shared.Domain.Exceptions;
using BuildFlow.Shared.Infrastructure;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace BuildFlow.Rag.Tests;
/// <summary>
/// Obsidian 同步的知识库所有权和增量行为测试。
/// </summary>
public sealed class ObsidianSyncSecurityTests : IDisposable
{
private readonly string _allowedRoot = Directory.CreateDirectory(Path.Combine(
Path.GetTempPath(),
$"buildflow-obsidian-sync-{Guid.NewGuid():N}")).FullName;
[Fact]
public async Task NonOwnerShouldReceiveNotFoundWithoutScanningFiles()
{
var owner = new TestCurrentUserContext { UserId = Guid.NewGuid().ToString() };
await using var db = CreateDb(owner);
var kb = new KnowledgeBaseEntity { Name = "private" };
db.KnowledgeBases.Add(kb);
await db.SaveChangesAsync();
var caller = new TestCurrentUserContext { UserId = Guid.NewGuid().ToString() };
var handler = new SyncObsidianVaultCommandHandler(db, caller, CreateScanner());
var act = () => handler.Handle(
new SyncObsidianVaultCommand(kb.Id, _allowedRoot),
CancellationToken.None);
await act.Should().ThrowAsync<NotFoundException>()
.Where(x => x.Code == ErrorCode.NotFound);
db.Documents.Should().BeEmpty();
}
[Fact]
public async Task OwnerSyncShouldAddThenSkipAnUnchangedFile()
{
var caller = new TestCurrentUserContext();
await using var db = CreateDb(caller);
var kb = new KnowledgeBaseEntity { Name = "owned" };
db.KnowledgeBases.Add(kb);
await db.SaveChangesAsync();
await System.IO.File.WriteAllTextAsync(Path.Combine(_allowedRoot, "note.md"), "hello");
var handler = new SyncObsidianVaultCommandHandler(db, caller, CreateScanner());
var first = await handler.Handle(
new SyncObsidianVaultCommand(kb.Id, _allowedRoot),
CancellationToken.None);
var second = await handler.Handle(
new SyncObsidianVaultCommand(kb.Id, _allowedRoot),
CancellationToken.None);
first.Added.Should().Be(1);
second.Skipped.Should().Be(1);
db.Documents.Should().ContainSingle();
}
private static RagDbContext CreateDb(TestCurrentUserContext userContext)
{
var options = new DbContextOptionsBuilder<RagDbContext>()
.UseInMemoryDatabase($"obsidian-sync-{Guid.NewGuid():N}")
.AddInterceptors(new AuditInterceptor(userContext))
.Options;
return new RagDbContext(options);
}
private ObsidianVaultScanner CreateScanner()
{
return new ObsidianVaultScanner(
Options.Create(new ObsidianOptions
{
AllowedRoots = [_allowedRoot],
MaxFiles = 100,
MaxFileBytes = 1024 * 1024,
MaxTotalBytes = 10 * 1024 * 1024,
}),
NullLogger<ObsidianVaultScanner>.Instance);
}
public void Dispose()
{
if (Directory.Exists(_allowedRoot)) Directory.Delete(_allowedRoot, recursive: true);
}
}