using HoneyBox.Admin.Business.Models; using HoneyBox.Admin.Business.Models.Goods; 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; /// /// GoodsService 单元测试 /// public class GoodsServiceTests : IDisposable { private readonly HoneyBoxDbContext _dbContext; private readonly Mock> _mockLogger; private readonly GoodsService _goodsService; public GoodsServiceTests() { // 使用 InMemory 数据库 var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; _dbContext = new HoneyBoxDbContext(options); _mockLogger = new Mock>(); _goodsService = new GoodsService(_dbContext, _mockLogger.Object); } public void Dispose() { _dbContext.Dispose(); } #region GetGoodsListAsync Tests [Fact] public async Task GetGoodsListAsync_WithNoFilters_ReturnsAllGoods() { // Arrange await SeedGoodsDataAsync(); var request = new GoodsListRequest { Page = 1, PageSize = 10 }; // Act var result = await _goodsService.GetGoodsListAsync(request); // Assert Assert.NotNull(result); Assert.Equal(3, result.Total); Assert.Equal(3, result.List.Count); } [Fact] public async Task GetGoodsListAsync_WithTitleFilter_ReturnsFilteredGoods() { // Arrange await SeedGoodsDataAsync(); var request = new GoodsListRequest { Title = "一番赏", Page = 1, PageSize = 10 }; // Act var result = await _goodsService.GetGoodsListAsync(request); // Assert Assert.NotNull(result); Assert.Equal(1, result.Total); Assert.Contains(result.List, g => g.Title.Contains("一番赏")); } [Fact] public async Task GetGoodsListAsync_WithStatusFilter_ReturnsFilteredGoods() { // Arrange await SeedGoodsDataAsync(); var request = new GoodsListRequest { Status = 1, Page = 1, PageSize = 10 }; // Act var result = await _goodsService.GetGoodsListAsync(request); // Assert Assert.NotNull(result); Assert.All(result.List, g => Assert.Equal(1, g.Status)); } [Fact] public async Task GetGoodsListAsync_WithTypeFilter_ReturnsFilteredGoods() { // Arrange await SeedGoodsDataAsync(); var request = new GoodsListRequest { Type = 1, Page = 1, PageSize = 10 }; // Act var result = await _goodsService.GetGoodsListAsync(request); // Assert Assert.NotNull(result); Assert.All(result.List, g => Assert.Equal(1, g.Type)); } [Fact] public async Task GetGoodsListAsync_WithPagination_ReturnsCorrectPage() { // Arrange await SeedGoodsDataAsync(); var request = new GoodsListRequest { Page = 1, PageSize = 2 }; // Act var result = await _goodsService.GetGoodsListAsync(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); } [Fact] public async Task GetGoodsListAsync_ExcludesDeletedGoods() { // Arrange await SeedGoodsDataAsync(); // Add a deleted goods _dbContext.Goods.Add(new Good { Title = "已删除商品", ImgUrl = "http://test.com/deleted.jpg", ImgUrlDetail = "http://test.com/deleted_detail.jpg", Price = 10, Type = 1, Status = 1, Stock = 10, DeletedAt = DateTime.Now, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }); await _dbContext.SaveChangesAsync(); var request = new GoodsListRequest { Page = 1, PageSize = 10 }; // Act var result = await _goodsService.GetGoodsListAsync(request); // Assert Assert.Equal(3, result.Total); // Should not include deleted goods } #endregion #region GetGoodsDetailAsync Tests [Fact] public async Task GetGoodsDetailAsync_WithExistingGoods_ReturnsDetail() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); // Act var result = await _goodsService.GetGoodsDetailAsync(goods.Id); // Assert Assert.NotNull(result); Assert.Equal(goods.Id, result.Id); Assert.Equal(goods.Title, result.Title); } [Fact] public async Task GetGoodsDetailAsync_WithNonExistingGoods_ReturnsNull() { // Act var result = await _goodsService.GetGoodsDetailAsync(99999); // Assert Assert.Null(result); } [Fact] public async Task GetGoodsDetailAsync_WithDeletedGoods_ReturnsNull() { // Arrange var deletedGoods = new Good { Title = "已删除商品", ImgUrl = "http://test.com/deleted.jpg", ImgUrlDetail = "http://test.com/deleted_detail.jpg", Price = 10, Type = 1, Status = 1, Stock = 10, DeletedAt = DateTime.Now, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; _dbContext.Goods.Add(deletedGoods); await _dbContext.SaveChangesAsync(); // Act var result = await _goodsService.GetGoodsDetailAsync(deletedGoods.Id); // Assert Assert.Null(result); } #endregion #region CreateGoodsAsync Tests [Fact] public async Task CreateGoodsAsync_WithValidRequest_CreatesGoods() { // Arrange var request = new GoodsCreateRequest { Title = "新商品", Price = 100, Type = 1, ImgUrl = "http://test.com/new.jpg", ImgUrlDetail = "http://test.com/new_detail.jpg", Stock = 50, Sort = 1 }; // Act var goodsId = await _goodsService.CreateGoodsAsync(request, 1); // Assert Assert.True(goodsId > 0); var createdGoods = await _dbContext.Goods.FindAsync(goodsId); Assert.NotNull(createdGoods); Assert.Equal("新商品", createdGoods.Title); Assert.Equal(100, createdGoods.Price); Assert.Equal(0, createdGoods.Status); // Default to offline } [Fact] public async Task CreateGoodsAsync_WithInvalidType_ThrowsException() { // Arrange var request = new GoodsCreateRequest { Title = "新商品", Price = 100, Type = 999, // Invalid type ImgUrl = "http://test.com/new.jpg", ImgUrlDetail = "http://test.com/new_detail.jpg", Stock = 50 }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.CreateGoodsAsync(request, 1)); Assert.Equal(BusinessErrorCodes.ValidationFailed, exception.Code); } [Fact] public async Task CreateGoodsAsync_WithWelfareHouseType_SetsIsFlw() { // Arrange var request = new GoodsCreateRequest { Title = "福利屋商品", Price = 100, Type = 15, // 福利屋 ImgUrl = "http://test.com/flw.jpg", ImgUrlDetail = "http://test.com/flw_detail.jpg", Stock = 50 }; // Act var goodsId = await _goodsService.CreateGoodsAsync(request, 1); // Assert var createdGoods = await _dbContext.Goods.FindAsync(goodsId); Assert.NotNull(createdGoods); Assert.Equal(1, createdGoods.IsFlw); } #endregion #region UpdateGoodsAsync Tests [Fact] public async Task UpdateGoodsAsync_WithValidRequest_UpdatesGoods() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); var request = new GoodsUpdateRequest { Title = "更新后的标题", Price = 200, Type = goods.Type, ImgUrl = goods.ImgUrl, ImgUrlDetail = goods.ImgUrlDetail, Stock = goods.Stock + 10 // Increase stock }; // Act var result = await _goodsService.UpdateGoodsAsync(goods.Id, request, 1); // Assert Assert.True(result); var updatedGoods = await _dbContext.Goods.FindAsync(goods.Id); Assert.Equal("更新后的标题", updatedGoods!.Title); Assert.Equal(200, updatedGoods.Price); } [Fact] public async Task UpdateGoodsAsync_WithNonExistingGoods_ThrowsException() { // Arrange var request = new GoodsUpdateRequest { Title = "更新", Price = 100, Type = 1, ImgUrl = "http://test.com/test.jpg", ImgUrlDetail = "http://test.com/test_detail.jpg", Stock = 10 }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.UpdateGoodsAsync(99999, request, 1)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } [Fact] public async Task UpdateGoodsAsync_WithReducedStock_ThrowsException() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); var request = new GoodsUpdateRequest { Title = goods.Title, Price = goods.Price, Type = goods.Type, ImgUrl = goods.ImgUrl, ImgUrlDetail = goods.ImgUrlDetail, Stock = goods.Stock - 1 // Reduce stock }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.UpdateGoodsAsync(goods.Id, request, 1)); Assert.Equal(BusinessErrorCodes.ValidationFailed, exception.Code); Assert.Contains("库存", exception.Message); } #endregion #region DeleteGoodsAsync Tests [Fact] public async Task DeleteGoodsAsync_WithExistingGoods_SoftDeletes() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); // Act var result = await _goodsService.DeleteGoodsAsync(goods.Id, 1); // Assert Assert.True(result); var deletedGoods = await _dbContext.Goods.FindAsync(goods.Id); Assert.NotNull(deletedGoods!.DeletedAt); } [Fact] public async Task DeleteGoodsAsync_WithNonExistingGoods_ThrowsException() { // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.DeleteGoodsAsync(99999, 1)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } #endregion #region SetGoodsStatusAsync Tests [Fact] public async Task SetGoodsStatusAsync_WithValidGoods_UpdatesStatus() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(g => g.Status == 0); // Act var result = await _goodsService.SetGoodsStatusAsync(goods.Id, 1, 1); // Assert Assert.True(result); var updatedGoods = await _dbContext.Goods.FindAsync(goods.Id); Assert.Equal(1, updatedGoods!.Status); } [Fact] public async Task SetGoodsStatusAsync_WithNonExistingGoods_ThrowsException() { // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.SetGoodsStatusAsync(99999, 1, 1)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } #endregion #region Prize Management Tests [Fact] public async Task GetPrizesAsync_WithExistingGoods_ReturnsPrizes() { // Arrange await SeedGoodsWithPrizesAsync(); var goods = await _dbContext.Goods.FirstAsync(); // Act var result = await _goodsService.GetPrizesAsync(goods.Id); // Assert Assert.NotNull(result); Assert.Equal(2, result.Count); } [Fact] public async Task GetPrizesAsync_WithNonExistingGoods_ThrowsException() { // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.GetPrizesAsync(99999)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } [Fact] public async Task AddPrizeAsync_WithValidRequest_CreatesPrize() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); var request = new PrizeCreateRequest { Title = "新奖品", ImgUrl = "http://test.com/prize.jpg", Stock = 10, Price = 50, Money = 30, ScMoney = 25, RealPro = 10, GoodsType = 1 }; // Act var prizeId = await _goodsService.AddPrizeAsync(goods.Id, request); // Assert Assert.True(prizeId > 0); var createdPrize = await _dbContext.GoodsItems.FindAsync(prizeId); Assert.NotNull(createdPrize); Assert.Equal("新奖品", createdPrize.Title); Assert.NotNull(createdPrize.PrizeCode); } [Fact] public async Task AddPrizeAsync_GeneratesUniquePrizeCode() { // Arrange await SeedGoodsDataAsync(); var goods = await _dbContext.Goods.FirstAsync(); var request = new PrizeCreateRequest { Title = "奖品", ImgUrl = "http://test.com/prize.jpg", Stock = 10, Price = 50, Money = 30, ScMoney = 25, RealPro = 10, GoodsType = 1 }; // Act var prizeId1 = await _goodsService.AddPrizeAsync(goods.Id, request); var prizeId2 = await _goodsService.AddPrizeAsync(goods.Id, request); // Assert var prize1 = await _dbContext.GoodsItems.FindAsync(prizeId1); var prize2 = await _dbContext.GoodsItems.FindAsync(prizeId2); Assert.NotEqual(prize1!.PrizeCode, prize2!.PrizeCode); } [Fact] public async Task UpdatePrizeAsync_WithValidRequest_UpdatesPrize() { // Arrange await SeedGoodsWithPrizesAsync(); var prize = await _dbContext.GoodsItems.FirstAsync(); var request = new PrizeUpdateRequest { Title = "更新后的奖品", ImgUrl = prize.ImgUrl, Stock = 20, Price = 100, Money = 60, ScMoney = 50, RealPro = 15, GoodsType = 1 }; // Act var result = await _goodsService.UpdatePrizeAsync(prize.Id, request); // Assert Assert.True(result); var updatedPrize = await _dbContext.GoodsItems.FindAsync(prize.Id); Assert.Equal("更新后的奖品", updatedPrize!.Title); Assert.Equal(100, updatedPrize.Price); } [Fact] public async Task UpdatePrizeAsync_WithNonExistingPrize_ThrowsException() { // Arrange var request = new PrizeUpdateRequest { Title = "更新", ImgUrl = "http://test.com/prize.jpg", Stock = 10, Price = 50, Money = 30, ScMoney = 25, RealPro = 10, GoodsType = 1 }; // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.UpdatePrizeAsync(99999, request)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } [Fact] public async Task DeletePrizeAsync_WithExistingPrize_DeletesPrize() { // Arrange await SeedGoodsWithPrizesAsync(); var prize = await _dbContext.GoodsItems.FirstAsync(); var goodsId = prize.GoodsId; var initialCount = await _dbContext.GoodsItems.CountAsync(gi => gi.GoodsId == goodsId); // Act var result = await _goodsService.DeletePrizeAsync(prize.Id); // Assert Assert.True(result); var deletedPrize = await _dbContext.GoodsItems.FindAsync(prize.Id); Assert.Null(deletedPrize); var newCount = await _dbContext.GoodsItems.CountAsync(gi => gi.GoodsId == goodsId); Assert.Equal(initialCount - 1, newCount); } [Fact] public async Task DeletePrizeAsync_WithNonExistingPrize_ThrowsException() { // Act & Assert var exception = await Assert.ThrowsAsync( () => _goodsService.DeletePrizeAsync(99999)); Assert.Equal(BusinessErrorCodes.NotFound, exception.Code); } #endregion #region GetGoodsTypesAsync Tests [Fact] public async Task GetGoodsTypesAsync_ReturnsAllTypes() { // Arrange await SeedGoodsTypesAsync(); // Act var result = await _goodsService.GetGoodsTypesAsync(); // Assert Assert.NotNull(result); Assert.Equal(3, result.Count); } #endregion #region Helper Methods private async Task SeedGoodsDataAsync() { var goods = new List { new() { Title = "一番赏测试商品", ImgUrl = "http://test.com/1.jpg", ImgUrlDetail = "http://test.com/1_detail.jpg", Price = 100, Type = 1, Status = 1, Stock = 100, SaleStock = 10, Sort = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { Title = "无限赏测试商品", ImgUrl = "http://test.com/2.jpg", ImgUrlDetail = "http://test.com/2_detail.jpg", Price = 50, Type = 2, Status = 1, Stock = 200, SaleStock = 50, Sort = 2, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { Title = "盲盒测试商品", ImgUrl = "http://test.com/3.jpg", ImgUrlDetail = "http://test.com/3_detail.jpg", Price = 30, Type = 8, Status = 0, Stock = 50, SaleStock = 0, Sort = 3, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.Goods.AddRange(goods); await _dbContext.SaveChangesAsync(); } private async Task SeedGoodsWithPrizesAsync() { var goods = new Good { Title = "带奖品的商品", ImgUrl = "http://test.com/goods.jpg", ImgUrlDetail = "http://test.com/goods_detail.jpg", Price = 100, Type = 1, Status = 1, Stock = 100, PrizeNum = 2, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; _dbContext.Goods.Add(goods); await _dbContext.SaveChangesAsync(); var prizes = new List { new() { GoodsId = goods.Id, Num = 1, Title = "A赏", ImgUrl = "http://test.com/prize1.jpg", Stock = 1, SurplusStock = 1, Price = 500, Money = 300, ScMoney = 250, RealPro = 1, GoodsType = 1, Sort = 1, PrizeCode = "PC001", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }, new() { GoodsId = goods.Id, Num = 2, Title = "B赏", ImgUrl = "http://test.com/prize2.jpg", Stock = 5, SurplusStock = 5, Price = 200, Money = 100, ScMoney = 80, RealPro = 5, GoodsType = 1, Sort = 2, PrizeCode = "PC002", CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now } }; _dbContext.GoodsItems.AddRange(prizes); await _dbContext.SaveChangesAsync(); } private async Task SeedGoodsTypesAsync() { var types = new List { new() { Name = "一番赏", Value = 1, SortOrder = 1, IsShow = 1, FlName = "一番赏" }, new() { Name = "无限赏", Value = 2, SortOrder = 2, IsShow = 1, FlName = "无限赏" }, new() { Name = "盲盒", Value = 8, SortOrder = 3, IsShow = 1, FlName = "盲盒" } }; _dbContext.GoodsTypes.AddRange(types); await _dbContext.SaveChangesAsync(); } #endregion }