Files
itworx-knowledge-hub-Public/tests/backend/ItWorx.KnowledgeHub.IntegrationTests/Infrastructure/KnowledgeHubIntegrationFactory.cs
T

158 lines
6.2 KiB
C#

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Npgsql;
namespace ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
/// <summary>
/// One isolated logical database per test class, on the shared
/// <see cref="PostgresContainerFixture"/> server. Real EF Core
/// migrations run at startup exactly as production does — this is not a mocked or
/// SQLite-backed substitute (docs/22-testing-strategy.md).
/// </summary>
public sealed class KnowledgeHubIntegrationFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly PostgresContainerFixture _postgres;
private readonly IReadOnlyDictionary<string, string?> _additionalConfiguration;
private readonly string _databaseName = $"test_{Guid.NewGuid():N}";
private readonly string _attachmentsDirectory =
Path.Combine(Path.GetTempPath(), "ikh-tests-attachments", Guid.NewGuid().ToString("N"));
private readonly string _importsDirectory =
Path.Combine(Path.GetTempPath(), "ikh-tests-imports", Guid.NewGuid().ToString("N"));
private readonly string _backupsDirectory =
Path.Combine(Path.GetTempPath(), "ikh-tests-backups", Guid.NewGuid().ToString("N"));
public KnowledgeHubIntegrationFactory(
PostgresContainerFixture postgres,
IReadOnlyDictionary<string, string?>? additionalConfiguration = null)
{
_postgres = postgres;
_additionalConfiguration = additionalConfiguration ?? new Dictionary<string, string?>();
}
public async Task InitializeAsync()
{
await using var connection = new NpgsqlConnection(_postgres.AdminConnectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = $"CREATE DATABASE \"{_databaseName}\"";
await command.ExecuteNonQueryAsync();
}
async Task IAsyncLifetime.DisposeAsync()
{
// Dispose the hosted TestServer/HttpClient first (base WebApplicationFactory
// behavior), then drop this test class's isolated database.
await base.DisposeAsync();
if (Directory.Exists(_attachmentsDirectory))
{
Directory.Delete(_attachmentsDirectory, recursive: true);
}
if (Directory.Exists(_importsDirectory))
{
Directory.Delete(_importsDirectory, recursive: true);
}
if (Directory.Exists(_backupsDirectory))
{
Directory.Delete(_backupsDirectory, recursive: true);
}
try
{
await using var connection = new NpgsqlConnection(_postgres.AdminConnectionString);
await connection.OpenAsync();
await using (var terminate = connection.CreateCommand())
{
terminate.CommandText =
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity " +
"WHERE datname = @name AND pid <> pg_backend_pid()";
terminate.Parameters.AddWithValue("name", _databaseName);
await terminate.ExecuteNonQueryAsync();
}
await using var drop = connection.CreateCommand();
drop.CommandText = $"DROP DATABASE IF EXISTS \"{_databaseName}\"";
await drop.ExecuteNonQueryAsync();
}
catch (NpgsqlException)
{
// Best-effort cleanup: the whole container is disposed at the end of the
// run regardless, so a failure to drop one logical database here is not a
// correctness problem for the test suite.
}
}
/// <summary>The isolated content-addressed attachment storage root for this test
/// class, so parallel test classes never share (or race on) blob files.</summary>
public string AttachmentsDirectory => _attachmentsDirectory;
/// <summary>The isolated LeafWiki imports mount root for this test class. Tests
/// write synthetic fixture files here directly (never a real snapshot).</summary>
public string ImportsDirectory => _importsDirectory;
/// <summary>The isolated backups root for this test class, so parallel/sequential
/// test classes never share (or have retention-prune) each other's backups.</summary>
public string BackupsDirectory => _backupsDirectory;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseContentRoot(
Path.Combine(
FindRepositoryRoot().FullName,
"src",
"backend",
"ItWorx.KnowledgeHub.Api"));
builder.UseEnvironment("IntegrationTest");
builder.ConfigureAppConfiguration((_, configuration) =>
{
var values = new Dictionary<string, string?>
{
["KnowledgeHub:Database:Host"] = _postgres.Host,
["KnowledgeHub:Database:Port"] = _postgres.Port.ToString(System.Globalization.CultureInfo.InvariantCulture),
["KnowledgeHub:Database:Name"] = _databaseName,
["KnowledgeHub:Database:Username"] = PostgresContainerFixture.Username,
["KnowledgeHub:Database:Password"] = PostgresContainerFixture.Password,
["KnowledgeHub:Paths:Attachments"] = _attachmentsDirectory,
["KnowledgeHub:Paths:Imports"] = _importsDirectory,
["KnowledgeHub:Paths:Backups"] = _backupsDirectory
};
foreach (var (key, value) in _additionalConfiguration)
{
values[key] = value;
}
configuration.AddInMemoryCollection(values);
});
}
private static DirectoryInfo FindRepositoryRoot()
{
DirectoryInfo? current = new(AppContext.BaseDirectory);
while (current is not null)
{
if (File.Exists(Path.Combine(current.FullName, "global.json"))
&& (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
|| File.Exists(Path.Combine(current.FullName, "PUBLIC_SOURCE_EXPORT.md"))))
{
return current;
}
current = current.Parent;
}
throw new DirectoryNotFoundException("Could not locate the repository root.");
}
}