mi-assessment/server/MiAssessment/tests/MiAssessment.Tests/Services/FinanceServicePropertyTests.cs
2026-02-03 14:25:01 +08:00

348 lines
12 KiB
C#

using FsCheck;
using FsCheck.Xunit;
using MiAssessment.Admin.Business.Models.Finance;
using MiAssessment.Admin.Business.Services;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace MiAssessment.Tests.Services;
/// <summary>
/// FinanceService 属性测试
/// </summary>
public class FinanceServicePropertyTests
{
private readonly Mock<ILogger<FinanceService>> _mockLogger = new();
#region Property 13: Consumption Ranking Sort Order
/// <summary>
/// **Feature: admin-business-migration, Property 13: Consumption Ranking Sort Order**
/// For any consumption ranking request, the returned users should be sorted
/// by total consumption in descending order.
/// Validates: Requirements 7.1
/// </summary>
[Property(MaxTest = 100)]
public bool ConsumptionRanking_ShouldBeSortedByTotalConsumptionDescending(PositiveInt userCount, PositiveInt orderCount)
{
var actualUserCount = (userCount.Get % 10) + 2; // 2-11 users
var actualOrderCount = (orderCount.Get % 30) + 10; // 10-39 orders
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new MiAssessmentDbContext(options);
var service = new FinanceService(dbContext, _mockLogger.Object);
// Seed users
SeedUsers(dbContext, actualUserCount);
// Seed orders with random amounts for random users
var random = new Random(userCount.Get);
for (int i = 0; i < actualOrderCount; i++)
{
var userId = random.Next(1, actualUserCount + 1);
var price = random.Next(10, 1000);
dbContext.Orders.Add(CreateOrder(userId, $"ORD{i:D5}", price));
}
dbContext.SaveChanges();
// Get consumption ranking
var request = new FinanceQueryRequest { Page = 1, PageSize = 100 };
var result = service.GetConsumptionRankingAsync(request).GetAwaiter().GetResult();
// Verify sorted by total consumption descending
for (int i = 0; i < result.List.Count - 1; i++)
{
if (result.List[i].TotalConsumption < result.List[i + 1].TotalConsumption)
{
return false;
}
}
return true;
}
/// <summary>
/// **Feature: admin-business-migration, Property 13: Consumption Ranking Sort Order**
/// For any consumption ranking, the total consumption should equal the sum of
/// WeChat, balance, and integral payments.
/// Validates: Requirements 7.1, 7.2
/// </summary>
[Property(MaxTest = 100)]
public bool ConsumptionRanking_TotalShouldEqualSumOfPayments(PositiveInt seed)
{
var userCount = (seed.Get % 5) + 2; // 2-6 users
var orderCount = (seed.Get % 20) + 5; // 5-24 orders
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new MiAssessmentDbContext(options);
var service = new FinanceService(dbContext, _mockLogger.Object);
// Seed users
SeedUsers(dbContext, userCount);
// Seed orders
var random = new Random(seed.Get);
for (int i = 0; i < orderCount; i++)
{
var userId = random.Next(1, userCount + 1);
var price = random.Next(100, 500);
var useMoney = random.Next(0, price / 3);
var useIntegral = random.Next(0, price / 3);
dbContext.Orders.Add(new Order
{
UserId = userId,
OrderNum = $"ORD{i:D5}",
Price = price,
UseMoney = useMoney,
UseIntegral = useIntegral,
UseScore = 0,
Status = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
GoodsId = 1,
GoodsTitle = "商品",
GoodsPrice = price,
OrderTotal = price,
OrderZheTotal = price,
Zhe = 1,
Num = 1,
PrizeNum = 1,
Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds()
});
}
dbContext.SaveChanges();
// Get consumption ranking
var request = new FinanceQueryRequest { Page = 1, PageSize = 100 };
var result = service.GetConsumptionRankingAsync(request).GetAwaiter().GetResult();
// Verify total equals sum of payments
foreach (var item in result.List)
{
var expectedTotal = item.WeChatPayment + item.BalancePayment + item.IntegralPayment;
if (Math.Abs(item.TotalConsumption - expectedTotal) > 0.01m)
{
return false;
}
}
return true;
}
#endregion
#region Property 14: Financial Query Filter Accuracy
/// <summary>
/// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy**
/// For any balance detail query with user ID filter, all returned records
/// should belong to the specified user.
/// Validates: Requirements 7.7
/// </summary>
[Property(MaxTest = 100)]
public bool BalanceDetails_FilterByUserId_ShouldReturnOnlyMatchingRecords(PositiveInt userId, PositiveInt recordCount)
{
var actualUserId = (userId.Get % 5) + 1; // 1-5
var actualRecordCount = (recordCount.Get % 20) + 10; // 10-29 records
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new MiAssessmentDbContext(options);
var service = new FinanceService(dbContext, _mockLogger.Object);
// Seed users
SeedUsers(dbContext, 5);
// Seed profit money records for multiple users
var random = new Random(userId.Get);
for (int i = 0; i < actualRecordCount; i++)
{
var recordUserId = random.Next(1, 6); // Random user 1-5
dbContext.ProfitMoneys.Add(new ProfitMoney
{
UserId = recordUserId,
ChangeMoney = 100,
Money = 100,
Type = 1,
Content = $"变动{i}",
ShareUid = 0,
CreatedAt = DateTime.Now
});
}
dbContext.SaveChanges();
// Query with user ID filter
var request = new FinanceQueryRequest { UserId = actualUserId, Page = 1, PageSize = 100 };
var result = service.GetBalanceDetailsAsync(request).GetAwaiter().GetResult();
// Verify all returned records belong to the specified user
return result.List.All(r => r.UserId == actualUserId);
}
/// <summary>
/// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy**
/// For any integral detail query with date range filter, all returned records
/// should fall within the specified date range.
/// Validates: Requirements 7.7
/// </summary>
[Property(MaxTest = 100)]
public bool IntegralDetails_FilterByDateRange_ShouldReturnOnlyMatchingRecords(PositiveInt seed)
{
var recordCount = (seed.Get % 20) + 10; // 10-29 records
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new MiAssessmentDbContext(options);
var service = new FinanceService(dbContext, _mockLogger.Object);
// Seed users
SeedUsers(dbContext, 1);
// Seed profit integral records with various dates
var baseDate = DateTime.Today;
for (int i = 0; i < recordCount; i++)
{
var daysOffset = (i % 21) - 10; // -10 to +10 days
dbContext.ProfitIntegrals.Add(new ProfitIntegral
{
UserId = 1,
ChangeMoney = 50,
Money = 50,
Type = 1,
Content = $"积分{i}",
ShareUid = 0,
CreatedAt = baseDate.AddDays(daysOffset)
});
}
dbContext.SaveChanges();
// Query with date range filter (last 5 days)
var startDate = baseDate.AddDays(-5);
var endDate = baseDate;
var request = new FinanceQueryRequest
{
StartDate = startDate,
EndDate = endDate,
Page = 1,
PageSize = 100
};
var result = service.GetIntegralDetailsAsync(request).GetAwaiter().GetResult();
// Verify all returned records fall within the date range
var effectiveEndDate = endDate.AddDays(1);
return result.List.All(r => r.CreatedAt >= startDate && r.CreatedAt < effectiveEndDate);
}
/// <summary>
/// **Feature: admin-business-migration, Property 14: Financial Query Filter Accuracy**
/// For any recharge record query with user ID filter, all returned records
/// should belong to the specified user.
/// Validates: Requirements 7.7
/// </summary>
[Property(MaxTest = 100)]
public bool RechargeRecords_FilterByUserId_ShouldReturnOnlyMatchingRecords(PositiveInt userId, PositiveInt recordCount)
{
var actualUserId = (userId.Get % 3) + 1; // 1-3
var actualRecordCount = (recordCount.Get % 15) + 5; // 5-19 records
var options = new DbContextOptionsBuilder<MiAssessmentDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var dbContext = new MiAssessmentDbContext(options);
var service = new FinanceService(dbContext, _mockLogger.Object);
// Seed users
SeedUsers(dbContext, 3);
// Seed profit pay records for multiple users
var random = new Random(userId.Get);
for (int i = 0; i < actualRecordCount; i++)
{
var recordUserId = random.Next(1, 4); // Random user 1-3
dbContext.ProfitPays.Add(new ProfitPay
{
UserId = recordUserId,
OrderNum = $"PAY{i:D5}",
ChangeMoney = 100,
Content = $"充值{i}",
PayType = 0,
CreatedAt = DateTime.Now
});
}
dbContext.SaveChanges();
// Query with user ID filter
var request = new FinanceQueryRequest { UserId = actualUserId, Page = 1, PageSize = 100 };
var result = service.GetRechargeRecordsAsync(request).GetAwaiter().GetResult();
// Verify all returned records belong to the specified user
return result.List.All(r => r.UserId == actualUserId);
}
#endregion
#region Helper Methods
private void SeedUsers(MiAssessmentDbContext dbContext, int count)
{
for (int i = 1; i <= count; i++)
{
dbContext.Users.Add(new User
{
Id = i,
Uid = $"U{i:D3}",
Nickname = $"测试用户{i}",
Mobile = $"1380013800{i}",
OpenId = $"openid{i}",
HeadImg = $"http://test.com/head{i}.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
});
}
dbContext.SaveChanges();
}
private Order CreateOrder(int userId, string orderNum, decimal price)
{
return new Order
{
UserId = userId,
OrderNum = orderNum,
Price = price,
UseMoney = 0,
UseIntegral = 0,
UseScore = 0,
Status = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
GoodsId = 1,
GoodsTitle = "商品",
GoodsPrice = price,
OrderTotal = price,
OrderZheTotal = price,
Zhe = 1,
Num = 1,
PrizeNum = 1,
Addtime = (int)DateTimeOffset.Now.ToUnixTimeSeconds()
};
}
#endregion
}