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 }