build-flow.api/tests/BuildFlow.Monitoring.Tests/Sync/PostgresMonitoringFixture.cs
2026-07-24 09:22:17 +08:00

119 lines
4.0 KiB
C#

using BuildFlow.Shared.Domain.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Monitoring.Infrastructure.Persistence;
using Npgsql;
using Xunit;
namespace BuildFlow.Monitoring.Tests.Sync;
/// <summary>
/// 为 Monitoring 集成测试创建并销毁独立 PostgreSQL 数据库。
/// </summary>
public sealed class PostgresMonitoringFixture : IAsyncLifetime
{
private const string DefaultConnection =
"Host=127.0.0.1;Port=55432;Username=buildflow;Password=buildflow;Database=postgres";
private string _databaseName = default!;
private string _adminConnectionString = default!;
/// <summary>当前测试数据库连接串。</summary>
public string ConnectionString { get; private set; } = default!;
public async Task InitializeAsync()
{
var configured = Environment.GetEnvironmentVariable("BUILDFLOW_MONITORING_TEST_CONNECTION")
?? DefaultConnection;
var adminBuilder = new NpgsqlConnectionStringBuilder(configured)
{
Database = "postgres",
Pooling = false,
};
_adminConnectionString = adminBuilder.ConnectionString;
_databaseName = $"monitoring_upsert_{Guid.NewGuid():N}";
await using (var connection = new NpgsqlConnection(_adminConnectionString))
{
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = $"CREATE DATABASE \"{_databaseName}\"";
await command.ExecuteNonQueryAsync();
}
var databaseBuilder = new NpgsqlConnectionStringBuilder(configured)
{
Database = _databaseName,
Pooling = true,
};
ConnectionString = databaseBuilder.ConnectionString;
await using var db = CreateDbContext();
await db.Database.EnsureCreatedAsync();
}
/// <summary>创建指向独立测试数据库的 MonitoringDbContext。</summary>
public MonitoringDbContext CreateDbContext(params IInterceptor[] interceptors)
{
var optionsBuilder = new DbContextOptionsBuilder<MonitoringDbContext>()
.UseNpgsql(ConnectionString)
.UseSnakeCaseNamingConvention();
if (interceptors.Length > 0)
{
optionsBuilder.AddInterceptors(interceptors);
}
var db = new MonitoringDbContext(optionsBuilder.Options);
db.SavingChanges += FillAuditFields;
return db;
}
public async Task DisposeAsync()
{
NpgsqlConnection.ClearAllPools();
await using var connection = new NpgsqlConnection(_adminConnectionString);
await connection.OpenAsync();
await using (var terminate = connection.CreateCommand())
{
terminate.CommandText =
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = @database AND pid <> pg_backend_pid()";
terminate.Parameters.AddWithValue("database", _databaseName);
await terminate.ExecuteNonQueryAsync();
}
await using var drop = connection.CreateCommand();
drop.CommandText = $"DROP DATABASE IF EXISTS \"{_databaseName}\"";
await drop.ExecuteNonQueryAsync();
}
private static void FillAuditFields(object? sender, EventArgs _)
{
if (sender is not DbContext db)
{
return;
}
var now = DateTime.UtcNow;
foreach (var entry in db.ChangeTracker.Entries())
{
if (entry.Entity is IAuditable auditable)
{
auditable.CreatedBy ??= "test";
auditable.UpdatedBy ??= "test";
if (auditable.CreatedAt == default)
{
auditable.CreatedAt = now;
}
auditable.UpdatedAt = now;
}
if (entry.Entity is IHasOperatorIP operatorEntity)
{
operatorEntity.OperatorIP ??= "127.0.0.1";
}
}
}
}