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;
///
/// Property 17: 跑腿评分计算
/// 对任意评价星级(1-5),分数变化应为:1星 -2, 2星 -1, 3星 0, 4星 +1, 5星 +2。
/// 跑腿总分应等于初始分 80 加上所有未禁用评价的分数变化之和,且总分范围在 0-100 之间。
/// **Feature: login-and-homepage, Property 17: 跑腿评分计算**
///
///
public class RunnerScorePropertyTests : 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);
}
///
/// 属性:单次评价的分数变化符合规则(1星 -2, 2星 -1, 3星 0, 4星 +1, 5星 +2)
///
[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();
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();
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();
var review = db.Reviews.First();
var runner = db.Users.Find(runnerId)!;
return review.ScoreChange == expectedChange
&& runner.RunnerScore == 80 + expectedChange;
}
}
///
/// 属性:跑腿总分始终在 0-100 之间
///
[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();
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();
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();
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();
}
}