203 lines
7.4 KiB
C#
203 lines
7.4 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Threading.Tasks;
|
||
using XplorePlane.Models;
|
||
using XplorePlane.Services;
|
||
using XplorePlane.Tests.Helpers;
|
||
using Xunit;
|
||
|
||
namespace XplorePlane.Tests.Pipeline
|
||
{
|
||
/// <summary>
|
||
/// 测试 PipelinePersistenceService(任务 6.5)
|
||
/// </summary>
|
||
public class PipelinePersistenceServiceTests : IDisposable
|
||
{
|
||
private readonly string _tempDir;
|
||
private readonly PipelinePersistenceService _svc;
|
||
|
||
public PipelinePersistenceServiceTests()
|
||
{
|
||
_tempDir = Path.Combine(Path.GetTempPath(), $"XplorePlaneTests_{Guid.NewGuid():N}");
|
||
Directory.CreateDirectory(_tempDir);
|
||
|
||
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur", "Sharpen" });
|
||
_svc = new PipelinePersistenceService(mockImageSvc.Object, _tempDir);
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (Directory.Exists(_tempDir))
|
||
Directory.Delete(_tempDir, recursive: true);
|
||
}
|
||
|
||
private static PipelineModel BuildModel(string name = "TestPipeline", params string[] keys)
|
||
{
|
||
var model = new PipelineModel { Name = name };
|
||
for (int i = 0; i < keys.Length; i++)
|
||
model.Nodes.Add(new PipelineNodeModel { OperatorKey = keys[i], Order = i });
|
||
return model;
|
||
}
|
||
|
||
// ── 往返一致性 ────────────────────────────────────────────────
|
||
|
||
[Fact]
|
||
public async Task SaveAndLoad_RoundTrip_NamePreserved()
|
||
{
|
||
var model = BuildModel("MyPipeline", "Blur", "Sharpen");
|
||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||
|
||
await _svc.SaveAsync(model, path);
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
Assert.Equal("MyPipeline", loaded.Name);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task SaveAndLoad_RoundTrip_NodeCountPreserved()
|
||
{
|
||
var model = BuildModel("P", "Blur", "Sharpen");
|
||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||
|
||
await _svc.SaveAsync(model, path);
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
Assert.Equal(2, loaded.Nodes.Count);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task SaveAndLoad_RoundTrip_OperatorKeysPreserved()
|
||
{
|
||
var model = BuildModel("P", "Blur", "Sharpen");
|
||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||
|
||
await _svc.SaveAsync(model, path);
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
Assert.Equal("Blur", loaded.Nodes[0].OperatorKey);
|
||
Assert.Equal("Sharpen", loaded.Nodes[1].OperatorKey);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task SaveAndLoad_RoundTrip_OrderIsSequential()
|
||
{
|
||
var model = BuildModel("P", "Blur", "Sharpen");
|
||
var path = Path.Combine(_tempDir, "test.pipeline.json");
|
||
|
||
await _svc.SaveAsync(model, path);
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
for (int i = 0; i < loaded.Nodes.Count; i++)
|
||
Assert.Equal(i, loaded.Nodes[i].Order);
|
||
}
|
||
|
||
// ── 非法路径被拒绝 ────────────────────────────────────────────
|
||
|
||
[Fact]
|
||
public async Task SaveAsync_PathTraversal_ThrowsUnauthorized()
|
||
{
|
||
var model = BuildModel("P");
|
||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||
() => _svc.SaveAsync(model, "../../../evil.json"));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task LoadAsync_PathTraversal_ThrowsUnauthorized()
|
||
{
|
||
await Assert.ThrowsAsync<UnauthorizedAccessException>(
|
||
() => _svc.LoadAsync("../../etc/passwd"));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task LoadAsync_EmptyPath_ThrowsArgumentException()
|
||
{
|
||
await Assert.ThrowsAsync<ArgumentException>(
|
||
() => _svc.LoadAsync(string.Empty));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task LoadAsync_NullPath_ThrowsArgumentException()
|
||
{
|
||
await Assert.ThrowsAsync<ArgumentException>(
|
||
() => _svc.LoadAsync(null!));
|
||
}
|
||
|
||
[Fact]
|
||
public async Task LoadAsync_FileNotFound_ThrowsFileNotFoundException()
|
||
{
|
||
var path = Path.Combine(_tempDir, "nonexistent.pipeline.json");
|
||
await Assert.ThrowsAsync<FileNotFoundException>(
|
||
() => _svc.LoadAsync(path));
|
||
}
|
||
|
||
// ── 非法 OperatorKey 被跳过 ───────────────────────────────────
|
||
|
||
[Fact]
|
||
public async Task Load_UnregisteredOperatorKey_NodeSkipped()
|
||
{
|
||
// 手动写入包含未注册 Key 的 JSON
|
||
var json = """
|
||
{
|
||
"Id": "00000000-0000-0000-0000-000000000001",
|
||
"Name": "Test",
|
||
"DeviceId": "",
|
||
"Nodes": [
|
||
{ "Id": "00000000-0000-0000-0000-000000000002", "OperatorKey": "Blur", "Order": 0, "IsEnabled": true, "Parameters": {} },
|
||
{ "Id": "00000000-0000-0000-0000-000000000003", "OperatorKey": "UNKNOWN_KEY", "Order": 1, "IsEnabled": true, "Parameters": {} }
|
||
]
|
||
}
|
||
""";
|
||
var path = Path.Combine(_tempDir, "mixed.pipeline.json");
|
||
await File.WriteAllTextAsync(path, json);
|
||
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
Assert.Single(loaded.Nodes);
|
||
Assert.Equal("Blur", loaded.Nodes[0].OperatorKey);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Load_AllUnregisteredKeys_ReturnsEmptyNodes()
|
||
{
|
||
var json = """
|
||
{
|
||
"Id": "00000000-0000-0000-0000-000000000001",
|
||
"Name": "Test",
|
||
"DeviceId": "",
|
||
"Nodes": [
|
||
{ "Id": "00000000-0000-0000-0000-000000000002", "OperatorKey": "FAKE1", "Order": 0, "IsEnabled": true, "Parameters": {} },
|
||
{ "Id": "00000000-0000-0000-0000-000000000003", "OperatorKey": "FAKE2", "Order": 1, "IsEnabled": true, "Parameters": {} }
|
||
]
|
||
}
|
||
""";
|
||
var path = Path.Combine(_tempDir, "all_fake.pipeline.json");
|
||
await File.WriteAllTextAsync(path, json);
|
||
|
||
var loaded = await _svc.LoadAsync(path);
|
||
|
||
Assert.Empty(loaded.Nodes);
|
||
}
|
||
|
||
// ── LoadAllAsync ──────────────────────────────────────────────
|
||
|
||
[Fact]
|
||
public async Task LoadAllAsync_EmptyDirectory_ReturnsEmpty()
|
||
{
|
||
var result = await _svc.LoadAllAsync(_tempDir);
|
||
Assert.Empty(result);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task LoadAllAsync_MultipleFiles_ReturnsAll()
|
||
{
|
||
var m1 = BuildModel("P1", "Blur");
|
||
var m2 = BuildModel("P2", "Sharpen");
|
||
await _svc.SaveAsync(m1, Path.Combine(_tempDir, "p1.pipeline.json"));
|
||
await _svc.SaveAsync(m2, Path.Combine(_tempDir, "p2.pipeline.json"));
|
||
|
||
var result = await _svc.LoadAllAsync(_tempDir);
|
||
|
||
Assert.Equal(2, result.Count);
|
||
}
|
||
}
|
||
} |