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

210 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 FsCheck;
using FsCheck.Xunit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace CampusErrand.Tests;
/// <summary>
/// Property 20: 完成订单状态转换
/// 对任意处于进行中状态的订单,跑腿提交完成后状态应变为"待确认"
/// 单主确认后状态应变为"已完成";单主拒绝后状态应保持"进行中"。
/// **Feature: login-and-homepage, Property 20: 完成订单状态转换**
///
/// </summary>
public class CompleteOrderPropertyTests : 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>
/// 准备进行中的订单数据(单主 + 跑腿 + 认证 + InProgress 订单)
/// </summary>
private (int ownerId, int runnerId, int orderId) SeedInProgressOrder(
WebApplicationFactory<Program> factory, string suffix)
{
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.InProgress,
ItemName = "测试物品", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = 5.0m, TotalAmount = 5.0m,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
return (ownerId, runnerId, order.Id);
}
/// <summary>
/// 属性:跑腿提交完成后,订单状态变为 WaitConfirm
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var dbName = $"complete_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var (_, runnerId, orderId) = SeedInProgressOrder(factory, Guid.NewGuid().ToString("N"));
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(runnerId, "Runner"));
var response = client.PostAsJsonAsync($"/api/orders/{orderId}/complete",
new { CompletionProof = seed.Get % 2 == 0 ? "https://img.test/proof.jpg" : (string?)null }).Result;
if (response.StatusCode != HttpStatusCode.OK) return false;
// 验证数据库中的状态
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var order = db.Orders.Find(orderId)!;
return order.Status == OrderStatus.WaitConfirm && order.CompletedAt != null;
}
/// <summary>
/// 属性:单主确认后,订单状态变为 Completed
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var dbName = $"confirm_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var (ownerId, runnerId, orderId) = SeedInProgressOrder(factory, Guid.NewGuid().ToString("N"));
var client = factory.CreateClient();
// 先让跑腿提交完成
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(runnerId, "Runner"));
var completeResp = client.PostAsJsonAsync($"/api/orders/{orderId}/complete",
new { CompletionProof = (string?)null }).Result;
if (completeResp.StatusCode != HttpStatusCode.OK) return false;
// 单主确认
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var confirmResp = client.PostAsync($"/api/orders/{orderId}/confirm", null).Result;
if (confirmResp.StatusCode != HttpStatusCode.OK) return false;
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var order = db.Orders.Find(orderId)!;
return order.Status == OrderStatus.Completed;
}
/// <summary>
/// 属性:单主拒绝后,订单状态回到 InProgress
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var dbName = $"reject_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var (ownerId, runnerId, orderId) = SeedInProgressOrder(factory, Guid.NewGuid().ToString("N"));
var client = factory.CreateClient();
// 先让跑腿提交完成
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(runnerId, "Runner"));
var completeResp = client.PostAsJsonAsync($"/api/orders/{orderId}/complete",
new { CompletionProof = (string?)null }).Result;
if (completeResp.StatusCode != HttpStatusCode.OK) return false;
// 单主拒绝
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
var rejectResp = client.PostAsync($"/api/orders/{orderId}/reject", null).Result;
if (rejectResp.StatusCode != HttpStatusCode.OK) return false;
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var order = db.Orders.Find(orderId)!;
return order.Status == OrderStatus.InProgress;
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}