using FsCheck; using FsCheck.Xunit; using MiAssessment.Admin.Data; using MiAssessment.Admin.Entities; using MiAssessment.Admin.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace MiAssessment.Tests.Admin; /// /// DictService 属性测试 /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// *For any* dictionary type query: /// - If source_type is 1 (static), the result SHALL contain only enabled items from dict_items table /// - If source_type is 2 (SQL), the result SHALL be the execution result of source_sql /// - If the dictionary type status is 0 (disabled), the query SHALL return empty or exclude this type /// - If a dictionary item status is 0 (disabled), it SHALL NOT appear in the query result /// /// **Validates: Requirements 4.4, 4.5, 4.6, 4.7** /// public class DictServicePropertyTests { private readonly Mock> _mockLogger = new(); /// /// 数据源类型:静态数据 /// private const byte SourceTypeStatic = 1; /// /// 数据源类型:SQL查询 /// private const byte SourceTypeSql = 2; /// /// 状态:禁用 /// private const byte StatusDisabled = 0; /// /// 状态:启用 /// private const byte StatusEnabled = 1; #region Property 3.1: Disabled Dict Types Return Empty Results /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.1: Disabled dict types (status=0) return empty results /// If the dictionary type status is 0 (disabled), the query SHALL return empty or exclude this type /// /// **Validates: Requirements 4.6** /// [Property(MaxTest = 100)] public bool DisabledDictType_ShouldReturnEmptyResults(PositiveInt seed) { // Arrange: Create a disabled dict type with some items var typeCode = $"disabled_type_{seed.Get}"; using var dbContext = CreateDbContext(); var dictType = new DictType { Code = typeCode, Name = $"禁用字典类型 {seed.Get}", Description = "测试禁用字典类型", SourceType = SourceTypeStatic, Status = StatusDisabled, // 禁用状态 Sort = seed.Get % 100, CreatedAt = DateTime.Now }; dbContext.DictTypes.Add(dictType); dbContext.SaveChanges(); // Add some enabled items to the disabled type var itemCount = (seed.Get % 5) + 1; for (int i = 0; i < itemCount; i++) { dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = $"标签{i}", Value = $"value{i}", Status = StatusEnabled, Sort = i, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Query items by type code var items = service.GetItemsByTypeCodeAsync(typeCode).GetAwaiter().GetResult(); // Assert: Should return empty list for disabled dict type return items.Count == 0; } #endregion #region Property 3.2: Disabled Dict Items Are Filtered Out /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.2: Disabled dict items (status=0) are filtered out from results /// If a dictionary item status is 0 (disabled), it SHALL NOT appear in the query result /// /// **Validates: Requirements 4.7** /// [Property(MaxTest = 100)] public bool DisabledDictItems_ShouldBeFilteredOut(PositiveInt seed) { // Arrange: Create an enabled dict type with mixed status items var typeCode = $"mixed_items_type_{seed.Get}"; using var dbContext = CreateDbContext(); var dictType = new DictType { Code = typeCode, Name = $"混合状态字典类型 {seed.Get}", Description = "测试混合状态字典项", SourceType = SourceTypeStatic, Status = StatusEnabled, // 启用状态 Sort = seed.Get % 100, CreatedAt = DateTime.Now }; dbContext.DictTypes.Add(dictType); dbContext.SaveChanges(); // Add items with mixed status (some enabled, some disabled) var enabledCount = (seed.Get % 5) + 1; var disabledCount = (seed.Get % 3) + 1; var disabledValues = new List(); // Add enabled items for (int i = 0; i < enabledCount; i++) { dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = $"启用标签{i}", Value = $"enabled_value_{i}", Status = StatusEnabled, Sort = i, CreatedAt = DateTime.Now }); } // Add disabled items for (int i = 0; i < disabledCount; i++) { var disabledValue = $"disabled_value_{i}"; disabledValues.Add(disabledValue); dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = $"禁用标签{i}", Value = disabledValue, Status = StatusDisabled, // 禁用状态 Sort = enabledCount + i, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Query items by type code var items = service.GetItemsByTypeCodeAsync(typeCode).GetAwaiter().GetResult(); // Assert: // 1. Should return exactly the enabled count // 2. None of the disabled values should appear in results var correctCount = items.Count == enabledCount; var noDisabledItems = !items.Any(item => disabledValues.Contains(item.Value)); return correctCount && noDisabledItems; } #endregion #region Property 3.3: Static Data Queries Return Items From dict_items Table /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.3: Static data queries return items from dict_items table /// If source_type is 1 (static), the result SHALL contain only enabled items from dict_items table /// /// **Validates: Requirements 4.4** /// [Property(MaxTest = 100)] public bool StaticDataQuery_ShouldReturnItemsFromDictItemsTable(PositiveInt seed) { // Arrange: Create a static dict type with items var typeCode = $"static_type_{seed.Get}"; using var dbContext = CreateDbContext(); var dictType = new DictType { Code = typeCode, Name = $"静态字典类型 {seed.Get}", Description = "测试静态数据查询", SourceType = SourceTypeStatic, // 静态数据类型 Status = StatusEnabled, Sort = seed.Get % 100, CreatedAt = DateTime.Now }; dbContext.DictTypes.Add(dictType); dbContext.SaveChanges(); // Add enabled items with specific values var itemCount = (seed.Get % 5) + 1; var expectedValues = new List(); var expectedLabels = new List(); for (int i = 0; i < itemCount; i++) { var value = $"static_value_{seed.Get}_{i}"; var label = $"静态标签_{seed.Get}_{i}"; expectedValues.Add(value); expectedLabels.Add(label); dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = label, Value = value, Status = StatusEnabled, Sort = i, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Query items by type code var items = service.GetItemsByTypeCodeAsync(typeCode).GetAwaiter().GetResult(); // Assert: // 1. Should return the correct count // 2. All expected values should be present // 3. All expected labels should be present var correctCount = items.Count == itemCount; var allValuesPresent = expectedValues.All(v => items.Any(item => item.Value == v)); var allLabelsPresent = expectedLabels.All(l => items.Any(item => item.Label == l)); return correctCount && allValuesPresent && allLabelsPresent; } #endregion #region Property 3.4: All Returned Items Have Status=1 (Enabled) /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.4: All returned items have status=1 (enabled) /// The result SHALL contain only enabled items /// /// **Validates: Requirements 4.4, 4.7** /// [Property(MaxTest = 100)] public bool AllReturnedItems_ShouldHaveEnabledStatus(PositiveInt seed) { // Arrange: Create an enabled dict type with various items var typeCode = $"all_enabled_type_{seed.Get}"; using var dbContext = CreateDbContext(); var dictType = new DictType { Code = typeCode, Name = $"全启用字典类型 {seed.Get}", Description = "测试返回项状态", SourceType = SourceTypeStatic, Status = StatusEnabled, Sort = seed.Get % 100, CreatedAt = DateTime.Now }; dbContext.DictTypes.Add(dictType); dbContext.SaveChanges(); // Add items with random status distribution var totalItems = (seed.Get % 10) + 1; for (int i = 0; i < totalItems; i++) { // Randomly assign status based on seed var status = ((seed.Get + i) % 3 == 0) ? StatusDisabled : StatusEnabled; dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = $"标签{i}", Value = $"value_{i}", Status = status, Sort = i, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Query items by type code var items = service.GetItemsByTypeCodeAsync(typeCode).GetAwaiter().GetResult(); // Assert: All returned items should have Status = 1 (enabled) return items.All(item => item.Status == StatusEnabled); } #endregion #region Property 3.5: Non-Existent Dict Type Returns Empty Results /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.5: Non-existent dict type returns empty results /// If the dictionary type does not exist, the query SHALL return empty /// /// **Validates: Requirements 4.6** /// [Property(MaxTest = 100)] public bool NonExistentDictType_ShouldReturnEmptyResults(NonEmptyString randomCode) { // Arrange: Create a db context without the requested type using var dbContext = CreateDbContext(); // Add some other dict types to ensure the query is working dbContext.DictTypes.Add(new DictType { Code = "existing_type", Name = "存在的类型", SourceType = SourceTypeStatic, Status = StatusEnabled, CreatedAt = DateTime.Now }); dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Generate a unique non-existent code var nonExistentCode = $"non_existent_{randomCode.Get}_{Guid.NewGuid():N}"; // Act: Query items by non-existent type code var items = service.GetItemsByTypeCodeAsync(nonExistentCode).GetAwaiter().GetResult(); // Assert: Should return empty list return items.Count == 0; } #endregion #region Property 3.6: GetTypesAsync Returns Only Enabled Types /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.6: GetTypesAsync returns only enabled types /// The GetTypesAsync method SHALL return only dict types with status=1 /// /// **Validates: Requirements 4.6** /// [Property(MaxTest = 100)] public bool GetTypesAsync_ShouldReturnOnlyEnabledTypes(PositiveInt seed) { // Arrange: Create dict types with mixed status using var dbContext = CreateDbContext(); var enabledCount = (seed.Get % 5) + 1; var disabledCount = (seed.Get % 3) + 1; var enabledCodes = new List(); // Add enabled types for (int i = 0; i < enabledCount; i++) { var code = $"enabled_type_{seed.Get}_{i}"; enabledCodes.Add(code); dbContext.DictTypes.Add(new DictType { Code = code, Name = $"启用类型 {i}", SourceType = SourceTypeStatic, Status = StatusEnabled, Sort = i, CreatedAt = DateTime.Now }); } // Add disabled types for (int i = 0; i < disabledCount; i++) { dbContext.DictTypes.Add(new DictType { Code = $"disabled_type_{seed.Get}_{i}", Name = $"禁用类型 {i}", SourceType = SourceTypeStatic, Status = StatusDisabled, Sort = enabledCount + i, CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Get all types var types = service.GetTypesAsync().GetAwaiter().GetResult(); // Assert: // 1. Should return exactly the enabled count // 2. All returned types should have Status = 1 // 3. All enabled codes should be present var correctCount = types.Count == enabledCount; var allEnabled = types.All(t => t.Status == StatusEnabled); var allEnabledCodesPresent = enabledCodes.All(c => types.Any(t => t.Code == c)); return correctCount && allEnabled && allEnabledCodesPresent; } #endregion #region Property 3.7: Items Are Sorted By Sort Field Then By Id /// /// Feature: framework-template, Property 3: Dictionary Data Query Filtering /// /// Property 3.7: Items are sorted by Sort field then by Id /// The returned items SHALL be ordered by Sort ascending, then by Id ascending /// /// **Validates: Requirements 4.4** /// [Property(MaxTest = 100)] public bool ReturnedItems_ShouldBeSortedBySortThenById(PositiveInt seed) { // Arrange: Create a dict type with items having various sort values var typeCode = $"sorted_type_{seed.Get}"; using var dbContext = CreateDbContext(); var dictType = new DictType { Code = typeCode, Name = $"排序测试类型 {seed.Get}", SourceType = SourceTypeStatic, Status = StatusEnabled, CreatedAt = DateTime.Now }; dbContext.DictTypes.Add(dictType); dbContext.SaveChanges(); // Add items with various sort values (not in order) var sortValues = new[] { 5, 1, 3, 2, 4 }; var itemCount = Math.Min(sortValues.Length, (seed.Get % 5) + 1); for (int i = 0; i < itemCount; i++) { dbContext.DictItems.Add(new DictItem { TypeId = dictType.Id, Label = $"标签{i}", Value = $"value_{i}", Status = StatusEnabled, Sort = sortValues[i], CreatedAt = DateTime.Now }); } dbContext.SaveChanges(); var service = new DictService(dbContext, _mockLogger.Object); // Act: Query items by type code var items = service.GetItemsByTypeCodeAsync(typeCode).GetAwaiter().GetResult(); // Assert: Items should be sorted by Sort ascending if (items.Count <= 1) return true; for (int i = 1; i < items.Count; i++) { var prev = items[i - 1]; var curr = items[i]; // Sort should be ascending, or if equal, Id should be ascending if (prev.Sort > curr.Sort) return false; if (prev.Sort == curr.Sort && prev.Id > curr.Id) return false; } return true; } #endregion #region Helper Methods /// /// 创建内存数据库上下文 /// private AdminDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new AdminDbContext(options); } #endregion }