HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Services/UserBusinessServicePropertyTests.cs
2026-01-17 03:24:20 +08:00

370 lines
13 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using HoneyBox.Admin.Business.Models.User;
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>
/// UserBusinessService 属性测试
/// </summary>
public class UserBusinessServicePropertyTests
{
private readonly Mock<ILogger<UserBusinessService>> _mockLogger = new();
#region Property 4: User List Pagination Consistency
/// <summary>
/// **Feature: admin-business-migration, Property 4: User List Pagination Consistency**
/// For any valid page and pageSize, the returned list should have at most pageSize items,
/// and the total count should be consistent across pages.
/// Validates: Requirements 4.1
/// </summary>
[Property(MaxTest = 50)]
public bool UserListPagination_ShouldReturnCorrectPageSize(PositiveInt seed)
{
var userCount = (seed.Get % 20) + 5; // 5 to 24 users
var pageSize = (seed.Get % 5) + 1; // 1 to 5 per page
var page = (seed.Get % 3) + 1; // page 1 to 3
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test users
for (int i = 0; i < userCount; i++)
{
dbContext.Users.Add(CreateTestUser($"User{i}"));
}
dbContext.SaveChanges();
var request = new UserListRequest { Page = page, PageSize = pageSize };
var result = service.GetUserListAsync(request).GetAwaiter().GetResult();
// Verify pagination consistency
var expectedItemsOnPage = Math.Min(pageSize, Math.Max(0, userCount - (page - 1) * pageSize));
return result.Total == userCount &&
result.List.Count <= pageSize &&
result.Page == page &&
result.PageSize == pageSize;
}
/// <summary>
/// **Feature: admin-business-migration, Property 4: User List Pagination Consistency**
/// The total count should remain consistent regardless of which page is requested.
/// Validates: Requirements 4.1
/// </summary>
[Property(MaxTest = 50)]
public bool UserListPagination_TotalShouldBeConsistentAcrossPages(PositiveInt seed)
{
var userCount = (seed.Get % 15) + 10; // 10 to 24 users
var pageSize = 5;
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test users
for (int i = 0; i < userCount; i++)
{
dbContext.Users.Add(CreateTestUser($"User{i}"));
}
dbContext.SaveChanges();
// Get multiple pages
var page1 = service.GetUserListAsync(new UserListRequest { Page = 1, PageSize = pageSize }).GetAwaiter().GetResult();
var page2 = service.GetUserListAsync(new UserListRequest { Page = 2, PageSize = pageSize }).GetAwaiter().GetResult();
var page3 = service.GetUserListAsync(new UserListRequest { Page = 3, PageSize = pageSize }).GetAwaiter().GetResult();
// Total should be consistent across all pages
return page1.Total == page2.Total && page2.Total == page3.Total && page1.Total == userCount;
}
#endregion
#region Property 5: User List Filter Accuracy
/// <summary>
/// **Feature: admin-business-migration, Property 5: User List Filter Accuracy**
/// When filtering by nickname, all returned users should contain the filter string in their nickname.
/// Validates: Requirements 4.3
/// </summary>
[Property(MaxTest = 50)]
public bool UserListFilter_ByNickname_ShouldReturnMatchingUsers(PositiveInt seed)
{
var filterNames = new[] { "Alice", "Bob", "Charlie", "David" };
var filterName = filterNames[seed.Get % filterNames.Length];
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create users with different nicknames
dbContext.Users.Add(CreateTestUser("Alice"));
dbContext.Users.Add(CreateTestUser("AliceSmith"));
dbContext.Users.Add(CreateTestUser("Bob"));
dbContext.Users.Add(CreateTestUser("BobJones"));
dbContext.Users.Add(CreateTestUser("Charlie"));
dbContext.Users.Add(CreateTestUser("David"));
dbContext.SaveChanges();
var request = new UserListRequest { Nickname = filterName };
var result = service.GetUserListAsync(request).GetAwaiter().GetResult();
// All returned users should contain the filter string
return result.List.All(u => u.Nickname != null && u.Nickname.Contains(filterName));
}
/// <summary>
/// **Feature: admin-business-migration, Property 5: User List Filter Accuracy**
/// When filtering by parent_id, all returned users should have that parent_id.
/// Validates: Requirements 4.3
/// </summary>
[Property(MaxTest = 50)]
public bool UserListFilter_ByParentId_ShouldReturnSubordinates(PositiveInt seed)
{
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create parent user
var parent = CreateTestUser("Parent");
dbContext.Users.Add(parent);
dbContext.SaveChanges();
// Create subordinate users
var subordinateCount = (seed.Get % 5) + 1; // 1 to 5 subordinates
for (int i = 0; i < subordinateCount; i++)
{
var child = CreateTestUser($"Child{i}");
child.Pid = parent.Id;
dbContext.Users.Add(child);
}
// Create other users without parent
for (int i = 0; i < 3; i++)
{
dbContext.Users.Add(CreateTestUser($"Other{i}"));
}
dbContext.SaveChanges();
var request = new UserListRequest { ParentId = parent.Id };
var result = service.GetUserListAsync(request).GetAwaiter().GetResult();
// All returned users should have the specified parent_id
return result.Total == subordinateCount &&
result.List.All(u => u.ParentId == parent.Id);
}
#endregion
#region Property 6: User Balance Change Audit Trail
/// <summary>
/// **Feature: admin-business-migration, Property 6: User Balance Change Audit Trail**
/// For any balance change operation, a corresponding record should be created in the profit table.
/// Validates: Requirements 4.4
/// </summary>
[Property(MaxTest = 50)]
public bool UserBalanceChange_ShouldCreateAuditRecord(PositiveInt seed)
{
var amount = (seed.Get % 100) + 1; // 1 to 100
var isAdd = seed.Get % 2 == 0;
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test user with sufficient balance
var user = CreateTestUser("TestUser");
user.Money = 1000; // Ensure enough balance for subtraction
dbContext.Users.Add(user);
dbContext.SaveChanges();
var request = new UserMoneyChangeRequest
{
Type = MoneyChangeType.Balance,
Amount = amount,
Operation = isAdd ? OperationType.Add : OperationType.Subtract,
Remark = "Property test"
};
var result = service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult();
if (!result) return false;
// Verify audit record was created
var auditRecord = dbContext.ProfitMoneys.FirstOrDefault(p => p.UserId == user.Id);
return auditRecord != null &&
auditRecord.ChangeMoney == (isAdd ? amount : -amount);
}
/// <summary>
/// **Feature: admin-business-migration, Property 6: User Balance Change Audit Trail**
/// After a balance change, the user's balance should reflect the change accurately.
/// Validates: Requirements 4.4
/// </summary>
[Property(MaxTest = 50)]
public bool UserBalanceChange_ShouldUpdateBalanceAccurately(PositiveInt seed)
{
var initialBalance = (seed.Get % 500) + 100; // 100 to 599
var changeAmount = (seed.Get % 50) + 1; // 1 to 50
var isAdd = seed.Get % 2 == 0;
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test user
var user = CreateTestUser("TestUser");
user.Money = initialBalance;
dbContext.Users.Add(user);
dbContext.SaveChanges();
var request = new UserMoneyChangeRequest
{
Type = MoneyChangeType.Balance,
Amount = changeAmount,
Operation = isAdd ? OperationType.Add : OperationType.Subtract
};
service.ChangeUserMoneyAsync(user.Id, request, 1).GetAwaiter().GetResult();
// Refresh user from database
var updatedUser = dbContext.Users.Find(user.Id);
var expectedBalance = isAdd ? initialBalance + changeAmount : initialBalance - changeAmount;
return updatedUser!.Money == expectedBalance;
}
#endregion
#region Property 7: User Status Toggle Consistency
/// <summary>
/// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency**
/// Toggling user status (ban/unban) should correctly update the status field.
/// Validates: Requirements 4.5, 4.6
/// </summary>
[Property(MaxTest = 50)]
public bool UserStatusToggle_ShouldUpdateStatusCorrectly(PositiveInt seed)
{
var initialStatus = (byte)(seed.Get % 2); // 0 or 1
var newStatus = (byte)(1 - initialStatus); // Toggle
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test user
var user = CreateTestUser("TestUser");
user.Status = initialStatus;
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Toggle status
var result = service.SetUserStatusAsync(user.Id, newStatus).GetAwaiter().GetResult();
if (!result) return false;
// Verify status was updated
var updatedUser = dbContext.Users.Find(user.Id);
return updatedUser!.Status == newStatus;
}
/// <summary>
/// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency**
/// Setting the same status multiple times should be idempotent.
/// Validates: Requirements 4.5, 4.6
/// </summary>
[Property(MaxTest = 50)]
public bool UserStatusToggle_ShouldBeIdempotent(PositiveInt seed)
{
var status = (byte)(seed.Get % 2); // 0 or 1
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test user
var user = CreateTestUser("TestUser");
user.Status = status;
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Set same status multiple times
service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult();
service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult();
service.SetUserStatusAsync(user.Id, status).GetAwaiter().GetResult();
// Verify status remains the same
var updatedUser = dbContext.Users.Find(user.Id);
return updatedUser!.Status == status;
}
/// <summary>
/// **Feature: admin-business-migration, Property 7: User Status Toggle Consistency**
/// Test account flag toggle should correctly update the is_test field.
/// Validates: Requirements 4.7
/// </summary>
[Property(MaxTest = 50)]
public bool TestAccountToggle_ShouldUpdateFlagCorrectly(PositiveInt seed)
{
var initialIsTest = seed.Get % 2; // 0 or 1
var newIsTest = 1 - initialIsTest; // Toggle
using var dbContext = CreateDbContext();
var service = new UserBusinessService(dbContext, _mockLogger.Object);
// Create test user
var user = CreateTestUser("TestUser");
user.IsTest = initialIsTest;
dbContext.Users.Add(user);
dbContext.SaveChanges();
// Toggle test account flag
var result = service.SetTestAccountAsync(user.Id, newIsTest).GetAwaiter().GetResult();
if (!result) return false;
// Verify flag was updated
var updatedUser = dbContext.Users.Find(user.Id);
return updatedUser!.IsTest == newIsTest;
}
#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 User CreateTestUser(string nickname)
{
return new User
{
OpenId = Guid.NewGuid().ToString("N"),
Uid = $"UID{DateTime.Now.Ticks}{Guid.NewGuid():N}".Substring(0, 20),
Nickname = nickname,
HeadImg = "https://example.com/avatar.png",
Mobile = $"138{new Random().Next(10000000, 99999999)}",
Money = 100,
Integral = 50,
Score = 30,
Status = 1,
IsTest = 0,
Pid = 0,
Vip = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
}
#endregion
}