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 { /// /// 测试 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); } } }