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; /// /// GoodsService 属性测试 /// public class GoodsServicePropertyTests { private readonly Mock> _mockLogger = new(); #region Property 8: Goods Stock Increase Prize Replication /// /// **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 /// [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() .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(); 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; } /// /// **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 /// [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() .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 /// /// **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 /// [Property(MaxTest = 100)] public bool AddPrize_ShouldGenerateUniquePrizeCode(PositiveInt prizeCount) { var actualPrizeCount = (prizeCount.Get % 20) + 5; // 5-24 prizes var options = new DbContextOptionsBuilder() .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(); 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)); } /// /// **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 /// [Property(MaxTest = 100)] public bool GeneratePrizeCode_ShouldBeUnique(PositiveInt seed) { var codeCount = (seed.Get % 100) + 50; // 50-149 codes var codes = new HashSet(); 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; } /// /// **Feature: admin-business-migration, Property 9: Prize Code Uniqueness** /// Prize codes should follow the expected format (starting with "PC"). /// Validates: Requirements 5.8 /// [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 }