using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Omika.Core.Observability.Deployment; using Omika.Core.Observability.Outbox; namespace Omika.Diagnostics.Tests; /// /// Tâche 13-D — Résolution centralisée ClientCode / ServerInstance / Environment : /// configuration prioritaire, fallback ParametreSysteme (fake), normalisation /// (trim, longueurs), fail-open, cache/concurrence, absence de secret. /// Aucune base HFSQL/PostgreSQL réelle : fake de /// et en mémoire. /// public class DiagnosticDeploymentContextProviderTests { // --------------------------- D1 : config ClientCode prioritaire --------------- [Fact] public async Task D1_ClientCodeFromConfig_IsUsed() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = "CLIENT-A" }); Assert.Equal("CLIENT-A", context.ClientCode); } // --------------------------- D2 : trim --------------------------------------- [Fact] public async Task D2_ClientCodeWithSpaces_IsTrimmed() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = " CLIENT-A " }); Assert.Equal("CLIENT-A", context.ClientCode); } // --------------------------- D3 : config vide → fallback --------------------- [Fact] public async Task D3_EmptyConfig_FallsBackToParametreSysteme() { var source = new FakeClientCodeSource("CLIENT-HFSQL"); DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = " " }, source: source); Assert.Equal("CLIENT-HFSQL", context.ClientCode); Assert.Equal(1, source.CallCount); } // --------------------------- D4 : ParametreSysteme présent ------------------- [Fact] public async Task D4_ParametreSystemeClientCode_IsUsed() { var source = new FakeClientCodeSource("CLIENT-HFSQL"); DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions(), source: source); Assert.Equal("CLIENT-HFSQL", context.ClientCode); } // --------------------------- D5 : ParametreSysteme absent → null ------------- [Fact] public async Task D5_ParametreSystemeAbsent_ClientCodeIsNull() { var source = new FakeClientCodeSource((string?)null); DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions(), source: source); Assert.Null(context.ClientCode); } // --------------------------- D6 : HFSQL lève → null, pas d'exception --------- [Fact] public async Task D6_HfsqlThrows_ClientCodeNull_NoException() { var source = new FakeClientCodeSource(new InvalidOperationException("HFSQL down")); DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions(), source: source); Assert.Null(context.ClientCode); // Fail-open : le provider continue et fournit quand même ServerInstance/Environment. Assert.NotNull(context.ServerInstance); Assert.Equal("Production", context.Environment); } // --------------------------- D7 : ServerInstance config ---------------------- [Fact] public async Task D7_ServerInstanceFromConfig_IsUsed() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ServerInstanceId = "API-SRV-01" }); Assert.Equal("API-SRV-01", context.ServerInstance); } // --------------------------- D8 : ServerInstance vide → MachineName ---------- [Fact] public async Task D8_EmptyServerInstance_FallsBackToMachineName() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ServerInstanceId = "" }); Assert.Equal(Environment.MachineName, context.ServerInstance); } // --------------------------- D9 : Environment Production --------------------- [Fact] public async Task D9_EnvironmentProduction_IsPreserved() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions(), environmentName: "Production"); Assert.Equal("Production", context.Environment); } // --------------------------- D10 : contexte complet -------------------------- [Fact] public async Task D10_FinalContext_ContainsAllThreeValues() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = "CLIENT-A", ServerInstanceId = "SRV-01" }, environmentName: "Staging"); Assert.Equal("CLIENT-A", context.ClientCode); Assert.Equal("SRV-01", context.ServerInstance); Assert.Equal("Staging", context.Environment); } // --------------------------- D11 : config présente → HFSQL non appelé -------- [Fact] public async Task D11_ClientCodeConfigPresent_HfsqlNotCalled() { var source = new FakeClientCodeSource("CLIENT-HFSQL"); DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = "CLIENT-CONFIG" }, source: source); Assert.Equal("CLIENT-CONFIG", context.ClientCode); Assert.Equal(0, source.CallCount); } // --------------------------- D12 : concurrence → une seule résolution -------- [Fact] public async Task D12_ConcurrentGetAsync_ResolvesClientCodeOnce() { // Délai pour élargir la fenêtre de concurrence réelle. var source = new FakeClientCodeSource("CLIENT-A", delay: TimeSpan.FromMilliseconds(50)); using ServiceProvider services = BuildServices(new DiagnosticDeploymentOptions(), "Production", source); IDiagnosticDeploymentContextProvider provider = services.GetRequiredService(); Task[] tasks = Enumerable.Range(0, 50) .Select(_ => provider.GetAsync()) .ToArray(); DiagnosticDeploymentContext[] results = await Task.WhenAll(tasks); Assert.All(results, r => Assert.Equal("CLIENT-A", r.ClientCode)); Assert.Equal(1, source.CallCount); } // --------------------------- D13 : ClientCode > 100 -------------------------- [Fact] public async Task D13_ClientCodeTooLong_IsIgnored() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = new string('X', 101) }); // Pas de source HFSQL → la valeur invalide est ignorée (warning) → null. Assert.Null(context.ClientCode); } // --------------------------- D14 : ServerInstance > 100 ---------------------- [Fact] public async Task D14_ServerInstanceTooLong_FallsBackToMachineName() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ServerInstanceId = new string('X', 101) }); Assert.Equal(Environment.MachineName, context.ServerInstance); } // --------------------------- D15 : Environment > 20 -------------------------- [Fact] public async Task D15_EnvironmentTooLong_FallsBackToProduction() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions(), environmentName: new string('E', 21)); Assert.Equal("Production", context.Environment); } // --------------------------- D16 : aucun secret ------------------------------ [Fact] public async Task D16_ContextAndOptions_NeverContainSecrets() { DiagnosticDeploymentContext context = await ResolveAsync( new DiagnosticDeploymentOptions { ClientCode = "CLIENT-A", ServerInstanceId = "SRV-01" }, environmentName: "Production"); string combined = $"{context.ClientCode}|{context.ServerInstance}|{context.Environment}"; Assert.DoesNotContain("CL_CleAPIInstall", combined, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("password", combined, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("SigningKey", combined, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("token", combined, StringComparison.OrdinalIgnoreCase); // Les options n'exposent que les deux clés de déploiement attendues (aucun // champ secret, aucun Environment : celui-ci vient d'IHostEnvironment). string[] properties = typeof(DiagnosticDeploymentOptions) .GetProperties() .Select(p => p.Name) .OrderBy(n => n, StringComparer.Ordinal) .ToArray(); Assert.Equal(["ClientCode", "ServerInstanceId"], properties); } // --------------------------- Helpers ----------------------------------------- private static async Task ResolveAsync( DiagnosticDeploymentOptions options, string environmentName = "Production", FakeClientCodeSource? source = null) { using ServiceProvider services = BuildServices(options, environmentName, source); IDiagnosticDeploymentContextProvider provider = services.GetRequiredService(); return await provider.GetAsync(); } private static ServiceProvider BuildServices( DiagnosticDeploymentOptions options, string environmentName, FakeClientCodeSource? source) { var services = new ServiceCollection(); services.AddSingleton(Options.Create(options)); services.AddSingleton(new FakeHostEnvironment { EnvironmentName = environmentName }); services.AddSingleton>( NullLogger.Instance); if (source is not null) { services.AddScoped(_ => source); } services.AddSingleton(); return services.BuildServiceProvider(); } private sealed class FakeClientCodeSource : IDiagnosticClientCodeSource { private readonly Func> _resolve; private int _callCount; public int CallCount => Volatile.Read(ref _callCount); public FakeClientCodeSource(string? value, TimeSpan? delay = null) { _resolve = async ct => { Interlocked.Increment(ref _callCount); if (delay is { } d) { await Task.Delay(d, ct).ConfigureAwait(false); } return value; }; } public FakeClientCodeSource(Exception exception) { _resolve = _ => { Interlocked.Increment(ref _callCount); return Task.FromException(exception); }; } public Task GetClientCodeAsync(CancellationToken cancellationToken = default) => _resolve(cancellationToken); } private sealed class FakeHostEnvironment : IHostEnvironment { public string EnvironmentName { get; set; } = "Production"; public string ApplicationName { get; set; } = "test"; public string ContentRootPath { get; set; } = AppContext.BaseDirectory; public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); } }