Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Omika.Core.Observability.Forwarding;
|
||||
using Omika.Core.Observability.Outbox;
|
||||
|
||||
namespace Omika.Tests.Observability.Forwarding;
|
||||
|
||||
/// <summary>Tests du client HTTP du forwarder (mapping de statuts, headers, jamais de levée).</summary>
|
||||
public class DiagnosticsForwarderClientTests
|
||||
{
|
||||
private static DiagnosticsForwarderClient NewClient(FakeHandler handler) =>
|
||||
new(new HttpClient(handler), "https://diagnostics.example/api/v1/diagnostics/batches", TimeSpan.FromSeconds(5));
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_2xx_ReturnsSuccess_WithAcceptedCount()
|
||||
{
|
||||
var handler = new FakeHandler(HttpStatusCode.OK, "{\"batchId\":\"00000000-0000-0000-0000-000000000000\",\"acceptedCount\":7}");
|
||||
var client = NewClient(handler);
|
||||
|
||||
DiagnosticsForwardResult result = await client.SendAsync(Batch(), "BATIK.01", "omkdiag_x");
|
||||
|
||||
Assert.Equal(DiagnosticsForwardOutcome.Success, result.Outcome);
|
||||
Assert.Equal(7, result.AcceptedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_401_ReturnsUnauthorized() =>
|
||||
await AssertOutcomeAsync(HttpStatusCode.Unauthorized, DiagnosticsForwardOutcome.Unauthorized);
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_403_ReturnsForbidden() =>
|
||||
await AssertOutcomeAsync(HttpStatusCode.Forbidden, DiagnosticsForwardOutcome.Forbidden);
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_400_ReturnsRejected() =>
|
||||
await AssertOutcomeAsync(HttpStatusCode.BadRequest, DiagnosticsForwardOutcome.Rejected);
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_503_ReturnsTransient() =>
|
||||
await AssertOutcomeAsync(HttpStatusCode.ServiceUnavailable, DiagnosticsForwardOutcome.Transient);
|
||||
|
||||
[Fact]
|
||||
public async Task SendAsync_SetsClientCodeAndKeyHeaders()
|
||||
{
|
||||
var handler = new FakeHandler(HttpStatusCode.OK, "{}");
|
||||
var client = NewClient(handler);
|
||||
|
||||
await client.SendAsync(Batch(), "BATIK.01", "omkdiag_secret_key");
|
||||
|
||||
Assert.Equal("BATIK.01", handler.LastRequest?.Headers.GetValues("X-Omika-Client-Code").FirstOrDefault());
|
||||
Assert.Equal("omkdiag_secret_key", handler.LastRequest?.Headers.GetValues("X-Omika-Diagnostics-Key").FirstOrDefault());
|
||||
}
|
||||
|
||||
private static async Task AssertOutcomeAsync(HttpStatusCode status, DiagnosticsForwardOutcome expected)
|
||||
{
|
||||
var handler = new FakeHandler(status, "{}");
|
||||
var client = NewClient(handler);
|
||||
|
||||
DiagnosticsForwardResult result = await client.SendAsync(Batch(), "BATIK.01", "omkdiag_x");
|
||||
|
||||
Assert.Equal(expected, result.Outcome);
|
||||
}
|
||||
|
||||
private static CentralDiagnosticBatchDto Batch() =>
|
||||
new() { Events = [ForwardingTestHarness.NewRecord()] };
|
||||
|
||||
private sealed class FakeHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpStatusCode _status;
|
||||
private readonly string _body;
|
||||
|
||||
public FakeHandler(HttpStatusCode status, string body)
|
||||
{
|
||||
_status = status;
|
||||
_body = body;
|
||||
}
|
||||
|
||||
public HttpRequestMessage? LastRequest { get; private set; }
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequest = request;
|
||||
return Task.FromResult(new HttpResponseMessage(_status)
|
||||
{
|
||||
Content = new StringContent(_body, Encoding.UTF8, "application/json")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Omika.Core.Observability.Forwarding;
|
||||
|
||||
namespace Omika.Tests.Observability.Forwarding;
|
||||
|
||||
/// <summary>Tests du spool disque du forwarder (checkpoint, bornes, suppression, quota, rétention).</summary>
|
||||
public class DiagnosticsForwarderSpoolTests : 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();
|
||||
|
||||
private static DiagnosticsForwardingSpoolOptions DefaultSpool => new();
|
||||
|
||||
public void Dispose() => ForwardingTestHarness.DeleteDirectory(_dir);
|
||||
|
||||
private DiagnosticsForwarderSpool NewSpool(DiagnosticsForwardingSpoolOptions? options = null) =>
|
||||
new(_dir, "forwarder-checkpoint.json", options ?? DefaultSpool);
|
||||
|
||||
[Fact]
|
||||
public void ReadNext_EmptyDirectory_ReturnsReachedEnd()
|
||||
{
|
||||
var spool = NewSpool();
|
||||
|
||||
DiagnosticsForwarderBatch batch = spool.ReadNext(null, 10, 1024);
|
||||
|
||||
Assert.Empty(batch.Records);
|
||||
Assert.True(batch.ReachedEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadNext_ReadsRecordsInOrder_AndResumesAcrossFiles()
|
||||
{
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File001,
|
||||
[ForwardingTestHarness.NewRecord(component: "a"), ForwardingTestHarness.NewRecord(component: "b")]);
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File002,
|
||||
[ForwardingTestHarness.NewRecord(component: "c")]);
|
||||
|
||||
var spool = NewSpool();
|
||||
|
||||
DiagnosticsForwarderBatch first = spool.ReadNext(null, 2, 1_000_000);
|
||||
Assert.Equal(2, first.Records.Count);
|
||||
Assert.Equal("a", first.Records[0].Event.Component);
|
||||
Assert.Equal("b", first.Records[1].Event.Component);
|
||||
Assert.NotNull(first.NextPosition);
|
||||
Assert.Equal(File001, first.NextPosition!.FileName);
|
||||
|
||||
DiagnosticsForwarderBatch second = spool.ReadNext(first.NextPosition, 2, 1_000_000);
|
||||
Assert.Single(second.Records);
|
||||
Assert.Equal("c", second.Records[0].Event.Component);
|
||||
Assert.Equal(File002, second.NextPosition!.FileName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadNext_RespectsMaxBytes_StillReturnsAtLeastOneRecord()
|
||||
{
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File001,
|
||||
[ForwardingTestHarness.NewRecord(), ForwardingTestHarness.NewRecord()]);
|
||||
|
||||
var spool = NewSpool();
|
||||
|
||||
DiagnosticsForwarderBatch batch = spool.ReadNext(null, 100, 10);
|
||||
|
||||
Assert.Single(batch.Records); // premier seuil (octets) : 1 seul, jamais zéro.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Commit_ThenLoadCheckpoint_ResumesFromNextLine()
|
||||
{
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File001,
|
||||
[ForwardingTestHarness.NewRecord(component: "a"), ForwardingTestHarness.NewRecord(component: "b"), ForwardingTestHarness.NewRecord(component: "c")]);
|
||||
|
||||
var spool = NewSpool();
|
||||
|
||||
DiagnosticsForwarderBatch first = spool.ReadNext(null, 1, 1_000_000);
|
||||
Assert.Single(first.Records);
|
||||
Assert.Equal("a", first.Records[0].Event.Component);
|
||||
spool.Commit(first.NextPosition!);
|
||||
|
||||
DiagnosticsForwarderPosition? checkpoint = spool.LoadCheckpoint();
|
||||
Assert.NotNull(checkpoint);
|
||||
Assert.Equal(File001, checkpoint!.FileName);
|
||||
Assert.Equal(0, checkpoint.LineIndex);
|
||||
|
||||
DiagnosticsForwarderBatch second = spool.ReadNext(checkpoint, 10, 1_000_000);
|
||||
Assert.Equal(2, second.Records.Count);
|
||||
Assert.Equal("b", second.Records[0].Event.Component);
|
||||
Assert.Equal("c", second.Records[1].Event.Component);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PruneConsumed_DeletesOlderFiles_NeverActiveFile()
|
||||
{
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord()]);
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File003, [ForwardingTestHarness.NewRecord()]);
|
||||
|
||||
var spool = NewSpool();
|
||||
|
||||
int deleted = spool.PruneConsumed(new DiagnosticsForwarderPosition { FileName = File002, LineIndex = 0 });
|
||||
|
||||
Assert.Equal(1, deleted); // seul 001 (antérieur) est supprimé.
|
||||
Assert.False(File.Exists(Path.Combine(_dir, File001)));
|
||||
Assert.True(File.Exists(Path.Combine(_dir, File002)));
|
||||
Assert.True(File.Exists(Path.Combine(_dir, File003))); // actif préservé.
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnforceBounds_Quota_DeletesOldestFiles()
|
||||
{
|
||||
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 = NewSpool(new DiagnosticsForwardingSpoolOptions { MaxTotalSizeMb = 1 });
|
||||
|
||||
int deleted = spool.EnforceBounds(DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.Equal(1, deleted); // le plus ancien est évincé, total ≤ 1 Mo.
|
||||
Assert.False(File.Exists(Path.Combine(_dir, File001)));
|
||||
Assert.True(File.Exists(Path.Combine(_dir, File002)));
|
||||
Assert.True(File.Exists(Path.Combine(_dir, File003)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnforceBounds_Retention_DeletesExpiredFiles_NotActive()
|
||||
{
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File001, [ForwardingTestHarness.NewRecord()]);
|
||||
ForwardingTestHarness.WriteSpoolFile(_dir, File002, [ForwardingTestHarness.NewRecord()]);
|
||||
|
||||
// 001 est ancien, 002 est l'actif (et récent).
|
||||
File.SetLastWriteTimeUtc(Path.Combine(_dir, File001), DateTime.UtcNow.AddDays(-2));
|
||||
File.SetLastWriteTimeUtc(Path.Combine(_dir, File002), DateTime.UtcNow);
|
||||
|
||||
var spool = NewSpool(new DiagnosticsForwardingSpoolOptions { RetentionDays = 1, MaxTotalSizeMb = 1024 });
|
||||
|
||||
int deleted = spool.EnforceBounds(DateTimeOffset.UtcNow);
|
||||
|
||||
Assert.Equal(1, deleted);
|
||||
Assert.False(File.Exists(Path.Combine(_dir, File001)));
|
||||
Assert.True(File.Exists(Path.Combine(_dir, File002)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Text.Json;
|
||||
using Omika.Core.Observability.External;
|
||||
using Omika.Core.Observability.Outbox;
|
||||
|
||||
namespace Omika.Tests.Observability.Forwarding;
|
||||
|
||||
/// <summary>Fabrique de test partagée pour le forwarder T19.</summary>
|
||||
internal static class ForwardingTestHarness
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static string CreateTempDirectory()
|
||||
{
|
||||
string dir = Path.Combine(Path.GetTempPath(), "omika-forwarding-tests", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static void DeleteDirectory(string dir)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
Directory.Delete(dir, recursive: true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
public static RemoteDiagnosticRecord NewRecord(
|
||||
string clientCode = "BATIK.01",
|
||||
string component = "test",
|
||||
Guid? eventId = null,
|
||||
string? screenCode = null,
|
||||
string? featureCode = null) =>
|
||||
new()
|
||||
{
|
||||
ReceivedAtUtc = DateTimeOffset.UtcNow,
|
||||
BatchId = Guid.NewGuid(),
|
||||
IngestionTraceId = Guid.NewGuid().ToString("D"),
|
||||
ClientCode = clientCode,
|
||||
ActorKey = "a" + new string('0', 63),
|
||||
ActorKeyVersion = 1,
|
||||
DeviceKey = "d" + new string('0', 63),
|
||||
DeviceKeyVersion = 1,
|
||||
LicenseNumber = "b600a641d61c485b8e928cd3aab281b1",
|
||||
ServerInstance = "API-SRV-01",
|
||||
Environment = "Production",
|
||||
Event = new DiagnosticEventDto
|
||||
{
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
EventCode = "OMK-MOB-APP-001",
|
||||
TimestampUtc = DateTimeOffset.UtcNow,
|
||||
Level = "Info",
|
||||
Priority = "P3",
|
||||
Component = component,
|
||||
ScreenCode = screenCode,
|
||||
FeatureCode = featureCode
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>Sérialise un enregistrement au format JSONL canonique (camelCase).</summary>
|
||||
public static string Serialize(RemoteDiagnosticRecord record) =>
|
||||
JsonSerializer.Serialize(record, JsonOptions);
|
||||
|
||||
/// <summary>Écrit un fichier JSONL de spool (nom exact, ex. omika-mobile-diagnostic-20261002-001.jsonl).</summary>
|
||||
public static void WriteSpoolFile(string directory, string fileName, IReadOnlyList<RemoteDiagnosticRecord> records)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllLines(Path.Combine(directory, fileName), records.Select(Serialize));
|
||||
}
|
||||
|
||||
/// <summary>Écrit un fichier de spool avec un contenu brut arbitraire (tests quota/taille).</summary>
|
||||
public static void WriteRawSpoolFile(string directory, string fileName, string content)
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
File.WriteAllText(Path.Combine(directory, fileName), content);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user