HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/PrizeAnnouncementServicePropertyTests.cs
2026-02-02 07:59:16 +08:00

492 lines
17 KiB
C#

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;
/// <summary>
/// PrizeAnnouncementService 属性测试(用户端)
/// 测试首页中大奖公告功能的用户端核心属性
/// </summary>
public class PrizeAnnouncementServicePropertyTests
{
private readonly Mock<ILogger<PrizeAnnouncementService>> _mockLogger = new();
private (HoneyBoxDbContext dbContext, PrizeAnnouncementService service) CreateService()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
var dbContext = new HoneyBoxDbContext(options);
var service = new PrizeAnnouncementService(dbContext, _mockLogger.Object);
return (dbContext, service);
}
/// <summary>
/// 生成有效的非空字符串(用于必填字段)
/// </summary>
private static string GenerateValidString(string prefix, int seed)
{
return $"{prefix}_{Math.Abs(seed) % 1000}";
}
/// <summary>
/// 创建测试用的公告实体
/// </summary>
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
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **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**
/// </summary>
[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();
}
}
/// <summary>
/// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation**
/// Only enabled announcements should be sorted and returned.
/// **Validates: Requirements 2.3, 1.7**
/// </summary>
[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();
}
}
/// <summary>
/// **Feature: homepage-prize-announcement, Property 3: Sort Ordering Preservation**
/// Single announcement should be returned correctly.
/// **Validates: Requirements 2.3, 1.7**
/// </summary>
[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
}