158 lines
6.2 KiB
C#
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.");
|
|
}
|
|
}
|