108 lines
5.0 KiB
C#
108 lines
5.0 KiB
C#
using BuildFlow.Platform.Domain.Entities.User;
|
||
using BuildFlow.Platform.Infrastructure.Persistence;
|
||
using BuildFlow.Shared.Domain.Common;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Configuration;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
|
||
namespace BuildFlow.Platform.Tests;
|
||
|
||
/// <summary>
|
||
/// Platform 模块测试基础设施:InMemory PlatformDbContext 工厂 + JWT 配置构造 + 审计字段填充 helper。
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// 不挂 AuditInterceptor(拦截器依赖 ICurrentUserContext,测试上下文里没有);
|
||
/// 约定:测试 seed 实体时调用 <see cref="WithAudit"/> 填充必填审计字段。
|
||
/// </remarks>
|
||
internal static class PlatformTestDb
|
||
{
|
||
/// <summary>构造一个独立的 InMemory PlatformDbContext(每个测试唯一库名,避免串扰)。</summary>
|
||
/// <remarks>
|
||
/// 挂载 SavingChanges 回调自动填充 IAuditable/IHasOperatorIP 字段(替代生产环境的 AuditInterceptor),
|
||
/// 这样 handler 内部 new 出来的 RefreshTokenEntity/UserRoleEntity/RolePermissionEntity 等实体也能通过非空校验。
|
||
/// </remarks>
|
||
public static PlatformDbContext CreateDbContext([System.Runtime.CompilerServices.CallerMemberName] string testName = "")
|
||
{
|
||
var services = new ServiceCollection();
|
||
services.AddDbContext<PlatformDbContext>(o => o.UseInMemoryDatabase($"platform-{testName}-{Guid.NewGuid()}"));
|
||
var db = services.BuildServiceProvider().GetRequiredService<PlatformDbContext>();
|
||
db.SavingChanges += AutoFillAuditFields;
|
||
return db;
|
||
}
|
||
|
||
/// <summary>SavingChanges 回调:自动填充审计字段,模拟生产 AuditInterceptor 行为。</summary>
|
||
private static void AutoFillAuditFields(object? sender, EventArgs _)
|
||
{
|
||
if (sender is not DbContext ctx) return;
|
||
foreach (var entry in ctx.ChangeTracker.Entries())
|
||
{
|
||
if (entry.Entity is IAuditable a)
|
||
{
|
||
a.CreatedBy ??= "test";
|
||
a.UpdatedBy ??= "test";
|
||
if (a.CreatedAt == default) a.CreatedAt = DateTime.UtcNow;
|
||
if (a.UpdatedAt == default) a.UpdatedAt = DateTime.UtcNow;
|
||
}
|
||
if (entry.Entity is IHasOperatorIP o)
|
||
o.OperatorIP ??= "127.0.0.1";
|
||
}
|
||
}
|
||
|
||
/// <summary>构造测试用 IConfiguration(提供 JWT 签发所需的 SigningKey/Issuer/Audience)。</summary>
|
||
/// 用字典实现而非 NSubstitute mock:因为 <c>GetValue<T></c> 是扩展方法,
|
||
/// 内部走 ConfigurationBinder 对 this[key] 做类型转换,mock 对未设置的 key 返回空串会导致 Int32 解析失败。
|
||
/// 字典实现让未设置的 key 返回 null,使扩展方法回退到默认值。
|
||
public static IConfiguration CreateConfig() => new DictionaryConfiguration
|
||
{
|
||
["Jwt:SigningKey"] = "TestSigningKeyMustBeAtLeast32CharsLong!!",
|
||
["Jwt:Issuer"] = "test-issuer",
|
||
["Jwt:Audience"] = "test-audience",
|
||
["Jwt:AccessTokenMinutes"] = "240",
|
||
["Jwt:RefreshTokenDays"] = "7",
|
||
};
|
||
|
||
/// <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<UserEntity> SeedUserAsync(
|
||
PlatformDbContext db,
|
||
Guid id,
|
||
string username,
|
||
string password = "Admin@123",
|
||
bool isActive = true,
|
||
int permissionVersion = 0)
|
||
{
|
||
var user = WithAudit(new UserEntity
|
||
{
|
||
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;
|
||
}
|
||
}
|