348 lines
12 KiB
C#
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
|
|
}
|