HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/OrderServicePropertyTests.cs
2026-01-17 03:24:20 +08:00

593 lines
22 KiB
C#

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;
/// <summary>
/// OrderService 属性测试
/// </summary>
public class OrderServicePropertyTests
{
private readonly Mock<ILogger<OrderService>> _mockLogger = new();
#region Property 10: Order List Filter Accuracy
/// <summary>
/// **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
/// </summary>
[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<HoneyBoxDbContext>()
.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);
}
/// <summary>
/// **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
/// </summary>
[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<HoneyBoxDbContext>()
.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);
}
/// <summary>
/// **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
/// </summary>
[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<HoneyBoxDbContext>()
.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));
}
/// <summary>
/// **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
/// </summary>
[Property(MaxTest = 100)]
public bool OrderListFilter_ByDateRange_ShouldReturnOnlyMatchingOrders(PositiveInt seed)
{
var orderCount = (seed.Get % 15) + 10; // 10-24 orders
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.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
/// <summary>
/// **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
/// </summary>
[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<HoneyBoxDbContext>()
.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<string, int>();
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;
}
/// <summary>
/// **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
/// </summary>
[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<HoneyBoxDbContext>()
.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<string, int>();
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
/// <summary>
/// **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
/// </summary>
[Property(MaxTest = 100)]
public bool CancelShippingOrder_ShouldRestoreAllPrizes(PositiveInt itemCount)
{
var actualItemCount = (itemCount.Get % 10) + 2; // 2-11 items
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.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<int>();
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);
}
/// <summary>
/// **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
/// </summary>
[Property(MaxTest = 100)]
public bool CancelShippingOrder_RestoredCount_ShouldMatchOriginal(PositiveInt seed)
{
var itemCount = (seed.Get % 8) + 3; // 3-10 items
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.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
}