305 lines
9.9 KiB
C#
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
|
|
}
|