HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/DesignatedPrizeServicePropertyTests.cs
2026-02-02 07:59:16 +08:00

802 lines
27 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Business.Models;
using HoneyBox.Admin.Business.Models.DesignatedPrize;
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>
/// DesignatedPrizeService 属性测试
/// </summary>
public class DesignatedPrizeServicePropertyTests
{
private readonly Mock<ILogger<DesignatedPrizeService>> _mockLogger = new();
private (HoneyBoxDbContext dbContext, DesignatedPrizeService service) CreateService()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new HoneyBoxDbContext(options);
var service = new DesignatedPrizeService(dbContext, _mockLogger.Object);
return (dbContext, service);
}
private async Task<(int goodsId, List<int> prizeIds, int userId)> SeedTestDataAsync(HoneyBoxDbContext dbContext, int prizeCount)
{
// Create user
var user = new User
{
OpenId = $"test_openid_{Guid.NewGuid()}",
Uid = $"test_uid_{Guid.NewGuid()}",
Nickname = "测试用户",
HeadImg = "http://test.com/avatar.jpg",
Mobile = "13800138000",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
// Create goods
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,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
await dbContext.SaveChangesAsync();
// Create prizes
var prizeIds = new List<int>();
for (int i = 0; i < prizeCount; i++)
{
var prize = new GoodsItem
{
GoodsId = goods.Id,
Num = i + 1,
Title = $"奖品{i + 1}",
ImgUrl = $"http://test.com/prize{i + 1}.jpg",
Stock = 1,
SurplusStock = 1,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = 1,
GoodsType = 1,
PrizeCode = $"PC{i + 1:D3}",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.Add(prize);
await dbContext.SaveChangesAsync();
prizeIds.Add(prize.Id);
}
return (goods.Id, prizeIds, user.Id);
}
#region Property 1: Unique Constraint Enforcement
/// <summary>
/// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
/// For any goods and goods_item combination, attempting to create a second designated prize
/// configuration SHALL result in an error, ensuring only one user can be designated per prize.
/// **Validates: Requirements 1.2, 4.5**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueConstraint_ShouldPreventDuplicateConfiguration(PositiveInt prizeCount, PositiveInt userId1, PositiveInt userId2)
{
var actualPrizeCount = Math.Max(1, prizeCount.Get % 5 + 1);
var actualUserId1 = userId1.Get;
var actualUserId2 = userId2.Get;
// Ensure different user IDs for the test
if (actualUserId1 == actualUserId2)
{
actualUserId2 = actualUserId1 + 1;
}
var (dbContext, service) = CreateService();
try
{
// Seed test data
var (goodsId, prizeIds, _) = SeedTestDataAsync(dbContext, actualPrizeCount).GetAwaiter().GetResult();
// Create additional users for the test
var user1 = new User
{
OpenId = $"user1_{Guid.NewGuid()}",
Uid = $"uid1_{Guid.NewGuid()}",
Nickname = "用户1",
HeadImg = "http://test.com/u1.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
var user2 = new User
{
OpenId = $"user2_{Guid.NewGuid()}",
Uid = $"uid2_{Guid.NewGuid()}",
Nickname = "用户2",
HeadImg = "http://test.com/u2.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.AddRange(user1, user2);
dbContext.SaveChanges();
var targetPrizeId = prizeIds[0];
// First creation should succeed
var request1 = new CreateDesignatedPrizeRequest
{
GoodsItemId = targetPrizeId,
UserId = user1.Id,
Remark = "First config"
};
var result1 = service.CreateAsync(goodsId, request1).GetAwaiter().GetResult();
// Second creation with same goods_id and goods_item_id should fail
var request2 = new CreateDesignatedPrizeRequest
{
GoodsItemId = targetPrizeId,
UserId = user2.Id,
Remark = "Second config"
};
try
{
service.CreateAsync(goodsId, request2).GetAwaiter().GetResult();
// If we reach here, the constraint was not enforced
return false;
}
catch (BusinessException ex)
{
// Expected: Should throw BusinessException with Conflict code
return ex.Code == BusinessErrorCodes.Conflict;
}
}
finally
{
dbContext.Dispose();
}
}
/// <summary>
/// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
/// Different prizes in the same goods can each have their own designated user.
/// **Validates: Requirements 1.2, 4.5**
/// </summary>
[Property(MaxTest = 100)]
public bool UniqueConstraint_ShouldAllowDifferentPrizesToHaveDifferentDesignatedUsers(PositiveInt prizeCount)
{
var actualPrizeCount = Math.Max(2, prizeCount.Get % 5 + 2);
var (dbContext, service) = CreateService();
try
{
// Seed test data
var (goodsId, prizeIds, _) = SeedTestDataAsync(dbContext, actualPrizeCount).GetAwaiter().GetResult();
// Create users for each prize
var users = new List<User>();
for (int i = 0; i < actualPrizeCount; i++)
{
var user = new User
{
OpenId = $"user{i}_{Guid.NewGuid()}",
Uid = $"uid{i}_{Guid.NewGuid()}",
Nickname = $"用户{i}",
HeadImg = $"http://test.com/u{i}.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
users.Add(user);
}
dbContext.Users.AddRange(users);
dbContext.SaveChanges();
// Each prize should be able to have its own designated user
for (int i = 0; i < actualPrizeCount; i++)
{
var request = new CreateDesignatedPrizeRequest
{
GoodsItemId = prizeIds[i],
UserId = users[i].Id,
Remark = $"Config for prize {i}"
};
try
{
var result = service.CreateAsync(goodsId, request).GetAwaiter().GetResult();
if (result == null)
{
return false;
}
}
catch
{
// Should not throw for different prizes
return false;
}
}
// Verify all configurations were created
var configs = service.GetByGoodsIdAsync(goodsId).GetAwaiter().GetResult();
return configs.Count == actualPrizeCount;
}
finally
{
dbContext.Dispose();
}
}
/// <summary>
/// **Feature: designated-prize-winner, Property 1: Unique Constraint Enforcement**
/// Same prize in different goods can have designated users (unique constraint is per goods+prize).
/// **Validates: Requirements 1.2, 4.5**
/// </summary>
[Property(MaxTest = 50)]
public bool UniqueConstraint_ShouldAllowSamePrizeIdInDifferentGoods(PositiveInt userId)
{
var (dbContext, service) = CreateService();
try
{
// Create user
var user = new User
{
OpenId = $"user_{Guid.NewGuid()}",
Uid = $"uid_{Guid.NewGuid()}",
Nickname = "测试用户",
HeadImg = "http://test.com/u.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Create two different goods
var goods1 = new Good
{
Title = "商品1",
ImgUrl = "http://test.com/g1.jpg",
ImgUrlDetail = "http://test.com/g1_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
var goods2 = new Good
{
Title = "商品2",
ImgUrl = "http://test.com/g2.jpg",
ImgUrlDetail = "http://test.com/g2_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.AddRange(goods1, goods2);
dbContext.SaveChanges();
// Create prizes for each goods
var prize1 = new GoodsItem
{
GoodsId = goods1.Id,
Num = 1,
Title = "奖品1",
ImgUrl = "http://test.com/p1.jpg",
Stock = 1,
SurplusStock = 1,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = 1,
GoodsType = 1,
PrizeCode = "PC001",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
var prize2 = new GoodsItem
{
GoodsId = goods2.Id,
Num = 1,
Title = "奖品2",
ImgUrl = "http://test.com/p2.jpg",
Stock = 1,
SurplusStock = 1,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = 1,
GoodsType = 1,
PrizeCode = "PC002",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.AddRange(prize1, prize2);
dbContext.SaveChanges();
// Both should succeed (different goods)
var request1 = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize1.Id,
UserId = user.Id,
Remark = "Config for goods1"
};
var request2 = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize2.Id,
UserId = user.Id,
Remark = "Config for goods2"
};
try
{
var result1 = service.CreateAsync(goods1.Id, request1).GetAwaiter().GetResult();
var result2 = service.CreateAsync(goods2.Id, request2).GetAwaiter().GetResult();
return result1 != null && result2 != null;
}
catch
{
return false;
}
}
finally
{
dbContext.Dispose();
}
}
#endregion
#region Property 8: Prize Data Immutability
/// <summary>
/// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
/// For any designated prize configuration CREATE operation, the underlying prize's stock,
/// real_pro, and other probability-related fields SHALL remain unchanged.
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool PrizeDataImmutability_CreateOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro, PositiveInt price)
{
var actualStock = Math.Max(1, stock.Get % 100 + 1);
var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);
var actualPrice = Math.Max(10, price.Get % 1000 + 10);
var (dbContext, service) = CreateService();
try
{
// Create user
var user = new User
{
OpenId = $"user_{Guid.NewGuid()}",
Uid = $"uid_{Guid.NewGuid()}",
Nickname = "测试用户",
HeadImg = "http://test.com/u.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Create goods
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/g.jpg",
ImgUrlDetail = "http://test.com/g_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Create prize with specific values
var prize = new GoodsItem
{
GoodsId = goods.Id,
Num = 1,
Title = "测试奖品",
ImgUrl = "http://test.com/p.jpg",
Stock = actualStock,
SurplusStock = actualStock,
Price = actualPrice,
Money = actualPrice / 2,
ScMoney = actualPrice / 3,
RealPro = actualRealPro,
GoodsType = 1,
PrizeCode = "PC001",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.Add(prize);
dbContext.SaveChanges();
// Store original values
var originalStock = prize.Stock;
var originalSurplusStock = prize.SurplusStock;
var originalRealPro = prize.RealPro;
var originalPrice = prize.Price;
var originalMoney = prize.Money;
var originalScMoney = prize.ScMoney;
// Create designated prize configuration
var request = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize.Id,
UserId = user.Id,
Remark = "Test config"
};
service.CreateAsync(goods.Id, request).GetAwaiter().GetResult();
// Reload prize from database
dbContext.Entry(prize).Reload();
// Verify all probability-related fields remain unchanged
return prize.Stock == originalStock &&
prize.SurplusStock == originalSurplusStock &&
prize.RealPro == originalRealPro &&
prize.Price == originalPrice &&
prize.Money == originalMoney &&
prize.ScMoney == originalScMoney;
}
finally
{
dbContext.Dispose();
}
}
/// <summary>
/// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
/// For any designated prize configuration UPDATE operation, the underlying prize's stock,
/// real_pro, and other probability-related fields SHALL remain unchanged.
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool PrizeDataImmutability_UpdateOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro)
{
var actualStock = Math.Max(1, stock.Get % 100 + 1);
var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);
var (dbContext, service) = CreateService();
try
{
// Create user
var user = new User
{
OpenId = $"user_{Guid.NewGuid()}",
Uid = $"uid_{Guid.NewGuid()}",
Nickname = "测试用户",
HeadImg = "http://test.com/u.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Create goods
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/g.jpg",
ImgUrlDetail = "http://test.com/g_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Create prize
var prize = new GoodsItem
{
GoodsId = goods.Id,
Num = 1,
Title = "测试奖品",
ImgUrl = "http://test.com/p.jpg",
Stock = actualStock,
SurplusStock = actualStock,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = actualRealPro,
GoodsType = 1,
PrizeCode = "PC001",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.Add(prize);
dbContext.SaveChanges();
// Create designated prize configuration
var createRequest = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize.Id,
UserId = user.Id,
Remark = "Initial config"
};
var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();
// Store original values
var originalStock = prize.Stock;
var originalSurplusStock = prize.SurplusStock;
var originalRealPro = prize.RealPro;
var originalPrice = prize.Price;
// Update the configuration
var updateRequest = new UpdateDesignatedPrizeRequest
{
IsActive = false,
Remark = "Updated config"
};
service.UpdateAsync(config.Id, updateRequest).GetAwaiter().GetResult();
// Reload prize from database
dbContext.Entry(prize).Reload();
// Verify all probability-related fields remain unchanged
return prize.Stock == originalStock &&
prize.SurplusStock == originalSurplusStock &&
prize.RealPro == originalRealPro &&
prize.Price == originalPrice;
}
finally
{
dbContext.Dispose();
}
}
/// <summary>
/// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
/// For any designated prize configuration DELETE operation, the underlying prize's stock,
/// real_pro, and other probability-related fields SHALL remain unchanged.
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 100)]
public bool PrizeDataImmutability_DeleteOperation_ShouldNotModifyPrizeFields(PositiveInt stock, PositiveInt realPro)
{
var actualStock = Math.Max(1, stock.Get % 100 + 1);
var actualRealPro = Math.Max(1, realPro.Get % 100 + 1);
var (dbContext, service) = CreateService();
try
{
// Create user
var user = new User
{
OpenId = $"user_{Guid.NewGuid()}",
Uid = $"uid_{Guid.NewGuid()}",
Nickname = "测试用户",
HeadImg = "http://test.com/u.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Create goods
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/g.jpg",
ImgUrlDetail = "http://test.com/g_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Create prize
var prize = new GoodsItem
{
GoodsId = goods.Id,
Num = 1,
Title = "测试奖品",
ImgUrl = "http://test.com/p.jpg",
Stock = actualStock,
SurplusStock = actualStock,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = actualRealPro,
GoodsType = 1,
PrizeCode = "PC001",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.Add(prize);
dbContext.SaveChanges();
// Create designated prize configuration
var createRequest = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize.Id,
UserId = user.Id,
Remark = "Config to delete"
};
var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();
// Store original values
var originalStock = prize.Stock;
var originalSurplusStock = prize.SurplusStock;
var originalRealPro = prize.RealPro;
var originalPrice = prize.Price;
// Delete the configuration
service.DeleteAsync(config.Id).GetAwaiter().GetResult();
// Reload prize from database
dbContext.Entry(prize).Reload();
// Verify all probability-related fields remain unchanged
return prize.Stock == originalStock &&
prize.SurplusStock == originalSurplusStock &&
prize.RealPro == originalRealPro &&
prize.Price == originalPrice;
}
finally
{
dbContext.Dispose();
}
}
/// <summary>
/// **Feature: designated-prize-winner, Property 8: Prize Data Immutability**
/// Multiple CRUD operations on designated prize configurations should not affect prize data.
/// **Validates: Requirements 5.2**
/// </summary>
[Property(MaxTest = 50)]
public bool PrizeDataImmutability_MultipleCrudOperations_ShouldNotModifyPrizeFields(PositiveInt operationCount)
{
var actualOperationCount = Math.Max(3, operationCount.Get % 10 + 3);
var (dbContext, service) = CreateService();
try
{
// Create users
var users = new List<User>();
for (int i = 0; i < actualOperationCount; i++)
{
var user = new User
{
OpenId = $"user{i}_{Guid.NewGuid()}",
Uid = $"uid{i}_{Guid.NewGuid()}",
Nickname = $"用户{i}",
HeadImg = $"http://test.com/u{i}.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
users.Add(user);
}
dbContext.Users.AddRange(users);
dbContext.SaveChanges();
// Create goods
var goods = new Good
{
Title = "测试商品",
ImgUrl = "http://test.com/g.jpg",
ImgUrlDetail = "http://test.com/g_detail.jpg",
Price = 100,
Type = 1,
Status = 1,
Stock = 100,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.Goods.Add(goods);
dbContext.SaveChanges();
// Create prize
var prize = new GoodsItem
{
GoodsId = goods.Id,
Num = 1,
Title = "测试奖品",
ImgUrl = "http://test.com/p.jpg",
Stock = 50,
SurplusStock = 50,
Price = 500,
Money = 300,
ScMoney = 250,
RealPro = 25,
GoodsType = 1,
PrizeCode = "PC001",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
dbContext.GoodsItems.Add(prize);
dbContext.SaveChanges();
// Store original values
var originalStock = prize.Stock;
var originalSurplusStock = prize.SurplusStock;
var originalRealPro = prize.RealPro;
var originalPrice = prize.Price;
var originalMoney = prize.Money;
var originalScMoney = prize.ScMoney;
// Perform multiple CRUD operations
for (int i = 0; i < actualOperationCount; i++)
{
// Create
var createRequest = new CreateDesignatedPrizeRequest
{
GoodsItemId = prize.Id,
UserId = users[i].Id,
Remark = $"Config {i}"
};
var config = service.CreateAsync(goods.Id, createRequest).GetAwaiter().GetResult();
// Update
var updateRequest = new UpdateDesignatedPrizeRequest
{
IsActive = i % 2 == 0,
Remark = $"Updated config {i}"
};
service.UpdateAsync(config.Id, updateRequest).GetAwaiter().GetResult();
// Delete
service.DeleteAsync(config.Id).GetAwaiter().GetResult();
}
// Reload prize from database
dbContext.Entry(prize).Reload();
// Verify all probability-related fields remain unchanged after all operations
return prize.Stock == originalStock &&
prize.SurplusStock == originalSurplusStock &&
prize.RealPro == originalRealPro &&
prize.Price == originalPrice &&
prize.Money == originalMoney &&
prize.ScMoney == originalScMoney;
}
finally
{
dbContext.Dispose();
}
}
#endregion
}