using System.Reflection; using FsCheck; using FsCheck.Xunit; using MiAssessment.Admin.Business.Attributes; using MiAssessment.Admin.Business.Constants; using MiAssessment.Admin.Business.Controllers; using Microsoft.AspNetCore.Mvc; using Xunit; namespace MiAssessment.Tests.Admin; /// /// 权限控制属性测试 /// 验证业务控制器的权限配置正确性 /// /// Property 16: Authorization Enforcement /// *For any* protected endpoint and any user without the required permission, /// the system SHALL return HTTP 403 Forbidden. /// /// **Validates: Requirements 18.2** /// public class AuthorizationPropertyTests { /// /// 获取所有业务控制器类型 /// private static readonly Type[] BusinessControllerTypes = typeof(BusinessControllerBase) .Assembly .GetTypes() .Where(t => t.IsClass && !t.IsAbstract && t.IsSubclassOf(typeof(BusinessControllerBase)) && t != typeof(BusinessControllerBase)) .ToArray(); /// /// 获取所有需要权限控制的控制器(排除健康检查等公开接口) /// private static readonly string[] ExcludedControllers = new[] { "HealthController", "UploadController" // 上传接口可能有单独的认证机制 }; /// /// 某些公开接口不需要权限控制(如获取配置键列表等) /// 注意:所有业务接口现在都已添加权限控制 /// private static readonly HashSet ExcludedMethods = new() { // 所有业务接口都已添加权限控制,无需排除 }; /// /// 获取所有需要权限控制的业务控制器 /// private static IEnumerable GetProtectedControllers() { return BusinessControllerTypes .Where(t => !ExcludedControllers.Contains(t.Name)); } /// /// 获取控制器的所有公开 Action 方法 /// private static IEnumerable GetActionMethods(Type controllerType) { return controllerType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(m => !m.IsSpecialName) // 排除属性的 getter/setter .Where(m => m.GetCustomAttributes().Any() || m.GetCustomAttributes().Any() || m.GetCustomAttributes().Any() || m.GetCustomAttributes().Any()); } /// /// 检查方法是否在排除列表中 /// private static bool IsExcludedMethod(Type controllerType, MethodInfo method) { return ExcludedMethods.Contains($"{controllerType.Name}.{method.Name}"); } #region Property 16: Authorization Enforcement /// /// Property 16: 所有业务控制器的 Action 方法都应该有 BusinessPermission 特性 /// *For any* business controller action method, it SHALL have a [BusinessPermission] attribute. /// /// 注意:此测试会检测缺少权限特性的方法,这些方法需要添加权限控制。 /// /// **Validates: Requirements 18.2** /// [Fact] public void AllBusinessControllerActions_ShouldHaveBusinessPermissionAttribute() { // Arrange var methodsWithoutPermission = new List(); // Act foreach (var controllerType in GetProtectedControllers()) { var actionMethods = GetActionMethods(controllerType); foreach (var method in actionMethods) { // 跳过排除的方法 if (IsExcludedMethod(controllerType, method)) continue; var hasPermissionAttribute = method.GetCustomAttributes().Any(); if (!hasPermissionAttribute) { methodsWithoutPermission.Add($"{controllerType.Name}.{method.Name}"); } } } // Assert Assert.True( methodsWithoutPermission.Count == 0, $"以下 Action 方法缺少 [BusinessPermission] 特性:\n{string.Join("\n", methodsWithoutPermission)}"); } /// /// Property 16: 权限编码格式验证 /// *For any* BusinessPermission attribute, the permission code SHALL follow the format "module:action" /// or "module:submodule:action". /// /// **Validates: Requirements 18.2** /// [Fact] public void AllPermissionCodes_ShouldFollowCorrectFormat() { // Arrange var invalidPermissionCodes = new List(); // 支持 module:action 或 module:submodule:action 格式 var permissionCodePattern = @"^[a-z]+:[a-z]+(:[a-z]+)?$"; // Act foreach (var controllerType in GetProtectedControllers()) { var actionMethods = GetActionMethods(controllerType); foreach (var method in actionMethods) { var permissionAttributes = method.GetCustomAttributes(); foreach (var attr in permissionAttributes) { if (string.IsNullOrWhiteSpace(attr.PermissionCode)) { invalidPermissionCodes.Add($"{controllerType.Name}.{method.Name}: 权限编码为空"); } else if (!System.Text.RegularExpressions.Regex.IsMatch(attr.PermissionCode, permissionCodePattern)) { invalidPermissionCodes.Add($"{controllerType.Name}.{method.Name}: '{attr.PermissionCode}' 不符合 'module:action' 或 'module:submodule:action' 格式"); } } } } // Assert Assert.True( invalidPermissionCodes.Count == 0, $"以下权限编码格式不正确:\n{string.Join("\n", invalidPermissionCodes)}"); } /// /// Property 16: 所有定义的权限都应该是有效的 /// *For any* permission defined in BusinessPermissions.cs, it SHALL have a valid format and non-empty values. /// /// **Validates: Requirements 18.2** /// [Fact] public void AllDefinedPermissions_ShouldBeValid() { // Arrange var invalidPermissions = new List(); // 支持 module:action 或 module:submodule:action 格式 var permissionCodePattern = @"^[a-z]+:[a-z]+(:[a-z]+)?$"; // Act foreach (var permission in BusinessPermissions.AllPermissions) { // 验证权限编码 if (string.IsNullOrWhiteSpace(permission.Code)) { invalidPermissions.Add($"权限编码为空: Name={permission.Name}"); } else if (!System.Text.RegularExpressions.Regex.IsMatch(permission.Code, permissionCodePattern)) { invalidPermissions.Add($"权限编码格式不正确: Code={permission.Code}"); } // 验证权限名称 if (string.IsNullOrWhiteSpace(permission.Name)) { invalidPermissions.Add($"权限名称为空: Code={permission.Code}"); } // 验证模块名称 if (string.IsNullOrWhiteSpace(permission.Module)) { invalidPermissions.Add($"模块名称为空: Code={permission.Code}"); } // 验证权限编码与模块名称一致性 if (!string.IsNullOrWhiteSpace(permission.Code) && !string.IsNullOrWhiteSpace(permission.Module)) { var codeModule = permission.Code.Split(':')[0]; if (codeModule != permission.Module) { invalidPermissions.Add($"权限编码模块与 Module 不一致: Code={permission.Code}, Module={permission.Module}"); } } } // Assert Assert.True( invalidPermissions.Count == 0, $"以下权限定义无效:\n{string.Join("\n", invalidPermissions)}"); } /// /// Property 16: 权限编码应该唯一 /// *For any* two permissions in BusinessPermissions.AllPermissions, their codes SHALL be unique. /// /// **Validates: Requirements 18.2** /// [Fact] public void AllPermissionCodes_ShouldBeUnique() { // Arrange var allCodes = BusinessPermissions.AllPermissions.Select(p => p.Code).ToList(); var duplicateCodes = allCodes .GroupBy(c => c) .Where(g => g.Count() > 1) .Select(g => g.Key) .ToList(); // Assert Assert.True( duplicateCodes.Count == 0, $"以下权限编码重复:\n{string.Join("\n", duplicateCodes)}"); } /// /// Property 16: 每个业务模块都应该定义了权限 /// *For any* business module (config, content, assessment, user, order, planner, distribution, dashboard), /// there SHALL be at least one permission defined. /// /// **Validates: Requirements 18.3, 18.4** /// [Fact] public void AllBusinessModules_ShouldHavePermissionsDefined() { // Arrange var expectedModules = new[] { "config", "content", "assessment", "user", "order", "planner", "distribution", "dashboard" }; var definedModules = BusinessPermissions.AllPermissions .Select(p => p.Module) .Distinct() .ToHashSet(); var missingModules = expectedModules .Where(m => !definedModules.Contains(m)) .ToList(); // Assert Assert.True( missingModules.Count == 0, $"以下业务模块缺少权限定义:\n{string.Join("\n", missingModules)}"); } /// /// Property 16: 权限粒度验证 /// *For any* module with CRUD operations, it SHALL support view, create, update, delete permissions. /// /// **Validates: Requirements 18.4** /// [Theory] [InlineData("content", new[] { "view", "create", "update", "delete" })] [InlineData("assessment", new[] { "view", "create", "update", "delete" })] [InlineData("planner", new[] { "view", "create", "update", "delete" })] public void CrudModules_ShouldHaveFullPermissionGranularity(string module, string[] expectedActions) { // Arrange var modulePermissions = BusinessPermissions.AllPermissions .Where(p => p.Module == module) .Select(p => p.Code.Split(':')[1]) .ToHashSet(); var missingActions = expectedActions .Where(a => !modulePermissions.Contains(a)) .ToList(); // Assert Assert.True( missingActions.Count == 0, $"模块 '{module}' 缺少以下权限操作:\n{string.Join("\n", missingActions.Select(a => $"{module}:{a}"))}"); } #endregion #region Property-Based Tests /// /// Property 16: 对于任意业务控制器,其所有 Action 方法的权限编码都应该格式正确 /// /// **Validates: Requirements 18.2** /// [Property(MaxTest = 50)] public bool AllUsedPermissionCodes_ShouldHaveValidFormat(PositiveInt seed) { // Arrange // 支持 module:action 或 module:submodule:action 格式 var permissionCodePattern = @"^[a-z]+:[a-z]+(:[a-z]+)?$"; var usedPermissionCodes = new HashSet(); // Act: 收集所有使用的权限编码 foreach (var controllerType in GetProtectedControllers()) { var actionMethods = GetActionMethods(controllerType); foreach (var method in actionMethods) { var permissionAttributes = method.GetCustomAttributes(); foreach (var attr in permissionAttributes) { if (!string.IsNullOrWhiteSpace(attr.PermissionCode)) { usedPermissionCodes.Add(attr.PermissionCode); } } } } // Assert: 所有使用的权限编码格式都应该正确 foreach (var code in usedPermissionCodes) { if (!System.Text.RegularExpressions.Regex.IsMatch(code, permissionCodePattern)) { return false; } } return true; } /// /// Property 16: 对于任意已定义的权限,其模块名称应该与权限编码的模块部分一致 /// /// **Validates: Requirements 18.2** /// [Property(MaxTest = 100)] public bool PermissionModuleConsistency_ShouldHold(PositiveInt index) { // Arrange var permissions = BusinessPermissions.AllPermissions; if (permissions.Length == 0) return true; var permissionIndex = index.Get % permissions.Length; var permission = permissions[permissionIndex]; // Act & Assert if (string.IsNullOrWhiteSpace(permission.Code) || string.IsNullOrWhiteSpace(permission.Module)) { return false; } var codeModule = permission.Code.Split(':')[0]; return codeModule == permission.Module; } #endregion #region 辅助测试方法 /// /// 验证控制器数量 /// [Fact] public void BusinessControllers_ShouldExist() { // Assert: 应该存在业务控制器 Assert.True( BusinessControllerTypes.Length > 0, "未找到任何业务控制器"); } /// /// 验证权限定义数量 /// [Fact] public void BusinessPermissions_ShouldExist() { // Assert: 应该存在权限定义 Assert.True( BusinessPermissions.AllPermissions.Length > 0, "未找到任何权限定义"); } /// /// 列出所有业务控制器及其 Action 方法(用于调试) /// [Fact] public void ListAllBusinessControllerActions() { var controllerActions = new List(); foreach (var controllerType in BusinessControllerTypes) { var actionMethods = GetActionMethods(controllerType); foreach (var method in actionMethods) { var permissionAttrs = method.GetCustomAttributes().ToList(); var permissionCodes = permissionAttrs.Any() ? string.Join(", ", permissionAttrs.Select(a => a.PermissionCode)) : "无权限特性"; controllerActions.Add($"{controllerType.Name}.{method.Name}: [{permissionCodes}]"); } } // 输出所有控制器和方法(用于调试) // 这个测试总是通过,只是用于查看当前状态 Assert.True(true, $"业务控制器 Action 列表:\n{string.Join("\n", controllerActions)}"); } #endregion }