Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="IDiagnosticClientCodeSource"/>
|
||||
/// et <see cref="IHostEnvironment"/> en mémoire.
|
||||
/// </summary>
|
||||
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<IDiagnosticDeploymentContextProvider>();
|
||||
|
||||
Task<DiagnosticDeploymentContext>[] 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<DiagnosticDeploymentContext> ResolveAsync(
|
||||
DiagnosticDeploymentOptions options,
|
||||
string environmentName = "Production",
|
||||
FakeClientCodeSource? source = null)
|
||||
{
|
||||
using ServiceProvider services = BuildServices(options, environmentName, source);
|
||||
IDiagnosticDeploymentContextProvider provider =
|
||||
services.GetRequiredService<IDiagnosticDeploymentContextProvider>();
|
||||
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<IHostEnvironment>(new FakeHostEnvironment { EnvironmentName = environmentName });
|
||||
services.AddSingleton<ILogger<DiagnosticDeploymentContextProvider>>(
|
||||
NullLogger<DiagnosticDeploymentContextProvider>.Instance);
|
||||
if (source is not null)
|
||||
{
|
||||
services.AddScoped<IDiagnosticClientCodeSource>(_ => source);
|
||||
}
|
||||
|
||||
services.AddSingleton<IDiagnosticDeploymentContextProvider, DiagnosticDeploymentContextProvider>();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private sealed class FakeClientCodeSource : IDiagnosticClientCodeSource
|
||||
{
|
||||
private readonly Func<CancellationToken, Task<string?>> _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<string?>(exception);
|
||||
};
|
||||
}
|
||||
|
||||
public Task<string?> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user