using FsCheck; using FsCheck.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MiAssessment.Core.Services; using MiAssessment.Model.Data; using MiAssessment.Model.Entities; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// AssessmentService 属性测试 /// 验证测评服务的列表查询排序正确性和分页查询一致性 /// public class AssessmentServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 2: 列表查询排序正确性 - Question /// /// Property 2: 题目列表按QuestionNo升序排列 /// *For any* Question list query, the returned items SHALL be ordered by QuestionNo /// field in ascending order. /// /// **Feature: miniapp-api, Property 2: 列表查询排序正确性** /// **Validates: Requirements 3.2** /// [Property(MaxTest = 100)] public bool QuestionListSortedByQuestionNoAscending(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var random = new Random(seed.Get); var typeId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建具有不同QuestionNo值的题目(随机顺序插入) var questionNos = new List(); for (int i = 0; i < 10; i++) { var questionNo = random.Next(1, 1000); questionNos.Add(questionNo); dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId, questionNo, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetQuestionListAsync(typeId).GetAwaiter().GetResult(); // Assert: 验证列表按QuestionNo升序排列 if (result.Count < 2) return true; for (int i = 0; i < result.Count - 1; i++) { if (result[i].QuestionNo > result[i + 1].QuestionNo) { return false; } } return true; } /// /// Property 2: 题目列表只返回启用状态的记录 /// *For any* Question list query, the returned items SHALL only include records /// with Status=1 and IsDeleted=false. /// /// **Feature: miniapp-api, Property 2: 列表查询排序正确性** /// **Validates: Requirements 3.2** /// [Property(MaxTest = 100)] public bool QuestionListOnlyReturnsEnabledRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var typeId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建启用的题目 for (int i = 0; i < 3; i++) { dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId, i + 1, status: 1, isDeleted: false)); } // 创建禁用的题目 for (int i = 0; i < 2; i++) { dbContext.Questions.Add(CreateQuestion(seed.Get + 100 + i, typeId, 100 + i, status: 0, isDeleted: false)); } // 创建已删除的题目 for (int i = 0; i < 2; i++) { dbContext.Questions.Add(CreateQuestion(seed.Get + 200 + i, typeId, 200 + i, status: 1, isDeleted: true)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetQuestionListAsync(typeId).GetAwaiter().GetResult(); // Assert: 验证返回结果只包含Status=1且未删除的记录 // 1. 返回的记录数应该等于启用且未删除的记录数 if (result.Count != 3) return false; // 2. 验证数据库中确实存在被过滤掉的记录 var allQuestions = dbContext.Questions.Where(q => q.AssessmentTypeId == typeId).ToList(); var disabledQuestions = allQuestions.Where(q => q.Status == 0 && !q.IsDeleted).ToList(); var deletedQuestions = allQuestions.Where(q => q.IsDeleted).ToList(); if (disabledQuestions.Count != 2 || deletedQuestions.Count != 2) return false; // 3. 验证返回的记录中不包含禁用或已删除的记录 var returnedIds = result.Select(q => q.Id).ToHashSet(); var disabledIds = disabledQuestions.Select(q => q.Id).ToHashSet(); var deletedIds = deletedQuestions.Select(q => q.Id).ToHashSet(); return !returnedIds.Intersect(disabledIds).Any() && !returnedIds.Intersect(deletedIds).Any(); } /// /// Property 2: 题目列表只返回指定测评类型的记录 /// *For any* Question list query with typeId, the returned items SHALL only include /// records with matching AssessmentTypeId. /// /// **Feature: miniapp-api, Property 2: 列表查询排序正确性** /// **Validates: Requirements 3.2** /// [Property(MaxTest = 100)] public bool QuestionListOnlyReturnsMatchingTypeRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var typeId1 = (long)seed.Get; var typeId2 = (long)(seed.Get + 1000); // 创建两个测评类型 dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId1)); dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId2)); // 为类型1创建题目 for (int i = 0; i < 3; i++) { dbContext.Questions.Add(CreateQuestion(seed.Get + i, typeId1, i + 1, status: 1, isDeleted: false)); } // 为类型2创建题目 for (int i = 0; i < 2; i++) { dbContext.Questions.Add(CreateQuestion(seed.Get + 100 + i, typeId2, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetQuestionListAsync(typeId1).GetAwaiter().GetResult(); // Assert: 只返回类型1的题目 if (result.Count != 3) return false; // 验证所有返回的题目都属于类型1 var returnedIds = result.Select(q => q.Id).ToHashSet(); var type1Questions = dbContext.Questions.Where(q => q.AssessmentTypeId == typeId1 && q.Status == 1 && !q.IsDeleted).ToList(); var type1Ids = type1Questions.Select(q => q.Id).ToHashSet(); return returnedIds.SetEquals(type1Ids); } #endregion #region Property 3: 分页查询一致性 - AssessmentHistory /// /// Property 3: 分页查询返回的记录数不超过pageSize /// *For any* paginated query, the returned items count SHALL not exceed pageSize. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Property(MaxTest = 100)] public bool PaginationReturnsCorrectCount(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20 // 创建测评类型 var typeId = (long)seed.Get; var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建多条测评记录(超过pageSize) var recordCount = pageSize + 10; for (int i = 0; i < recordCount; i++) { var order = CreateOrder(seed.Get + i, userId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetHistoryListAsync(userId, 1, pageSize).GetAwaiter().GetResult(); // Assert: 返回的记录数不超过pageSize return result.List.Count <= pageSize; } /// /// Property 3: 分页查询的Total等于满足条件的总记录数 /// *For any* paginated query, Total SHALL equal the count of all matching records. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Property(MaxTest = 100)] public bool PaginationTotalEqualsMatchingRecordsCount(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); // 创建测评类型 var typeId = (long)seed.Get; dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId)); // 为当前用户创建测评记录 var userRecordCount = Math.Max(1, seed.Get % 15 + 1); // 1-15 for (int i = 0; i < userRecordCount; i++) { var order = CreateOrder(seed.Get + i, userId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId)); } // 为其他用户创建测评记录(不应计入Total) for (int i = 0; i < 5; i++) { var order = CreateOrder(seed.Get + 100 + i, otherUserId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + 100 + i, otherUserId, order.Id, typeId)); } // 创建已删除的记录(不应计入Total) for (int i = 0; i < 3; i++) { var order = CreateOrder(seed.Get + 200 + i, userId); dbContext.Orders.Add(order); var deletedRecord = CreateAssessmentRecord(seed.Get + 200 + i, userId, order.Id, typeId); deletedRecord.IsDeleted = true; dbContext.AssessmentRecords.Add(deletedRecord); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetHistoryListAsync(userId, 1, 20).GetAwaiter().GetResult(); // Assert: Total等于当前用户未删除的记录数 return result.Total == userRecordCount; } /// /// Property 3: 遍历所有页面能获取所有满足条件的记录 /// *For any* paginated query, traversing all pages SHALL return all matching records. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Property(MaxTest = 50)] public bool PaginationTraversalReturnsAllRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = Math.Max(1, seed.Get % 5 + 1); // 1-5 (小pageSize以测试多页) // 创建测评类型 var typeId = (long)seed.Get; dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId)); // 创建测评记录 var totalRecords = Math.Max(1, seed.Get % 12 + 1); // 1-12 var expectedIds = new HashSet(); for (int i = 0; i < totalRecords; i++) { var order = CreateOrder(seed.Get + i, userId); dbContext.Orders.Add(order); var record = CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId); dbContext.AssessmentRecords.Add(record); expectedIds.Add(record.Id); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 遍历所有页面 var allRetrievedIds = new HashSet(); var page = 1; var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环 while (page <= maxPages) { var result = service.GetHistoryListAsync(userId, page, pageSize).GetAwaiter().GetResult(); if (result.List.Count == 0) break; foreach (var item in result.List) { allRetrievedIds.Add(item.Id); } if (page >= result.TotalPages) break; page++; } // Assert: 遍历所有页面后获取的记录ID集合应等于预期的ID集合 return allRetrievedIds.SetEquals(expectedIds); } /// /// Property 3: 分页查询的TotalPages计算正确 /// *For any* paginated query, TotalPages SHALL equal ceil(Total / PageSize). /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Property(MaxTest = 100)] public bool PaginationTotalPagesCalculatedCorrectly(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20 // 创建测评类型 var typeId = (long)seed.Get; dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId)); // 创建测评记录 var totalRecords = Math.Max(1, seed.Get % 50 + 1); // 1-50 for (int i = 0; i < totalRecords; i++) { var order = CreateOrder(seed.Get + i, userId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetHistoryListAsync(userId, 1, pageSize).GetAwaiter().GetResult(); // Assert: TotalPages = ceil(Total / PageSize) var expectedTotalPages = (int)Math.Ceiling((double)result.Total / pageSize); return result.TotalPages == expectedTotalPages; } /// /// Property 3: 分页查询不同页面返回不重复的记录 /// *For any* paginated query, different pages SHALL return non-overlapping records. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Property(MaxTest = 50)] public bool PaginationPagesReturnNonOverlappingRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = 3; // 固定pageSize以确保多页 // 创建测评类型 var typeId = (long)seed.Get; dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId)); // 创建足够多的测评记录以产生多页 var totalRecords = 10; for (int i = 0; i < totalRecords; i++) { var order = CreateOrder(seed.Get + i, userId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(seed.Get + i, userId, order.Id, typeId)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取前两页 var page1 = service.GetHistoryListAsync(userId, 1, pageSize).GetAwaiter().GetResult(); var page2 = service.GetHistoryListAsync(userId, 2, pageSize).GetAwaiter().GetResult(); // Assert: 两页的记录ID不重叠 var page1Ids = page1.List.Select(r => r.Id).ToHashSet(); var page2Ids = page2.List.Select(r => r.Id).ToHashSet(); return !page1Ids.Intersect(page2Ids).Any(); } #endregion #region Property 6: 邀请码使用一次性 /// /// Property 6: 已分配的邀请码验证通过 /// *For any* assigned invite code (Status=2), the verification SHALL return IsValid=true /// and the correct InviteCodeId. /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1, 5.4** /// [Property(MaxTest = 100)] public bool AssignedInviteCodeVerificationReturnsValid(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态 dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult(); // Assert: 已分配的邀请码验证应该通过 return result.IsValid && result.InviteCodeId == inviteCode.Id && string.IsNullOrEmpty(result.ErrorMessage); } /// /// Property 6: 已使用的邀请码验证失败 /// *For any* used invite code (Status=3), the verification SHALL return IsValid=false /// and ErrorMessage="邀请码已被使用". /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UsedInviteCodeVerificationReturnsAlreadyUsedError(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var inviteCode = CreateInviteCode(seed.Get, status: 3); // 已使用状态 inviteCode.UseUserId = seed.Get + 1000; inviteCode.UseOrderId = seed.Get + 2000; inviteCode.UseTime = DateTime.Now.AddDays(-1); dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult(); // Assert: 已使用的邀请码验证应该失败,返回"邀请码已被使用" return !result.IsValid && result.ErrorMessage == "邀请码已被使用" && result.InviteCodeId == null; } /// /// Property 6: 未分配的邀请码验证失败 /// *For any* unassigned invite code (Status=1), the verification SHALL return IsValid=false /// and ErrorMessage="邀请码有误,请重新输入". /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1** /// [Property(MaxTest = 100)] public bool UnassignedInviteCodeVerificationReturnsInvalidError(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var inviteCode = CreateInviteCode(seed.Get, status: 1); // 未分配状态 dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult(); // Assert: 未分配的邀请码验证应该失败,返回"邀请码有误,请重新输入" return !result.IsValid && result.ErrorMessage == "邀请码有误,请重新输入" && result.InviteCodeId == null; } /// /// Property 6: 不存在的邀请码验证失败 /// *For any* non-existent invite code, the verification SHALL return IsValid=false /// and ErrorMessage="邀请码有误,请重新输入". /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1** /// [Property(MaxTest = 100)] public bool NonExistentInviteCodeVerificationReturnsInvalidError(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); // 不添加任何邀请码到数据库 var nonExistentCode = GenerateRandomInviteCode(seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.VerifyInviteCodeAsync(nonExistentCode).GetAwaiter().GetResult(); // Assert: 不存在的邀请码验证应该失败 return !result.IsValid && result.ErrorMessage == "邀请码有误,请重新输入" && result.InviteCodeId == null; } /// /// Property 6: 邀请码验证不区分大小写 /// *For any* valid invite code, verification with different case variations SHALL return /// the same result. /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1, 5.4** /// [Property(MaxTest = 100)] public bool InviteCodeVerificationIsCaseInsensitive(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态 dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 使用小写验证 var lowerCaseResult = service.VerifyInviteCodeAsync(inviteCode.Code.ToLower()).GetAwaiter().GetResult(); // Act: 使用大写验证 var upperCaseResult = service.VerifyInviteCodeAsync(inviteCode.Code.ToUpper()).GetAwaiter().GetResult(); // Assert: 大小写不同的验证结果应该相同 return lowerCaseResult.IsValid == upperCaseResult.IsValid && lowerCaseResult.InviteCodeId == upperCaseResult.InviteCodeId; } /// /// Property 6: 已删除的邀请码验证失败 /// *For any* deleted invite code (IsDeleted=true), the verification SHALL return IsValid=false /// regardless of its status. /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1** /// [Property(MaxTest = 100)] public bool DeletedInviteCodeVerificationReturnsInvalidError(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var inviteCode = CreateInviteCode(seed.Get, status: 2); // 已分配状态 inviteCode.IsDeleted = true; // 但已被软删除 dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.VerifyInviteCodeAsync(inviteCode.Code).GetAwaiter().GetResult(); // Assert: 已删除的邀请码验证应该失败 return !result.IsValid && result.ErrorMessage == "邀请码有误,请重新输入" && result.InviteCodeId == null; } /// /// Property 6: 邀请码状态互斥性 /// *For any* invite code, only Status=2 (已分配) SHALL pass verification; /// Status=1 (未分配) and Status=3 (已使用) SHALL fail. /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.1, 5.3, 5.4** /// [Property(MaxTest = 100)] public bool InviteCodeStatusDeterminesVerificationResult(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); // 创建三种状态的邀请码 var unassignedCode = CreateInviteCode(seed.Get, status: 1); var assignedCode = CreateInviteCode(seed.Get + 1000, status: 2); var usedCode = CreateInviteCode(seed.Get + 2000, status: 3); usedCode.UseUserId = seed.Get + 3000; usedCode.UseTime = DateTime.Now.AddDays(-1); dbContext.InviteCodes.AddRange(unassignedCode, assignedCode, usedCode); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var unassignedResult = service.VerifyInviteCodeAsync(unassignedCode.Code).GetAwaiter().GetResult(); var assignedResult = service.VerifyInviteCodeAsync(assignedCode.Code).GetAwaiter().GetResult(); var usedResult = service.VerifyInviteCodeAsync(usedCode.Code).GetAwaiter().GetResult(); // Assert: 只有已分配状态的邀请码验证通过 return !unassignedResult.IsValid && assignedResult.IsValid && !usedResult.IsValid && assignedResult.InviteCodeId == assignedCode.Id; } /// /// Property 6: 邀请码验证返回正确的邀请码ID /// *For any* valid invite code verification, the returned InviteCodeId SHALL match /// the actual invite code's Id in the database. /// /// **Feature: miniapp-api, Property 6: 邀请码使用一次性** /// **Validates: Requirements 5.4** /// [Property(MaxTest = 100)] public bool ValidInviteCodeVerificationReturnsCorrectId(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); // 创建多个已分配的邀请码 var codes = new List(); for (int i = 0; i < 5; i++) { var code = CreateInviteCode(seed.Get + i * 100, status: 2); codes.Add(code); dbContext.InviteCodes.Add(code); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act & Assert: 验证每个邀请码返回正确的ID foreach (var code in codes) { var result = service.VerifyInviteCodeAsync(code.Code).GetAwaiter().GetResult(); if (!result.IsValid || result.InviteCodeId != code.Id) { return false; } } return true; } #endregion #region Property 9: 测评答案提交状态变更 /// /// Property 9: 提交答案后测评记录状态变为"生成中" /// *For any* valid answer submission, the assessment record status SHALL change /// from "待测评"(1) or "测评中"(2) to "生成中"(3). /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1, 4.2** /// [Property(MaxTest = 100)] public bool SubmitAnswersChangesStatusToGenerating(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = Math.Max(1, seed.Get % 10 + 1); // 1-10 questions var initialStatus = (seed.Get % 2) + 1; // 1 or 2 (待测评 or 测评中) // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(初始状态为待测评或测评中) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = initialStatus; dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 // 1-10 }).ToList() }; // Act var result = service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证状态变为"生成中"(3) var updatedRecord = dbContext.AssessmentRecords.Find(recordId); return result.Success && updatedRecord != null && updatedRecord.Status == 3; } /// /// Property 9: 提交答案后答案数量等于题目数量 /// *For any* valid answer submission, the saved answer count SHALL equal the question count. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 100)] public bool SubmitAnswersSavesCorrectAnswerCount(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = Math.Max(1, seed.Get % 15 + 1); // 1-15 questions // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(初始状态为待测评) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 1; // 待测评 dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; // Act service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证保存的答案数量等于题目数量 var savedAnswerCount = dbContext.AssessmentAnswers.Count(a => a.RecordId == recordId); return savedAnswerCount == questionCount; } /// /// Property 9: 只有待测评或测评中状态才能提交答案 /// *For any* assessment record with status other than "待测评"(1) or "测评中"(2), /// answer submission SHALL fail. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 100)] public bool SubmitAnswersFailsForInvalidStatus(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = 5; // 使用无效状态:3(生成中) 或 4(已完成) var invalidStatus = (seed.Get % 2) + 3; // 3 or 4 // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(无效状态) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = invalidStatus; dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; // Act & Assert: 验证提交失败 try { service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (InvalidOperationException ex) { return ex.Message.Contains("当前测评状态不允许提交答案"); } } /// /// Property 9: 答案数量不匹配时提交失败 /// *For any* answer submission where answer count does not match question count, /// the submission SHALL fail. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 100)] public bool SubmitAnswersFailsWhenAnswerCountMismatch(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = Math.Max(3, seed.Get % 10 + 3); // 3-12 questions var answerCount = questionCount - 1; // 少一个答案 // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录 var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 1; // 待测评 dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求(答案数量少于题目数量) var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Take(answerCount).Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; // Act & Assert: 验证提交失败 try { service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (InvalidOperationException ex) { return ex.Message.Contains("答案数量") && ex.Message.Contains("题目数量") && ex.Message.Contains("不匹配"); } } /// /// Property 9: 非本人记录提交答案失败 /// *For any* answer submission to a record not belonging to the current user, /// the submission SHALL fail with unauthorized error. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 100)] public bool SubmitAnswersFailsForOtherUserRecord(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = 5; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单(属于其他用户) var order = CreateOrder(orderId, otherUserId); dbContext.Orders.Add(order); // 创建测评记录(属于其他用户) var record = CreateAssessmentRecord(recordId, otherUserId, orderId, typeId); record.Status = 1; // 待测评 dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; // Act & Assert: 验证当前用户提交其他用户的记录失败 try { service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (UnauthorizedAccessException ex) { return ex.Message.Contains("无权限"); } } /// /// Property 9: 提交答案后SubmitTime被设置 /// *For any* valid answer submission, the assessment record's SubmitTime SHALL be set. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 100)] public bool SubmitAnswersSetsSubmitTime(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = Math.Max(1, seed.Get % 5 + 1); // 1-5 questions // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(初始SubmitTime为null) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 1; // 待测评 record.SubmitTime = null; dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var beforeSubmit = DateTime.Now.AddSeconds(-1); var service = new AssessmentService(dbContext, _mockLogger.Object); // 创建答案请求 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; // Act service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); var afterSubmit = DateTime.Now.AddSeconds(1); // Assert: 验证SubmitTime被设置且在合理范围内 var updatedRecord = dbContext.AssessmentRecords.Find(recordId); return updatedRecord != null && updatedRecord.SubmitTime != null && updatedRecord.SubmitTime >= beforeSubmit && updatedRecord.SubmitTime <= afterSubmit; } /// /// Property 9: 重复提交答案会覆盖之前的答案 /// *For any* re-submission of answers, the previous answers SHALL be replaced. /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.1** /// [Property(MaxTest = 50)] public bool ResubmitAnswersReplacesExistingAnswers(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = 3; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录 var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 1; // 待测评 dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 第一次提交(答案值都是1) var request1 = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select(qId => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = 1 }).ToList() }; service.SubmitAnswersAsync(userId, request1).GetAwaiter().GetResult(); // 重置状态以允许重新提交 record.Status = 1; dbContext.SaveChanges(); // 第二次提交(答案值都是5) var request2 = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select(qId => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = 5 }).ToList() }; service.SubmitAnswersAsync(userId, request2).GetAwaiter().GetResult(); // Assert: 验证答案被覆盖 var savedAnswers = dbContext.AssessmentAnswers.Where(a => a.RecordId == recordId).ToList(); return savedAnswers.Count == questionCount && savedAnswers.All(a => a.AnswerValue == 5); } /// /// Property 9: 提交答案后可以通过GetResultStatus查询到状态变更 /// *For any* valid answer submission, GetResultStatus SHALL return Status=3 (生成中). /// /// **Feature: miniapp-api, Property 9: 测评答案提交状态变更** /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool GetResultStatusReturnsGeneratingAfterSubmit(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; var questionCount = Math.Max(1, seed.Get % 5 + 1); // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); assessmentType.QuestionCount = questionCount; dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录 var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 1; // 待测评 dbContext.AssessmentRecords.Add(record); // 创建题目 var questionIds = new List(); for (int i = 0; i < questionCount; i++) { var questionId = seed.Get + i + 1000; questionIds.Add(questionId); dbContext.Questions.Add(CreateQuestion(questionId, typeId, i + 1, status: 1, isDeleted: false)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // 提交答案 var request = new MiAssessment.Model.Models.Assessment.SubmitAnswersRequest { RecordId = recordId, Answers = questionIds.Select((qId, index) => new MiAssessment.Model.Models.Assessment.AnswerItem { QuestionId = qId, AnswerValue = (index % 10) + 1 }).ToList() }; service.SubmitAnswersAsync(userId, request).GetAwaiter().GetResult(); // Act: 查询状态 var status = service.GetResultStatusAsync(userId, recordId).GetAwaiter().GetResult(); // Assert: 验证状态为"生成中"(3)且未完成 return status != null && status.Status == 3 && !status.IsCompleted; } #endregion #region Property 4: 用户数据隔离 - GetResult /// /// Property 4: 用户只能获取自己的测评结果 /// *For any* GetResult request, the returned data SHALL only belong to the current /// logged-in user. Requests for other users' records SHALL return null. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3, 4.5** /// [Property(MaxTest = 100)] public bool GetResultOnlyReturnsOwnRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单(属于当前用户) var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(属于当前用户,已完成状态) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 4; // 已完成 record.CompleteTime = DateTime.Now.AddMinutes(-10); dbContext.AssessmentRecords.Add(record); // 创建报告分类 var category = CreateReportCategory(seed.Get, typeId, categoryType: 1); dbContext.ReportCategories.Add(category); // 创建测评结果 var result = CreateAssessmentResult(seed.Get, recordId, category.Id); dbContext.AssessmentResults.Add(result); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 当前用户获取自己的记录 var ownResult = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult(); // Act: 其他用户尝试获取该记录 var otherResult = service.GetResultAsync(otherUserId, recordId).GetAwaiter().GetResult(); // Assert: 当前用户可以获取,其他用户不能获取 return ownResult != null && ownResult.Id == recordId && otherResult == null; } /// /// Property 4: 用户无法获取其他用户的测评结果 /// *For any* GetResult request with a recordId belonging to another user, /// the result SHALL be null (no data returned). /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool GetResultReturnsNullForOtherUserRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单(属于其他用户) var order = CreateOrder(orderId, otherUserId); dbContext.Orders.Add(order); // 创建测评记录(属于其他用户,已完成状态) var record = CreateAssessmentRecord(recordId, otherUserId, orderId, typeId); record.Status = 4; // 已完成 record.CompleteTime = DateTime.Now.AddMinutes(-10); dbContext.AssessmentRecords.Add(record); // 创建报告分类 var category = CreateReportCategory(seed.Get, typeId, categoryType: 1); dbContext.ReportCategories.Add(category); // 创建测评结果 var result = CreateAssessmentResult(seed.Get, recordId, category.Id); dbContext.AssessmentResults.Add(result); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 当前用户尝试获取其他用户的记录 var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult(); // Assert: 应该返回null return resultDto == null; } /// /// Property 4: 多用户场景下数据隔离正确 /// *For any* multi-user scenario, each user SHALL only see their own assessment results, /// and the total count of accessible records SHALL match their own record count. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3, 4.5** /// [Property(MaxTest = 50)] public bool MultiUserDataIsolationIsCorrect(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var user1Id = (long)seed.Get; var user2Id = (long)(seed.Get + 10000); var user3Id = (long)(seed.Get + 20000); var typeId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建报告分类 var category = CreateReportCategory(seed.Get, typeId, categoryType: 1); dbContext.ReportCategories.Add(category); // 为每个用户创建不同数量的测评记录 var user1RecordCount = Math.Max(1, seed.Get % 3 + 1); // 1-3 var user2RecordCount = Math.Max(1, (seed.Get + 1) % 3 + 1); // 1-3 var user3RecordCount = Math.Max(1, (seed.Get + 2) % 3 + 1); // 1-3 var user1RecordIds = new List(); var user2RecordIds = new List(); var user3RecordIds = new List(); // 创建用户1的记录 for (int i = 0; i < user1RecordCount; i++) { var recordId = seed.Get + i; var orderId = seed.Get + i; var order = CreateOrder(orderId, user1Id); dbContext.Orders.Add(order); var record = CreateAssessmentRecord(recordId, user1Id, orderId, typeId); record.Status = 4; record.CompleteTime = DateTime.Now.AddMinutes(-i); dbContext.AssessmentRecords.Add(record); dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + i, recordId, category.Id)); user1RecordIds.Add(recordId); } // 创建用户2的记录 for (int i = 0; i < user2RecordCount; i++) { var recordId = seed.Get + 1000 + i; var orderId = seed.Get + 1000 + i; var order = CreateOrder(orderId, user2Id); dbContext.Orders.Add(order); var record = CreateAssessmentRecord(recordId, user2Id, orderId, typeId); record.Status = 4; record.CompleteTime = DateTime.Now.AddMinutes(-i); dbContext.AssessmentRecords.Add(record); dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + 1000 + i, recordId, category.Id)); user2RecordIds.Add(recordId); } // 创建用户3的记录 for (int i = 0; i < user3RecordCount; i++) { var recordId = seed.Get + 2000 + i; var orderId = seed.Get + 2000 + i; var order = CreateOrder(orderId, user3Id); dbContext.Orders.Add(order); var record = CreateAssessmentRecord(recordId, user3Id, orderId, typeId); record.Status = 4; record.CompleteTime = DateTime.Now.AddMinutes(-i); dbContext.AssessmentRecords.Add(record); dbContext.AssessmentResults.Add(CreateAssessmentResult(seed.Get + 2000 + i, recordId, category.Id)); user3RecordIds.Add(recordId); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act & Assert: 验证每个用户只能访问自己的记录 // 用户1可以访问自己的记录 foreach (var recordId in user1RecordIds) { var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult(); if (result == null || result.Id != recordId) return false; } // 用户1不能访问用户2的记录 foreach (var recordId in user2RecordIds) { var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult(); if (result != null) return false; } // 用户1不能访问用户3的记录 foreach (var recordId in user3RecordIds) { var result = service.GetResultAsync(user1Id, recordId).GetAwaiter().GetResult(); if (result != null) return false; } // 用户2可以访问自己的记录 foreach (var recordId in user2RecordIds) { var result = service.GetResultAsync(user2Id, recordId).GetAwaiter().GetResult(); if (result == null || result.Id != recordId) return false; } // 用户2不能访问用户1的记录 foreach (var recordId in user1RecordIds) { var result = service.GetResultAsync(user2Id, recordId).GetAwaiter().GetResult(); if (result != null) return false; } return true; } /// /// Property 4: 未完成的测评记录不返回结果 /// *For any* GetResult request for an incomplete record (Status != 4), /// the result SHALL be null even if the record belongs to the current user. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3** /// [Property(MaxTest = 100)] public bool GetResultReturnsNullForIncompleteRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 使用非完成状态:1(待测评), 2(测评中), 3(生成中) var incompleteStatus = (seed.Get % 3) + 1; // 1, 2, or 3 // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(未完成状态) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = incompleteStatus; dbContext.AssessmentRecords.Add(record); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取未完成的记录 var result = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } /// /// Property 4: 已删除的测评记录不返回结果 /// *For any* GetResult request for a deleted record (IsDeleted=true), /// the result SHALL be null even if the record belongs to the current user. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3** /// [Property(MaxTest = 100)] public bool GetResultReturnsNullForDeletedRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(已完成但已删除) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 4; // 已完成 record.IsDeleted = true; // 已删除 record.CompleteTime = DateTime.Now.AddMinutes(-10); dbContext.AssessmentRecords.Add(record); // 创建报告分类 var category = CreateReportCategory(seed.Get, typeId, categoryType: 1); dbContext.ReportCategories.Add(category); // 创建测评结果 var result = CreateAssessmentResult(seed.Get, recordId, category.Id); dbContext.AssessmentResults.Add(result); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取已删除的记录 var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult(); // Assert: 应该返回null return resultDto == null; } /// /// Property 4: 返回的测评结果数据完整性 /// *For any* valid GetResult request, the returned data SHALL contain complete /// report structure including intelligences, traits, abilities, etc. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3** /// [Property(MaxTest = 50)] public bool GetResultReturnsCompleteDataStructure(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单 var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(已完成状态) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 4; record.CompleteTime = DateTime.Now.AddMinutes(-10); dbContext.AssessmentRecords.Add(record); // 创建多种类型的报告分类和结果 var categoryTypes = new[] { 1, 2, 3, 4, 5, 6, 7, 8 }; // 八种分类类型 for (int i = 0; i < categoryTypes.Length; i++) { var category = CreateReportCategory(seed.Get + i * 100, typeId, categoryTypes[i]); dbContext.ReportCategories.Add(category); var result = CreateAssessmentResult(seed.Get + i * 100, recordId, category.Id); dbContext.AssessmentResults.Add(result); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var resultDto = service.GetResultAsync(userId, recordId).GetAwaiter().GetResult(); // Assert: 验证返回的数据结构完整 if (resultDto == null) return false; // 验证基本信息 if (resultDto.Id != recordId) return false; if (resultDto.TypeId != typeId) return false; if (string.IsNullOrEmpty(resultDto.AssessmentName)) return false; if (string.IsNullOrEmpty(resultDto.Name)) return false; if (string.IsNullOrEmpty(resultDto.TestDate)) return false; // 验证各分类结果列表已初始化 if (resultDto.Intelligences == null) return false; if (resultDto.Traits == null) return false; if (resultDto.Abilities == null) return false; if (resultDto.InnateLearningTypes == null) return false; if (resultDto.KeyLearningAbilities == null) return false; if (resultDto.BrainTypes == null) return false; if (resultDto.PersonalityTypes == null) return false; if (resultDto.FutureDevelopmentAbilities == null) return false; // 验证各分类结果数量(每种类型各1个) if (resultDto.Intelligences.Count != 1) return false; if (resultDto.Traits.Count != 1) return false; if (resultDto.Abilities.Count != 1) return false; if (resultDto.InnateLearningTypes.Count != 1) return false; if (resultDto.KeyLearningAbilities.Count != 1) return false; if (resultDto.BrainTypes.Count != 1) return false; if (resultDto.PersonalityTypes.Count != 1) return false; if (resultDto.FutureDevelopmentAbilities.Count != 1) return false; return true; } /// /// Property 4: 不存在的记录ID返回null /// *For any* GetResult request with a non-existent recordId, /// the result SHALL be null. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3** /// [Property(MaxTest = 100)] public bool GetResultReturnsNullForNonExistentRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var nonExistentRecordId = (long)(seed.Get + 999999); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 获取不存在的记录 var result = service.GetResultAsync(userId, nonExistentRecordId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } /// /// Property 4: 用户数据隔离在GetResultStatus中也生效 /// *For any* GetResultStatus request, the returned data SHALL only belong to /// the current logged-in user. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 4.3, 4.5** /// [Property(MaxTest = 100)] public bool GetResultStatusOnlyReturnsOwnRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var typeId = (long)seed.Get; var recordId = (long)seed.Get; var orderId = (long)seed.Get; // 创建测评类型 var assessmentType = CreateAssessmentType(typeId); dbContext.AssessmentTypes.Add(assessmentType); // 创建订单(属于当前用户) var order = CreateOrder(orderId, userId); dbContext.Orders.Add(order); // 创建测评记录(属于当前用户) var record = CreateAssessmentRecord(recordId, userId, orderId, typeId); record.Status = 3; // 生成中 dbContext.AssessmentRecords.Add(record); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 当前用户获取自己的记录状态 var ownStatus = service.GetResultStatusAsync(userId, recordId).GetAwaiter().GetResult(); // Act: 其他用户尝试获取该记录状态 var otherStatus = service.GetResultStatusAsync(otherUserId, recordId).GetAwaiter().GetResult(); // Assert: 当前用户可以获取,其他用户不能获取 return ownStatus != null && ownStatus.Status == 3 && otherStatus == null; } #endregion #region 边界条件测试 /// /// Property 2: 空数据库返回空题目列表 /// /// **Feature: miniapp-api, Property 2: 列表查询排序正确性** /// **Validates: Requirements 3.2** /// [Fact] public void EmptyDatabaseReturnsEmptyQuestionList() { // Arrange using var dbContext = CreateTestDbContext(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetQuestionListAsync(1).GetAwaiter().GetResult(); // Assert Assert.Empty(result); } /// /// Property 3: 空数据库返回空分页结果 /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Fact] public void EmptyDatabaseReturnsEmptyPagedResult() { // Arrange using var dbContext = CreateTestDbContext(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetHistoryListAsync(1, 1, 20).GetAwaiter().GetResult(); // Assert Assert.Empty(result.List); Assert.Equal(0, result.Total); Assert.Equal(0, result.TotalPages); } /// /// Property 3: 分页参数边界值处理 /// /// **Feature: miniapp-api, Property 3: 分页查询一致性** /// **Validates: Requirements 6.1** /// [Theory] [InlineData(0, 20)] // page < 1 [InlineData(-1, 20)] // page < 1 [InlineData(1, 0)] // pageSize < 1 [InlineData(1, -1)] // pageSize < 1 [InlineData(1, 200)] // pageSize > 100 public void PaginationHandlesBoundaryValues(int page, int pageSize) { // Arrange using var dbContext = CreateTestDbContext(); var userId = 1L; // 创建测评类型 var typeId = 1L; dbContext.AssessmentTypes.Add(CreateAssessmentType(typeId)); // 创建测评记录 for (int i = 0; i < 5; i++) { var order = CreateOrder(i + 1, userId); dbContext.Orders.Add(order); dbContext.AssessmentRecords.Add(CreateAssessmentRecord(i + 1, userId, order.Id, typeId)); } dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act var result = service.GetHistoryListAsync(userId, page, pageSize).GetAwaiter().GetResult(); // Assert: 服务应该处理边界值,不抛出异常 Assert.NotNull(result); Assert.True(result.Page >= 1); Assert.True(result.PageSize >= 1 && result.PageSize <= 100); } #endregion #region 辅助方法 /// /// 创建测试用内存数据库上下文 /// 使用自定义配置忽略外键关系验证 /// private TestAssessmentDbContext CreateTestDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new TestAssessmentDbContext(options); } /// /// 创建测试测评类型 /// private AssessmentType CreateAssessmentType(long id) { return new AssessmentType { Id = id, Name = $"Test Assessment Type {id}", Code = $"TEST_{id}", ImageUrl = $"https://example.com/type_{id}.jpg", IntroContent = $"

