using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Admin.Business.Data;
using MiAssessment.Admin.Business.Models.Distribution;
using MiAssessment.Admin.Business.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Services;
///
/// DistributionService 属性测试
///
public class DistributionServicePropertyTests
{
private readonly Mock> _mockLogger = new();
#region Property 13: Invite Code Generation Uniqueness
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// For any batch of generated invite codes, all codes SHALL be unique 5-character uppercase letter strings.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 100)]
public bool GenerateInviteCodes_AllCodesShouldBeUnique(PositiveInt seed)
{
var count = (seed.Get % 50) + 1; // 1 to 50 codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
var result = service.GenerateInviteCodesAsync(count).GetAwaiter().GetResult();
// All codes should be unique
var uniqueCodes = result.Codes.Distinct().Count();
return uniqueCodes == result.Codes.Count && result.Codes.Count == count;
}
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// For any batch of generated invite codes, all codes SHALL be 5-character uppercase letter strings.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 100)]
public bool GenerateInviteCodes_AllCodesShouldBe5CharUppercaseLetters(PositiveInt seed)
{
var count = (seed.Get % 30) + 1; // 1 to 30 codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
var result = service.GenerateInviteCodesAsync(count).GetAwaiter().GetResult();
// All codes should be 5 characters and uppercase letters only
return result.Codes.All(code =>
code.Length == 5 &&
code.All(c => c >= 'A' && c <= 'Z'));
}
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// For any batch of generated invite codes, all codes in the same batch SHALL share the same BatchNo.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 100)]
public bool GenerateInviteCodes_AllCodesShouldShareSameBatchNo(PositiveInt seed)
{
var count = (seed.Get % 20) + 1; // 1 to 20 codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
var result = service.GenerateInviteCodesAsync(count).GetAwaiter().GetResult();
// Verify BatchNo is not empty
if (string.IsNullOrEmpty(result.BatchNo))
return false;
// Verify all codes in database have the same BatchNo
var codesInDb = dbContext.InviteCodes
.Where(c => result.Codes.Contains(c.Code))
.ToList();
return codesInDb.Count == count &&
codesInDb.All(c => c.BatchNo == result.BatchNo);
}
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// Multiple batches of generated invite codes should not have duplicate codes across batches.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 50)]
public bool GenerateInviteCodes_MultipleBatchesShouldNotHaveDuplicates(PositiveInt seed)
{
var batch1Count = (seed.Get % 10) + 5; // 5 to 14 codes
var batch2Count = (seed.Get % 10) + 5; // 5 to 14 codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
// Generate first batch
var result1 = service.GenerateInviteCodesAsync(batch1Count).GetAwaiter().GetResult();
// Generate second batch
var result2 = service.GenerateInviteCodesAsync(batch2Count).GetAwaiter().GetResult();
// Verify no duplicates between batches
var allCodes = result1.Codes.Concat(result2.Codes).ToList();
var uniqueCodes = allCodes.Distinct().Count();
return uniqueCodes == allCodes.Count &&
result1.BatchNo != result2.BatchNo;
}
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// Generated invite codes should be persisted to the database with correct initial status.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 50)]
public bool GenerateInviteCodes_ShouldPersistWithCorrectStatus(PositiveInt seed)
{
var count = (seed.Get % 15) + 1; // 1 to 15 codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
var result = service.GenerateInviteCodesAsync(count).GetAwaiter().GetResult();
// Verify all codes are persisted with status = 1 (未分配)
var codesInDb = dbContext.InviteCodes
.Where(c => result.Codes.Contains(c.Code))
.ToList();
return codesInDb.Count == count &&
codesInDb.All(c => c.Status == 1 && !c.IsDeleted);
}
#endregion
#region Additional Uniqueness Tests
///
/// **Feature: admin-business-modules, Property 13: Invite Code Generation Uniqueness**
/// When generating codes with existing codes in database, new codes should not duplicate existing ones.
/// **Validates: Requirements 14.3, 14.4**
///
[Property(MaxTest = 50)]
public bool GenerateInviteCodes_ShouldNotDuplicateExistingCodesInDb(PositiveInt seed)
{
var existingCount = (seed.Get % 5) + 1; // 1 to 5 existing codes
var newCount = (seed.Get % 10) + 1; // 1 to 10 new codes
using var dbContext = CreateDbContext();
var service = new DistributionService(dbContext, _mockLogger.Object);
// First, generate some existing codes
var existingResult = service.GenerateInviteCodesAsync(existingCount).GetAwaiter().GetResult();
// Then generate new codes
var newResult = service.GenerateInviteCodesAsync(newCount).GetAwaiter().GetResult();
// Verify no overlap between batches
var overlap = existingResult.Codes.Intersect(newResult.Codes).Count();
return overlap == 0 &&
existingResult.Codes.Count == existingCount &&
newResult.Codes.Count == newCount;
}
#endregion
#region Helper Methods
private AdminBusinessDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
return new AdminBusinessDbContext(options);
}
private static string GenerateRandomCode(Random random)
{
const string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
var chars = new char[5];
for (int i = 0; i < 5; i++)
{
chars[i] = charset[random.Next(charset.Length)];
}
return new string(chars);
}
#endregion
}