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); } } }