Files
XplorePlane/XplorePlane.Tests/Pipeline/PipelinePersistenceServiceTests.cs
T
zhengxuan.zhang 741874e85d 基于角色的权限控制
1、用户角色枚举、权限枚举、结果记录和密码存储模型
IPermissionService 接口及包含认证、权限检查、密码管理和登出功能的 PermissionService 单例
2、支持层级化角色-权限映射的权限矩阵(SuperAdmin ⊇ Admin ⊇ User)
密码持久化至 passwords.json 文件,并提供工厂默认值回退机制
3、UI 层
LoginDialog — 启动时弹出模态登录对话框,支持密码掩码输入、错误提示以及取消退出功能
RibbonStatusAreaView — 在Ribbon右侧区域始终显示角色标签和“切换用户”按钮
权限感知的CncEditorViewModel — 用户角色无法使用CNC编辑控件
权限感知的CncInspectionModulePipelineViewModel — 用户角色无法进行流程编辑
设置导航可见性 — Admin/User角色隐藏Factory_Settings,User角色隐藏Report_Settings
PasswordManagementView — 仅SuperAdmin可访问的修改角色密码对话框
PermissionTooltipHelper — 附加属性,在禁用控件上显示“当前角色无权访问此功能”提示
2026-06-01 17:15:59 +08:00

232 lines
8.9 KiB
C#
Raw 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 Moq;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services;
using XplorePlane.Services.Permission;
using XplorePlane.Services.Storage;
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" });
var mockPermissionSvc = new Mock<IPermissionService>();
mockPermissionSvc.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<PipelinePersistenceService>()).Returns(mockLogger.Object);
_svc = new PipelinePersistenceService(mockImageSvc.Object, mockPermissionSvc.Object, mockLogger.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.xpm"));
await _svc.SaveAsync(m2, Path.Combine(_tempDir, "p2.xpm"));
var result = await _svc.LoadAllAsync(_tempDir);
Assert.Equal(2, result.Count);
}
[Fact]
public async Task LoadAllAsync_UsesToolsPath_WhenConstructedWithDataPathService()
{
var mockImageSvc = TestHelpers.CreateMockImageService(new[] { "Blur" });
var mockPermissionSvc = new Mock<IPermissionService>();
mockPermissionSvc.Setup(p => p.HasPermission(It.IsAny<Permission>())).Returns(true);
var mockLogger = new Mock<ILoggerService>();
mockLogger.Setup(l => l.ForModule<PipelinePersistenceService>()).Returns(mockLogger.Object);
var mockDataPathSvc = new Mock<IXpDataPathService>();
mockDataPathSvc.SetupGet(s => s.ToolsPath).Returns(_tempDir);
var service = new PipelinePersistenceService(mockImageSvc.Object, mockPermissionSvc.Object, mockLogger.Object, mockDataPathSvc.Object);
await service.SaveAsync(BuildModel("P3", "Blur"), Path.Combine(_tempDir, "p3.xpm"));
var result = await service.LoadAllAsync(null);
Assert.Single(result);
Assert.Equal("P3", result[0].Name);
}
}
}