campus-errand/server.tests/WithdrawalValidationPropertyTests.cs
2026-03-12 18:12:10 +08:00

221 lines
8.6 KiB
C#
Raw Permalink 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 22: 提现金额校验
/// 对任意提现申请,提现金额应大于等于 1 元、小于等于可提现余额,
/// 且小数位不超过 2 位。不满足条件的申请应被拒绝。
/// **Feature: login-and-homepage, Property 22: 提现金额校验**
///
/// </summary>
public class WithdrawalValidationPropertyTests : 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 void SeedAvailableEarnings(WebApplicationFactory<Program> factory, int userId, decimal amount, string suffix)
{
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User
{
Id = userId, OpenId = $"user_{suffix}", Phone = "13900000001",
Nickname = "测试用户", Role = UserRole.Runner, CreatedAt = DateTime.UtcNow
});
// 创建一个关联订单
var order = new Order
{
OrderNo = $"ORD{suffix}"[..Math.Min(20, 3 + suffix.Length)],
OwnerId = 999, OrderType = OrderType.Pickup, Status = OrderStatus.Completed,
ItemName = "测试", DeliveryLocation = "测试", Phone = "13800138000",
Commission = amount, TotalAmount = amount, CreatedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
// 创建可提现收益记录
db.Earnings.Add(new Earning
{
UserId = userId,
OrderId = order.Id,
Commission = amount,
PlatformFee = 0,
NetEarning = amount,
Status = EarningStatus.Available,
FrozenUntil = DateTime.UtcNow.AddDays(-1),
CreatedAt = DateTime.UtcNow
});
db.SaveChanges();
}
private static WithdrawRequest MakeWithdrawRequest(decimal amount) => new()
{
Amount = amount,
PaymentMethod = "WeChat",
QrCodeImage = "https://img.test/qr.jpg"
};
/// <summary>
/// 属性:提现金额低于 1 元时应被拒绝
/// </summary>
[Property(MaxTest = 20)]
public bool 1(PositiveInt seed)
{
// 生成 0.01 ~ 0.99 之间的金额
var amount = Math.Round((seed.Get % 99 + 1) * 0.01m, 2);
if (amount >= 1.0m) return true;
var dbName = $"wd_low_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var userId = 100;
var suffix = Guid.NewGuid().ToString("N");
SeedAvailableEarnings(factory, userId, 100m, suffix);
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(userId, "Runner"));
var resp = client.PostAsJsonAsync("/api/earnings/withdraw", MakeWithdrawRequest(amount)).Result;
return resp.StatusCode == HttpStatusCode.BadRequest;
}
/// <summary>
/// 属性:提现金额超出可提现余额时应被拒绝
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var availableAmount = 50.0m;
// 生成超出余额的金额
var amount = availableAmount + Math.Round((seed.Get % 100 + 1) * 1.0m, 2);
var dbName = $"wd_over_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var userId = 100;
var suffix = Guid.NewGuid().ToString("N");
SeedAvailableEarnings(factory, userId, availableAmount, suffix);
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(userId, "Runner"));
var resp = client.PostAsJsonAsync("/api/earnings/withdraw", MakeWithdrawRequest(amount)).Result;
return resp.StatusCode == HttpStatusCode.BadRequest;
}
/// <summary>
/// 属性:合法提现金额(>=1, <=余额, 小数位<=2应成功
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var availableAmount = 100.0m;
// 生成 1.00 ~ 100.00 之间的合法金额2 位小数)
var amount = Math.Round(1.0m + (seed.Get % 9900) * 0.01m, 2);
if (amount > availableAmount) amount = availableAmount;
var dbName = $"wd_ok_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var userId = 100;
var suffix = Guid.NewGuid().ToString("N");
SeedAvailableEarnings(factory, userId, availableAmount, suffix);
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(userId, "Runner"));
var resp = client.PostAsJsonAsync("/api/earnings/withdraw", MakeWithdrawRequest(amount)).Result;
return resp.StatusCode == HttpStatusCode.Created;
}
/// <summary>
/// 属性:提现金额小数位超过 2 位时应被拒绝
/// </summary>
[Property(MaxTest = 20)]
public bool 2(PositiveInt seed)
{
// 生成 3 位小数的金额,如 1.123
var amount = 1.0m + (seed.Get % 900) * 0.001m;
if (amount == Math.Round(amount, 2)) amount += 0.001m;
var dbName = $"wd_dec_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var userId = 100;
var suffix = Guid.NewGuid().ToString("N");
SeedAvailableEarnings(factory, userId, 100m, suffix);
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(userId, "Runner"));
var resp = client.PostAsJsonAsync("/api/earnings/withdraw", MakeWithdrawRequest(amount)).Result;
return resp.StatusCode == HttpStatusCode.BadRequest;
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}