using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Omika.Diagnostics.Configuration; using Omika.Diagnostics.Data; using Omika.Diagnostics.Import; using Omika.Diagnostics.Worker; namespace Omika.Diagnostics.Tests; /// /// Vérifie que le conteneur DI du Diagnostics Worker construit réellement /// et les hosted services (régression de /// l'incident production « Unable to resolve service for type /// DiagnosticImportOptions »). Le host est construit avec /// ValidateOnBuild + ValidateScopes pour détecter ce type d'erreur /// à la construction (le test échouait avec l'ancien code). /// public class DiagnosticWorkerDependencyInjectionTests { [Fact] public void WorkerHost_ServiceProviderBuilds_WithValidation() { // Avec l'ancien constructeur (DiagnosticImportOptions concret), ce Build() // levait « Unable to resolve service for type DiagnosticImportOptions ». using IHost host = BuildWorkerHost(); Assert.NotNull(host.Services); } [Fact] public void WorkerHost_DiagnosticImporter_IsResolvable() { using IHost host = BuildWorkerHost(); DiagnosticImporter importer = host.Services.GetRequiredService(); Assert.NotNull(importer); } [Fact] public void WorkerHost_DiagnosticImportOptions_AreResolvedAndBound() { using IHost host = BuildWorkerHost(); DiagnosticImportOptions options = host.Services .GetRequiredService>() .Value; Assert.NotNull(options); // La configuration in-memory prime sur les valeurs par défaut (BatchSize=100). Assert.Equal(50, options.BatchSize); Assert.Equal("import-checkpoint.json", options.CheckpointFileName); Assert.Equal(@"C:\Omika\Data\DiagnosticsWorker\Checkpoint", options.CheckpointDirectory); } [Fact] public void WorkerHost_DiagnosticStoreOptions_ConnectionStringOverride_Wins() { using IHost host = BuildWorkerHost(); DiagnosticStoreOptions store = host.Services .GetRequiredService>() .Value; // La surcharge standard ConnectionStrings:DiagnosticsDatabase (variable // d'environnement serveur) prime sur Diagnostics:ConnectionString. Assert.Equal( "Host=localhost;Database=omika_diagnostics;Username=diagnostics_worker", store.ConnectionString); } [Fact] public void WorkerHost_HostedServices_AreResolvable() { using IHost host = BuildWorkerHost(); List hosted = host.Services.GetServices().ToList(); Assert.Contains(hosted, s => s is DiagnosticImportWorker); Assert.Contains(hosted, s => s is DiagnosticRetentionWorker); Assert.Contains(hosted, s => s is DiagnosticTelemetryRetentionWorker); } private static IHost BuildWorkerHost() { // Configuration in-memory : aucune valeur secrète. La chaîne PostgreSQL // est un placeholder sans mot de passe (jamais ouverte : les repositories // sont construits sans connexion). IConfiguration configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Diagnostics:ConnectionString"] = "Host=ignored;Database=ignored", ["Diagnostics:Import:BatchSize"] = "50", ["Diagnostics:Import:CheckpointFileName"] = "import-checkpoint.json", ["Diagnostics:Import:CheckpointDirectory"] = @"C:\Omika\Data\DiagnosticsWorker\Checkpoint", ["Observability:Remote:Directory"] = @"C:\Omika\Data\Api\Diagnostics\Jsonl", // Surcharge standard (serveur) : prime sur Diagnostics:ConnectionString. ["ConnectionStrings:DiagnosticsDatabase"] = "Host=localhost;Database=omika_diagnostics;Username=diagnostics_worker" }) .Build(); // Reproduit Program.cs du Diagnostics Worker, avec validation DI stricte // (ValidateOnBuild + ValidateScopes) pour piéger les dépendances non // enregistrées. return new HostBuilder() .UseDefaultServiceProvider(options => { options.ValidateOnBuild = true; options.ValidateScopes = true; }) .ConfigureServices(services => { services.AddLogging(); services.AddOmikaDiagnosticData(configuration); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); }) .Build(); } }