using ManagerService.Controllers;
using ManagerService.Tests.Infrastructure;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace ManagerService.Tests.Controllers
{
///
/// Dry run de la bascule Mongo → Postgres, et comparaison des comptages par entité.
/// C'est la seule preuve que les écarts (b), (c) et (e) sont fermés : la lecture de
/// code ne dit pas ce qui arrive vraiment dans la base.
///
/// Se saute si aucun Mongo n'écoute sur MIGRATION_TEST_MONGO (défaut
/// mongodb://localhost:27018) — la suite reste donc verte sans dépendance.
/// Pour l'exécuter : importer `migration-data/` dans un Mongo jetable, cf. STATUS.md §1quinquies.
///
public class MigrationDryRunTests
{
private readonly ITestOutputHelper _output;
public MigrationDryRunTests(ITestOutputHelper output) => _output = output;
private static string MongoUri =>
Environment.GetEnvironmentVariable("MIGRATION_TEST_MONGO") ?? "mongodb://localhost:27018";
private static bool MongoIsReachable(out Dictionary counts)
{
counts = new Dictionary();
try
{
var settings = MongoClientSettings.FromConnectionString(MongoUri);
settings.ServerSelectionTimeout = TimeSpan.FromSeconds(2);
var db = new MongoClient(settings).GetDatabase("TabletDb");
foreach (var name in new[] { "Instances", "Users", "Configurations", "Resources", "Sections", "Devices" })
counts[name] = db.GetCollection(name)
.CountDocuments(MongoDB.Bson.BsonDocument.Parse("{}"));
return counts["Instances"] > 0;
}
catch
{
return false;
}
}
///
/// Les tailles de fichier passent par un HEAD sur chaque URL. Sur 2374 ressources
/// cela interrogerait Firebase pendant plusieurs minutes, pour une information que
/// ce test ne vérifie pas : il compare des comptages. On coupe court.
///
private sealed class UnreachableHttpClientFactory : IHttpClientFactory
{
private sealed class Handler : HttpMessageHandler
{
protected override Task SendAsync(HttpRequestMessage request, CancellationToken ct) =>
Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
public HttpClient CreateClient(string name) => new HttpClient(new Handler());
}
[Fact]
public async Task DryRun_MatchesMongoCounts()
{
if (!MongoIsReachable(out var mongo))
{
_output.WriteLine($"Mongo injoignable sur {MongoUri} — test sauté.");
return;
}
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:TabletDb"] = MongoUri })
.Build();
using var db = DbContextFactory.Create();
var controller = new MigrationController(
db,
new Manager.Services.InstanceDatabaseService(config),
new Manager.Services.ConfigurationDatabaseService(config),
new Manager.Services.SectionDatabaseService(config),
new Manager.Services.ResourceDatabaseService(config),
new Manager.Services.UserDatabaseService(config),
new Manager.Services.DeviceDatabaseService(config),
new UnreachableHttpClientFactory());
var result = await controller.Run(dryRun: true);
var report = Assert.IsType(result.Value);
_output.WriteLine($"FatalError : {report.FatalError ?? "(aucun)"}");
_output.WriteLine($"Erreurs : {report.Errors.Count}");
foreach (var e in report.Errors.Take(15)) _output.WriteLine(" " + e);
_output.WriteLine("");
_output.WriteLine($"{"Entité",-18}{"Mongo",8}{"Migré",8}");
_output.WriteLine($"{"Instances",-18}{mongo["Instances"],8}{report.Migrated.Instances,8}");
_output.WriteLine($"{"Users",-18}{mongo["Users"],8}{report.Migrated.Users,8}");
_output.WriteLine($"{"Configurations",-18}{mongo["Configurations"],8}{report.Migrated.Configurations,8}");
_output.WriteLine($"{"Resources",-18}{mongo["Resources"],8}{report.Migrated.Resources,8}");
_output.WriteLine($"{"Sections",-18}{mongo["Sections"],8}{report.Migrated.Sections,8}");
_output.WriteLine($"{"Devices",-18}{mongo["Devices"],8}{report.Migrated.Devices,8}");
_output.WriteLine($"{"ApplicationInstances",-18}{"—",8}{report.Migrated.ApplicationInstances,8}");
_output.WriteLine($"{"AppConfigLinks",-18}{"—",8}{report.Migrated.AppConfigurationLinks,8}");
Assert.Null(report.FatalError);
// Une entité migrée sur deux, c'est une bascule qui se déclare réussie en
// ayant perdu la moitié du contenu. C'est exactement ce que le dry run doit
// attraper avant le jour J.
Assert.Equal(mongo["Instances"], report.Migrated.Instances);
Assert.Equal(mongo["Users"], report.Migrated.Users);
Assert.Equal(mongo["Configurations"], report.Migrated.Configurations);
// Les sections dont la configuration a été supprimée dans Mongo ne peuvent pas
// être migrées, mais elles doivent être **signalées**. L'invariant qui compte
// n'est donc pas « tout est migré » : c'est « rien ne disparaît en silence ».
var skippedSections = report.Skipped.Count(s => s.StartsWith("Section "));
_output.WriteLine($"{" dont orphelines",-18}{"",8}{skippedSections,8}");
Assert.Equal(mongo["Sections"], report.Migrated.Sections + skippedSections);
Assert.Equal(mongo["Devices"], report.Migrated.Devices);
Assert.Equal(mongo["Resources"], report.Migrated.Resources);
}
}
}