using System.Net; namespace Zcbot.WindowsNode.Tests; public sealed class TransferRetryPolicyTests { [Theory] [InlineData(HttpStatusCode.Unauthorized)] [InlineData(HttpStatusCode.Forbidden)] public void AuthenticationFailuresAreClassifiedSeparately(HttpStatusCode statusCode) { Assert.Equal( TransferFailureKind.Authentication, TransferRetryPolicy.Classify(statusCode)); Assert.Null(TransferRetryPolicy.RetryDelay(HttpMethod.Get, 0, statusCode)); } [Theory] [InlineData(HttpStatusCode.RequestTimeout)] [InlineData(HttpStatusCode.TooManyRequests)] [InlineData(HttpStatusCode.InternalServerError)] [InlineData(HttpStatusCode.ServiceUnavailable)] public void SafeDownloadsRetryTransientResponses(HttpStatusCode statusCode) { Assert.Equal(TransferFailureKind.Transient, TransferRetryPolicy.Classify(statusCode)); Assert.NotNull(TransferRetryPolicy.RetryDelay(HttpMethod.Get, 0, statusCode)); Assert.Null(TransferRetryPolicy.RetryDelay(HttpMethod.Post, 0, statusCode)); } [Fact] public void CancellationIsNeverClassifiedAsANetworkFailure() { Assert.Equal( TransferFailureKind.Cancelled, TransferRetryPolicy.Classify(new OperationCanceledException())); } [Fact] public async Task HttpClientRetriesGetAndInjectsAuthenticationWithoutSecretsInTheUrl() { var handler = new SequenceHandler( HttpStatusCode.ServiceUnavailable, HttpStatusCode.OK); using var client = new NodeHttpClient(CreateConfig(), handler); using var response = await client.SendWithRetryAsync( () => client.CreateRequest(HttpMethod.Get, "/v1/software-jobs/example/input"), HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(2, handler.RequestCount); Assert.Equal("Bearer node-secret", handler.Authorization); Assert.Equal("node-secret", CreateConfig().NodeToken); Assert.DoesNotContain("node-secret", handler.RequestUri!.ToString()); Assert.StartsWith("zcbot-windows-node/", handler.UserAgent); } [Fact] public async Task HttpClientDoesNotRetryAuthenticationFailure() { var handler = new SequenceHandler(HttpStatusCode.Unauthorized, HttpStatusCode.OK); using var client = new NodeHttpClient(CreateConfig(), handler); using var response = await client.SendWithRetryAsync( () => client.CreateRequest(HttpMethod.Get, "/v1/software-jobs/example/input"), HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Equal(1, handler.RequestCount); } private static NodeConfig CreateConfig() => new( new Uri("https://node.example/"), Guid.Parse("11111111-1111-1111-1111-111111111111"), Guid.Parse("22222222-2222-2222-2222-222222222222"), "test-node", "node-secret", 30, ["origin.plot@v2"]); private sealed class SequenceHandler(params HttpStatusCode[] statuses) : HttpMessageHandler { private int index; internal int RequestCount { get; private set; } internal string? Authorization { get; private set; } internal Uri? RequestUri { get; private set; } internal string UserAgent { get; private set; } = ""; protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { RequestCount++; Authorization = request.Headers.Authorization?.ToString(); RequestUri = request.RequestUri; UserAgent = request.Headers.UserAgent.ToString(); var status = statuses[Math.Min(index++, statuses.Length - 1)]; return Task.FromResult(new HttpResponseMessage(status) { Content = new ByteArrayContent([]), RequestMessage = request, }); } } }