- 在JWT中添加permver权限版本号claim,用于实时权限变更检测 - 实现PermissionVersionMiddleware中间件,在认证后授权前校验权限版本 - 修改AssignPermissionsCommandHandler,角色权限变更时自增相关用户权限版本号 - 修改AssignRolesCommandHandler,用户角色变更时自增权限版本号强制重登录 - 集成Redis存储权限版本镜像,支持分布式环境下的版本同步 - 添加PermissionVersion字段到User实体,用于存储权限版本号 - 实现分布式登录失败计数与账号锁定功能,增强安全防护 - 配置文件中新增AccessTokenMinutes和RefreshTokenDays可配置项 - 更新CORS策略配置,添加127.0.0.1本地访问支持 - 完善异常处理,统一使用ErrorCode枚举进行错误码管理 - 添加SOP测试单元设计文档,规范端到端测试流程
88 lines
4.1 KiB
C#
88 lines
4.1 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using NSubstitute;
|
||
using BuildFlow.Platform.Domain.Entities;
|
||
using BuildFlow.Platform.Infrastructure.Persistence;
|
||
|
||
namespace BuildFlow.Platform.Tests;
|
||
|
||
/// <summary>
|
||
/// Platform 模块测试基础设施:InMemory PlatformDbContext 工厂 + JWT 配置构造 + 审计字段填充 helper。
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// 照抄 <c>MenuSortTests.BuildInMemoryDb</c> 的 DI 方式(不挂 AuditInterceptor,手动填审计字段)。
|
||
/// 不挂拦截器是因为拦截器依赖 ICurrentUserContext/sp.GetRequiredService,测试上下文里没有;
|
||
/// 故约定:测试 seed 实体时调用 <see cref="WithAudit"/> 填充必填审计字段。
|
||
/// </remarks>
|
||
internal static class PlatformTestDb
|
||
{
|
||
/// <summary>构造一个独立的 InMemory PlatformDbContext(每个测试唯一库名,避免串扰)。</summary>
|
||
public static PlatformDbContext CreateDbContext([System.Runtime.CompilerServices.CallerMemberName] string testName = "")
|
||
{
|
||
var services = new ServiceCollection();
|
||
services.AddDbContext<PlatformDbContext>(o => o.UseInMemoryDatabase($"platform-{testName}-{Guid.NewGuid()}"));
|
||
return services.BuildServiceProvider().GetRequiredService<PlatformDbContext>();
|
||
}
|
||
|
||
/// <summary>构造测试用 IConfiguration(提供 JWT 签发所需的 SigningKey/Issuer/Audience)。</summary>
|
||
public static IConfiguration CreateConfig()
|
||
{
|
||
var config = Substitute.For<IConfiguration>();
|
||
config["Jwt:SigningKey"] = "TestSigningKeyMustBeAtLeast32CharsLong!!";
|
||
config["Jwt:Issuer"] = "test-issuer";
|
||
config["Jwt:Audience"] = "test-audience";
|
||
config["Jwt:AccessTokenMinutes"] = "240";
|
||
config["Jwt:RefreshTokenDays"] = "7";
|
||
// GetValue<T> 在 IConfiguration 扩展方法里,NSubstitute 无法直接 mock,LoginCommandHandler 用的是 config.GetValue("Jwt:AccessTokenMinutes", 240)
|
||
// 但由于 LoginCommandHandler 实际调用 GetValue 是扩展方法(内部走 this[key] 再 Convert),mock this[key] 即可生效
|
||
return config;
|
||
}
|
||
|
||
/// <summary>填充 IFullAudit 实体的必填审计字段,避免 EF 保存时非空校验失败。</summary>
|
||
public static T WithAudit<T>(T entity) where T : class
|
||
{
|
||
var createdBy = entity.GetType().GetProperty("CreatedBy");
|
||
var updatedBy = entity.GetType().GetProperty("UpdatedBy");
|
||
var operatorIP = entity.GetType().GetProperty("OperatorIP");
|
||
var createdAt = entity.GetType().GetProperty("CreatedAt");
|
||
var updatedAt = entity.GetType().GetProperty("UpdatedAt");
|
||
|
||
if (createdBy is not null && createdBy.GetValue(entity) is null or "")
|
||
createdBy.SetValue(entity, "test");
|
||
if (updatedBy is not null && updatedBy.GetValue(entity) is null or "")
|
||
updatedBy.SetValue(entity, "test");
|
||
if (operatorIP is not null && operatorIP.GetValue(entity) is null or "")
|
||
operatorIP.SetValue(entity, "127.0.0.1");
|
||
if (createdAt is not null && (DateTime)createdAt.GetValue(entity)! == default)
|
||
createdAt.SetValue(entity, DateTime.UtcNow);
|
||
if (updatedAt is not null && (DateTime)updatedAt.GetValue(entity)! == default)
|
||
updatedAt.SetValue(entity, DateTime.UtcNow);
|
||
|
||
return entity;
|
||
}
|
||
|
||
/// <summary>构造一个已持久化的测试用户(含 BCrypt 密码哈希)。</summary>
|
||
public static async Task<User> SeedUserAsync(
|
||
PlatformDbContext db,
|
||
Guid id,
|
||
string username,
|
||
string password = "Admin@123",
|
||
bool isActive = true,
|
||
int permissionVersion = 0)
|
||
{
|
||
var user = WithAudit(new User
|
||
{
|
||
Id = id,
|
||
Username = username,
|
||
Email = $"{username}@test.local",
|
||
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
|
||
IsActive = isActive,
|
||
PermissionVersion = permissionVersion,
|
||
});
|
||
db.Users.Add(user);
|
||
await db.SaveChangesAsync();
|
||
return user;
|
||
}
|
||
}
|