using FsCheck; using FsCheck.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MiAssessment.Admin.Business.Data; using MiAssessment.Admin.Business.Entities; using MiAssessment.Admin.Business.Models; using MiAssessment.Admin.Business.Models.Assessment; using MiAssessment.Admin.Business.Models.Common; using MiAssessment.Admin.Business.Services; using Moq; using Xunit; namespace MiAssessment.Tests.Admin; /// /// Question 属性测试 /// 验证题目服务的正确性属性 /// public class QuestionPropertyTests { private readonly Mock> _mockLogger = new(); #region Property 5: Required Field Validation /// /// Property 5: 创建题目时缺少测评类型ID应该失败 /// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity. /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_MissingAssessmentTypeId_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = 0, // 无效的测评类型ID QuestionNo = seed.Get, Content = $"Test content {seed.Get}", Sort = 1, Status = 1 }; // Act & Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // 验证没有创建任何题目 var questionCount = dbContext.Questions.Count(); return ex.Code == ErrorCodes.ParamError && questionCount == 0; } } /// /// Property 5: 创建题目时缺少题号应该失败 /// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity. /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_MissingQuestionNo_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 先创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = 0, // 无效的题号 Content = $"Test content {seed.Get}", Sort = 1, Status = 1 }; // Act & Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // 验证没有创建任何题目 var questionCount = dbContext.Questions.Count(); return ex.Code == ErrorCodes.ParamError && questionCount == 0; } } /// /// Property 5: 创建题目时缺少内容应该失败 /// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity. /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_MissingContent_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 先创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = "", // 空内容 Sort = 1, Status = 1 }; // Act & Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // 验证没有创建任何题目 var questionCount = dbContext.Questions.Count(); return ex.Code == ErrorCodes.ParamError && questionCount == 0; } } /// /// Property 5: 创建题目时内容为空白字符串应该失败 /// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity. /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_WhitespaceContent_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 先创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = " ", // 空白字符串 Sort = 1, Status = 1 }; // Act & Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // 验证没有创建任何题目 var questionCount = dbContext.Questions.Count(); return ex.Code == ErrorCodes.ParamError && questionCount == 0; } } /// /// Property 5: 创建题目时测评类型不存在应该失败 /// *For any* create operation with missing required fields, the service SHALL return a validation error and SHALL NOT create the entity. /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_NonExistentAssessmentType_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = seed.Get + 1000, // 不存在的测评类型ID QuestionNo = seed.Get, Content = $"Test content {seed.Get}", Sort = 1, Status = 1 }; // Act & Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { // 验证没有创建任何题目 var questionCount = dbContext.Questions.Count(); return ex.Code == ErrorCodes.AssessmentTypeNotFound && questionCount == 0; } } /// /// Property 5: 提供所有必填字段时创建题目应该成功 /// /// **Validates: Requirements 5.2** /// [Property(MaxTest = 100)] public bool RequiredFieldValidation_AllRequiredFieldsProvided_ShouldSucceed(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 先创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Test content {seed.Get}", Sort = 1, Status = 1 }; // Act try { var questionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult(); // Assert: 验证题目已创建 var createdQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == questionId); return createdQuestion != null && createdQuestion.AssessmentTypeId == assessmentType.Id && createdQuestion.QuestionNo == seed.Get && createdQuestion.Content == $"Test content {seed.Get}"; } catch (BusinessException) { return false; // 不应该抛出异常 } } #endregion #region Property 8: Unique Constraint Enforcement (QuestionNo) /// /// Property 8: 同一测评类型内创建重复题号应该失败 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UniqueQuestionNoEnforcement_DuplicateQuestionNoInSameType_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 先创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); // 创建一个已存在的题目 var existingQuestion = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Existing question {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Questions.Add(existingQuestion); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试创建具有相同题号的题目 var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, // 使用相同的题号 Content = $"New question {seed.Get}", Sort = 2, Status = 1 }; // Assert try { service.CreateQuestionAsync(request).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { return ex.Code == ErrorCodes.QuestionNoExists; } } /// /// Property 8: 不同测评类型内可以使用相同题号 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UniqueQuestionNoEnforcement_SameQuestionNoInDifferentTypes_ShouldSucceed(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建两个测评类型 var assessmentType1 = CreateAssessmentType(dbContext, seed.Get, "TYPE_A"); var assessmentType2 = CreateAssessmentType(dbContext, seed.Get + 1000, "TYPE_B"); // 在第一个测评类型中创建题目 var existingQuestion = new Question { AssessmentTypeId = assessmentType1.Id, QuestionNo = seed.Get, Content = $"Question in type 1 {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Questions.Add(existingQuestion); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 在第二个测评类型中创建具有相同题号的题目 var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType2.Id, QuestionNo = seed.Get, // 使用相同的题号,但在不同的测评类型中 Content = $"Question in type 2 {seed.Get}", Sort = 1, Status = 1 }; // Assert try { var newQuestionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult(); var newQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == newQuestionId); return newQuestion != null && newQuestion.AssessmentTypeId == assessmentType2.Id && newQuestion.QuestionNo == seed.Get; } catch (BusinessException) { return false; // 不应该抛出异常 } } /// /// Property 8: 更新题目时使用已存在的题号应该失败 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UniqueQuestionNoEnforcement_UpdateWithDuplicateQuestionNo_ShouldFail(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); // 创建两个题目 var question1 = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Question 1 {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; var question2 = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get + 1, Content = $"Question 2 {seed.Get}", Sort = 2, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Questions.AddRange(question1, question2); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试将 question2 的题号更新为 question1 的题号 var updateRequest = new UpdateQuestionRequest { Id = question2.Id, AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, // 使用 question1 的题号 Content = $"Updated question 2 {seed.Get}", Sort = 2, Status = 1 }; // Assert try { service.UpdateQuestionAsync(updateRequest).GetAwaiter().GetResult(); return false; // 应该抛出异常 } catch (BusinessException ex) { return ex.Code == ErrorCodes.QuestionNoExists; } } /// /// Property 8: 更新题目时保持自身题号不变应该成功 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UniqueQuestionNoEnforcement_UpdateWithSameQuestionNo_ShouldSucceed(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); // 创建一个题目 var question = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Original question {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Questions.Add(question); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 更新题目,保持题号不变 var updateRequest = new UpdateQuestionRequest { Id = question.Id, AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, // 保持相同的题号 Content = $"Updated question {seed.Get}", Sort = 2, Status = 1 }; var result = service.UpdateQuestionAsync(updateRequest).GetAwaiter().GetResult(); // Assert var updatedQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == question.Id); return result && updatedQuestion != null && updatedQuestion.QuestionNo == seed.Get && updatedQuestion.Content == $"Updated question {seed.Get}"; } /// /// Property 8: 软删除的题目题号可以被重用 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 100)] public bool UniqueQuestionNoEnforcement_DeletedQuestionNoCanBeReused(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); // 创建并软删除一个题目 var deletedQuestion = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Deleted question {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = true // 已软删除 }; dbContext.Questions.Add(deletedQuestion); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 尝试创建具有相同题号的新题目 var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, // 使用已删除题目的题号 Content = $"New question {seed.Get}", Sort = 1, Status = 1 }; // Assert try { var newQuestionId = service.CreateQuestionAsync(request).GetAwaiter().GetResult(); var newQuestion = dbContext.Questions.FirstOrDefault(q => q.Id == newQuestionId); return newQuestion != null && newQuestion.QuestionNo == seed.Get && !newQuestion.IsDeleted; } catch (BusinessException) { return false; // 不应该抛出异常 } } /// /// Property 8: 批量导入时重复题号应该被检测并报错 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 50)] public bool UniqueQuestionNoEnforcement_BatchImportWithDuplicateQuestionNo_ShouldReportError(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); // 创建一个已存在的题目 var existingQuestion = new Question { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get, Content = $"Existing question {seed.Get}", Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.Questions.Add(existingQuestion); dbContext.SaveChanges(); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 批量导入包含重复题号的题目 var request = new BatchImportQuestionsRequest { AssessmentTypeId = assessmentType.Id, Questions = new List { new BatchImportQuestionItem { QuestionNo = seed.Get, // 与已存在的题目重复 Content = $"Import question 1 {seed.Get}", Sort = 1, Status = 1 }, new BatchImportQuestionItem { QuestionNo = seed.Get + 1, // 新题号 Content = $"Import question 2 {seed.Get}", Sort = 2, Status = 1 } } }; var result = service.BatchImportQuestionsAsync(request).GetAwaiter().GetResult(); // Assert: 应该有一个失败(重复题号),一个成功 return result.FailedCount == 1 && result.SuccessCount == 1 && result.Errors.Any(e => e.QuestionNo == seed.Get && e.ErrorMessage.Contains("已存在")); } /// /// Property 8: 批量导入时导入数据内部重复题号应该被检测并报错 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 50)] public bool UniqueQuestionNoEnforcement_BatchImportWithInternalDuplicates_ShouldReportError(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); // Act: 批量导入包含内部重复题号的题目 var request = new BatchImportQuestionsRequest { AssessmentTypeId = assessmentType.Id, Questions = new List { new BatchImportQuestionItem { QuestionNo = seed.Get, Content = $"Import question 1 {seed.Get}", Sort = 1, Status = 1 }, new BatchImportQuestionItem { QuestionNo = seed.Get, // 与第一个重复 Content = $"Import question 2 {seed.Get}", Sort = 2, Status = 1 }, new BatchImportQuestionItem { QuestionNo = seed.Get + 1, // 新题号 Content = $"Import question 3 {seed.Get}", Sort = 3, Status = 1 } } }; var result = service.BatchImportQuestionsAsync(request).GetAwaiter().GetResult(); // Assert: 第一个成功,第二个失败(内部重复),第三个成功 return result.FailedCount == 1 && result.SuccessCount == 2 && result.Errors.Any(e => e.ErrorMessage.Contains("重复")); } /// /// Property 8: 多个不同题号的题目应该都能创建成功 /// *For any* Question within the same AssessmentTypeId, the QuestionNo SHALL be unique. /// /// **Validates: Requirements 5.3** /// [Property(MaxTest = 50)] public bool UniqueQuestionNoEnforcement_MultipleUniqueQuestionNosAllSucceed(PositiveInt seed) { // Arrange using var dbContext = CreateDbContext(); // 创建一个测评类型 var assessmentType = CreateAssessmentType(dbContext, seed.Get); var service = new AssessmentService(dbContext, _mockLogger.Object); var createdIds = new List(); // Act: 创建多个具有不同题号的题目 for (int i = 0; i < 3; i++) { var request = new CreateQuestionRequest { AssessmentTypeId = assessmentType.Id, QuestionNo = seed.Get + i, Content = $"Question {seed.Get}_{i}", Sort = i, Status = 1 }; try { var id = service.CreateQuestionAsync(request).GetAwaiter().GetResult(); createdIds.Add(id); } catch (BusinessException) { return false; // 不应该抛出异常 } } // Assert: 所有题目都应该创建成功 var allCreated = createdIds.Count == 3; var allUnique = dbContext.Questions .Where(q => q.AssessmentTypeId == assessmentType.Id && !q.IsDeleted) .Select(q => q.QuestionNo) .Distinct() .Count() == 3; return allCreated && allUnique; } #endregion #region 辅助方法 /// /// 创建内存数据库上下文 /// private AdminBusinessDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new AdminBusinessDbContext(options); } /// /// 创建测评类型 /// private AssessmentType CreateAssessmentType(AdminBusinessDbContext dbContext, int seed, string? codePrefix = null) { var code = codePrefix ?? $"CODE_{seed}"; var assessmentType = new AssessmentType { Name = $"Test Type {seed}", Code = code, Price = 99.99m, QuestionCount = 80, Sort = 1, Status = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); return assessmentType; } #endregion }