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
}