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

730 lines
21 KiB
C#

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;
/// <summary>
/// GoodsService 单元测试
/// </summary>
public class GoodsServiceTests : IDisposable
{
private readonly HoneyBoxDbContext _dbContext;
private readonly Mock<ILogger<GoodsService>> _mockLogger;
private readonly GoodsService _goodsService;
public GoodsServiceTests()
{
// 使用 InMemory 数据库
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_dbContext = new HoneyBoxDbContext(options);
_mockLogger = new Mock<ILogger<GoodsService>>();
_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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<BusinessException>(
() => _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<Good>
{
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<GoodsItem>
{
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<GoodsType>
{
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
}