Files
Omika/tests/Omika.Tests/Observability/Forwarding/DiagnosticsForwarderTests.cs
T
2026-10-23 18:46:50 +02:00

230 lines
9.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Extensions.Logging.Abstractions;
using Omika.Core.Observability.Deployment;
using Omika.Core.Observability.Forwarding;
using Omika.Core.Observability.Outbox;
namespace Omika.Tests.Observability.Forwarding;
/// <summary>Tests de l'orchestrateur du forwarder (ACK → checkpoint/suppression, erreurs, reprise).</summary>
public class DiagnosticsForwarderTests : IDisposable
{
private const string File001 = "omika-mobile-diagnostic-20261002-001.jsonl";
private const string File002 = "omika-mobile-diagnostic-20261002-002.jsonl";
private const string File003 = "omika-mobile-diagnostic-20261002-003.jsonl";
private readonly string _dir = ForwardingTestHarness.CreateTempDirectory();
public void Dispose() => ForwardingTestHarness.DeleteDirectory(_dir);
private static DiagnosticsForwardingOptions Options(int batchSize = 100) =>
new() { Enabled = true, Endpoint = "https://diagnostics.example/api/v1/diagnostics/batches", ApiKey = "omkdiag_x", BatchSize = batchSize, MaxBatchBytes = 1024 * 1024 };
private DiagnosticsForwarderSpool NewSpool() =>
new(_dir, "forwarder-checkpoint.json", new DiagnosticsForwardingSpoolOptions());
[Fact]
public async Task ForwardOnce_Success_CommitsCheckpoint()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
new FakeClient(DiagnosticsForwardOutcome.Success, accepted: 1),
Options(),
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance);
bool progressed = await forwarder.FlushAsync();
Assert.True(progressed);
Assert.NotNull(NewSpool().LoadCheckpoint());
Assert.True(File.Exists(Path.Combine(_dir, File001))); // fichier actif non supprimé.
}
[Fact]
public async Task ForwardOnce_Success_DrainsAndPrunesOlderFiles()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord()]);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
new FakeClient(DiagnosticsForwardOutcome.Success, accepted: 1),
Options(batchSize: 1),
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance);
Assert.True(await forwarder.FlushAsync()); // 001 forwardé
Assert.True(File.Exists(Path.Combine(_dir, File001)));
Assert.True(await forwarder.FlushAsync()); // 002 forwardé → 001 purgé
Assert.False(File.Exists(Path.Combine(_dir, File001)));
Assert.True(File.Exists(Path.Combine(_dir, File002)));
}
[Fact]
public async Task ForwardOnce_Unauthorized_DoesNotCommit_KeepsSpool()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
new FakeClient(DiagnosticsForwardOutcome.Unauthorized),
Options(),
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance);
bool progressed = await forwarder.FlushAsync();
Assert.False(progressed);
Assert.Null(NewSpool().LoadCheckpoint()); // aucun avancement.
Assert.True(File.Exists(Path.Combine(_dir, File001))); // spool conservé.
}
[Fact]
public async Task ForwardOnce_Rejected_AdvancesCheckpoint_DropsBatch()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
new FakeClient(DiagnosticsForwardOutcome.Rejected),
Options(),
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance);
bool progressed = await forwarder.FlushAsync();
Assert.False(progressed);
Assert.NotNull(NewSpool().LoadCheckpoint()); // avancé pour ne pas rejouer indéfiniment.
}
[Fact]
public async Task ForwardOnce_BacklogOverQuota_StillEnforcesBounds()
{
// T22 : le quota doit borner le spool même pendant une panne centrale
// (client Transient = jamais d'ACK → le spool n'est jamais « vide »).
int chunk = 400 * 1024; // 3 × 400 Ko = 1.2 Mo > 1 Mo de quota.
string padding = new string('a', chunk);
ForwardingTestHarness.WriteRawSpoolFile(_dir, File001, padding);
ForwardingTestHarness.WriteRawSpoolFile(_dir, File002, padding);
ForwardingTestHarness.WriteRawSpoolFile(_dir, File003, padding);
var spool = new DiagnosticsForwarderSpool(
_dir,
"forwarder-checkpoint.json",
new DiagnosticsForwardingSpoolOptions { MaxTotalSizeMb = 1 });
var forwarder = new DiagnosticsForwarder(
spool,
new FakeClient(DiagnosticsForwardOutcome.Transient),
Options(),
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance);
bool progressed = await forwarder.FlushAsync();
Assert.False(progressed);
Assert.False(File.Exists(Path.Combine(_dir, File001))); // plus ancien évincé par quota (sans ACK).
Assert.True(File.Exists(Path.Combine(_dir, File002)));
Assert.True(File.Exists(Path.Combine(_dir, File003))); // actif préservé.
}
[Fact]
public async Task ForwardOnce_NoClientCode_DoesNotSend()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var client = new FakeClient(DiagnosticsForwardOutcome.Success, accepted: 1);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
client,
Options(),
new FakeDeployment(null),
NullLogger<DiagnosticsForwarder>.Instance);
bool progressed = await forwarder.FlushAsync();
Assert.False(progressed);
Assert.Equal(0, client.SendCount);
Assert.True(File.Exists(Path.Combine(_dir, File001)));
}
// ------------------------- T20 §38 : Production + HTTP central refusé ----
[Fact]
public async Task ForwardOnce_HttpEndpointInProduction_RefusesSend_KeepsSpool()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var client = new FakeClient(DiagnosticsForwardOutcome.Success, accepted: 1);
var options = new DiagnosticsForwardingOptions
{
Enabled = true,
Endpoint = "http://diagnostics.example/api/v1/diagnostics/batches",
ApiKey = "omkdiag_x",
BatchSize = 100,
MaxBatchBytes = 1024 * 1024
};
var forwarder = new DiagnosticsForwarder(
NewSpool(),
client,
options,
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance,
isProduction: true);
bool progressed = await forwarder.FlushAsync();
Assert.False(progressed);
Assert.Equal(0, client.SendCount); // aucun envoi réseau.
Assert.Null(NewSpool().LoadCheckpoint()); // spool conservé, checkpoint inchangé.
Assert.True(File.Exists(Path.Combine(_dir, File001)));
}
[Fact]
public async Task ForwardOnce_HttpsEndpointInProduction_Sends()
{
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
var client = new FakeClient(DiagnosticsForwardOutcome.Success, accepted: 1);
var forwarder = new DiagnosticsForwarder(
NewSpool(),
client,
Options(), // https://
new FakeDeployment("BATIK.01"),
NullLogger<DiagnosticsForwarder>.Instance,
isProduction: true);
bool progressed = await forwarder.FlushAsync();
Assert.True(progressed);
Assert.Equal(1, client.SendCount);
}
// ------------------------- Options : règle Production/HTTPS (pure) ----
[Theory]
[InlineData("http://diagnostics.example/x", true, false)]
[InlineData("https://diagnostics.example/x", true, true)]
[InlineData("http://diagnostics.example/x", false, true)]
[InlineData("https://diagnostics.example/x", false, true)]
[InlineData("", true, false)]
public void Options_IsAllowedForEnvironment(string endpoint, bool isProduction, bool expected)
{
var options = new DiagnosticsForwardingOptions { Endpoint = endpoint };
Assert.Equal(expected, options.IsAllowedForEnvironment(isProduction));
}
private sealed class FakeClient(DiagnosticsForwardOutcome outcome, int accepted = 0) : IDiagnosticsForwarderClient
{
public int SendCount { get; private set; }
public Task<DiagnosticsForwardResult> SendAsync(
CentralDiagnosticBatchDto batch,
string clientCode,
string apiKey,
CancellationToken cancellationToken = default)
{
SendCount++;
return Task.FromResult(new DiagnosticsForwardResult(outcome, accepted));
}
}
private sealed class FakeDeployment(string? clientCode) : IDiagnosticDeploymentContextProvider
{
public Task<DiagnosticDeploymentContext> GetAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(new DiagnosticDeploymentContext { ClientCode = clientCode });
}
}