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;
///
/// Property 22: 提现金额校验
/// 对任意提现申请,提现金额应大于等于 1 元、小于等于可提现余额,
/// 且小数位不超过 2 位。不满足条件的申请应被拒绝。
/// **Feature: login-and-homepage, Property 22: 提现金额校验**
///
///
public class WithdrawalValidationPropertyTests : IDisposable
{
private const string JwtSecret = "YourSuperSecretKeyForJwtTokenGeneration_AtLeast32Chars!";
private const string JwtIssuer = "CampusErrand";
private const string JwtAudience = "CampusErrandApp";
private readonly List> _factories = [];
private WebApplicationFactory CreateFactory(string dbName)
{
var factory = new WebApplicationFactory()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
var efDescriptors = services
.Where(d =>
d.ServiceType.FullName?.Contains("EntityFrameworkCore") == true
|| d.ServiceType == typeof(DbContextOptions)
|| d.ServiceType == typeof(DbContextOptions)
|| d.ImplementationType?.FullName?.Contains("EntityFrameworkCore") == true)
.ToList();
foreach (var d in efDescriptors) services.Remove(d);
services.AddDbContext(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);
}
///
/// 为用户准备可提现收益
///
private void SeedAvailableEarnings(WebApplicationFactory factory, int userId, decimal amount, string suffix)
{
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
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"
};
///
/// 属性:提现金额低于 1 元时应被拒绝
///
[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;
}
///
/// 属性:提现金额超出可提现余额时应被拒绝
///
[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;
}
///
/// 属性:合法提现金额(>=1, <=余额, 小数位<=2)应成功
///
[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;
}
///
/// 属性:提现金额小数位超过 2 位时应被拒绝
///
[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();
}
}