campus-errand/server.tests/ReviewVisibilityPropertyTests.cs
2026-03-01 05:01:47 +08:00

194 lines
8.1 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;
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;
/// <summary>
/// Property 18: 评价可见性
/// 对任意 API 请求,当请求者为跑腿用户时,评价内容不应被返回;仅管理员可查看评价内容。
/// **Feature: login-and-homepage, Property 18: 评价可见性**
/// **Validates: Requirements 22.5**
/// </summary>
public class ReviewVisibilityPropertyTests : 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>
/// 准备含评价的数据
/// </summary>
private void SeedReviewData(WebApplicationFactory<Program> factory, string suffix)
{
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var owner = new User { Id = 100, OpenId = $"owner_{suffix}", Phone = "13900000001", Nickname = "单主", CreatedAt = DateTime.UtcNow };
var runner = new User { Id = 200, OpenId = $"runner_{suffix}", Phone = "13900000002", Nickname = "跑腿", Role = UserRole.Runner, RunnerScore = 80, CreatedAt = DateTime.UtcNow };
var admin = new User { Id = 300, OpenId = $"admin_{suffix}", Phone = "13900000003", Nickname = "管理员", Role = UserRole.Admin, CreatedAt = DateTime.UtcNow };
db.Users.AddRange(owner, runner, admin);
var order = new Order
{
OrderNo = $"ORD{Guid.NewGuid():N}"[..20],
OwnerId = 100, RunnerId = 200,
OrderType = OrderType.Pickup, Status = OrderStatus.Completed,
ItemName = "测试", DeliveryLocation = "测试地点",
Phone = "13800138000", Commission = 5.0m, TotalAmount = 5.0m,
IsReviewed = true,
CreatedAt = DateTime.UtcNow, AcceptedAt = DateTime.UtcNow, CompletedAt = DateTime.UtcNow
};
db.Orders.Add(order);
db.SaveChanges();
db.Reviews.Add(new Review
{
OrderId = order.Id, RunnerId = 200, Rating = 4,
Content = "服务很好", ScoreChange = 1, CreatedAt = DateTime.UtcNow
});
db.SaveChanges();
}
/// <summary>
/// 属性单主提交评价后返回的响应中不包含评价内容Content 为 null
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var rating = (seed.Get % 5) + 1;
var dbName = $"vis_submit_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
// 准备数据(未评价的已完成订单)
using (var scope = factory.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Users.Add(new User { Id = 100, OpenId = $"owner_{dbName}", Phone = "13900000001", Nickname = "单主", CreatedAt = DateTime.UtcNow });
db.Users.Add(new User { Id = 200, OpenId = $"runner_{dbName}", Phone = "13900000002", Nickname = "跑腿", Role = UserRole.Runner, RunnerScore = 80, CreatedAt = DateTime.UtcNow });
db.Orders.Add(new Order
{
OrderNo = $"ORD{Guid.NewGuid():N}"[..20],
OwnerId = 100, RunnerId = 200,
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(100));
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;
var json = response.Content.ReadFromJsonAsync<JsonElement>().Result;
// 评价内容不应在普通用户的响应中返回
if (json.TryGetProperty("content", out var contentProp))
{
return contentProp.ValueKind == JsonValueKind.Null;
}
return true; // 没有 content 字段也算通过
}
/// <summary>
/// 属性:管理员可以查看评价内容
/// </summary>
[Property(MaxTest = 20)]
public bool (PositiveInt seed)
{
var dbName = $"vis_admin_{Guid.NewGuid()}";
using var factory = CreateFactory(dbName);
SeedReviewData(factory, Guid.NewGuid().ToString("N"));
var client = factory.CreateClient();
// 使用管理员 token
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", GenerateToken(300, "Admin"));
var response = client.GetAsync("/api/admin/reviews").Result;
if (response.StatusCode != HttpStatusCode.OK) return false;
var json = response.Content.ReadFromJsonAsync<JsonElement>().Result;
if (json.GetArrayLength() == 0) return false;
var firstReview = json[0];
// 管理员应能看到评价内容
if (firstReview.TryGetProperty("content", out var contentProp))
{
return contentProp.ValueKind == JsonValueKind.String && contentProp.GetString() == "服务很好";
}
return false;
}
public void Dispose()
{
foreach (var f in _factories) f.Dispose();
_factories.Clear();
}
}