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