using System.Net; using System.Net.Http.Headers; using System.Text; using Omika.Core.Observability.Outbox; namespace Omika.Tests.Observability.Outbox; /// Tests du client HTTP d'upload : classification des statuts, timeout, anti-boucle. public class DiagnosticUploadClientTests { private static DiagnosticBatchDto NewBatch() => new() { Events = [ new DiagnosticEventDto { EventId = Guid.NewGuid(), EventCode = "OMK-MOB-APP-001" } ] }; private static DiagnosticUploadClient CreateClient( StubHandler handler, DiagnosticUploadOptions? options = null, string? token = "token-1") => new( new HttpClient(handler), "http://localhost:5000", () => token, options ?? new DiagnosticUploadOptions { Timeout = TimeSpan.FromSeconds(5) }); [Fact] public async Task NoToken_ReturnsUnauthorized_WithoutHttp() { var handler = new StubHandler { StatusCode = HttpStatusCode.OK }; DiagnosticUploadClient client = CreateClient(handler, token: null); DiagnosticUploadResult result = await client.UploadAsync(NewBatch()); Assert.Equal(DiagnosticUploadOutcome.Unauthorized, result.Outcome); Assert.Equal(0, handler.Calls); } [Fact] public async Task Success_MapsToSuccess_WithAcceptedCount() { var handler = new StubHandler { StatusCode = HttpStatusCode.OK, ResponseBody = """{"batchId":"00000000-0000-0000-0000-000000000001","acceptedCount":3}""" }; DiagnosticUploadClient client = CreateClient(handler); DiagnosticUploadResult result = await client.UploadAsync(NewBatch()); Assert.Equal(DiagnosticUploadOutcome.Success, result.Outcome); Assert.Equal(3, result.AcceptedCount); } [Theory] [InlineData(HttpStatusCode.Unauthorized, DiagnosticUploadOutcome.Unauthorized)] [InlineData(HttpStatusCode.Forbidden, DiagnosticUploadOutcome.Unauthorized)] [InlineData(HttpStatusCode.BadRequest, DiagnosticUploadOutcome.Rejected)] [InlineData(HttpStatusCode.RequestEntityTooLarge, DiagnosticUploadOutcome.TooLarge)] [InlineData(HttpStatusCode.InternalServerError, DiagnosticUploadOutcome.Transient)] [InlineData(HttpStatusCode.ServiceUnavailable, DiagnosticUploadOutcome.Transient)] [InlineData(HttpStatusCode.GatewayTimeout, DiagnosticUploadOutcome.Transient)] public async Task StatusCodes_MapToExpectedOutcome(HttpStatusCode status, DiagnosticUploadOutcome expected) { var handler = new StubHandler { StatusCode = status }; DiagnosticUploadClient client = CreateClient(handler); DiagnosticUploadResult result = await client.UploadAsync(NewBatch()); Assert.Equal(expected, result.Outcome); Assert.Equal((int)status, result.StatusCode); } [Fact] public async Task RateLimited_HonorsRetryAfter() { var handler = new StubHandler { StatusCode = HttpStatusCode.TooManyRequests, RetryAfterSeconds = 42 }; DiagnosticUploadClient client = CreateClient(handler); DiagnosticUploadResult result = await client.UploadAsync(NewBatch()); Assert.Equal(DiagnosticUploadOutcome.RateLimited, result.Outcome); Assert.Equal(TimeSpan.FromSeconds(42), result.RetryAfter); } [Fact] public async Task Timeout_MapsToTransient() { var handler = new StubHandler { DelayMs = 2000, StatusCode = HttpStatusCode.OK }; DiagnosticUploadClient client = CreateClient(handler, new DiagnosticUploadOptions { Timeout = TimeSpan.FromMilliseconds(10) }); DiagnosticUploadResult result = await client.UploadAsync(NewBatch()); Assert.Equal(DiagnosticUploadOutcome.Transient, result.Outcome); } [Fact] public async Task Upload_SendsBearerToken() { var handler = new StubHandler { StatusCode = HttpStatusCode.OK }; DiagnosticUploadClient client = CreateClient(handler, token: "secret-token"); await client.UploadAsync(NewBatch()); Assert.Equal("Bearer secret-token", handler.LastAuthorization); } private sealed class StubHandler : HttpMessageHandler { public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK; public int DelayMs { get; set; } public string ResponseBody { get; set; } = "{}"; public int? RetryAfterSeconds { get; set; } public int Calls { get; private set; } public string? LastAuthorization { get; private set; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Calls++; LastAuthorization = request.Headers.Authorization?.ToString(); if (DelayMs > 0) { await Task.Delay(DelayMs, cancellationToken); } var response = new HttpResponseMessage(StatusCode); if (RetryAfterSeconds is { } seconds) { response.Headers.RetryAfter = new RetryConditionHeaderValue(TimeSpan.FromSeconds(seconds)); } response.Content = new StringContent(ResponseBody, Encoding.UTF8, "application/json"); return response; } } }