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;
///
/// Property 18: 评价可见性
/// 对任意 API 请求,当请求者为跑腿用户时,评价内容不应被返回;仅管理员可查看评价内容。
/// **Feature: login-and-homepage, Property 18: 评价可见性**
///
///
public class ReviewVisibilityPropertyTests : 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);
}
///
/// 准备含评价的数据
///
private void SeedReviewData(WebApplicationFactory factory, string suffix)
{
using var scope = factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
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();
}
///
/// 属性:单主提交评价后,返回的响应中不包含评价内容(Content 为 null)
///
[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();
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();
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().Result;
// 评价内容不应在普通用户的响应中返回
if (json.TryGetProperty("content", out var contentProp))
{
return contentProp.ValueKind == JsonValueKind.Null;
}
return true; // 没有 content 字段也算通过
}
///
/// 属性:管理员可以查看评价内容
///
[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().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();
}
}