using FsCheck; using FsCheck.Xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using MiAssessment.Core.Interfaces; using MiAssessment.Core.Services; using MiAssessment.Model.Data; using MiAssessment.Model.Entities; using MiAssessment.Model.Models.Order; using Moq; using Xunit; namespace MiAssessment.Tests.Services; /// /// 小程序API OrderService 属性测试 /// 验证订单服务的分页查询一致性和用户数据隔离 /// public class ApiOrderServicePropertyTests { private readonly Mock> _mockLogger = new(); private readonly Mock _mockWechatPayService = new(); #region Property 3: 分页查询一致性- Order List /// /// Property 3: 分页查询返回的记录数不超过pageSize /// *For any* paginated query, the returned items count SHALL not exceed pageSize. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性* /// **Validates: Requirements 7.1** /// [Property(MaxTest = 100)] public bool OrderPaginationReturnsCorrectCount(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20 // 创建多条订单记录(超过pageSize�? var orderCount = pageSize + 10; for (int i = 0; i < orderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, pageSize, null).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 7.1** /// [Property(MaxTest = 100)] public bool OrderPaginationTotalEqualsMatchingRecordsCount(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); // 为当前用户创建订单记录 var userOrderCount = Math.Max(1, seed.Get % 15 + 1); // 1-15 for (int i = 0; i < userOrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } // 为其他用户创建订单记录(不应计入Total�? for (int i = 0; i < 5; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, otherUserId, orderType: 1, status: 2)); } // 创建已删除的记录(不应计入Total�? for (int i = 0; i < 3; i++) { var deletedOrder = CreateOrder(seed.Get + 200 + i, userId, orderType: 1, status: 2); deletedOrder.IsDeleted = true; dbContext.Orders.Add(deletedOrder); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, 20, null).GetAwaiter().GetResult(); // Assert: Total等于当前用户未删除的订单�? return result.Total == userOrderCount; } /// /// Property 3: 遍历所有页面能获取所有满足条件的记录 /// *For any* paginated query, traversing all pages SHALL return all matching records. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性* /// **Validates: Requirements 7.1** /// [Property(MaxTest = 50)] public bool OrderPaginationTraversalReturnsAllRecords(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 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, orderType: 1, status: 2); dbContext.Orders.Add(order); expectedIds.Add(order.Id); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 遍历所有页�? var allRetrievedIds = new HashSet(); var page = 1; var maxPages = (totalRecords / pageSize) + 2; // 防止无限循环 while (page <= maxPages) { var result = service.GetListAsync(userId, page, pageSize, null).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 7.1** /// [Property(MaxTest = 100)] public bool OrderPaginationTotalPagesCalculatedCorrectly(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = Math.Max(1, seed.Get % 20 + 1); // 1-20 // 创建订单记录 var totalRecords = Math.Max(1, seed.Get % 50 + 1); // 1-50 for (int i = 0; i < totalRecords; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, pageSize, null).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 7.1** /// [Property(MaxTest = 50)] public bool OrderPaginationPagesReturnNonOverlappingRecords(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var pageSize = 3; // 固定pageSize以确保多页 // 创建足够多的订单记录以产生多页 var totalRecords = 10; for (int i = 0; i < totalRecords; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 获取前两�? var page1 = service.GetListAsync(userId, 1, pageSize, null).GetAwaiter().GetResult(); var page2 = service.GetListAsync(userId, 2, pageSize, null).GetAwaiter().GetResult(); // Assert: 两页的记录ID不重叠 var page1Ids = page1.List.Select(o => o.Id).ToHashSet(); var page2Ids = page2.List.Select(o => o.Id).ToHashSet(); return !page1Ids.Intersect(page2Ids).Any(); } /// /// Property 3: 按订单类型筛选时分页查询一致性 /// *For any* paginated query with orderType filter, Total SHALL equal the count /// of all matching records with that orderType. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性* /// **Validates: Requirements 7.1** /// [Property(MaxTest = 100)] public bool OrderPaginationWithTypeFilterReturnsCorrectTotal(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var filterType = (seed.Get % 2) + 1; // 1 or 2 // 创建测评订单(类型�? var type1Count = Math.Max(1, seed.Get % 10 + 1); // 1-10 for (int i = 0; i < type1Count; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } // 创建规划订单(类型�? var type2Count = Math.Max(1, (seed.Get + 5) % 10 + 1); // 1-10 for (int i = 0; i < type2Count; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, userId, orderType: 2, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, 20, filterType).GetAwaiter().GetResult(); // Assert: Total等于指定类型的订单数 var expectedCount = filterType == 1 ? type1Count : type2Count; return result.Total == expectedCount; } /// /// Property 3: 按订单类型筛选时只返回匹配类型的记录 /// *For any* paginated query with orderType filter, all returned records SHALL /// have the specified orderType. /// /// **Feature: miniapp-api, Property 3: 分页查询一致性* /// **Validates: Requirements 7.1** /// [Property(MaxTest = 100)] public bool OrderPaginationWithTypeFilterReturnsOnlyMatchingType(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var filterType = (seed.Get % 2) + 1; // 1 or 2 // 创建测评订单(类型�? for (int i = 0; i < 5; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } // 创建规划订单(类型�? for (int i = 0; i < 5; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, userId, orderType: 2, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, 20, filterType).GetAwaiter().GetResult(); // Assert: 所有返回的记录都是指定类型 return result.List.All(o => o.OrderType == filterType); } #endregion #region Property 4: 用户数据隔离 - Order List /// /// Property 4: 订单列表只返回当前用户的订单 /// *For any* order list query, the returned data SHALL only belong to the current /// logged-in user and SHALL NOT contain data from other users. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.1** /// [Property(MaxTest = 100)] public bool OrderListOnlyReturnsCurrentUserOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); // 为当前用户创建订单 var userOrderCount = Math.Max(1, seed.Get % 10 + 1); // 1-10 for (int i = 0; i < userOrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } // 为其他用户创建订单 for (int i = 0; i < 5; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 100 + i, otherUserId, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 当前用户查询订单列表 var result = service.GetListAsync(userId, 1, 100, null).GetAwaiter().GetResult(); // Assert: 返回的订单数等于当前用户的订单数,且不包含其他用户的订单 if (result.Total != userOrderCount) return false; if (result.List.Count != userOrderCount) return false; // 验证数据库中确实存在其他用户的订单 var otherUserOrders = dbContext.Orders.Where(o => o.UserId == otherUserId && !o.IsDeleted).ToList(); if (otherUserOrders.Count != 5) return false; // 验证返回的订单中不包含其他用户的订单ID var returnedIds = result.List.Select(o => o.Id).ToHashSet(); var otherUserIds = otherUserOrders.Select(o => o.Id).ToHashSet(); return !returnedIds.Intersect(otherUserIds).Any(); } /// /// Property 4: 多用户场景下订单列表数据隔离正确 /// *For any* multi-user scenario, each user SHALL only see their own orders, /// and the total count of accessible orders SHALL match their own order count. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.1** /// [Property(MaxTest = 50)] public bool MultiUserOrderListDataIsolationIsCorrect(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 user1OrderCount = Math.Max(1, seed.Get % 5 + 1); // 1-5 var user2OrderCount = Math.Max(1, (seed.Get + 1) % 5 + 1); // 1-5 var user3OrderCount = Math.Max(1, (seed.Get + 2) % 5 + 1); // 1-5 // 创建用户1的订单 for (int i = 0; i < user1OrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, user1Id, orderType: 1, status: 2)); } // 创建用户2的订单 for (int i = 0; i < user2OrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 1000 + i, user2Id, orderType: 1, status: 2)); } // 创建用户3的订单 for (int i = 0; i < user3OrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + 2000 + i, user3Id, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act & Assert: 验证每个用户只能看到自己的订单 var user1Result = service.GetListAsync(user1Id, 1, 100, null).GetAwaiter().GetResult(); var user2Result = service.GetListAsync(user2Id, 1, 100, null).GetAwaiter().GetResult(); var user3Result = service.GetListAsync(user3Id, 1, 100, null).GetAwaiter().GetResult(); // 验证每个用户的订单数量正确 if (user1Result.Total != user1OrderCount) return false; if (user2Result.Total != user2OrderCount) return false; if (user3Result.Total != user3OrderCount) return false; // 验证订单ID不重叠 var user1Ids = user1Result.List.Select(o => o.Id).ToHashSet(); var user2Ids = user2Result.List.Select(o => o.Id).ToHashSet(); var user3Ids = user3Result.List.Select(o => o.Id).ToHashSet(); if (user1Ids.Intersect(user2Ids).Any()) return false; if (user1Ids.Intersect(user3Ids).Any()) return false; if (user2Ids.Intersect(user3Ids).Any()) return false; return true; } #endregion #region Property 4: 用户数据隔离 - Order Detail /// /// Property 4: 用户只能获取自己的订单详情 /// *For any* GetDetail request, the returned data SHALL only belong to the current /// logged-in user. Requests for other users' orders SHALL return null. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.2, 7.3** /// [Property(MaxTest = 100)] public bool OrderDetailOnlyReturnsOwnOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var orderId = (long)seed.Get; // 创建当前用户的订单 var order = CreateOrder(orderId, userId, orderType: 1, status: 2); dbContext.Orders.Add(order); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 当前用户获取自己的订单 var ownResult = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult(); // Act: 其他用户尝试获取该订单 var otherResult = service.GetDetailAsync(otherUserId, orderId).GetAwaiter().GetResult(); // Assert: 当前用户可以获取,其他用户不能获取 return ownResult != null && ownResult.Id == orderId && otherResult == null; } /// /// Property 4: 用户无法获取其他用户的订单详情 /// *For any* GetDetail request with an orderId belonging to another user, /// the result SHALL be null (no data returned). /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.3** /// [Property(MaxTest = 100)] public bool OrderDetailReturnsNullForOtherUserOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var orderId = (long)seed.Get; // 创建其他用户的订单 var order = CreateOrder(orderId, otherUserId, orderType: 1, status: 2); dbContext.Orders.Add(order); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 当前用户尝试获取其他用户的订单 var result = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } /// /// Property 4: 多用户场景下订单详情数据隔离正确 /// *For any* multi-user scenario, each user SHALL only access their own order details, /// and attempts to access other users' orders SHALL return null. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.2, 7.3** /// [Property(MaxTest = 50)] public bool MultiUserOrderDetailDataIsolationIsCorrect(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 user1OrderIds = new List(); var user2OrderIds = new List(); var user3OrderIds = new List(); // 创建用户1的订单 for (int i = 0; i < 3; i++) { var orderId = seed.Get + i; dbContext.Orders.Add(CreateOrder(orderId, user1Id, orderType: 1, status: 2)); user1OrderIds.Add(orderId); } // 创建用户2的订单 for (int i = 0; i < 3; i++) { var orderId = seed.Get + 1000 + i; dbContext.Orders.Add(CreateOrder(orderId, user2Id, orderType: 1, status: 2)); user2OrderIds.Add(orderId); } // 创建用户3的订单 for (int i = 0; i < 3; i++) { var orderId = seed.Get + 2000 + i; dbContext.Orders.Add(CreateOrder(orderId, user3Id, orderType: 1, status: 2)); user3OrderIds.Add(orderId); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act & Assert: 验证每个用户只能访问自己的订单详情 // 用户1可以访问自己的订单 foreach (var orderId in user1OrderIds) { var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult(); if (result == null || result.Id != orderId) return false; } // 用户1不能访问用户2的订单 foreach (var orderId in user2OrderIds) { var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult(); if (result != null) return false; } // 用户1不能访问用户3的订单 foreach (var orderId in user3OrderIds) { var result = service.GetDetailAsync(user1Id, orderId).GetAwaiter().GetResult(); if (result != null) return false; } // 用户2可以访问自己的订单 foreach (var orderId in user2OrderIds) { var result = service.GetDetailAsync(user2Id, orderId).GetAwaiter().GetResult(); if (result == null || result.Id != orderId) return false; } // 用户2不能访问用户1的订单 foreach (var orderId in user1OrderIds) { var result = service.GetDetailAsync(user2Id, orderId).GetAwaiter().GetResult(); if (result != null) return false; } return true; } #endregion #region Property 4: 用户数据隔离 - Pay Result /// /// Property 4: 用户只能查询自己订单的支付结�? /// *For any* GetPayResult request, the returned data SHALL only belong to the current /// logged-in user. Requests for other users' orders SHALL return null. /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.3** /// [Property(MaxTest = 100)] public bool PayResultOnlyReturnsOwnOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var orderId = (long)seed.Get; // 创建当前用户的订单 var order = CreateOrder(orderId, userId, orderType: 1, status: 2); dbContext.Orders.Add(order); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 当前用户查询自己订单的支付结�? var ownResult = service.GetPayResultAsync(userId, orderId).GetAwaiter().GetResult(); // Act: 其他用户尝试查询该订单的支付结果 var otherResult = service.GetPayResultAsync(otherUserId, orderId).GetAwaiter().GetResult(); // Assert: 当前用户可以查询,其他用户不能查�? return ownResult != null && otherResult == null; } /// /// Property 4: 用户无法查询其他用户订单的支付结�? /// *For any* GetPayResult request with an orderId belonging to another user, /// the result SHALL be null (no data returned). /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.3** /// [Property(MaxTest = 100)] public bool PayResultReturnsNullForOtherUserOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var otherUserId = (long)(seed.Get + 10000); var orderId = (long)seed.Get; // 创建其他用户的订单 var order = CreateOrder(orderId, otherUserId, orderType: 1, status: 2); dbContext.Orders.Add(order); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 当前用户尝试查询其他用户订单的支付结�? var result = service.GetPayResultAsync(userId, orderId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } #endregion #region 边界条件测试 /// /// Property 3: 空数据库返回空分页结�? /// /// **Feature: miniapp-api, Property 3: 分页查询一致性* /// **Validates: Requirements 7.1** /// [Fact] public void EmptyDatabaseReturnsEmptyOrderPagedResult() { // Arrange using var dbContext = CreateTestDbContext(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(1, 1, 20, null).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 7.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 OrderPaginationHandlesBoundaryValues(int page, int pageSize) { // Arrange using var dbContext = CreateTestDbContext(); var userId = 1L; // 创建订单记录 for (int i = 0; i < 5; i++) { dbContext.Orders.Add(CreateOrder(i + 1, userId, orderType: 1, status: 2)); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, page, pageSize, null).GetAwaiter().GetResult(); // Assert: 服务应该处理边界值,不抛出异�? Assert.NotNull(result); Assert.True(result.Page >= 1); Assert.True(result.PageSize >= 1 && result.PageSize <= 100); } /// /// Property 4: 不存在的订单ID返回null /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.2** /// [Property(MaxTest = 100)] public bool OrderDetailReturnsNullForNonExistentOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var nonExistentOrderId = (long)(seed.Get + 999999); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 获取不存在的订单 var result = service.GetDetailAsync(userId, nonExistentOrderId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } /// /// Property 4: 已删除的订单返回null /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.2** /// [Property(MaxTest = 100)] public bool OrderDetailReturnsNullForDeletedOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var orderId = (long)seed.Get; // 创建已删除的订单 var order = CreateOrder(orderId, userId, orderType: 1, status: 2); order.IsDeleted = true; dbContext.Orders.Add(order); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act: 获取已删除的订单 var result = service.GetDetailAsync(userId, orderId).GetAwaiter().GetResult(); // Assert: 应该返回null return result == null; } /// /// Property 4: 已删除的订单不出现在列表�? /// /// **Feature: miniapp-api, Property 4: 用户数据隔离** /// **Validates: Requirements 7.1** /// [Property(MaxTest = 100)] public bool OrderListExcludesDeletedOrders(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; // 创建正常订单 var normalOrderCount = Math.Max(1, seed.Get % 5 + 1); // 1-5 for (int i = 0; i < normalOrderCount; i++) { dbContext.Orders.Add(CreateOrder(seed.Get + i, userId, orderType: 1, status: 2)); } // 创建已删除的订单 var deletedOrderCount = Math.Max(1, (seed.Get + 3) % 5 + 1); // 1-5 var deletedOrderIds = new HashSet(); for (int i = 0; i < deletedOrderCount; i++) { var orderId = seed.Get + 100 + i; var order = CreateOrder(orderId, userId, orderType: 1, status: 2); order.IsDeleted = true; dbContext.Orders.Add(order); deletedOrderIds.Add(orderId); } dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // Act var result = service.GetListAsync(userId, 1, 100, null).GetAwaiter().GetResult(); // Assert: 返回的订单数等于正常订单数,且不包含已删除的订单 if (result.Total != normalOrderCount) return false; var returnedIds = result.List.Select(o => o.Id).ToHashSet(); return !returnedIds.Intersect(deletedOrderIds).Any(); } #endregion #region Property 5: 订单创建完整�? /// /// Property 5: 测评订单创建后同时存在订单记录和测评记录 /// *For any* assessment order creation request, after successful creation there SHALL exist /// both an order record and an assessment record, and the assessment record's OrderId /// SHALL point to the newly created order. /// /// **Feature: miniapp-api, Property 5: 订单创建完整�?* /// **Validates: Requirements 8.1** /// [Property(MaxTest = 100)] public bool AssessmentOrderCreationCreatesOrderAndAssessmentRecord(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 1, // 测评订单 ProductId = productId, AssessmentInfo = CreateAssessmentInfo(seed.Get) }; // Act var result = service.CreateAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证订单记录存在 var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId); if (order == null) return false; // Assert: 验证测评记录存在 var assessmentRecord = dbContext.AssessmentRecords.FirstOrDefault(r => r.OrderId == result.OrderId); if (assessmentRecord == null) return false; // Assert: 验证测评记录的OrderId指向新创建的订单 if (assessmentRecord.OrderId != order.Id) return false; // Assert: 验证返回的AssessmentRecordId正确 if (result.AssessmentRecordId != assessmentRecord.Id) return false; // Assert: 验证订单和测评记录的用户ID一�? if (order.UserId != userId || assessmentRecord.UserId != userId) return false; return true; } /// /// Property 5: 规划订单创建后同时存在订单记录和规划预约记录 /// *For any* planner order creation request, after successful creation there SHALL exist /// both an order record and a planner booking record, and the booking record's OrderId /// SHALL point to the newly created order. /// /// **Feature: miniapp-api, Property 5: 订单创建完整�?* /// **Validates: Requirements 8.2** /// [Property(MaxTest = 100)] public bool PlannerOrderCreationCreatesOrderAndBookingRecord(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); // 创建规划�? var planner = CreatePlanner(productId); dbContext.Planners.Add(planner); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 2, // 规划订单 ProductId = productId, PlannerInfo = CreatePlannerInfo(seed.Get) }; // Act var result = service.CreateAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证订单记录存在 var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId); if (order == null) return false; // Assert: 验证规划预约记录存在 var plannerBooking = dbContext.PlannerBookings.FirstOrDefault(b => b.OrderId == result.OrderId); if (plannerBooking == null) return false; // Assert: 验证规划预约记录的OrderId指向新创建的订单 if (plannerBooking.OrderId != order.Id) return false; // Assert: 验证订单和规划预约记录的用户ID一�? if (order.UserId != userId || plannerBooking.UserId != userId) return false; return true; } /// /// Property 5: 使用邀请码创建订单时金额设�?并标记邀请码已使�? /// *For any* order creation with invite code, the order amount SHALL be set to 0 /// and the invite code SHALL be marked as used. /// /// **Feature: miniapp-api, Property 5: 订单创建完整�?* /// **Validates: Requirements 8.3** /// [Property(MaxTest = 100)] public bool OrderCreationWithInviteCodeSetsAmountToZeroAndMarksCodeUsed(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); var inviteCodeId = (long)(seed.Get + 1000); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); // 创建已分配的邀请码(状�?=已分配) var inviteCode = CreateInviteCode(inviteCodeId, status: 2); dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 1, ProductId = productId, AssessmentInfo = CreateAssessmentInfo(seed.Get), InviteCodeId = inviteCodeId }; // Act var result = service.CreateAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证支付金额�? if (result.PayAmount != 0) return false; // Assert: 验证不需要支�? if (result.NeedPay) return false; // Assert: 验证订单记录的PayAmount�? var order = dbContext.Orders.FirstOrDefault(o => o.Id == result.OrderId); if (order == null || order.PayAmount != 0) return false; // Assert: 验证邀请码状态变为已使用�?�? var updatedInviteCode = dbContext.InviteCodes.FirstOrDefault(ic => ic.Id == inviteCodeId); if (updatedInviteCode == null || updatedInviteCode.Status != 3) return false; // Assert: 验证邀请码记录了使用者和订单信息 if (updatedInviteCode.UseUserId != userId) return false; if (updatedInviteCode.UseOrderId != result.OrderId) return false; return true; } /// /// Property 5: 订单创建后关联记录的数据完整�? /// *For any* order creation, the associated record (assessment or booking) SHALL contain /// all the information provided in the request. /// /// **Feature: miniapp-api, Property 5: 订单创建完整�?* /// **Validates: Requirements 8.1, 8.2** /// [Property(MaxTest = 100)] public bool OrderCreationPreservesAllRequestData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var assessmentInfo = CreateAssessmentInfo(seed.Get); var request = new CreateOrderRequest { OrderType = 1, ProductId = productId, AssessmentInfo = assessmentInfo }; // Act var result = service.CreateAsync(userId, request).GetAwaiter().GetResult(); // Assert: 验证测评记录包含所有请求数�? var assessmentRecord = dbContext.AssessmentRecords.FirstOrDefault(r => r.OrderId == result.OrderId); if (assessmentRecord == null) return false; // 验证所有字�? if (assessmentRecord.Name != assessmentInfo.Name) return false; if (assessmentRecord.Phone != assessmentInfo.Phone) return false; if (assessmentRecord.Gender != assessmentInfo.Gender) return false; if (assessmentRecord.Age != assessmentInfo.Age) return false; if (assessmentRecord.EducationStage != assessmentInfo.EducationStage) return false; if (assessmentRecord.Province != assessmentInfo.Province) return false; if (assessmentRecord.City != assessmentInfo.City) return false; if (assessmentRecord.District != assessmentInfo.District) return false; return true; } #endregion #region Property 10: 订单事务回滚 /// /// Property 10: 无效的测评类型ID导致订单创建失败时无残留数据 /// *For any* order creation with invalid product ID, there SHALL be no partially created /// order or associated records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool InvalidProductIdCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var invalidProductId = (long)(seed.Get + 999999); // 不存在的产品ID // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 1, ProductId = invalidProductId, AssessmentInfo = CreateAssessmentInfo(seed.Get) }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何测评记录 var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count(); if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false; return true; } /// /// Property 10: 无效的规划师ID导致订单创建失败时无残留数据 /// *For any* planner order creation with invalid planner ID, there SHALL be no partially /// created order or booking records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool InvalidPlannerIdCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var invalidPlannerId = (long)(seed.Get + 999999); // 不存在的规划师ID // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var bookingCountBefore = dbContext.PlannerBookings.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 2, ProductId = invalidPlannerId, PlannerInfo = CreatePlannerInfo(seed.Get) }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何规划预约记录 var bookingCountAfter = dbContext.PlannerBookings.Count(); if (bookingCountAfter != bookingCountBefore) return false; return true; } /// /// Property 10: 已使用的邀请码导致订单创建失败时无残留数据 /// *For any* order creation with already used invite code, there SHALL be no partially /// created order or associated records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool UsedInviteCodeCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); var inviteCodeId = (long)(seed.Get + 1000); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); // 创建已使用的邀请码(状�?=已使用) var inviteCode = CreateInviteCode(inviteCodeId, status: 3); dbContext.InviteCodes.Add(inviteCode); dbContext.SaveChanges(); // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 1, ProductId = productId, AssessmentInfo = CreateAssessmentInfo(seed.Get), InviteCodeId = inviteCodeId }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何测评记录 var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count(); if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false; return true; } /// /// Property 10: 不存在的邀请码ID导致订单创建失败时无残留数据 /// *For any* order creation with non-existent invite code ID, there SHALL be no partially /// created order or associated records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool NonExistentInviteCodeCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); var nonExistentInviteCodeId = (long)(seed.Get + 999999); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = 1, ProductId = productId, AssessmentInfo = CreateAssessmentInfo(seed.Get), InviteCodeId = nonExistentInviteCodeId }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何测评记录 var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count(); if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false; return true; } /// /// Property 10: 缺少必要信息导致订单创建失败时无残留数据 /// *For any* order creation with missing required info (e.g., assessment info for assessment order), /// there SHALL be no partially created order or associated records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool MissingRequiredInfoCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); // 创建测评类型 var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); // 测评订单但不提供AssessmentInfo var request = new CreateOrderRequest { OrderType = 1, ProductId = productId, AssessmentInfo = null // 缺少必要信息 }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何测评记录 var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count(); if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false; return true; } /// /// Property 10: 无效的订单类型导致订单创建失败时无残留数�? /// *For any* order creation with invalid order type, there SHALL be no partially /// created order or associated records in the database. /// /// **Feature: miniapp-api, Property 10: 订单事务回滚** /// **Validates: Requirements 8.4** /// [Property(MaxTest = 100)] public bool InvalidOrderTypeCausesNoPartialData(PositiveInt seed) { // Arrange using var dbContext = CreateTestDbContext(); var userId = (long)seed.Get; var productId = (long)(seed.Get % 100 + 1); var invalidOrderType = (seed.Get % 10) + 3; // 3-12,都是无效的订单类型 // 创建测评类型(以防万一�? var assessmentType = CreateAssessmentType(productId); dbContext.AssessmentTypes.Add(assessmentType); dbContext.SaveChanges(); // 记录创建前的记录�? var orderCountBefore = dbContext.Orders.Count(); var assessmentRecordCountBefore = dbContext.AssessmentRecords.Count(); var bookingCountBefore = dbContext.PlannerBookings.Count(); var service = new OrderService(dbContext, _mockLogger.Object, _mockWechatPayService.Object); var request = new CreateOrderRequest { OrderType = invalidOrderType, ProductId = productId, AssessmentInfo = CreateAssessmentInfo(seed.Get) }; // Act & Assert try { service.CreateAsync(userId, request).GetAwaiter().GetResult(); // 如果没有抛出异常,测试失�? return false; } catch (ArgumentException) { // 预期的异�? } // Assert: 验证没有创建任何订单记录 var orderCountAfter = dbContext.Orders.Count(); if (orderCountAfter != orderCountBefore) return false; // Assert: 验证没有创建任何测评记录 var assessmentRecordCountAfter = dbContext.AssessmentRecords.Count(); if (assessmentRecordCountAfter != assessmentRecordCountBefore) return false; // Assert: 验证没有创建任何规划预约记录 var bookingCountAfter = dbContext.PlannerBookings.Count(); if (bookingCountAfter != bookingCountBefore) return false; return true; } #endregion #region 辅助方法 /// /// 创建测试用内存数据库上下�? /// private TestOrderDbContext CreateTestDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; return new TestOrderDbContext(options); } /// /// 创建测试订单 /// private Order CreateOrder(long id, long userId, int orderType, int status) { return new Order { Id = id, OrderNo = $"ORD{id:D10}", UserId = userId, OrderType = orderType, ProductId = 1, ProductName = orderType == 1 ? "多元智能测评" : "学业规划服务", Amount = 99.00m, PayAmount = 99.00m, Status = status, PayTime = status >= 2 ? DateTime.Now.AddMinutes(-10) : null, CreateTime = DateTime.Now.AddMinutes(-id), // 不同的创建时间以测试排序 UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试用测评类型 /// private AssessmentType CreateAssessmentType(long id) { return new AssessmentType { Id = id, Name = $"测评类型{id}", Code = $"TYPE{id}", Price = 99.00m, Status = 1, // 已上�? QuestionCount = 80, Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试用规划师 /// private Planner CreatePlanner(long id) { return new Planner { Id = id, Name = $"规划师{id}", Avatar = $"https://example.com/avatar{id}.jpg", Introduction = $"规划师{id}的简介", Price = 199.00m, Status = 1, // 启用 Sort = 1, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试用邀请码 /// private InviteCode CreateInviteCode(long id, int status) { return new InviteCode { Id = id, Code = $"CODE{id % 100000:D5}", Status = status, CreateTime = DateTime.Now, UpdateTime = DateTime.Now, IsDeleted = false }; } /// /// 创建测试用测评信息 /// private AssessmentInfoDto CreateAssessmentInfo(int seed) { return new AssessmentInfoDto { Name = $"测试用户{seed}", Phone = $"138{seed % 100000000:D8}", Gender = (seed % 2) + 1, // 1或2 Age = (seed % 50) + 6, // 6-55岁 EducationStage = (seed % 6) + 1, // 1-6 Province = "北京市", City = "北京市", District = "海淀区" }; } /// /// 创建测试用规划预约信息 /// private PlannerInfoDto CreatePlannerInfo(int seed) { return new PlannerInfoDto { Name = $"预约用户{seed}", Phone = $"139{seed % 100000000:D8}", Remark = $"备注{seed}" }; } #endregion } /// /// 测试用DbContext,继承自MiAssessmentDbContext但忽略外键关系验证 /// public class TestOrderDbContext : MiAssessmentDbContext { public TestOrderDbContext(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("orders"); modelBuilder.Entity().ToTable("assessment_records"); modelBuilder.Entity().ToTable("assessment_types"); modelBuilder.Entity().ToTable("planners"); modelBuilder.Entity().ToTable("planner_bookings"); modelBuilder.Entity().ToTable("invite_codes"); // 忽略导航属�? modelBuilder.Entity().Ignore(e => e.AssessmentType); } }