802 lines
27 KiB
C#
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
|
|
|
|
}
|