using FsCheck; using FsCheck.Xunit; using HoneyBox.Admin.Business.Models; using HoneyBox.Admin.Business.Models.FloatBall; using HoneyBox.Admin.Business.Models.WelfareHouse; using HoneyBox.Admin.Business.Services; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using Moq; using Xunit; namespace HoneyBox.Tests.Services; /// /// 内容与辅助模块前端属性测试 /// Feature: content-auxiliary-frontend /// public class ContentAuxiliaryFrontendPropertyTests { private readonly Mock> _mockFloatBallLogger = new(); private readonly Mock> _mockWelfareHouseLogger = new(); #region Property 1: 分页参数正确传递 /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// For any pagination request to FloatBall list, the returned list should have at most pageSize items, /// and the page and pageSize in response should match the request. /// **Validates: Requirements 3.4** /// [Property(MaxTest = 100)] public bool FloatBallPagination_ShouldReturnCorrectPageSize(PositiveInt seed) { var itemCount = (seed.Get % 30) + 10; // 10 to 39 items var pageSize = (seed.Get % 10) + 1; // 1 to 10 per page var page = (seed.Get % 5) + 1; // page 1 to 5 using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); // Verify pagination parameters are correctly passed return result.Total == itemCount && result.List.Count <= pageSize && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// For any pagination request to WelfareHouse list, the returned list should have at most pageSize items, /// and the page and pageSize in response should match the request. /// **Validates: Requirements 7.4** /// [Property(MaxTest = 100)] public bool WelfareHousePagination_ShouldReturnCorrectPageSize(PositiveInt seed) { var itemCount = (seed.Get % 30) + 10; // 10 to 39 items var pageSize = (seed.Get % 10) + 1; // 1 to 10 per page var page = (seed.Get % 5) + 1; // page 1 to 5 using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Create test welfare house entries for (int i = 0; i < itemCount; i++) { dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); } dbContext.SaveChanges(); var request = new WelfareHouseListRequest { Page = page, PageSize = pageSize }; var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); // Verify pagination parameters are correctly passed return result.Total == itemCount && result.List.Count <= pageSize && result.Page == page && result.PageSize == pageSize; } /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// The total count should remain consistent regardless of which page is requested for FloatBall. /// **Validates: Requirements 3.4** /// [Property(MaxTest = 100)] public bool FloatBallPagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed) { var itemCount = (seed.Get % 20) + 15; // 15 to 34 items var pageSize = 5; using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); // Get multiple pages var page1 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); var page3 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult(); // Total should be consistent across all pages return page1.Total == page2.Total && page2.Total == page3.Total && page1.Total == itemCount; } /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// The total count should remain consistent regardless of which page is requested for WelfareHouse. /// **Validates: Requirements 7.4** /// [Property(MaxTest = 100)] public bool WelfareHousePagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed) { var itemCount = (seed.Get % 20) + 15; // 15 to 34 items var pageSize = 5; using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Create test welfare house entries for (int i = 0; i < itemCount; i++) { dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); } dbContext.SaveChanges(); // Get multiple pages var page1 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); var page3 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult(); // Total should be consistent across all pages return page1.Total == page2.Total && page2.Total == page3.Total && page1.Total == itemCount; } /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// Different pages should return different items (no overlap) for FloatBall. /// **Validates: Requirements 3.4** /// [Property(MaxTest = 100)] public bool FloatBallPagination_DifferentPagesShouldNotOverlap(PositiveInt seed) { var itemCount = (seed.Get % 15) + 20; // 20 to 34 items var pageSize = 5; using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); // Get first two pages var page1 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetFloatBallsAsync(new FloatBallListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); // IDs should not overlap between pages var page1Ids = page1.List.Select(f => f.Id).ToHashSet(); var page2Ids = page2.List.Select(f => f.Id).ToHashSet(); return !page1Ids.Overlaps(page2Ids); } /// /// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递** /// Different pages should return different items (no overlap) for WelfareHouse. /// **Validates: Requirements 7.4** /// [Property(MaxTest = 100)] public bool WelfareHousePagination_DifferentPagesShouldNotOverlap(PositiveInt seed) { var itemCount = (seed.Get % 15) + 20; // 20 to 34 items var pageSize = 5; using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Create test welfare house entries for (int i = 0; i < itemCount; i++) { dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); } dbContext.SaveChanges(); // Get first two pages var page1 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult(); var page2 = service.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult(); // IDs should not overlap between pages var page1Ids = page1.List.Select(w => w.Id).ToHashSet(); var page2Ids = page2.List.Select(w => w.Id).ToHashSet(); return !page1Ids.Overlaps(page2Ids); } #endregion #region Helper Methods private HoneyBoxDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } private FloatBallConfig CreateTestFloatBall(string title) { return new FloatBallConfig { Title = title, Type = 1, Image = "http://test.com/floatball.jpg", LinkUrl = string.Empty, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; } private WelfareHouse CreateTestWelfareHouse(string name, int sort) { return new WelfareHouse { Name = name, Image = "http://test.com/welfare.jpg", Url = "/welfare/test", Sort = sort, Status = 1, CreateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() }; } #endregion } /// /// 内容与辅助模块前端属性测试 - 第二部分 /// Feature: content-auxiliary-frontend /// public class ContentAuxiliaryFrontendPropertyTests_Part2 { private readonly Mock> _mockFloatBallLogger = new(); private readonly Mock> _mockWelfareHouseLogger = new(); #region Property 2: 表单必填字段验证 /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall type is invalid (not 1 or 2), the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithInvalidType_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Use invalid type values var invalidTypes = new[] { 0, 3, 4, 5, -1, 100 }; var invalidType = invalidTypes[seed.Get % invalidTypes.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = invalidType, Image = "http://test.com/img.jpg", PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("类型必须为1(展示图片)或2(跳转页面)"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall image is empty, the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithEmptyImage_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var emptyImages = new[] { "", " ", null }; var emptyImage = emptyImages[seed.Get % emptyImages.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = emptyImage ?? string.Empty, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("悬浮球图片不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall position X is empty, the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithEmptyPositionX_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var emptyValues = new[] { "", " " }; var emptyValue = emptyValues[seed.Get % emptyValues.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = "http://test.com/img.jpg", PositionX = emptyValue, PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("X轴位置不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall position Y is empty, the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithEmptyPositionY_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var emptyValues = new[] { "", " " }; var emptyValue = emptyValues[seed.Get % emptyValues.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = "http://test.com/img.jpg", PositionX = "10", PositionY = emptyValue, Width = "50", Height = "50", Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("Y轴位置不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall width is empty, the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithEmptyWidth_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var emptyValues = new[] { "", " " }; var emptyValue = emptyValues[seed.Get % emptyValues.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = "http://test.com/img.jpg", PositionX = "10", PositionY = "20", Width = emptyValue, Height = "50", Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("宽度不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall height is empty, the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithEmptyHeight_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var emptyValues = new[] { "", " " }; var emptyValue = emptyValues[seed.Get % emptyValues.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = "http://test.com/img.jpg", PositionX = "10", PositionY = "20", Width = "50", Height = emptyValue, Effect = 0, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("高度不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When FloatBall effect is invalid (not 0 or 1), the system should reject the creation. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithInvalidEffect_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Use invalid effect values var invalidEffects = new[] { 2, 3, -1, 100 }; var invalidEffect = invalidEffects[seed.Get % invalidEffects.Length]; var request = new FloatBallCreateRequest { Title = "Test FloatBall", Type = 1, Image = "http://test.com/img.jpg", PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = invalidEffect, Status = 1 }; try { service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("特效必须为0(无)或1(缩放动画)"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When WelfareHouse name is empty, the system should reject the creation. /// **Validates: Requirements 8.2** /// [Property(MaxTest = 100)] public bool WelfareHouseCreate_WithEmptyName_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); var emptyNames = new[] { "", " " }; var emptyName = emptyNames[seed.Get % emptyNames.Length]; var request = new WelfareHouseCreateRequest { Name = emptyName, Image = "http://test.com/img.jpg", Url = "/welfare/test", Sort = 1, Status = 1 }; try { service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("名称不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When WelfareHouse image is empty, the system should reject the creation. /// **Validates: Requirements 8.2** /// [Property(MaxTest = 100)] public bool WelfareHouseCreate_WithEmptyImage_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); var emptyImages = new[] { "", " " }; var emptyImage = emptyImages[seed.Get % emptyImages.Length]; var request = new WelfareHouseCreateRequest { Name = "Test WelfareHouse", Image = emptyImage, Url = "/welfare/test", Sort = 1, Status = 1 }; try { service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("图片不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When WelfareHouse URL is empty, the system should reject the creation. /// **Validates: Requirements 8.2** /// [Property(MaxTest = 100)] public bool WelfareHouseCreate_WithEmptyUrl_ShouldFail(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); var emptyUrls = new[] { "", " " }; var emptyUrl = emptyUrls[seed.Get % emptyUrls.Length]; var request = new WelfareHouseCreateRequest { Name = "Test WelfareHouse", Image = "http://test.com/img.jpg", Url = emptyUrl, Sort = 1, Status = 1 }; try { service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); return false; // Should have thrown exception } catch (BusinessException ex) { return ex.Message.Contains("跳转链接不能为空"); } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When all required fields are valid, FloatBall creation should succeed. /// **Validates: Requirements 4.2** /// [Property(MaxTest = 100)] public bool FloatBallCreate_WithValidData_ShouldSucceed(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var validTypes = new[] { 1, 2 }; var validEffects = new[] { 0, 1 }; var request = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = validTypes[seed.Get % validTypes.Length], Image = "http://test.com/img.jpg", PositionX = (seed.Get % 100).ToString(), PositionY = (seed.Get % 100).ToString(), Width = ((seed.Get % 50) + 20).ToString(), Height = ((seed.Get % 50) + 20).ToString(), Effect = validEffects[seed.Get % validEffects.Length], Status = 1 }; try { var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); var created = dbContext.FloatBallConfigs.Find(id); return created != null && created.Type == request.Type && created.Image == request.Image && created.PositionX == request.PositionX && created.PositionY == request.PositionY && created.Width == request.Width && created.Height == request.Height && created.Effect == request.Effect; } catch { return false; } } /// /// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证** /// When all required fields are valid, WelfareHouse creation should succeed. /// **Validates: Requirements 8.2** /// [Property(MaxTest = 100)] public bool WelfareHouseCreate_WithValidData_ShouldSucceed(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); var request = new WelfareHouseCreateRequest { Name = $"Test WelfareHouse {seed.Get}", Image = "http://test.com/img.jpg", Url = $"/welfare/test{seed.Get}", Sort = seed.Get % 100, Status = 1 }; try { var id = service.CreateWelfareHouseAsync(request).GetAwaiter().GetResult(); var created = dbContext.WelfareHouses.Find(id); return created != null && created.Name == request.Name && created.Image == request.Image && created.Url == request.Url && created.Sort == request.Sort; } catch { return false; } } #endregion #region Helper Methods private HoneyBoxDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } #endregion } /// /// 内容与辅助模块前端属性测试 - 第三部分 /// Feature: content-auxiliary-frontend /// public class ContentAuxiliaryFrontendPropertyTests_Part3 { private readonly Mock> _mockFloatBallLogger = new(); #region Property 3: 条件显示字段正确切换 /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// When FloatBall type is 1 (展示图片), the LinkUrl field should be optional and can be empty. /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallType1_LinkUrlShouldBeOptional(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Type 1 = 展示图片, LinkUrl should be optional var request = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = 1, // 展示图片 Image = "http://test.com/img.jpg", LinkUrl = null, // Empty link URL PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; try { var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); var created = dbContext.FloatBallConfigs.Find(id); // For type 1, creation should succeed even without LinkUrl return created != null && created.Type == 1; } catch { return false; } } /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// When FloatBall type is 2 (跳转页面), the LinkUrl field can be provided for navigation. /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallType2_LinkUrlShouldBeUsed(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var linkUrl = $"/page/test{seed.Get}"; // Type 2 = 跳转页面, LinkUrl should be used var request = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = 2, // 跳转页面 Image = "http://test.com/img.jpg", LinkUrl = linkUrl, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; try { var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); var created = dbContext.FloatBallConfigs.Find(id); // For type 2, LinkUrl should be stored correctly return created != null && created.Type == 2 && created.LinkUrl == linkUrl; } catch { return false; } } /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// When FloatBall type changes from 1 to 2, the LinkUrl should be updatable. /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallTypeChange_LinkUrlShouldBeUpdatable(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create with type 1 (no link) var createRequest = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = 1, Image = "http://test.com/img.jpg", LinkUrl = null, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var id = service.CreateFloatBallAsync(createRequest).GetAwaiter().GetResult(); // Update to type 2 with link var newLinkUrl = $"/page/updated{seed.Get}"; var updateRequest = new FloatBallUpdateRequest { Title = $"Test FloatBall {seed.Get}", Type = 2, // Change to 跳转页面 Image = "http://test.com/img.jpg", LinkUrl = newLinkUrl, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var result = service.UpdateFloatBallAsync(id, updateRequest).GetAwaiter().GetResult(); if (!result) return false; var updated = dbContext.FloatBallConfigs.Find(id); return updated != null && updated.Type == 2 && updated.LinkUrl == newLinkUrl; } /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// When FloatBall type changes from 2 to 1, the LinkUrl should be preserved but not used. /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallTypeChange_FromType2ToType1_ShouldPreserveLinkUrl(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var originalLinkUrl = $"/page/original{seed.Get}"; // Create with type 2 (with link) var createRequest = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = 2, Image = "http://test.com/img.jpg", LinkUrl = originalLinkUrl, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var id = service.CreateFloatBallAsync(createRequest).GetAwaiter().GetResult(); // Update to type 1 (展示图片) var updateRequest = new FloatBallUpdateRequest { Title = $"Test FloatBall {seed.Get}", Type = 1, // Change to 展示图片 Image = "http://test.com/img.jpg", LinkUrl = originalLinkUrl, // Keep the link URL PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var result = service.UpdateFloatBallAsync(id, updateRequest).GetAwaiter().GetResult(); if (!result) return false; var updated = dbContext.FloatBallConfigs.Find(id); // Type should be 1, and LinkUrl should be preserved (even if not used) return updated != null && updated.Type == 1 && updated.LinkUrl == originalLinkUrl; } /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// For any FloatBall, the type field should correctly determine the behavior. /// Type 1 = 展示图片 (show image), Type 2 = 跳转页面 (jump to page). /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallType_ShouldDetermineBehavior(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var validTypes = new[] { 1, 2 }; var selectedType = validTypes[seed.Get % validTypes.Length]; var linkUrl = selectedType == 2 ? $"/page/test{seed.Get}" : null; var request = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = selectedType, Image = "http://test.com/img.jpg", LinkUrl = linkUrl, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); var created = dbContext.FloatBallConfigs.Find(id); // Verify type is correctly stored return created != null && created.Type == selectedType; } /// /// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换** /// The response should correctly reflect the type and LinkUrl relationship. /// **Validates: Requirements 4.5** /// [Property(MaxTest = 100)] public bool FloatBallResponse_ShouldReflectTypeAndLinkUrl(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var validTypes = new[] { 1, 2 }; var selectedType = validTypes[seed.Get % validTypes.Length]; var linkUrl = selectedType == 2 ? $"/page/test{seed.Get}" : string.Empty; var request = new FloatBallCreateRequest { Title = $"Test FloatBall {seed.Get}", Type = selectedType, Image = "http://test.com/img.jpg", LinkUrl = linkUrl, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1 }; var id = service.CreateFloatBallAsync(request).GetAwaiter().GetResult(); var response = service.GetFloatBallByIdAsync(id).GetAwaiter().GetResult(); // Verify response correctly reflects type and LinkUrl return response != null && response.Type == selectedType && response.LinkUrl == (linkUrl ?? string.Empty); } #endregion #region Helper Methods private HoneyBoxDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } #endregion } /// /// 内容与辅助模块前端属性测试 - 第四部分 /// Feature: content-auxiliary-frontend /// public class ContentAuxiliaryFrontendPropertyTests_Part4 { private readonly Mock> _mockFloatBallLogger = new(); private readonly Mock> _mockWelfareHouseLogger = new(); #region Property 4: API响应格式一致性 /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any FloatBall list API response, the response format should conform to the unified /// PagedResult structure with correct pagination parameters. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool FloatBallApiResponse_ShouldHaveConsistentPagedStructure(PositiveInt seed) { var itemCount = (seed.Get % 20) + 5; var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize && result.TotalPages == (int)Math.Ceiling((double)result.Total / result.PageSize); } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any WelfareHouse list API response, the response format should conform to the unified /// PagedResult structure with correct pagination parameters. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool WelfareHouseApiResponse_ShouldHaveConsistentPagedStructure(PositiveInt seed) { var itemCount = (seed.Get % 20) + 5; var page = (seed.Get % 3) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Create test welfare house entries for (int i = 0; i < itemCount; i++) { dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); } dbContext.SaveChanges(); var request = new WelfareHouseListRequest { Page = page, PageSize = pageSize }; var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); // Verify PagedResult structure return result != null && result.List != null && result.Total >= 0 && result.Page == page && result.PageSize == pageSize && result.TotalPages == (int)Math.Ceiling((double)result.Total / result.PageSize); } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any FloatBall detail API response, all required fields should be present. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool FloatBallDetailResponse_ShouldHaveAllRequiredFields(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var floatBall = CreateTestFloatBall($"FloatBall{seed.Get}"); dbContext.FloatBallConfigs.Add(floatBall); dbContext.SaveChanges(); var response = service.GetFloatBallByIdAsync(floatBall.Id).GetAwaiter().GetResult(); // Verify all required fields are present return response != null && response.Id > 0 && !string.IsNullOrEmpty(response.Image) && !string.IsNullOrEmpty(response.PositionX) && !string.IsNullOrEmpty(response.PositionY) && !string.IsNullOrEmpty(response.Width) && !string.IsNullOrEmpty(response.Height) && response.Type >= 1 && response.Type <= 2 && response.Effect >= 0 && response.Effect <= 1 && response.Status >= 0 && response.Status <= 1; } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any WelfareHouse detail API response, all required fields should be present. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool WelfareHouseDetailResponse_ShouldHaveAllRequiredFields(PositiveInt seed) { using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); var welfareHouse = CreateTestWelfareHouse($"WelfareHouse{seed.Get}", seed.Get % 100); dbContext.WelfareHouses.Add(welfareHouse); dbContext.SaveChanges(); var response = service.GetWelfareHouseByIdAsync(welfareHouse.Id).GetAwaiter().GetResult(); // Verify all required fields are present return response != null && response.Id > 0 && !string.IsNullOrEmpty(response.Name) && !string.IsNullOrEmpty(response.Image) && !string.IsNullOrEmpty(response.Url) && response.Sort >= 0 && response.Status >= 0 && response.Status <= 1; } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any FloatBall list item, all required fields should be present. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool FloatBallListItem_ShouldHaveAllRequiredFields(PositiveInt seed) { var itemCount = (seed.Get % 10) + 1; using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); var request = new FloatBallListRequest { Page = 1, PageSize = 100 }; var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); // Verify all items have required fields return result.List.All(item => item.Id > 0 && !string.IsNullOrEmpty(item.Image) && !string.IsNullOrEmpty(item.PositionX) && !string.IsNullOrEmpty(item.PositionY) && !string.IsNullOrEmpty(item.Width) && !string.IsNullOrEmpty(item.Height) && item.Type >= 1 && item.Type <= 2 && item.Effect >= 0 && item.Effect <= 1 && item.Status >= 0 && item.Status <= 1); } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// For any WelfareHouse list item, all required fields should be present. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool WelfareHouseListItem_ShouldHaveAllRequiredFields(PositiveInt seed) { var itemCount = (seed.Get % 10) + 1; using var dbContext = CreateDbContext(); var service = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Create test welfare house entries for (int i = 0; i < itemCount; i++) { dbContext.WelfareHouses.Add(CreateTestWelfareHouse($"WelfareHouse{i}", i)); } dbContext.SaveChanges(); var request = new WelfareHouseListRequest { Page = 1, PageSize = 100 }; var result = service.GetWelfareHousesAsync(request).GetAwaiter().GetResult(); // Verify all items have required fields return result.List.All(item => item.Id > 0 && !string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Image) && !string.IsNullOrEmpty(item.Url) && item.Sort >= 0 && item.Status >= 0 && item.Status <= 1); } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// PagedResult should correctly calculate HasNextPage and HasPreviousPage. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool PagedResult_ShouldCorrectlyCalculateNavigationFlags(PositiveInt seed) { var itemCount = (seed.Get % 30) + 15; // 15 to 44 items var pageSize = 5; var totalPages = (int)Math.Ceiling((double)itemCount / pageSize); var page = (seed.Get % totalPages) + 1; // Valid page number using var dbContext = CreateDbContext(); var service = new FloatBallService(dbContext, _mockFloatBallLogger.Object); // Create test float balls for (int i = 0; i < itemCount; i++) { dbContext.FloatBallConfigs.Add(CreateTestFloatBall($"FloatBall{i}")); } dbContext.SaveChanges(); var request = new FloatBallListRequest { Page = page, PageSize = pageSize }; var result = service.GetFloatBallsAsync(request).GetAwaiter().GetResult(); // Verify navigation flags var expectedHasNextPage = page < result.TotalPages; var expectedHasPreviousPage = page > 1; return result.HasNextPage == expectedHasNextPage && result.HasPreviousPage == expectedHasPreviousPage; } /// /// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性** /// Empty result should return valid PagedResult with empty list. /// **Validates: Requirements 11.4, 11.5** /// [Property(MaxTest = 100)] public bool EmptyResult_ShouldReturnValidPagedResult(PositiveInt seed) { var page = (seed.Get % 5) + 1; var pageSize = (seed.Get % 10) + 5; using var dbContext = CreateDbContext(); var floatBallService = new FloatBallService(dbContext, _mockFloatBallLogger.Object); var welfareHouseService = new WelfareHouseService(dbContext, _mockWelfareHouseLogger.Object); // Don't add any data - test empty result var floatBallResult = floatBallService.GetFloatBallsAsync(new FloatBallListRequest { Page = page, PageSize = pageSize }).GetAwaiter().GetResult(); var welfareHouseResult = welfareHouseService.GetWelfareHousesAsync(new WelfareHouseListRequest { Page = page, PageSize = pageSize }).GetAwaiter().GetResult(); // Verify empty results have valid structure return floatBallResult != null && floatBallResult.List != null && floatBallResult.List.Count == 0 && floatBallResult.Total == 0 && floatBallResult.Page == page && floatBallResult.PageSize == pageSize && welfareHouseResult != null && welfareHouseResult.List != null && welfareHouseResult.List.Count == 0 && welfareHouseResult.Total == 0 && welfareHouseResult.Page == page && welfareHouseResult.PageSize == pageSize; } #endregion #region Helper Methods private HoneyBoxDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; return new HoneyBoxDbContext(options); } private FloatBallConfig CreateTestFloatBall(string title) { return new FloatBallConfig { Title = title, Type = 1, Image = "http://test.com/floatball.jpg", LinkUrl = string.Empty, PositionX = "10", PositionY = "20", Width = "50", Height = "50", Effect = 0, Status = 1, CreatedAt = DateTime.Now, UpdatedAt = DateTime.Now }; } private WelfareHouse CreateTestWelfareHouse(string name, int sort) { return new WelfareHouse { Name = name, Image = "http://test.com/welfare.jpg", Url = "/welfare/test", Sort = sort, Status = 1, CreateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds(), UpdateTime = (int)DateTimeOffset.Now.ToUnixTimeSeconds() }; } #endregion }