campus-errand/server.tests/EarningFreezePropertyTests.cs
2026-03-01 05:01:47 +08:00

191 lines
8.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using CampusErrand.Data;
using CampusErrand.Models;
using CampusErrand.Models.Dtos;
using FsCheck;
using FsCheck.Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace CampusErrand.Tests;
/// <summary>
/// Property 21: 收益冻结与解冻
/// 对任意已完成的订单,对应的收益记录初始状态应为"冻结中"
/// 冻结期满后状态应自动变为"待提现"。
/// **Feature: login-and-homepage, Property 21: 收益冻结与解冻**
/// **Validates: Requirements 27.2, 27.3**
/// </summary>
public class EarningFreezePropertyTests : IDisposable
{
private const string JwtSecret = "YourSuperSecretKeyForJwtTokenGeneration_AtLeast32Chars!";
private const string JwtIssuer = "CampusErrand";
private const string JwtAudience = "CampusErrandApp";
private readonly List<WebApplicationFactory<Program>> _factories = [];
private WebApplicationFactory<Program> CreateFactory(string dbName)
{
var factory = new WebApplicationFactory<Program>()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var efDescriptors = services
.Where(d =>
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true
|| d.ServiceType == typeof(DbContextOptions<AppDbContext>)
|| d.ServiceType == typeof(DbContextOptions)
|| d.ImplementationType?.FullName?.Contains("EntityFrameworkCore") == true)
.ToList();
foreach (var d in efDescriptors) services.Remove(d);
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(dbName));
});
});
_factories.Add(factory);
return factory;
}
private static string GenerateToken(int userId, string role = "User")
{
var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes(JwtSecret));
var credentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(
key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.NameIdentifier, userId.ToString()),
new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role)
};
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: JwtIssuer, audience: JwtAudience, claims: claims,
expires: DateTime.UtcNow.AddHours(1), signingCredentials: credentials);
return new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 准备已完成订单数据并触发收益创建
/// </summary>
private (int ownerId, int runnerId, int orderId) SeedAndCompleteOrder(
WebApplicationFactory<Program> factory, string suffix, decimal commission)
{
var ownerId = 100;
var runnerId = 200;
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User
{
Id = ownerId, OpenId = $"owner_{suffix}", Phone = "13900000001",
Nickname = "单主", CreatedAt = DateTime.UtcNow
});
db.Users.Add(new User
{
Id = runnerId, OpenId = $"runner_{suffix}", Phone = "13900000002",
Nickname = "跑腿", Role = UserRole.Runner, CreatedAt = DateTime.UtcNow
});
db.RunnerCertifications.Add(new RunnerCertification
{
UserId = runnerId, RealName = "测试跑腿", Phone = "13900000002",
Status = CertificationStatus.Approved, CreatedAt = DateTime.UtcNow
});
// 创建待确认订单(跑腿已提交完成)
var order = new Order
{
OrderNo = $"ORD{suffix}"[..Math.Min(20, 3 + suffix.Length)],
OwnerId = ownerId, RunnerId = runnerId,
OrderType = OrderType.Pickup, Status = OrderStatus.WaitConfirm,
ItemName = "测试物品", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = commission, TotalAmount = commission,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow,
CompletedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
return (ownerId, runnerId, order.Id);
}
/// <summary>
/// 属性:订单确认完成后,收益记录初始状态为 Frozen
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var commission = Math.Round(1.0m + (seed.Get % 990) * 0.1m, 1);
var dbName = $"freeze_init_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var suffix = Guid.NewGuid().ToString("N");
var (ownerId, runnerId, orderId) = SeedAndCompleteOrder(factory, suffix, commission);
// 单主确认订单完成(触发收益创建)
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var resp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (resp.StatusCode != HttpStatusCode.OK) return false;
// 验证收益记录状态为 Frozen
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.FirstOrDefault(e => e.OrderId == orderId);
return earning != null && earning.Status == EarningStatus.Frozen;
}
/// <summary>
/// 属性:冻结期满后,查询收益时状态自动变为 Available
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var commission = Math.Round(1.0m + (seed.Get % 990) * 0.1m, 1);
var dbName = $"freeze_unfreeze_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var suffix = Guid.NewGuid().ToString("N");
var (ownerId, runnerId, orderId) = SeedAndCompleteOrder(factory, suffix, commission);
// 单主确认订单完成
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var resp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (resp.StatusCode != HttpStatusCode.OK) return false;
// 手动将冻结截止时间设为过去(模拟冻结期满)
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.First(e => e.OrderId == orderId);
earning.FrozenUntil = DateTime.UtcNow.AddHours(-1);
db.SaveChanges();
}
// 跑腿查询收益概览(触发解冻逻辑)
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(runnerId, "Runner"));
var overviewResp = client.GetFromJsonAsync<EarningsOverviewResponse>("/api/earnings").Result;
// 验证数据库中收益状态已变为 Available
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var earning = db.Earnings.First(e => e.OrderId == orderId);
return earning.Status == EarningStatus.Available;
}
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}