370 lines
13 KiB
C#
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
|
|
}
|