97 lines
3.5 KiB
C#
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);
|
|
}
|
|
}
|