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

270 lines
10 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.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 8: 打包费计算
/// 对任意美食街订单,当门店配置为总打包费模式时,额外费用等于固定打包费金额;
/// 当门店配置为单份打包费模式时,额外费用等于菜品总份数乘以单份打包费金额。
/// **Feature: login-and-homepage, Property 8: 打包费计算**
///
/// </summary>
public class PackingFeePropertyTests : 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 GenerateUserToken(int userId = 1)
{
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, "User")
};
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 static (Shop shop, Dish dish) SeedShopAndDish(
AppDbContext db, PackingFeeType feeType, decimal feeAmount, decimal dishPrice)
{
var shop = new Shop
{
Name = "测试门店",
Photo = "https://example.com/photo.jpg",
Location = "测试位置",
PackingFeeType = feeType,
PackingFeeAmount = feeAmount,
IsEnabled = true
};
db.Shops.Add(shop);
db.SaveChanges();
var dish = new Dish
{
ShopId = shop.Id,
Name = "测试菜品",
Photo = "https://example.com/dish.jpg",
Price = dishPrice,
IsEnabled = true
};
db.Dishes.Add(dish);
db.SaveChanges();
return (shop, dish);
}
/// <summary>
/// 属性:总打包费模式下,额外费用等于固定打包费金额(不论菜品数量)
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt quantitySeed, PositiveInt feeSeed)
{
// 生成测试数据:数量 1~10打包费 0.5~5.0
var quantity = (quantitySeed.Get % 10) + 1;
var packingFeeAmount = Math.Round(0.5m + (feeSeed.Get % 10) * 0.5m, 2);
var dishPrice = 10.0m;
var dbName = $"packing_fixed_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
// 准备数据
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var (shop, dish) = SeedShopAndDish(db, PackingFeeType.Fixed, packingFeeAmount, dishPrice);
// 创建美食街订单
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateUserToken());
var request = new CreateOrderRequest
{
OrderType = "Food",
ItemName = "美食街订单",
DeliveryLocation = "测试送达地点",
Phone = "13800138000",
Commission = 3.0m,
FoodItems =
[
new FoodOrderItemRequest
{
ShopId = shop.Id,
DishId = dish.Id,
Quantity = quantity,
UnitPrice = dishPrice
}
]
};
var response = client.PostAsJsonAsync("/api/orders", request).Result;
if (!response.IsSuccessStatusCode) return false;
var order = response.Content.ReadFromJsonAsync<OrderResponse>().Result;
if (order == null) return false;
// 验证:商品总金额 = 菜品总额 + 固定打包费
var expectedGoodsAmount = (dishPrice * quantity) + packingFeeAmount;
return order.GoodsAmount == expectedGoodsAmount;
}
/// <summary>
/// 属性:单份打包费模式下,额外费用等于菜品总份数乘以单份打包费金额
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt quantitySeed, PositiveInt feeSeed)
{
// 生成测试数据:数量 1~10单份打包费 0.5~5.0
var quantity = (quantitySeed.Get % 10) + 1;
var packingFeePerItem = Math.Round(0.5m + (feeSeed.Get % 10) * 0.5m, 2);
var dishPrice = 10.0m;
var dbName = $"packing_peritem_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
// 准备数据
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var (shop, dish) = SeedShopAndDish(db, PackingFeeType.PerItem, packingFeePerItem, dishPrice);
// 创建美食街订单
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateUserToken());
var request = new CreateOrderRequest
{
OrderType = "Food",
ItemName = "美食街订单",
DeliveryLocation = "测试送达地点",
Phone = "13800138000",
Commission = 3.0m,
FoodItems =
[
new FoodOrderItemRequest
{
ShopId = shop.Id,
DishId = dish.Id,
Quantity = quantity,
UnitPrice = dishPrice
}
]
};
var response = client.PostAsJsonAsync("/api/orders", request).Result;
if (!response.IsSuccessStatusCode) return false;
var order = response.Content.ReadFromJsonAsync<OrderResponse>().Result;
if (order == null) return false;
// 验证:商品总金额 = 菜品总额 + (份数 × 单份打包费)
var expectedPackingFee = packingFeePerItem * quantity;
var expectedGoodsAmount = (dishPrice * quantity) + expectedPackingFee;
return order.GoodsAmount == expectedGoodsAmount;
}
/// <summary>
/// 属性:支付总金额 = 商品总金额(含打包费) + 跑腿佣金
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt quantitySeed, PositiveInt feeSeed, PositiveInt commSeed)
{
var quantity = (quantitySeed.Get % 5) + 1;
var packingFeeAmount = Math.Round(1.0m + (feeSeed.Get % 5) * 0.5m, 2);
var commission = Math.Round(1.0m + (commSeed.Get % 50) * 0.1m, 1);
var dishPrice = 15.0m;
var dbName = $"packing_total_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var (shop, dish) = SeedShopAndDish(db, PackingFeeType.Fixed, packingFeeAmount, dishPrice);
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateUserToken());
var request = new CreateOrderRequest
{
OrderType = "Food",
ItemName = "美食街订单",
DeliveryLocation = "测试送达地点",
Phone = "13800138000",
Commission = commission,
FoodItems =
[
new FoodOrderItemRequest
{
ShopId = shop.Id,
DishId = dish.Id,
Quantity = quantity,
UnitPrice = dishPrice
}
]
};
var response = client.PostAsJsonAsync("/api/orders", request).Result;
if (!response.IsSuccessStatusCode) return false;
var order = response.Content.ReadFromJsonAsync<OrderResponse>().Result;
if (order == null) return false;
// 验证:支付总金额 = 商品总金额 + 佣金
var expectedGoodsAmount = (dishPrice * quantity) + packingFeeAmount;
var expectedTotal = expectedGoodsAmount + commission;
return order.TotalAmount == expectedTotal && order.GoodsAmount == expectedGoodsAmount;
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}