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

195 lines
8.4 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 17: 跑腿评分计算
/// 对任意评价星级1-5分数变化应为1星 -2, 2星 -1, 3星 0, 4星 +1, 5星 +2。
/// 跑腿总分应等于初始分 80 加上所有未禁用评价的分数变化之和,且总分范围在 0-100 之间。
/// **Feature: login-and-homepage, Property 17: 跑腿评分计算**
///
/// </summary>
public class RunnerScorePropertyTests : 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>
/// 属性单次评价的分数变化符合规则1星 -2, 2星 -1, 3星 0, 4星 +1, 5星 +2
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
// 生成 1-5 的星级
var rating = (seed.Get % 5) + 1;
var expectedChange = rating - 3;
var dbName = $"score_single_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
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_{dbName}", Phone = "13900000001", Nickname = "单主", CreatedAt = DateTime.UtcNow });
db.Users.Add(new User { Id = runnerId, OpenId = $"runner_{dbName}", Phone = "13900000002", Nickname = "跑腿", Role = UserRole.Runner, RunnerScore = 80, 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{Guid.NewGuid():N}"[..20],
OwnerId = ownerId, RunnerId = runnerId,
OrderType = OrderType.Pickup, Status = OrderStatus.Completed,
ItemName = "测试", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = 5.0m, TotalAmount = 5.0m,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow, CompletedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
}
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
// 获取订单 ID
int orderId;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
orderId = db.Orders.First().Id;
}
var response = client.PostAsJsonAsync($"/api/orders/{orderId}/review", new { Rating = rating, Content = "测试评价" }).Result;
if (response.StatusCode != HttpStatusCode.OK) return false;
// 验证数据库
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var review = db.Reviews.First();
var runner = db.Users.Find(runnerId)!;
return review.ScoreChange == expectedChange
&& runner.RunnerScore == 80 + expectedChange;
}
}
/// <summary>
/// 属性:跑腿总分始终在 0-100 之间
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var rating = (seed.Get % 5) + 1;
var dbName = $"score_range_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
var ownerId = 100;
var runnerId = 200;
// 使用极端初始分数来测试边界
var initialScore = seed.Get % 2 == 0 ? 1 : 99;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User { Id = ownerId, OpenId = $"owner_{dbName}", Phone = "13900000001", Nickname = "单主", CreatedAt = DateTime.UtcNow });
db.Users.Add(new User { Id = runnerId, OpenId = $"runner_{dbName}", Phone = "13900000002", Nickname = "跑腿", Role = UserRole.Runner, RunnerScore = initialScore, CreatedAt = DateTime.UtcNow });
db.RunnerCertifications.Add(new RunnerCertification { UserId = runnerId, RealName = "测试", Phone = "13900000002", Status = CertificationStatus.Approved, CreatedAt = DateTime.UtcNow });
db.Orders.Add(new Order
{
OrderNo = $"ORD{Guid.NewGuid():N}"[..20],
OwnerId = ownerId, RunnerId = runnerId,
OrderType = OrderType.Pickup, Status = OrderStatus.Completed,
ItemName = "测试", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = 5.0m, TotalAmount = 5.0m,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow, CompletedAt = DateTime.UtcNow
});
db.SaveChanges();
}
var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(ownerId));
int orderId;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
orderId = db.Orders.First().Id;
}
var response = client.PostAsJsonAsync($"/api/orders/{orderId}/review", new { Rating = rating }).Result;
if (response.StatusCode != HttpStatusCode.OK) return false;
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var runner = db.Users.Find(runnerId)!;
return runner.RunnerScore >= 0 && runner.RunnerScore <= 100;
}
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}