using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Business.Models; using HoneyBox.Admin.Business.Models.Order; using HoneyBox.Admin.Business.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// OrderService 属性测试 /// public class OrderServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 10: Order List Filter Accuracy /// /// **Feature: admin-business-migration, Property 10: Order List Filter Accuracy** /// For any order list request with filter parameters, all returned orders /// should match all specified filter criteria. /// Validates: Requirements 6.2 /// [Property(MaxTest = 100)] public bool OrderListFilter_ByUserId_ShouldReturnOnlyMatchingOrders(PositiveInt userId, PositiveInt orderCount) { var actualUserId = (userId.Get % 10) + 1; // 1-10 var actualOrderCount = (orderCount.Get % 20) + 5; // 5-24 orders var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 10); // Seed orders with various user IDs var random = new Random(userId.Get); for (int i = 0; i < actualOrderCount; i++) { var orderUserId = random.Next(1, 11); // Random user ID 1-10 dbContext.Orders.Add(CreateOrder(orderUserId, $"ORD{i:D5}", 1)); } dbContext.SaveChanges(); // Query with user ID filter var request = new OrderListRequest { UserId = actualUserId, Page = 1, PageSize = 100 }; var result = service.GetOrderListAsync(request).GetAwaiter().GetResult(); // Verify all returned orders belong to the specified user return result.List.All(o => o.UserId == actualUserId); } /// /// **Feature: admin-business-migration, Property 10: Order List Filter Accuracy** /// For any order list request with status filter, all returned orders /// should have the specified status. /// Validates: Requirements 6.2 /// [Property(MaxTest = 100)] public bool OrderListFilter_ByStatus_ShouldReturnOnlyMatchingOrders(PositiveInt statusSeed, PositiveInt orderCount) { var targetStatus = (byte)(statusSeed.Get % 3); // 0, 1, or 2 var actualOrderCount = (orderCount.Get % 20) + 10; // 10-29 orders var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 5); // Seed orders with various statuses var random = new Random(statusSeed.Get); for (int i = 0; i < actualOrderCount; i++) { var status = (byte)random.Next(0, 3); // Random status 0-2 dbContext.Orders.Add(CreateOrder(1, $"ORD{i:D5}", status)); } dbContext.SaveChanges(); // Query with status filter var request = new OrderListRequest { Status = targetStatus, Page = 1, PageSize = 100 }; var result = service.GetOrderListAsync(request).GetAwaiter().GetResult(); // Verify all returned orders have the specified status return result.List.All(o => o.Status == targetStatus); } /// /// **Feature: admin-business-migration, Property 10: Order List Filter Accuracy** /// For any order list request with order number filter, all returned orders /// should contain the specified order number substring. /// Validates: Requirements 6.2 /// [Property(MaxTest = 100)] public bool OrderListFilter_ByOrderNum_ShouldReturnOnlyMatchingOrders(PositiveInt seed) { var orderCount = (seed.Get % 20) + 10; // 10-29 orders var searchPrefix = $"ORD{(seed.Get % 5):D2}"; // Search for ORD00, ORD01, etc. var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 3); // Seed orders with various order numbers for (int i = 0; i < orderCount; i++) { var orderNum = $"ORD{(i % 10):D2}{i:D3}"; // ORD00000, ORD01001, etc. dbContext.Orders.Add(CreateOrder(1, orderNum, 1)); } dbContext.SaveChanges(); // Query with order number filter var request = new OrderListRequest { OrderNum = searchPrefix, Page = 1, PageSize = 100 }; var result = service.GetOrderListAsync(request).GetAwaiter().GetResult(); // Verify all returned orders contain the search string return result.List.All(o => o.OrderNum.Contains(searchPrefix)); } /// /// **Feature: admin-business-migration, Property 10: Order List Filter Accuracy** /// For any order list request with date range filter, all returned orders /// should fall within the specified date range. /// Validates: Requirements 6.2 /// [Property(MaxTest = 100)] public bool OrderListFilter_ByDateRange_ShouldReturnOnlyMatchingOrders(PositiveInt seed) { var orderCount = (seed.Get % 15) + 10; // 10-24 orders var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed users SeedUsers(dbContext, 3); // Seed orders with various dates var baseDate = DateTime.Today; for (int i = 0; i < orderCount; i++) { var order = CreateOrder(1, $"ORD{i:D5}", 1); // Spread orders across -10 to +10 days var daysOffset = (i % 21) - 10; order.Addtime = (int)new DateTimeOffset(baseDate.AddDays(daysOffset)).ToUnixTimeSeconds(); order.CreatedAt = baseDate.AddDays(daysOffset); dbContext.Orders.Add(order); } dbContext.SaveChanges(); // Query with date range filter (last 5 days to today) // Note: The service adds 1 day to EndDate internally, so we use today as EndDate var startDate = baseDate.AddDays(-5); var endDate = baseDate; // Service will add 1 day, making it < baseDate.AddDays(1) var request = new OrderListRequest { StartDate = startDate, EndDate = endDate, Page = 1, PageSize = 100 }; var result = service.GetOrderListAsync(request).GetAwaiter().GetResult(); // Verify all returned orders fall within the date range // Service filters: CreatedAt >= startDate AND CreatedAt < endDate.AddDays(1) var effectiveEndDate = endDate.AddDays(1); return result.List.All(o => o.CreatedAt >= startDate && o.CreatedAt < effectiveEndDate); } #endregion #region Property 11: Order Prize Grouping /// /// **Feature: admin-business-migration, Property 11: Order Prize Grouping** /// For any order detail request, the returned prize list should be correctly /// grouped by prize_code with accurate counts. /// Validates: Requirements 6.4 /// [Property(MaxTest = 100)] public bool OrderDetail_PrizeGrouping_ShouldGroupByPrizeCode(PositiveInt prizeTypeCount, PositiveInt itemsPerType) { var actualPrizeTypes = (prizeTypeCount.Get % 5) + 2; // 2-6 prize types var actualItemsPerType = (itemsPerType.Get % 5) + 1; // 1-5 items per type var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed user SeedUsers(dbContext, 1); // Create order var order = CreateOrder(1, "ORD00001", 1); dbContext.Orders.Add(order); dbContext.SaveChanges(); // Create order items with different prize codes var expectedGroups = new Dictionary(); for (int typeIndex = 0; typeIndex < actualPrizeTypes; typeIndex++) { var prizeCode = $"PC{typeIndex:D3}"; expectedGroups[prizeCode] = actualItemsPerType; for (int itemIndex = 0; itemIndex < actualItemsPerType; itemIndex++) { dbContext.OrderItems.Add(new OrderItem { OrderId = order.Id, UserId = 1, Status = 0, GoodsId = 1, Num = typeIndex * actualItemsPerType + itemIndex + 1, ShangId = typeIndex + 1, GoodslistId = typeIndex + 1, GoodslistTitle = $"奖品类型{typeIndex + 1}", GoodslistImgurl = $"http://test.com/prize{typeIndex + 1}.jpg", GoodslistPrice = 100 + typeIndex * 50, GoodslistMoney = 50 + typeIndex * 25, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = prizeCode, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); } } dbContext.SaveChanges(); // Get order detail var result = service.GetOrderDetailAsync(order.Id).GetAwaiter().GetResult(); if (result == null) return false; // Verify grouping if (result.PrizeGroups.Count != actualPrizeTypes) return false; // Verify each group has correct count foreach (var group in result.PrizeGroups) { if (!expectedGroups.ContainsKey(group.PrizeCode)) return false; if (group.Count != expectedGroups[group.PrizeCode]) return false; if (group.Items.Count != expectedGroups[group.PrizeCode]) return false; } return true; } /// /// **Feature: admin-business-migration, Property 11: Order Prize Grouping** /// For any order with items having the same prize_code, they should be grouped together. /// Validates: Requirements 6.4 /// [Property(MaxTest = 100)] public bool OrderDetail_SamePrizeCode_ShouldBeGroupedTogether(PositiveInt seed) { var totalItems = (seed.Get % 20) + 5; // 5-24 items var prizeCodeCount = (seed.Get % 3) + 2; // 2-4 unique prize codes var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed user SeedUsers(dbContext, 1); // Create order var order = CreateOrder(1, "ORD00001", 1); dbContext.Orders.Add(order); dbContext.SaveChanges(); // Create order items with random prize codes var random = new Random(seed.Get); var expectedCounts = new Dictionary(); for (int i = 0; i < totalItems; i++) { var prizeCodeIndex = random.Next(0, prizeCodeCount); var prizeCode = $"PC{prizeCodeIndex:D3}"; if (!expectedCounts.ContainsKey(prizeCode)) expectedCounts[prizeCode] = 0; expectedCounts[prizeCode]++; dbContext.OrderItems.Add(new OrderItem { OrderId = order.Id, UserId = 1, Status = 0, GoodsId = 1, Num = i + 1, ShangId = prizeCodeIndex + 1, GoodslistId = prizeCodeIndex + 1, GoodslistTitle = $"奖品{prizeCodeIndex + 1}", GoodslistImgurl = $"http://test.com/prize{prizeCodeIndex + 1}.jpg", GoodslistPrice = 100, GoodslistMoney = 50, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = prizeCode, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); } dbContext.SaveChanges(); // Get order detail var result = service.GetOrderDetailAsync(order.Id).GetAwaiter().GetResult(); if (result == null) return false; // Verify number of groups matches unique prize codes if (result.PrizeGroups.Count != expectedCounts.Count) return false; // Verify each group count matches expected foreach (var group in result.PrizeGroups) { if (!expectedCounts.ContainsKey(group.PrizeCode)) return false; if (group.Count != expectedCounts[group.PrizeCode]) return false; } // Verify total items count var totalGroupedItems = result.PrizeGroups.Sum(g => g.Count); return totalGroupedItems == totalItems; } #endregion #region Property 12: Shipping Order Cancellation Inventory Restoration /// /// **Feature: admin-business-migration, Property 12: Shipping Order Cancellation Inventory Restoration** /// For any shipping order cancellation, all prizes in the order should be restored /// to the user's inventory (status changed from shipped to available). /// Validates: Requirements 6.6 /// [Property(MaxTest = 100)] public bool CancelShippingOrder_ShouldRestoreAllPrizes(PositiveInt itemCount) { var actualItemCount = (itemCount.Get % 10) + 2; // 2-11 items var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed user SeedUsers(dbContext, 1); // Create shipping order var sendNum = $"SEND{Guid.NewGuid():N}".Substring(0, 20); var shippingOrder = new OrderItemsSend { UserId = 1, SendNum = sendNum, Freight = 10, Status = 1, // 待发货 Count = actualItemCount, Name = "测试用户", Mobile = "13800138001", Address = "测试地址", Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; dbContext.OrderItemsSends.Add(shippingOrder); dbContext.SaveChanges(); // Create order items linked to shipping order var orderItemIds = new List(); for (int i = 0; i < actualItemCount; i++) { var orderItem = new OrderItem { OrderId = 1, UserId = 1, SendNum = sendNum, Status = 2, // 已发货状态 GoodsId = 1, Num = i + 1, ShangId = 1, GoodslistId = 1, GoodslistTitle = $"奖品{i + 1}", GoodslistImgurl = "http://test.com/prize.jpg", GoodslistPrice = 100, GoodslistMoney = 50, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = $"PC{i:D3}", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; dbContext.OrderItems.Add(orderItem); dbContext.SaveChanges(); orderItemIds.Add(orderItem.Id); } // Cancel shipping order try { var result = service.CancelShippingOrderAsync(shippingOrder.Id, 1).GetAwaiter().GetResult(); if (!result) return false; } catch { return false; } // Verify shipping order status changed to cancelled var updatedShippingOrder = dbContext.OrderItemsSends.Find(shippingOrder.Id); if (updatedShippingOrder == null || updatedShippingOrder.Status != 4) return false; // Verify all order items are restored (status = 0, SendNum = null) var restoredItems = dbContext.OrderItems .Where(oi => orderItemIds.Contains(oi.Id)) .ToList(); return restoredItems.All(oi => oi.Status == 0 && oi.SendNum == null); } /// /// **Feature: admin-business-migration, Property 12: Shipping Order Cancellation Inventory Restoration** /// For any shipping order cancellation, the count of restored items should equal /// the original shipping order item count. /// Validates: Requirements 6.6 /// [Property(MaxTest = 100)] public bool CancelShippingOrder_RestoredCount_ShouldMatchOriginal(PositiveInt seed) { var itemCount = (seed.Get % 8) + 3; // 3-10 items var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; using var dbContext = new HoneyBoxDbContext(options); var service = new OrderService(dbContext, _mockLogger.Object); // Seed user SeedUsers(dbContext, 1); // Create shipping order var sendNum = $"SEND{Guid.NewGuid():N}".Substring(0, 20); var shippingOrder = new OrderItemsSend { UserId = 1, SendNum = sendNum, Freight = 10, Status = 1, // 待发货 Count = itemCount, Name = "测试用户", Mobile = "13800138001", Address = "测试地址", Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; dbContext.OrderItemsSends.Add(shippingOrder); dbContext.SaveChanges(); // Create order items linked to shipping order for (int i = 0; i < itemCount; i++) { dbContext.OrderItems.Add(new OrderItem { OrderId = 1, UserId = 1, SendNum = sendNum, Status = 2, // 已发货状态 GoodsId = 1, Num = i + 1, ShangId = 1, GoodslistId = 1, GoodslistTitle = $"奖品{i + 1}", GoodslistImgurl = "http://test.com/prize.jpg", GoodslistPrice = 100, GoodslistMoney = 50, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = $"PC{i:D3}", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); } dbContext.SaveChanges(); // Count items before cancellation var itemsBeforeCancellation = dbContext.OrderItems .Count(oi => oi.SendNum == sendNum && oi.Status == 2); // Cancel shipping order try { service.CancelShippingOrderAsync(shippingOrder.Id, 1).GetAwaiter().GetResult(); } catch { return false; } // Count restored items after cancellation var restoredItems = dbContext.OrderItems .Count(oi => oi.Status == 0 && oi.SendNum == null); // The number of restored items should match the original count return restoredItems >= itemsBeforeCancellation; } #endregion #region Helper Methods private void SeedUsers(HoneyBoxDbContext dbContext, int count) { for (int i = 1; i <= count; i++) { dbContext.Users.Add(new User { Id = i, Uid = $"U{i:D3}", Nickname = $"测试用户{i}", Mobile = $"1380013800{i}", OpenId = $"openid{i}", HeadImg = $"http://test.com/head{i}.jpg", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); } dbContext.SaveChanges(); } private Order CreateOrder(int userId, string orderNum, byte status) { return new Order { UserId = userId, OrderNum = orderNum, OrderTotal = 100, OrderZheTotal = 90, Price = 90, UseMoney = 0, UseIntegral = 0, UseScore = 0, Zhe = 0.9m, GoodsId = 1, Num = 1, GoodsPrice = 100, GoodsTitle = "测试商品", PrizeNum = 1, Status = status, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayTime = status == 1 ? (int)DateTimeOffset.Now.ToUnixTimeSeconds() : 0, PayType = status == 1 ? (byte)1 : (byte)0, OrderType = 0, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; } #endregion }