using DotNet.Testcontainers.Builders;
using ManagerService.Data;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Testcontainers.PostgreSql;
using Xunit;
namespace ManagerService.Tests.Infrastructure
{
///
/// Un vrai PostgreSQL, construit depuis Deployment/Dockerfile.postgres —
/// donc le même PostGIS + pgvector que la production, à l'image épinglée près.
///
///
/// Le vector store ne peut pas être éprouvé sur EF InMemory : le type vector,
/// l'index HNSW et le post-filtrage n'y existent pas, et le modèle exclut d'ailleurs
/// explicitement ContentEmbedding hors Npgsql.
///
/// Sans démon Docker, la fixture ne lève pas : passe à false
/// et les tests se sautent. La suite reste verte sur une machine sans Docker, comme
/// MigrationDryRunTests le fait déjà sans Mongo.
///
public class PostgresFixture : IAsyncLifetime
{
private const string ImageTag = "myinfomate-postgres-tests:latest";
private PostgreSqlContainer _container;
public bool Available { get; private set; }
public string SkipReason { get; private set; }
public string ConnectionString { get; private set; }
public async Task InitializeAsync()
{
try
{
BuildImage();
_container = new PostgreSqlBuilder()
.WithImage(ImageTag)
.WithDatabase("my_info_mate")
.WithUsername("postgres")
.WithPassword("postgres")
.Build();
await _container.StartAsync();
ConnectionString = _container.GetConnectionString();
Available = true;
}
catch (Exception ex)
{
SkipReason = $"Docker indisponible : {ex.Message}";
Available = false;
}
}
///
/// Construit l'image via le CLI docker plutôt que par le constructeur d'images de
/// Testcontainers : celui-ci relit le FROM pour pré-tirer l'image de base et
/// ne sait pas parser la forme tag@sha256:…. Or ce digest est justement ce
/// qui protège la base d'un changement de glibc sous ses index — il n'est pas
/// négociable pour arranger un test.
///
private static void BuildImage()
{
var dockerfileDirectory = Path.Combine(
CommonDirectoryPath.GetSolutionDirectory().DirectoryPath, "ManagerService", "Deployment");
var process = Process.Start(new ProcessStartInfo("docker")
{
ArgumentList = { "build", "-f", "Dockerfile.postgres", "-t", ImageTag, "." },
WorkingDirectory = dockerfileDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true
});
process.WaitForExit((int)TimeSpan.FromMinutes(10).TotalMilliseconds);
if (process.ExitCode != 0)
throw new InvalidOperationException(
$"docker build a échoué :\n{process.StandardError.ReadToEnd()}");
}
public async Task DisposeAsync()
{
if (_container != null)
await _container.DisposeAsync();
}
///
/// Contexte sur une base neuve, migrations appliquées. Les extensions PostGIS et
/// pgvector sont créées par les migrations, pas par l'image : un CREATE EXTENSION
/// manuel masquerait justement l'oubli qu'on veut voir.
///
public MyInfoMateDbContext CreateContext(string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionString) { Database = databaseName };
var dataSourceBuilder = new NpgsqlDataSourceBuilder(builder.ConnectionString);
dataSourceBuilder.UseNetTopologySuite();
dataSourceBuilder.UseVector();
dataSourceBuilder.EnableDynamicJson();
var options = new DbContextOptionsBuilder()
.UseNpgsql(dataSourceBuilder.Build(), o => o.UseNetTopologySuite().UseVector())
.Options;
return new MyInfoMateDbContext(options, new HttpContextAccessor());
}
/// Crée une base vide puis y applique les migrations.
///
/// Coupe le parcours séquentiel pour la base entière. Sur les volumes d'un test,
/// un seq scan reste moins cher qu'un index HNSW, et le post-filtrage — ce qu'on
/// vient justement éprouver — resterait invisible. Le réglage est posé sur la base
/// et non sur la session : un SET par ExecuteSqlRaw serait effacé au retour de la
/// connexion au pool, et le test passerait au vert sans rien avoir prouvé.
///
public MyInfoMateDbContext CreateMigratedContext(string databaseName, bool forceIndexScan = false)
{
NpgsqlConnection.ClearAllPools();
using (var admin = new NpgsqlConnection(ConnectionString))
{
admin.Open();
using var drop = new NpgsqlCommand($"DROP DATABASE IF EXISTS \"{databaseName}\"", admin);
drop.ExecuteNonQuery();
using var create = new NpgsqlCommand($"CREATE DATABASE \"{databaseName}\"", admin);
create.ExecuteNonQuery();
if (forceIndexScan)
{
using var tune = new NpgsqlCommand($"ALTER DATABASE \"{databaseName}\" SET enable_seqscan = off", admin);
tune.ExecuteNonQuery();
}
}
var db = CreateContext(databaseName);
db.Database.Migrate();
return db;
}
/// Connexion brute sur une base du conteneur, hors EF.
public NpgsqlConnection OpenRawConnection(string databaseName)
{
var builder = new NpgsqlConnectionStringBuilder(ConnectionString) { Database = databaseName };
var dataSourceBuilder = new NpgsqlDataSourceBuilder(builder.ConnectionString);
dataSourceBuilder.UseVector();
var connection = dataSourceBuilder.Build().OpenConnection();
return connection;
}
}
[CollectionDefinition(Name)]
public class PostgresCollection : ICollectionFixture
{
public const string Name = "postgres";
}
}