230 lines
9.4 KiB
C#
230 lines
9.4 KiB
C#
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 });
|
||
}
|
||
}
|