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 OrderServiceTests : IDisposable { private readonly HoneyBoxDbContext _dbContext; private readonly Mock> _mockLogger; private readonly OrderService _orderService; public OrderServiceTests() { // 使用 InMemory 数据库 var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _dbContext = new HoneyBoxDbContext(options); _mockLogger = new Mock>(); _orderService = new OrderService(_dbContext, _mockLogger.Object); } public void Dispose() { _dbContext.Dispose(); } #region GetOrderListAsync Tests [Fact] public async Task GetOrderListAsync_WithNoFilters_ReturnsAllOrders() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.Equal(3, result.Total); Assert.Equal(3, result.List.Count); } [Fact] public async Task GetOrderListAsync_WithUserIdFilter_ReturnsFilteredOrders() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { UserId = 1, Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.All(result.List, o => Assert.Equal(1, o.UserId)); } [Fact] public async Task GetOrderListAsync_WithOrderNumFilter_ReturnsFilteredOrders() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { OrderNum = "ORD001", Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.Single(result.List); Assert.Contains("ORD001", result.List[0].OrderNum); } [Fact] public async Task GetOrderListAsync_WithStatusFilter_ReturnsFilteredOrders() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { Status = 1, Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.All(result.List, o => Assert.Equal(1, o.Status)); } [Fact] public async Task GetOrderListAsync_WithDateRangeFilter_ReturnsFilteredOrders() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { StartDate = DateTime.Today.AddDays(-1), EndDate = DateTime.Today.AddDays(1), Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.True(result.Total > 0); } [Fact] public async Task GetOrderListAsync_WithPagination_ReturnsCorrectPage() { // Arrange await SeedOrderDataAsync(); var request = new OrderListRequest { Page = 1, PageSize = 2 }; // Act var result = await _orderService.GetOrderListAsync(request); // Assert Assert.NotNull(result); Assert.Equal(3, result.Total); Assert.Equal(2, result.List.Count); Assert.Equal(1, result.Page); Assert.Equal(2, result.PageSize); } #endregion #region GetOrderDetailAsync Tests [Fact] public async Task GetOrderDetailAsync_WithExistingOrder_ReturnsDetail() { // Arrange await SeedOrderWithItemsAsync(); var order = await _dbContext.Orders.FirstAsync(); // Act var result = await _orderService.GetOrderDetailAsync(order.Id); // Assert Assert.NotNull(result); Assert.Equal(order.Id, result.Id); Assert.Equal(order.OrderNum, result.OrderNum); } [Fact] public async Task GetOrderDetailAsync_WithNonExistingOrder_ReturnsNull() { // Act var result = await _orderService.GetOrderDetailAsync(99999); // Assert Assert.Null(result); } [Fact] public async Task GetOrderDetailAsync_GroupsPrizesByPrizeCode() { // Arrange await SeedOrderWithItemsAsync(); var order = await _dbContext.Orders.FirstAsync(); // Act var result = await _orderService.GetOrderDetailAsync(order.Id); // Assert Assert.NotNull(result); Assert.NotEmpty(result.PrizeGroups); // Verify grouping foreach (var group in result.PrizeGroups) { Assert.True(group.Count > 0); Assert.NotEmpty(group.Items); } } #endregion #region GetStuckOrdersAsync Tests [Fact] public async Task GetStuckOrdersAsync_ReturnsOrdersWithPendingItems() { // Arrange await SeedOrderWithItemsAsync(); var request = new OrderListRequest { Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetStuckOrdersAsync(request); // Assert Assert.NotNull(result); // All returned orders should have pending items foreach (var order in result.List) { var hasPendingItems = await _dbContext.OrderItems .AnyAsync(oi => oi.OrderId == order.Id && oi.Status == 0); Assert.True(hasPendingItems); } } #endregion #region GetShippingOrdersAsync Tests [Fact] public async Task GetShippingOrdersAsync_WithNoFilters_ReturnsAllShippingOrders() { // Arrange await SeedShippingOrderDataAsync(); var request = new ShippingOrderListRequest { Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetShippingOrdersAsync(request); // Assert Assert.NotNull(result); Assert.Equal(2, result.Total); } [Fact] public async Task GetShippingOrdersAsync_WithStatusFilter_ReturnsFilteredOrders() { // Arrange await SeedShippingOrderDataAsync(); var request = new ShippingOrderListRequest { Status = 1, Page = 1, PageSize = 10 }; // Act var result = await _orderService.GetShippingOrdersAsync(request); // Assert Assert.NotNull(result); Assert.All(result.List, s => Assert.Equal(1, s.Status)); } #endregion #region ShipOrderAsync Tests [Fact] public async Task ShipOrderAsync_WithValidOrder_UpdatesStatus() { // Arrange await SeedShippingOrderDataAsync(); var shippingOrder = await _dbContext.OrderItemsSends.FirstAsync(s => s.Status == 1); var request = new ShipOrderRequest { CourierName = "顺丰速运", CourierNumber = "SF123456789" }; // Act var result = await _orderService.ShipOrderAsync(shippingOrder.Id, request, 1); // Assert Assert.True(result); var updatedOrder = await _dbContext.OrderItemsSends.FindAsync(shippingOrder.Id); Assert.Equal(2, updatedOrder!.Status); // 已发货 Assert.Equal("顺丰速运", updatedOrder.CourierName); Assert.Equal("SF123456789", updatedOrder.CourierNumber); } [Fact] public async Task ShipOrderAsync_WithNonExistingOrder_ThrowsException() { // Arrange var request = new ShipOrderRequest { CourierName = "顺丰速运", CourierNumber = "SF123456789" }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _orderService.ShipOrderAsync(99999, request, 1)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } [Fact] public async Task ShipOrderAsync_WithInvalidStatus_ThrowsException() { // Arrange await SeedShippingOrderDataAsync(); var shippingOrder = await _dbContext.OrderItemsSends.FirstAsync(s => s.Status == 2); // 已发货 var request = new ShipOrderRequest { CourierName = "顺丰速运", CourierNumber = "SF123456789" }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _orderService.ShipOrderAsync(shippingOrder.Id, request, 1)); Assert.Equal(BusinessErrorCodes.ValidationFailed, exception.Code); } #endregion #region CancelShippingOrderAsync Tests [Fact] public async Task CancelShippingOrderAsync_WithValidOrder_RestoresPrizes() { // Arrange await SeedShippingOrderWithItemsAsync(); var shippingOrder = await _dbContext.OrderItemsSends.FirstAsync(s => s.Status == 1); var itemsCount = await _dbContext.OrderItems.CountAsync(oi => oi.SendNum == shippingOrder.SendNum); // Act var result = await _orderService.CancelShippingOrderAsync(shippingOrder.Id, 1); // Assert Assert.True(result); var updatedOrder = await _dbContext.OrderItemsSends.FindAsync(shippingOrder.Id); Assert.Equal(4, updatedOrder!.Status); // 已取消 // Verify items are restored var restoredItems = await _dbContext.OrderItems .Where(oi => oi.SendNum == null && oi.Status == 0) .CountAsync(); Assert.True(restoredItems >= itemsCount); } [Fact] public async Task CancelShippingOrderAsync_WithNonExistingOrder_ThrowsException() { // Act & Assert var exception = await Assert.ThrowsAsync( () => _orderService.CancelShippingOrderAsync(99999, 1)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } #endregion #region ExportOrdersAsync Tests [Fact] public async Task ExportOrdersAsync_ReturnsValidCsvBytes() { // Arrange await SeedOrderDataAsync(); var request = new OrderExportRequest(); // Act var result = await _orderService.ExportOrdersAsync(request); // Assert Assert.NotNull(result); Assert.True(result.Length > 0); // Verify it's valid UTF-8 with BOM Assert.Equal(0xEF, result[0]); Assert.Equal(0xBB, result[1]); Assert.Equal(0xBF, result[2]); } [Fact] public async Task ExportOrdersAsync_WithFilters_ExportsFilteredData() { // Arrange await SeedOrderDataAsync(); var request = new OrderExportRequest { Status = 1 }; // Act var result = await _orderService.ExportOrdersAsync(request); // Assert Assert.NotNull(result); Assert.True(result.Length > 0); } #endregion #region Helper Methods private async Task SeedUserDataAsync() { var users = new List { new() { Id = 1, Uid = "U001", Nickname = "测试用户1", Mobile = "13800138001", OpenId = "openid1", HeadImg = "http://test.com/head1.jpg", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { Id = 2, Uid = "U002", Nickname = "测试用户2", Mobile = "13800138002", OpenId = "openid2", HeadImg = "http://test.com/head2.jpg", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.Users.AddRange(users); await _dbContext.SaveChangesAsync(); } private async Task SeedOrderDataAsync() { await SeedUserDataAsync(); var orders = new List { new() { UserId = 1, OrderNum = "ORD001", OrderTotal = 100, OrderZheTotal = 90, Price = 90, UseMoney = 0, UseIntegral = 0, UseScore = 0, Zhe = 0.9m, GoodsId = 1, Num = 1, GoodsPrice = 100, GoodsTitle = "测试商品1", PrizeNum = 1, Status = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayType = 1, OrderType = 0, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { UserId = 1, OrderNum = "ORD002", OrderTotal = 200, OrderZheTotal = 180, Price = 180, UseMoney = 0, UseIntegral = 0, UseScore = 0, Zhe = 0.9m, GoodsId = 2, Num = 2, GoodsPrice = 100, GoodsTitle = "测试商品2", PrizeNum = 2, Status = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayType = 1, OrderType = 0, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { UserId = 2, OrderNum = "ORD003", OrderTotal = 50, OrderZheTotal = 50, Price = 50, UseMoney = 0, UseIntegral = 0, UseScore = 0, Zhe = 1, GoodsId = 1, Num = 1, GoodsPrice = 50, GoodsTitle = "测试商品3", PrizeNum = 1, Status = 0, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PayTime = 0, PayType = 0, OrderType = 0, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.Orders.AddRange(orders); await _dbContext.SaveChangesAsync(); } private async Task SeedOrderWithItemsAsync() { await SeedOrderDataAsync(); var order = await _dbContext.Orders.FirstAsync(); var orderItems = new List { new() { OrderId = order.Id, UserId = order.UserId, Status = 0, // 待处理 GoodsId = order.GoodsId, Num = 1, ShangId = 1, GoodslistId = 1, GoodslistTitle = "A赏", GoodslistImgurl = "http://test.com/prize1.jpg", GoodslistPrice = 500, GoodslistMoney = 300, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = "PC001", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { OrderId = order.Id, UserId = order.UserId, Status = 0, // 待处理 GoodsId = order.GoodsId, Num = 2, ShangId = 2, GoodslistId = 2, GoodslistTitle = "B赏", GoodslistImgurl = "http://test.com/prize2.jpg", GoodslistPrice = 200, GoodslistMoney = 100, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = "PC002", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { OrderId = order.Id, UserId = order.UserId, Status = 0, // 待处理 GoodsId = order.GoodsId, Num = 3, ShangId = 2, GoodslistId = 2, GoodslistTitle = "B赏", GoodslistImgurl = "http://test.com/prize2.jpg", GoodslistPrice = 200, GoodslistMoney = 100, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = "PC002", // Same prize code for grouping test CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.OrderItems.AddRange(orderItems); await _dbContext.SaveChangesAsync(); } private async Task SeedShippingOrderDataAsync() { await SeedUserDataAsync(); var shippingOrders = new List { new() { UserId = 1, SendNum = "SEND001", Freight = 10, Status = 1, // 待发货 Count = 2, Name = "张三", Mobile = "13800138001", Address = "北京市朝阳区xxx", Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { UserId = 2, SendNum = "SEND002", Freight = 15, Status = 2, // 已发货 Count = 1, Name = "李四", Mobile = "13800138002", Address = "上海市浦东新区xxx", CourierName = "顺丰速运", CourierNumber = "SF987654321", Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), SendTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.OrderItemsSends.AddRange(shippingOrders); await _dbContext.SaveChangesAsync(); } private async Task SeedShippingOrderWithItemsAsync() { await SeedShippingOrderDataAsync(); var shippingOrder = await _dbContext.OrderItemsSends.FirstAsync(s => s.Status == 1); var orderItems = new List { new() { OrderId = 1, UserId = shippingOrder.UserId, SendNum = shippingOrder.SendNum, Status = 2, // 已发货状态 GoodsId = 1, Num = 1, ShangId = 1, GoodslistId = 1, GoodslistTitle = "A赏", GoodslistImgurl = "http://test.com/prize1.jpg", GoodslistPrice = 500, GoodslistMoney = 300, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = "PC001", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { OrderId = 1, UserId = shippingOrder.UserId, SendNum = shippingOrder.SendNum, Status = 2, // 已发货状态 GoodsId = 1, Num = 2, ShangId = 2, GoodslistId = 2, GoodslistTitle = "B赏", GoodslistImgurl = "http://test.com/prize2.jpg", GoodslistPrice = 200, GoodslistMoney = 100, GoodslistType = 1, Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), PrizeCode = "PC002", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.OrderItems.AddRange(orderItems); await _dbContext.SaveChangesAsync(); } #endregion }