517 lines
17 KiB
C#
517 lines
17 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
public class DictServicePropertyTests
|
||
{
|
||
private readonly Mock<ILogger<DictService>> _mockLogger = new();
|
||
|
||
/// <summary>
|
||
/// 数据源类型:静态数据
|
||
/// </summary>
|
||
private const byte SourceTypeStatic = 1;
|
||
|
||
/// <summary>
|
||
/// 数据源类型:SQL查询
|
||
/// </summary>
|
||
private const byte SourceTypeSql = 2;
|
||
|
||
/// <summary>
|
||
/// 状态:禁用
|
||
/// </summary>
|
||
private const byte StatusDisabled = 0;
|
||
|
||
/// <summary>
|
||
/// 状态:启用
|
||
/// </summary>
|
||
private const byte StatusEnabled = 1;
|
||
|
||
#region Property 3.1: Disabled Dict Types Return Empty Results
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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<string>();
|
||
|
||
// 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
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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<string>();
|
||
var expectedLabels = new List<string>();
|
||
|
||
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)
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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<string>();
|
||
|
||
// 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
|
||
|
||
/// <summary>
|
||
/// 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**
|
||
/// </summary>
|
||
[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
|
||
|
||
/// <summary>
|
||
/// 创建内存数据库上下文
|
||
/// </summary>
|
||
private AdminDbContext CreateDbContext()
|
||
{
|
||
var options = new DbContextOptionsBuilder<AdminDbContext>()
|
||
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
||
.Options;
|
||
|
||
return new AdminDbContext(options);
|
||
}
|
||
|
||
#endregion
|
||
}
|