HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/ContentAuxiliaryFrontendPropertyTests.cs
2026-01-18 11:18:09 +08:00

1372 lines
49 KiB
C#

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;
/// <summary>
/// 内容与辅助模块前端属性测试
/// Feature: content-auxiliary-frontend
/// </summary>
public class ContentAuxiliaryFrontendPropertyTests
{
private readonly Mock<ILogger<FloatBallService>> _mockFloatBallLogger = new();
private readonly Mock<ILogger<WelfareHouseService>> _mockWelfareHouseLogger = new();
#region Property 1:
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递**
/// The total count should remain consistent regardless of which page is requested for FloatBall.
/// **Validates: Requirements 3.4**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递**
/// The total count should remain consistent regardless of which page is requested for WelfareHouse.
/// **Validates: Requirements 7.4**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递**
/// Different pages should return different items (no overlap) for FloatBall.
/// **Validates: Requirements 3.4**
/// </summary>
[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);
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 1: 分页参数正确传递**
/// Different pages should return different items (no overlap) for WelfareHouse.
/// **Validates: Requirements 7.4**
/// </summary>
[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<HoneyBoxDbContext>()
.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
}
/// <summary>
/// 内容与辅助模块前端属性测试 - 第二部分
/// Feature: content-auxiliary-frontend
/// </summary>
public class ContentAuxiliaryFrontendPropertyTests_Part2
{
private readonly Mock<ILogger<FloatBallService>> _mockFloatBallLogger = new();
private readonly Mock<ILogger<WelfareHouseService>> _mockWelfareHouseLogger = new();
#region Property 2:
/// <summary>
/// **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**
/// </summary>
[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(跳转页面)");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When FloatBall image is empty, the system should reject the creation.
/// **Validates: Requirements 4.2**
/// </summary>
[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("悬浮球图片不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When FloatBall position X is empty, the system should reject the creation.
/// **Validates: Requirements 4.2**
/// </summary>
[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轴位置不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When FloatBall position Y is empty, the system should reject the creation.
/// **Validates: Requirements 4.2**
/// </summary>
[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轴位置不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When FloatBall width is empty, the system should reject the creation.
/// **Validates: Requirements 4.2**
/// </summary>
[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("宽度不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When FloatBall height is empty, the system should reject the creation.
/// **Validates: Requirements 4.2**
/// </summary>
[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("高度不能为空");
}
}
/// <summary>
/// **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**
/// </summary>
[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(缩放动画)");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When WelfareHouse name is empty, the system should reject the creation.
/// **Validates: Requirements 8.2**
/// </summary>
[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("名称不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When WelfareHouse image is empty, the system should reject the creation.
/// **Validates: Requirements 8.2**
/// </summary>
[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("图片不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When WelfareHouse URL is empty, the system should reject the creation.
/// **Validates: Requirements 8.2**
/// </summary>
[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("跳转链接不能为空");
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When all required fields are valid, FloatBall creation should succeed.
/// **Validates: Requirements 4.2**
/// </summary>
[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;
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 2: 表单必填字段验证**
/// When all required fields are valid, WelfareHouse creation should succeed.
/// **Validates: Requirements 8.2**
/// </summary>
[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<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new HoneyBoxDbContext(options);
}
#endregion
}
/// <summary>
/// 内容与辅助模块前端属性测试 - 第三部分
/// Feature: content-auxiliary-frontend
/// </summary>
public class ContentAuxiliaryFrontendPropertyTests_Part3
{
private readonly Mock<ILogger<FloatBallService>> _mockFloatBallLogger = new();
#region Property 3:
/// <summary>
/// **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**
/// </summary>
[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;
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换**
/// When FloatBall type is 2 (跳转页面), the LinkUrl field can be provided for navigation.
/// **Validates: Requirements 4.5**
/// </summary>
[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;
}
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换**
/// When FloatBall type changes from 1 to 2, the LinkUrl should be updatable.
/// **Validates: Requirements 4.5**
/// </summary>
[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;
}
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 3: 条件显示字段正确切换**
/// The response should correctly reflect the type and LinkUrl relationship.
/// **Validates: Requirements 4.5**
/// </summary>
[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<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
return new HoneyBoxDbContext(options);
}
#endregion
}
/// <summary>
/// 内容与辅助模块前端属性测试 - 第四部分
/// Feature: content-auxiliary-frontend
/// </summary>
public class ContentAuxiliaryFrontendPropertyTests_Part4
{
private readonly Mock<ILogger<FloatBallService>> _mockFloatBallLogger = new();
private readonly Mock<ILogger<WelfareHouseService>> _mockWelfareHouseLogger = new();
#region Property 4: API响应格式一致性
/// <summary>
/// **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**
/// </summary>
[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);
}
/// <summary>
/// **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**
/// </summary>
[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);
}
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **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**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性**
/// For any FloatBall list item, all required fields should be present.
/// **Validates: Requirements 11.4, 11.5**
/// </summary>
[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);
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性**
/// For any WelfareHouse list item, all required fields should be present.
/// **Validates: Requirements 11.4, 11.5**
/// </summary>
[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);
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性**
/// PagedResult should correctly calculate HasNextPage and HasPreviousPage.
/// **Validates: Requirements 11.4, 11.5**
/// </summary>
[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;
}
/// <summary>
/// **Feature: content-auxiliary-frontend, Property 4: API响应格式一致性**
/// Empty result should return valid PagedResult with empty list.
/// **Validates: Requirements 11.4, 11.5**
/// </summary>
[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<HoneyBoxDbContext>()
.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
}