forked from xiongyuxing/tiku-backend.net
feat: establish dotnet engineering foundation
This commit is contained in:
182
Tiku.IntegrationTests/Infrastructure/PostgresTestDatabase.cs
Normal file
182
Tiku.IntegrationTests/Infrastructure/PostgresTestDatabase.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.IntegrationTests.Infrastructure;
|
||||
|
||||
internal sealed class PostgresTestDatabase : IDisposable
|
||||
{
|
||||
private const string DatabasePrefix = "tiku_it_";
|
||||
private static readonly Lazy<PostgresTestDatabaseTemplate> Template = new(
|
||||
PostgresTestDatabaseTemplate.Create,
|
||||
LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
private readonly string adminConnectionString;
|
||||
private bool disposed;
|
||||
|
||||
private PostgresTestDatabase(string adminConnectionString, string databaseName, string connectionString)
|
||||
{
|
||||
this.adminConnectionString = adminConnectionString;
|
||||
DatabaseName = databaseName;
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public string DatabaseName { get; }
|
||||
|
||||
public string ConnectionString { get; }
|
||||
|
||||
public static PostgresTestDatabase Create()
|
||||
{
|
||||
var template = Template.Value;
|
||||
var adminConnectionString = template.AdminConnectionString;
|
||||
var databaseName = $"{DatabasePrefix}{Guid.NewGuid():N}";
|
||||
var adminBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString);
|
||||
|
||||
using (var adminConnection = new NpgsqlConnection(adminBuilder.ConnectionString))
|
||||
{
|
||||
adminConnection.Open();
|
||||
using var createDatabase = adminConnection.CreateCommand();
|
||||
createDatabase.CommandText =
|
||||
$"CREATE DATABASE {QuoteIdentifier(databaseName)} TEMPLATE {QuoteIdentifier(template.DatabaseName)}";
|
||||
createDatabase.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var databaseBuilder = new NpgsqlConnectionStringBuilder(adminBuilder.ConnectionString)
|
||||
{
|
||||
Database = databaseName,
|
||||
Pooling = true,
|
||||
MaxPoolSize = 10
|
||||
};
|
||||
var database = new PostgresTestDatabase(
|
||||
adminBuilder.ConnectionString,
|
||||
databaseName,
|
||||
databaseBuilder.ConnectionString);
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
var builder = new NpgsqlConnectionStringBuilder(adminConnectionString);
|
||||
using var adminConnection = new NpgsqlConnection(builder.ConnectionString);
|
||||
adminConnection.Open();
|
||||
|
||||
using (var terminateConnections = adminConnection.CreateCommand())
|
||||
{
|
||||
terminateConnections.CommandText =
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()";
|
||||
terminateConnections.Parameters.AddWithValue(DatabaseName);
|
||||
terminateConnections.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
using var dropDatabase = adminConnection.CreateCommand();
|
||||
dropDatabase.CommandText = $"DROP DATABASE IF EXISTS {QuoteIdentifier(DatabaseName)}";
|
||||
dropDatabase.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
internal static string QuoteIdentifier(string identifier)
|
||||
{
|
||||
if (!identifier.StartsWith(DatabasePrefix, StringComparison.Ordinal) ||
|
||||
identifier.Any(character => !char.IsAsciiLetterOrDigit(character) && character != '_'))
|
||||
{
|
||||
throw new InvalidOperationException("Refusing to use an unsafe integration-test database name.");
|
||||
}
|
||||
|
||||
return $"\"{identifier}\"";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PostgresTestDatabaseTemplate : IDisposable
|
||||
{
|
||||
private const string TemplatePrefix = "tiku_it_template_";
|
||||
private bool disposed;
|
||||
|
||||
private PostgresTestDatabaseTemplate(string adminConnectionString, string databaseName)
|
||||
{
|
||||
AdminConnectionString = adminConnectionString;
|
||||
DatabaseName = databaseName;
|
||||
}
|
||||
|
||||
public string AdminConnectionString { get; }
|
||||
|
||||
public string DatabaseName { get; }
|
||||
|
||||
public static PostgresTestDatabaseTemplate Create()
|
||||
{
|
||||
var adminConnectionString = Environment.GetEnvironmentVariable("TIKU_TEST_POSTGRES_ADMIN") ??
|
||||
$"Host=localhost;Database=postgres;Username={Environment.UserName};Pooling=false;Timeout=5;Command Timeout=60";
|
||||
var adminBuilder = new NpgsqlConnectionStringBuilder(adminConnectionString);
|
||||
if (!string.Equals(adminBuilder.Database, "postgres", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"TIKU_TEST_POSTGRES_ADMIN must target the postgres maintenance database.");
|
||||
}
|
||||
|
||||
var databaseName = $"{TemplatePrefix}{Environment.ProcessId}_{Guid.NewGuid():N}";
|
||||
var template = new PostgresTestDatabaseTemplate(adminBuilder.ConnectionString, databaseName);
|
||||
|
||||
try
|
||||
{
|
||||
using (var adminConnection = new NpgsqlConnection(template.AdminConnectionString))
|
||||
{
|
||||
adminConnection.Open();
|
||||
using var createDatabase = adminConnection.CreateCommand();
|
||||
createDatabase.CommandText =
|
||||
$"CREATE DATABASE {PostgresTestDatabase.QuoteIdentifier(databaseName)}";
|
||||
createDatabase.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
var databaseBuilder = new NpgsqlConnectionStringBuilder(template.AdminConnectionString)
|
||||
{
|
||||
Database = databaseName,
|
||||
Pooling = false
|
||||
};
|
||||
var options = new DbContextOptionsBuilder<TikuDbContext>()
|
||||
.UseNpgsql(databaseBuilder.ConnectionString, npgsql =>
|
||||
npgsql.MigrationsAssembly(typeof(TikuDbContext).Assembly.FullName))
|
||||
.Options;
|
||||
using (var dbContext = new TikuDbContext(options))
|
||||
{
|
||||
dbContext.Database.Migrate();
|
||||
}
|
||||
|
||||
using (var adminConnection = new NpgsqlConnection(template.AdminConnectionString))
|
||||
{
|
||||
adminConnection.Open();
|
||||
using var preventConnections = adminConnection.CreateCommand();
|
||||
preventConnections.CommandText =
|
||||
$"ALTER DATABASE {PostgresTestDatabase.QuoteIdentifier(databaseName)} ALLOW_CONNECTIONS false";
|
||||
preventConnections.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) => template.Dispose();
|
||||
return template;
|
||||
}
|
||||
catch
|
||||
{
|
||||
template.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disposed = true;
|
||||
using var adminConnection = new NpgsqlConnection(AdminConnectionString);
|
||||
adminConnection.Open();
|
||||
using var dropDatabase = adminConnection.CreateCommand();
|
||||
dropDatabase.CommandText =
|
||||
$"DROP DATABASE IF EXISTS {PostgresTestDatabase.QuoteIdentifier(DatabaseName)}";
|
||||
dropDatabase.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user