using Xunit;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Tests.Helpers;
using System.Threading.Tasks;
using System.IO;
using System;
namespace XplorePlane.Tests.Pipeline
{
///
/// 测试 PipelinePersistenceService(任务 6.5)
///
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(
() => _svc.SaveAsync(model, "../../../evil.json"));
}
[Fact]
public async Task LoadAsync_PathTraversal_ThrowsUnauthorized()
{
await Assert.ThrowsAsync(
() => _svc.LoadAsync("../../etc/passwd"));
}
[Fact]
public async Task LoadAsync_EmptyPath_ThrowsArgumentException()
{
await Assert.ThrowsAsync(
() => _svc.LoadAsync(string.Empty));
}
[Fact]
public async Task LoadAsync_NullPath_ThrowsArgumentException()
{
await Assert.ThrowsAsync(
() => _svc.LoadAsync(null!));
}
[Fact]
public async Task LoadAsync_FileNotFound_ThrowsFileNotFoundException()
{
var path = Path.Combine(_tempDir, "nonexistent.pipeline.json");
await Assert.ThrowsAsync(
() => _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);
}
}
}