Initial curated itworx-knowledge-hub source from 3b1751d0d9dee551aa6994eee0093704dc334817
This commit is contained in:
commit
ad8ad252a9
825 files changed
+124216
No files matched your search
+157
@@ -0,0 +1,157 @@
|
||||
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.");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user