Introduction for type {id}

", Price = 99.00m, QuestionCount = 80, Sort = (int)(id % 100), Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试题目 /// private Question CreateQuestion(long id, long typeId, int questionNo, int status, bool isDeleted) { return new Question { Id = id, AssessmentTypeId = typeId, QuestionNo = questionNo, Content = $"Question {questionNo} content for type {typeId}", Sort = questionNo, Status = status, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = isDeleted }; } /// /// 创建测试订单 /// private Order CreateOrder(long id, long userId) { return new Order { Id = id, OrderNo = $"ORD{id:D10}", UserId = userId, OrderType = 1, ProductId = 1, ProductName = "Test Assessment", Amount = 99.00m, PayAmount = 99.00m, Status = 2, // 已支付 CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试测评记录 /// private AssessmentRecord CreateAssessmentRecord(long id, long userId, long orderId, long typeId) { return new AssessmentRecord { Id = id, UserId = userId, OrderId = orderId, AssessmentTypeId = typeId, Name = $"TestUser{userId}", Phone = "13800138000", Gender = 1, Age = 18, EducationStage = 3, Province = "北京市", City = "北京市", District = "朝阳区", Status = 4, // 已完成 CreateTime = DateTime.Now.AddMinutes(-id), // 不同的创建时间以测试排序 UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试邀请码 /// /// 种子值,用于生成唯一ID和Code /// 状态:1未分配 2已分配 3已使用 private InviteCode CreateInviteCode(long seed, int status) { return new InviteCode { Id = seed, Code = GenerateRandomInviteCode(seed), BatchNo = $"BATCH{seed / 100:D6}", AssignUserId = status >= 2 ? seed + 500 : null, AssignTime = status >= 2 ? DateTime.Now.AddDays(-7) : null, UseUserId = status == 3 ? seed + 600 : null, UseOrderId = status == 3 ? seed + 700 : null, UseTime = status == 3 ? DateTime.Now.AddDays(-1) : null, Status = status, CreateTime = DateTime.Now.AddDays(-10), UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试报告分类 /// /// 分类ID /// 测评类型ID /// 分类类型:1八大智能 2个人特质 3细分能力 4先天学习 5学习能力 6大脑类型 7性格类型 8未来能力 private ReportCategory CreateReportCategory(long id, long typeId, int categoryType) { var categoryNames = new Dictionary { { 1, "语言智能" }, { 2, "领导力" }, { 3, "逻辑推理" }, { 4, "视觉学习" }, { 5, "专注力" }, { 6, "左脑型" }, { 7, "外向型" }, { 8, "创新能力" } }; var categoryCodes = new Dictionary { { 1, "LINGUISTIC" }, { 2, "LEADERSHIP" }, { 3, "LOGICAL" }, { 4, "VISUAL" }, { 5, "FOCUS" }, { 6, "LEFT_BRAIN" }, { 7, "EXTROVERT" }, { 8, "INNOVATION" } }; return new ReportCategory { Id = id, AssessmentTypeId = typeId, ParentId = 0, Name = categoryNames.GetValueOrDefault(categoryType, $"Category_{categoryType}"), Code = categoryCodes.GetValueOrDefault(categoryType, $"CAT_{categoryType}"), CategoryType = categoryType, ScoreRule = 1, Sort = categoryType, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试测评结果 /// /// 结果ID /// 测评记录ID /// 分类ID private AssessmentResult CreateAssessmentResult(long id, long recordId, long categoryId) { var random = new Random((int)(id % int.MaxValue)); var score = (decimal)(random.Next(60, 100)); var maxScore = 100m; var percentage = score / maxScore * 100; var starLevel = score >= 90 ? 5 : score >= 80 ? 4 : score >= 70 ? 3 : score >= 60 ? 2 : 1; return new AssessmentResult { Id = id, RecordId = recordId, CategoryId = categoryId, Score = score, MaxScore = maxScore, Percentage = percentage, Rank = 1, StarLevel = starLevel, CreateTime = DateTime.Now }; } /// /// 生成随机邀请码(5位大写字母) /// /// 种子值 private static string GenerateRandomInviteCode(long seed) { const string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; var random = new Random((int)(seed % int.MaxValue)); var chars = new char[5]; for (int i = 0; i < 5; i++) { chars[i] = charset[random.Next(charset.Length)]; } return new string(chars); } #endregion } /// /// 测试用DbContext,继承自MiAssessmentDbContext但忽略外键关系验证 /// public class TestAssessmentDbContext : MiAssessmentDbContext { public TestAssessmentDbContext(DbContextOptions options) : base(CreateBaseOptions(options)) { } private static DbContextOptions CreateBaseOptions(DbContextOptions options) { var builder = new DbContextOptionsBuilder(); builder.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)); return builder.Options; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 不调用基类的OnModelCreating,避免外键关系验证 // 只配置必要的表映射 modelBuilder.Entity().ToTable("assessment_types"); modelBuilder.Entity().ToTable("questions"); modelBuilder.Entity().ToTable("assessment_records"); modelBuilder.Entity().ToTable("assessment_answers"); modelBuilder.Entity().ToTable("orders"); modelBuilder.Entity().ToTable("invite_codes"); modelBuilder.Entity().ToTable("report_categories"); modelBuilder.Entity().ToTable("assessment_results"); // 忽略导航属性 modelBuilder.Entity().Ignore(e => e.AssessmentType); modelBuilder.Entity().Ignore(e => e.AssessmentType); modelBuilder.Entity().Ignore(e => e.Record); modelBuilder.Entity().Ignore(e => e.Question); modelBuilder.Entity().Ignore(e => e.AssessmentType); modelBuilder.Entity().Ignore(e => e.Record); modelBuilder.Entity().Ignore(e => e.Category); } }