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

305 lines
9.9 KiB
C#

using FsCheck;
using FsCheck.Xunit;
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 GoodsServicePropertyTests
{
private readonly Mock<ILogger<GoodsService>> _mockLogger = new();
#region Property 8: Goods Stock Increase Prize Replication
/// <summary>
/// **Feature: admin-business-migration, Property 8: Goods Stock Increase Prize Replication**
/// For any goods stock increase operation, the number of prize configurations for new sets
/// should equal the number of prize configurations in the first set.
/// Validates: Requirements 5.6
/// </summary>
[Property(MaxTest = 100)]
public bool StockIncrease_ShouldReplicatePrizeConfigurations(PositiveInt initialStock, PositiveInt stockIncrease, PositiveInt prizeCount)
{
// Limit values to reasonable ranges
var actualInitialStock = (initialStock.Get % 10) + 1; // 1-10
var actualStockIncrease = (stockIncrease.Get % 5) + 1; // 1-5
var actualPrizeCount = (prizeCount.Get % 5) + 1; // 1-5 prizes
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new HoneyBoxDbContext(options);
var service = new GoodsService(dbContext, _mockLogger.Object);
// Create a goods item
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/img.jpg",
ImgUrlDetail = "http://test.com/detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = actualInitialStock,
SaleStock = 0,
PrizeNum = actualPrizeCount,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Create initial prize configurations
var initialPrizes = new List<GoodsItem>();
for (int i = 0; i < actualPrizeCount; i++)
{
initialPrizes.Add(new GoodsItem
{
GoodsId = goods.Id,
Num = i + 1,
Title = $"奖品{i + 1}",
ImgUrl = $"http://test.com/prize{i + 1}.jpg",
Stock = 1,
SurplusStock = 1,
Price = 50 + i * 10,
Money = 30 + i * 5,
ScMoney = 25 + i * 5,
RealPro = 10,
GoodsType = 1,
Sort = i + 1,
PrizeCode = GoodsService.GeneratePrizeCode(),
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
});
}
dbContext.GoodsItems.AddRange(initialPrizes);
dbContext.SaveChanges();
var initialPrizeCountInDb = dbContext.GoodsItems.Count(gi => gi.GoodsId == goods.Id);
// Update goods with increased stock
var updateRequest = new GoodsUpdateRequest
{
Title = goods.Title,
Price = goods.Price,
Type = goods.Type,
ImgUrl = goods.ImgUrl,
ImgUrlDetail = goods.ImgUrlDetail,
Stock = actualInitialStock + actualStockIncrease // Increase stock
};
try
{
service.UpdateGoodsAsync(goods.Id, updateRequest, 1).GetAwaiter().GetResult();
}
catch
{
return false;
}
// Verify prize replication
var finalPrizeCount = dbContext.GoodsItems.Count(gi => gi.GoodsId == goods.Id);
var expectedPrizeCount = initialPrizeCountInDb + (actualStockIncrease * actualPrizeCount);
return finalPrizeCount == expectedPrizeCount;
}
/// <summary>
/// **Feature: admin-business-migration, Property 8: Goods Stock Increase Prize Replication**
/// For any goods with no initial prizes, stock increase should not create any new prizes.
/// Validates: Requirements 5.6
/// </summary>
[Property(MaxTest = 100)]
public bool StockIncrease_WithNoPrizes_ShouldNotCreatePrizes(PositiveInt initialStock, PositiveInt stockIncrease)
{
var actualInitialStock = (initialStock.Get % 10) + 1;
var actualStockIncrease = (stockIncrease.Get % 5) + 1;
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new HoneyBoxDbContext(options);
var service = new GoodsService(dbContext, _mockLogger.Object);
// Create a goods item without prizes
var goods = new Good
{
Title = "无奖品商品",
ImgUrl = "http://test.com/img.jpg",
ImgUrlDetail = "http://test.com/detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = actualInitialStock,
SaleStock = 0,
PrizeNum = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Update goods with increased stock
var updateRequest = new GoodsUpdateRequest
{
Title = goods.Title,
Price = goods.Price,
Type = goods.Type,
ImgUrl = goods.ImgUrl,
ImgUrlDetail = goods.ImgUrlDetail,
Stock = actualInitialStock + actualStockIncrease
};
try
{
service.UpdateGoodsAsync(goods.Id, updateRequest, 1).GetAwaiter().GetResult();
}
catch
{
return false;
}
// Verify no prizes were created
var prizeCount = dbContext.GoodsItems.Count(gi => gi.GoodsId == goods.Id);
return prizeCount == 0;
}
#endregion
#region Property 9: Prize Code Uniqueness
/// <summary>
/// **Feature: admin-business-migration, Property 9: Prize Code Uniqueness**
/// For any prize added to a box, the generated prize_code should be unique
/// within the entire goods_list table.
/// Validates: Requirements 5.8
/// </summary>
[Property(MaxTest = 100)]
public bool AddPrize_ShouldGenerateUniquePrizeCode(PositiveInt prizeCount)
{
var actualPrizeCount = (prizeCount.Get % 20) + 5; // 5-24 prizes
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new HoneyBoxDbContext(options);
var service = new GoodsService(dbContext, _mockLogger.Object);
// Create a goods item
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/img.jpg",
ImgUrlDetail = "http://test.com/detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
SaleStock = 0,
PrizeNum = 0,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Add multiple prizes
var prizeIds = new List<int>();
for (int i = 0; i < actualPrizeCount; i++)
{
var request = new PrizeCreateRequest
{
Title = $"奖品{i + 1}",
ImgUrl = $"http://test.com/prize{i + 1}.jpg",
Stock = 1,
Price = 50,
Money = 30,
ScMoney = 25,
RealPro = 10,
GoodsType = 1
};
try
{
var prizeId = service.AddPrizeAsync(goods.Id, request).GetAwaiter().GetResult();
prizeIds.Add(prizeId);
}
catch
{
return false;
}
}
// Verify all prize codes are unique
var prizeCodes = dbContext.GoodsItems
.Where(gi => prizeIds.Contains(gi.Id))
.Select(gi => gi.PrizeCode)
.ToList();
var uniqueCodes = prizeCodes.Distinct().Count();
return uniqueCodes == prizeCodes.Count && prizeCodes.All(c => !string.IsNullOrEmpty(c));
}
/// <summary>
/// **Feature: admin-business-migration, Property 9: Prize Code Uniqueness**
/// For any two prizes added at different times, their prize codes should be different.
/// Validates: Requirements 5.8
/// </summary>
[Property(MaxTest = 100)]
public bool GeneratePrizeCode_ShouldBeUnique(PositiveInt seed)
{
var codeCount = (seed.Get % 100) + 50; // 50-149 codes
var codes = new HashSet<string>();
for (int i = 0; i < codeCount; i++)
{
var code = GoodsService.GeneratePrizeCode();
if (codes.Contains(code))
{
return false; // Duplicate found
}
codes.Add(code);
}
return codes.Count == codeCount;
}
/// <summary>
/// **Feature: admin-business-migration, Property 9: Prize Code Uniqueness**
/// Prize codes should follow the expected format (starting with "PC").
/// Validates: Requirements 5.8
/// </summary>
[Property(MaxTest = 100)]
public bool GeneratePrizeCode_ShouldFollowFormat(PositiveInt seed)
{
var iterations = (seed.Get % 50) + 10;
for (int i = 0; i < iterations; i++)
{
var code = GoodsService.GeneratePrizeCode();
// Verify format: starts with "PC", followed by timestamp and random chars
if (string.IsNullOrEmpty(code) || !code.StartsWith("PC") || code.Length < 20)
{
return false;
}
}
return true;
}
#endregion
}