using FsCheck; using FsCheck.Xunit; using HoneyBox.Core.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// PrizeAnnouncementService 属性测试(用户端) /// 测试首页中大奖公告功能的用户端核心属性 /// public class PrizeAnnouncementServicePropertyTests { private readonly Mock> _mockLogger = new(); private (HoneyBoxDbContext dbContext, PrizeAnnouncementService service) CreateService() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; var dbContext = new HoneyBoxDbContext(options); var service = new PrizeAnnouncementService(dbContext, _mockLogger.Object); return (dbContext, service); } /// /// 生成有效的非空字符串(用于必填字段) /// private static string GenerateValidString(string prefix, int seed) { return $"{prefix}_{Math.Abs(seed) % 1000}"; } /// /// 创建测试用的公告实体 /// private static PrizeAnnouncement CreateAnnouncement(int seed, bool isEnabled, int sort) { return new PrizeAnnouncement { UserAvatar = $"http://test.com/avatar_{seed}.jpg", UserName = GenerateValidString("User", seed), PrizeLevel = GenerateValidString("Level", seed), PrizeName = GenerateValidString("Prize", seed), Sort = sort, IsEnabled = isEnabled, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; } #region Property 2: Enabled Filter Correctness /// /// **Feature: homepage-prize-announcement, Property 2: Enabled Filter Correctness** /// For any set of announcements with mixed enabled/disabled states, the user-facing API /// should only return announcements where IsEnabled is true. /// **Validates: Requirements 2.2, 1.6** /// [Property(MaxTest = 100)] public bool EnabledFilterCorrectness_OnlyReturnsEnabledAnnouncements( PositiveInt enabledCount, PositiveInt disabledCount) { var actualEnabledCount = Math.Max(1, enabledCount.Get % 10); var actualDisabledCount = disabledCount.Get % 10; var (dbContext, service) = CreateService(); try { // Create enabled announcements for (int i = 0; i < actualEnabledCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: i); dbContext.PrizeAnnouncements.Add(announcement); } // Create disabled announcements for (int i = 0; i < actualDisabledCount; i++) { var announcement = CreateAnnouncement(i + 1000, isEnabled: false, sort: i + 1000); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: all returned items should be enabled // Since we're testing the service, we verify by checking the count matches enabled count return result.Count == actualEnabledCount; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 2: Enabled Filter Correctness** /// For any set of announcements, the count of returned items should equal /// the count of enabled announcements in the database. /// **Validates: Requirements 2.2, 1.6** /// [Property(MaxTest = 100)] public bool EnabledFilterCorrectness_CountMatchesEnabledInDatabase(PositiveInt totalCount) { var actualTotalCount = Math.Max(1, totalCount.Get % 20); var random = new Random(totalCount.Get); var (dbContext, service) = CreateService(); try { var expectedEnabledCount = 0; // Create announcements with random enabled states for (int i = 0; i < actualTotalCount; i++) { var isEnabled = random.Next(2) == 1; if (isEnabled) expectedEnabledCount++; var announcement = CreateAnnouncement(i, isEnabled, sort: i); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: count should match expected enabled count return result.Count == expectedEnabledCount; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 2: Enabled Filter Correctness** /// When all announcements are disabled, the API should return an empty list. /// **Validates: Requirements 2.2, 1.6** /// [Property(MaxTest = 100)] public bool EnabledFilterCorrectness_AllDisabled_ReturnsEmptyList(PositiveInt count) { var actualCount = Math.Max(1, count.Get % 10); var (dbContext, service) = CreateService(); try { // Create only disabled announcements for (int i = 0; i < actualCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: false, sort: i); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: should return empty list return result.Count == 0; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 2: Enabled Filter Correctness** /// When all announcements are enabled, the API should return all of them. /// **Validates: Requirements 2.2, 1.6** /// [Property(MaxTest = 100)] public bool EnabledFilterCorrectness_AllEnabled_ReturnsAll(PositiveInt count) { var actualCount = Math.Max(1, count.Get % 10); var (dbContext, service) = CreateService(); try { // Create only enabled announcements for (int i = 0; i < actualCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: i); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: should return all announcements return result.Count == actualCount; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 2: Enabled Filter Correctness** /// When database is empty, the API should return an empty list. /// **Validates: Requirements 2.2, 1.6** /// [Fact] public void EnabledFilterCorrectness_EmptyDatabase_ReturnsEmptyList() { var (dbContext, service) = CreateService(); try { // Get enabled announcements from empty database var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: should return empty list Assert.Empty(result); } finally { dbContext.Dispose(); } } #endregion #region Property 3: Sort Ordering Preservation /// /// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation** /// For any set of enabled announcements with different sort values, the user-facing API /// should return them in ascending order by the sort field. /// **Validates: Requirements 2.3, 1.7** /// [Property(MaxTest = 100)] public bool SortOrderingPreservation_ReturnsInAscendingOrder(PositiveInt count, PositiveInt seed) { var actualCount = Math.Max(2, count.Get % 10 + 2); var random = new Random(seed.Get); var (dbContext, service) = CreateService(); try { // Create announcements with random sort values var sortValues = Enumerable.Range(0, actualCount) .Select(_ => random.Next(1000)) .ToList(); for (int i = 0; i < actualCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: sortValues[i]); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: for any two consecutive items, first item's sort should be <= second item's sort // Since DTO doesn't have Sort field, we verify by checking the order matches expected var expectedOrder = sortValues.OrderBy(s => s).ToList(); // We need to verify the order by checking the UserName pattern which contains the index // The announcements should be returned in the order of their sort values for (int i = 0; i < result.Count - 1; i++) { // Extract the original index from UserName (format: "User_{index}") var currentIndex = int.Parse(result[i].UserName.Split('_')[1]); var nextIndex = int.Parse(result[i + 1].UserName.Split('_')[1]); var currentSort = sortValues[currentIndex]; var nextSort = sortValues[nextIndex]; if (currentSort > nextSort) { return false; } } return true; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation** /// For any two consecutive items in the result, the first item's sort value /// should be less than or equal to the second item's sort value. /// **Validates: Requirements 2.3, 1.7** /// [Property(MaxTest = 100)] public bool SortOrderingPreservation_ConsecutiveItemsOrdered(PositiveInt count) { var actualCount = Math.Max(2, count.Get % 15 + 2); var (dbContext, service) = CreateService(); try { // Create announcements with sequential sort values in reverse order // This tests that the service correctly orders them for (int i = 0; i < actualCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: actualCount - i); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: items should be in ascending order by sort // The first item should have sort = 1, last should have sort = actualCount for (int i = 0; i < result.Count - 1; i++) { var currentIndex = int.Parse(result[i].UserName.Split('_')[1]); var nextIndex = int.Parse(result[i + 1].UserName.Split('_')[1]); var currentSort = actualCount - currentIndex; var nextSort = actualCount - nextIndex; if (currentSort > nextSort) { return false; } } return true; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation** /// Announcements with the same sort value should be returned (order among them is stable). /// **Validates: Requirements 2.3, 1.7** /// [Property(MaxTest = 100)] public bool SortOrderingPreservation_SameSortValues_AllReturned(PositiveInt count) { var actualCount = Math.Max(2, count.Get % 10 + 2); var (dbContext, service) = CreateService(); try { // Create announcements with the same sort value for (int i = 0; i < actualCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: 100); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: all announcements should be returned return result.Count == actualCount; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation** /// Only enabled announcements should be sorted and returned. /// **Validates: Requirements 2.3, 1.7** /// [Property(MaxTest = 100)] public bool SortOrderingPreservation_OnlyEnabledAreSorted( PositiveInt enabledCount, PositiveInt disabledCount) { var actualEnabledCount = Math.Max(2, enabledCount.Get % 10 + 2); var actualDisabledCount = disabledCount.Get % 5; var (dbContext, service) = CreateService(); try { // Create enabled announcements with specific sort values for (int i = 0; i < actualEnabledCount; i++) { var announcement = CreateAnnouncement(i, isEnabled: true, sort: actualEnabledCount - i); dbContext.PrizeAnnouncements.Add(announcement); } // Create disabled announcements with sort values that would appear first if included for (int i = 0; i < actualDisabledCount; i++) { var announcement = CreateAnnouncement(i + 1000, isEnabled: false, sort: 0); dbContext.PrizeAnnouncements.Add(announcement); } dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: only enabled announcements are returned and sorted if (result.Count != actualEnabledCount) { return false; } // Verify ordering for (int i = 0; i < result.Count - 1; i++) { var currentIndex = int.Parse(result[i].UserName.Split('_')[1]); var nextIndex = int.Parse(result[i + 1].UserName.Split('_')[1]); var currentSort = actualEnabledCount - currentIndex; var nextSort = actualEnabledCount - nextIndex; if (currentSort > nextSort) { return false; } } return true; } finally { dbContext.Dispose(); } } /// /// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation** /// Single announcement should be returned correctly. /// **Validates: Requirements 2.3, 1.7** /// [Property(MaxTest = 100)] public bool SortOrderingPreservation_SingleAnnouncement_ReturnsCorrectly(PositiveInt sort) { var actualSort = sort.Get % 1000; var (dbContext, service) = CreateService(); try { // Create single enabled announcement var announcement = CreateAnnouncement(0, isEnabled: true, sort: actualSort); dbContext.PrizeAnnouncements.Add(announcement); dbContext.SaveChanges(); // Get enabled announcements via service var result = service.GetEnabledAnnouncementsAsync().GetAwaiter().GetResult(); // Verify: single announcement is returned return result.Count == 1 && result[0].UserName == "User_0"; } finally { dbContext.Dispose(); } } #endregion }