using System.Reflection; using Admin.NET.Core; using Admin.NET.Plugin.AiDOP.Controllers; using Admin.NET.Plugin.AiDOP.Controllers.S8; using Admin.NET.Plugin.AiDOP.FinishedWarehouse; using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.Infrastructure.S8; using Admin.NET.Plugin.AiDOP.MaterialWarehouse; using Admin.NET.Plugin.AiDOP.Order; using Admin.NET.Plugin.AiDOP.Production; using Admin.NET.Plugin.ApprovalFlow.Service; using Xunit; namespace Admin.NET.Plugin.AiDOP.Tests.Security; /// App 业务 API 必须复用 PC 菜单 path 权限,且保留既有业务内层守卫。 public class AppApiPcMenuGuardTests { private const string ApprovalCenter = "/aidop/flowManage/approvalFlowCenter"; [Fact] public void ApprovalAppEndpoints_RequireApprovalCenterMenu() { AssertMethodsGuarded(typeof(FlowTaskService), ApprovalCenter, "MyPendingPage", "MyDonePage", "MyInitiatedPage", "MyPendingCount", "Approve", "Reject", "Transfer", "Withdraw", "ReturnToPrev", "AddSign", "Urge", "BatchApprove", "BatchReject"); AssertMethodsGuarded(typeof(FlowInstanceService), ApprovalCenter, "GetDetail", "GetTimeline"); AssertMethodsGuarded(typeof(ApprovalFlowService), ApprovalCenter, "GetBizTypes"); } [Fact] public void AidopQueryEndpoints_RequireCorrespondingPcMenus() { Assert.NotNull(typeof(AidopKanbanController).GetMethod("GetDashboardPage")! .GetCustomAttribute()); AssertMethodsGuarded(typeof(AidopKanbanController), "/aidop/smart-ops/s1", "GetSmartOpsFilterOptions"); AssertMethodsGuarded(typeof(StockQueryService), "/aidop/s5/inventory/stock-query", "GetList", "GetTypeOptions", "GetLocationOptions"); AssertMethodsGuarded(typeof(WorkOrderProgressDashboardService), "/aidop/s2/collaboration-kanban/work-order-progress", "GetList"); AssertMethodsGuarded(typeof(OrderDeliveryService), "/aidop/s1/order-mgmt/orderDelivery", "GetDeliveryList", "GetDeliveryFlow"); AssertMethodsGuarded(typeof(FqcInspBillService), "/aidop/s7/fqc/task-list", "GetTaskList"); AssertMethodsGuarded(typeof(FqcInspBillService), "/aidop/s7/fqc/result-list", "GetResultList"); AssertMethodsGuarded(typeof(FqcInspBillService), "/aidop/s7/fqc/result-detail", "GetDetail"); } [Fact] public void S8AppEndpoints_RequirePcMenuAndKeepSecondLayer() { AssertClassGuarded(typeof(AdoS8DashboardController), "/aidop/s8/dashboard"); AssertClassGuarded(typeof(AdoS8ExceptionsController), "/aidop/s8/exceptions"); AssertClassGuarded(typeof(AdoS8MasterDataController), "/aidop/s8/exceptions"); AssertMethodsGuarded(typeof(AdoS8ConfigActionPermissionController), "/aidop/s8/exceptions", "MineAsync"); Assert.NotNull(typeof(AdoS8DashboardController).GetMethod("OverviewAsync")! .GetCustomAttribute()); Assert.NotNull(typeof(AdoS8ExceptionsController).GetMethod("ClaimAsync")! .GetCustomAttribute()); } [Fact] public void ApprovalTaskOwnershipGuard_RemainsInEngine() { var source = ReadRepoFile("server", "Plugins", "Admin.NET.Plugin.ApprovalFlow", "Service", "FlowEngine", "FlowEngineService.cs"); Assert.Contains("task.AssigneeId != _userManager.UserId", source); Assert.Contains("当前用户不是该任务的审批人", source); } [Fact] public void AidopTenantHelper_DoesNotTrustRequestTenantId() { var source = ReadRepoFile("server", "Plugins", "Admin.NET.Plugin.AiDOP", "Infrastructure", "AidopTenantHelper.cs"); Assert.Contains("AidopTenantScope.ResolveOrThrow", source); Assert.DoesNotContain("Request.Query", source); Assert.DoesNotContain("return requestTenantId", source); Assert.DoesNotContain("SqlSugarConst.DefaultTenantId", source); } private static void AssertClassGuarded(Type type, string expectedPath) { var paths = type.GetCustomAttributes(true) .Select(x => x.MenuPath); Assert.Contains(expectedPath, paths); } private static void AssertMethodsGuarded(Type type, string expectedPath, params string[] methodNames) { foreach (var methodName in methodNames) { var method = type.GetMethods(BindingFlags.Instance | BindingFlags.Public) .SingleOrDefault(x => x.Name == methodName); Assert.NotNull(method); var paths = method!.GetCustomAttributes(true) .Select(x => x.MenuPath); Assert.Contains(expectedPath, paths); } } private static string ReadRepoFile(params string[] parts) { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir != null && !Directory.Exists(Path.Combine(dir.FullName, "server"))) dir = dir.Parent; Assert.NotNull(dir); return File.ReadAllText(Path.Combine(new[] { dir!.FullName }.Concat(parts).ToArray())); } }