492 lines
17 KiB
C#
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
|
|
}
|