mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Admin/DictServicePropertyTests.cs
2026-02-03 14:25:01 +08:00

517 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}