using System.Net; using System.Text; using RudderSdk.Core; using RudderSdk.Core.Models.Player; using Xunit; namespace RudderSdk.Core.Tests; public sealed class HttpClientTransportTests { [Fact] public async Task Maps_404_To_NotFoundException_With_Code_And_RequestId() { var transport = CreateTransport(_ => Error(HttpStatusCode.NotFound, """{"code":"store_not_found","error":"Store not found","requestId":"req-123"}""")); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/stores/main", null, "token")); Assert.Equal(404, exception.StatusCode); Assert.Equal("store_not_found", exception.Code); Assert.Equal("req-123", exception.RequestId); Assert.Equal("Store not found", exception.Message); } [Fact] public async Task Maps_429_To_RateLimitException() { var transport = CreateTransport(_ => Error(HttpStatusCode.TooManyRequests, """{"code":"rate_limited","error":"Slow down"}""")); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/player/information", null, null)); Assert.Equal(429, exception.StatusCode); Assert.Equal("rate_limited", exception.Code); } [Fact] public async Task Maps_401_To_AuthException() { var transport = CreateTransport(_ => Error(HttpStatusCode.Unauthorized, """{"error":"Unauthorized"}""")); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/player/information", null, null)); Assert.Equal(401, exception.StatusCode); Assert.Null(exception.RequestId); } [Fact] public async Task Maps_500_To_Base_ApiException() { var transport = CreateTransport(_ => Error(HttpStatusCode.InternalServerError, """{"code":"internal","error":"Boom"}""")); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/player/information", null, null)); Assert.Equal(typeof(RudderApiException), exception.GetType()); Assert.Equal(500, exception.StatusCode); Assert.Equal("internal", exception.Code); } [Fact] public async Task Non_Json_Error_Body_Falls_Back_To_Reason_Phrase() { var transport = CreateTransport(_ => new HttpResponseMessage(HttpStatusCode.BadGateway) { Content = new StringContent("bad gateway", Encoding.UTF8, "text/html"), ReasonPhrase = "Bad Gateway" }); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/player/information", null, null)); Assert.Equal(502, exception.StatusCode); Assert.Equal(string.Empty, exception.Code); Assert.Equal("Bad Gateway", exception.Message); } [Fact] public async Task Network_Failure_Maps_To_NetworkException() { var transport = CreateTransport(_ => throw new HttpRequestException("Connection refused")); var exception = await Assert.ThrowsAsync( () => transport.SendAsync("GET", "/sdk/v1/player/information", null, null)); Assert.Equal(0, exception.StatusCode); Assert.Equal("Connection refused", exception.Message); } [Fact] public async Task Success_Deserializes_Response_And_Sends_Bearer_Token() { StubHandler? handler = null; var transport = CreateTransport(request => { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"player":{"id":"p1"},"wallets":[]}""", Encoding.UTF8, "application/json") }; }, h => handler = h); var profile = await transport.SendAsync("GET", "/sdk/v1/player/information", null, "access-token"); Assert.Equal("p1", profile.Player.Id); Assert.Equal("Bearer", handler!.LastRequest!.Headers.Authorization!.Scheme); Assert.Equal("access-token", handler.LastRequest.Headers.Authorization.Parameter); Assert.Equal("http://localhost:8082/sdk/v1/player/information", handler.LastRequest.RequestUri!.ToString()); } private static HttpClientTransport CreateTransport( Func respond, Action? capture = null) { var handler = new StubHandler(respond); capture?.Invoke(handler); return new HttpClientTransport("http://localhost:8082/", new HttpClient(handler)); } private static HttpResponseMessage Error(HttpStatusCode statusCode, string json) { return new HttpResponseMessage(statusCode) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; } private sealed class StubHandler : HttpMessageHandler { private readonly Func _respond; public StubHandler(Func respond) => _respond = respond; public HttpRequestMessage? LastRequest { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { LastRequest = request; return Task.FromResult(_respond(request)); } } }