Files
XplorePlane/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs
2026-04-13 14:36:18 +08:00

203 lines
7.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}