using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using CampusErrand.Data; using CampusErrand.Models; using FsCheck; using FsCheck.Xunit; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace CampusErrand.Tests; /// /// Property 16: 改价差额计算 /// 对任意改价申请,当新价格高于原价时应产生补缴金额(新价 - 原价), /// 当新价格低于原价时应产生退款金额(原价 - 新价)。 /// **Feature: login-and-homepage, Property 16: 改价差额计算** /// /// public class PriceChangeDifferencePropertyTests : 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 int SeedInProgressOrder(WebApplicationFactory factory, string suffix, decimal commission, decimal goodsAmount) { using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Users.Add(new User { Id = 100, OpenId = $"owner_{suffix}", Phone = "13900000001", Nickname = "单主", CreatedAt = DateTime.UtcNow }); db.Users.Add(new User { Id = 200, OpenId = $"runner_{suffix}", Phone = "13900000002", Nickname = "跑腿", Role = UserRole.Runner, CreatedAt = DateTime.UtcNow }); db.RunnerCertifications.Add(new RunnerCertification { UserId = 200, RealName = "测试", Phone = "13900000002", Status = CertificationStatus.Approved, CreatedAt = DateTime.UtcNow }); var order = new Order { OrderNo = $"ORD{Guid.NewGuid():N}"[..20], OwnerId = 100, RunnerId = 200, OrderType = OrderType.Purchase, Status = OrderStatus.InProgress, ItemName = "测试商品", DeliveryLocation = "测试地点", Phone = "13800138000", Commission = commission, GoodsAmount = goodsAmount, TotalAmount = goodsAmount + commission, CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow }; db.Orders.Add(order); db.SaveChanges(); return order.Id; } /// /// 属性:改价差额 = 新价格 - 原价格(正数为补缴,负数为退款) /// [Property(MaxTest = 20)] public bool 改价差额等于新价减原价(PositiveInt seed) { // 生成合理的原始佣金和新佣金 var originalCommission = 1.0m + (seed.Get % 100); var newCommission = 1.0m + ((seed.Get * 7) % 100); var dbName = $"pricediff_{Guid.NewGuid()}"; using var factory = CreateFactory(dbName); var orderId = SeedInProgressOrder(factory, Guid.NewGuid().ToString("N"), originalCommission, 50.0m); var client = factory.CreateClient(); // 单主发起改价 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(100)); var response = client.PostAsJsonAsync($"/api/orders/{orderId}/price-change", new { ChangeType = "Commission", NewPrice = newCommission }).Result; if (response.StatusCode != HttpStatusCode.Created) return false; var json = response.Content.ReadFromJsonAsync().Result; var difference = json.GetProperty("difference").GetDecimal(); var expectedDifference = newCommission - originalCommission; return difference == expectedDifference; } /// /// 属性:同意改价后订单金额正确更新 /// [Property(MaxTest = 20)] public bool 同意改价后订单金额正确更新(PositiveInt seed) { var originalCommission = 1.0m + (seed.Get % 50); var newCommission = 1.0m + ((seed.Get * 3) % 50); var goodsAmount = 10.0m + (seed.Get % 90); var dbName = $"priceaccept_{Guid.NewGuid()}"; using var factory = CreateFactory(dbName); var orderId = SeedInProgressOrder(factory, Guid.NewGuid().ToString("N"), originalCommission, goodsAmount); var client = factory.CreateClient(); // 单主发起佣金改价 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(100)); var createResp = client.PostAsJsonAsync($"/api/orders/{orderId}/price-change", new { ChangeType = "Commission", NewPrice = newCommission }).Result; if (createResp.StatusCode != HttpStatusCode.Created) return false; var createJson = createResp.Content.ReadFromJsonAsync().Result; var changeId = createJson.GetProperty("id").GetInt32(); // 跑腿同意改价 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(200, "Runner")); var acceptResp = client.PutAsJsonAsync($"/api/orders/{orderId}/price-change/{changeId}", new { Action = "Accepted" }).Result; if (acceptResp.StatusCode != HttpStatusCode.OK) return false; // 验证订单金额 using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var order = db.Orders.Find(orderId)!; return order.Commission == newCommission && order.TotalAmount == goodsAmount + newCommission; } public void Dispose() { foreach (var f in _factories) f.Dispose(); _factories.Clear(); } }