Skip to content

Commit 365c237

Browse files
Fix: Handle NoContent and null response in DeserializeResponseAsync (#461)
1 parent d247fe9 commit 365c237

File tree

4 files changed

+193
-17
lines changed

4 files changed

+193
-17
lines changed

src/CheckoutSdk/ApiClient.cs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -313,10 +313,18 @@ private async Task<TResult> DeserializeResponseAsync<TResult>(HttpResponseMessag
313313

314314
private async Task<dynamic> DeserializeResponseAsync(HttpResponseMessage httpResponse, Type resultType)
315315
{
316-
dynamic deserializedObject;
317-
if (httpResponse.StatusCode == HttpStatusCode.NoContent || httpResponse.Content.Headers.ContentLength == 0)
316+
dynamic deserializedObject = null;
317+
318+
if (httpResponse.StatusCode == HttpStatusCode.NoContent ||
319+
httpResponse.Content == null ||
320+
(httpResponse.Content.Headers.ContentLength.HasValue && httpResponse.Content.Headers.ContentLength.Value == 0))
318321
{
319322
deserializedObject = Activator.CreateInstance(resultType);
323+
324+
if (deserializedObject == null)
325+
{
326+
deserializedObject = new DefaultHttpMetadata();
327+
}
320328
}
321329
else if (resultType == typeof(ContentsResponse))
322330
{
@@ -327,23 +335,34 @@ private async Task<dynamic> DeserializeResponseAsync(HttpResponseMessage httpRes
327335
var json = await httpResponse.Content.ReadAsStringAsync();
328336
deserializedObject = _serializer.Deserialize(json, resultType);
329337
}
330-
331-
await SetHttpMetadata(httpResponse, deserializedObject);
338+
339+
if (deserializedObject is HttpMetadata metadata)
340+
{
341+
metadata.Body = metadata.Body ?? string.Empty;
342+
metadata.HttpStatusCode = metadata.HttpStatusCode == null ? 0 : (int)httpResponse.StatusCode;
343+
metadata.ResponseHeaders = metadata.ResponseHeaders ?? new Dictionary<string, string>();
344+
}
345+
346+
if (deserializedObject != null)
347+
{
348+
await SetHttpMetadata(httpResponse, deserializedObject);
349+
}
332350

333351
return deserializedObject;
334352
}
335353

354+
336355
private static async Task SetHttpMetadata(HttpResponseMessage httpResponse, dynamic deserializedObject)
337356
{
338-
((HttpMetadata)deserializedObject).Body = await httpResponse.Content.ReadAsStringAsync();
339-
((HttpMetadata)deserializedObject).HttpStatusCode = (int)httpResponse.StatusCode;
340-
IDictionary<string, string> headers = new Dictionary<string, string>();
341-
foreach (var header in httpResponse.Headers)
357+
if (deserializedObject is HttpMetadata metadata)
342358
{
343-
headers.Add(header.Key, header.Value.First());
359+
metadata.Body = await (httpResponse.Content?.ReadAsStringAsync() ?? Task.FromResult(string.Empty));
360+
metadata.HttpStatusCode = (int)httpResponse.StatusCode;
361+
metadata.ResponseHeaders = httpResponse.Headers?.ToDictionary(
362+
h => h.Key,
363+
h => h.Value.FirstOrDefault() ?? string.Empty
364+
) ?? new Dictionary<string, string>();
344365
}
345-
346-
((HttpMetadata)deserializedObject).ResponseHeaders = headers;
347366
}
348367
}
349368
}

src/CheckoutSdk/HttpMetadata.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,14 @@ public class HttpMetadata
1010

1111
public IDictionary<string, string> ResponseHeaders { get; set; }
1212
}
13+
14+
public class DefaultHttpMetadata : HttpMetadata
15+
{
16+
public DefaultHttpMetadata()
17+
{
18+
Body = string.Empty;
19+
HttpStatusCode = 0;
20+
ResponseHeaders = new Dictionary<string, string>();
21+
}
22+
}
1323
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using System;
2+
using System.Net;
3+
using System.Net.Http;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Moq;
8+
using Moq.Protected;
9+
using Xunit;
10+
11+
namespace Checkout
12+
{
13+
public class ApiClientTests : UnitTestFixture, IDisposable
14+
{
15+
private readonly ApiClient _apiClient;
16+
private readonly Mock<HttpMessageHandler> _httpMessageHandlerMock;
17+
private readonly HttpClient _httpClient;
18+
19+
public ApiClientTests()
20+
{
21+
_httpMessageHandlerMock = new Mock<HttpMessageHandler>();
22+
_httpClient = new HttpClient(_httpMessageHandlerMock.Object);
23+
24+
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
25+
httpClientFactoryMock
26+
.Setup(factory => factory.CreateClient())
27+
.Returns(_httpClient);
28+
29+
_apiClient = new ApiClient(httpClientFactoryMock.Object, new Uri("https://api.example.com"), false);
30+
}
31+
32+
[Fact]
33+
public async Task ShouldGetReturnDeserializedObject()
34+
{
35+
// Arrange
36+
var jsonResponse = "{\"message\":\"success\"}";
37+
using var httpResponse = new HttpResponseMessage(HttpStatusCode.OK);
38+
httpResponse.Content = new StringContent(jsonResponse, Encoding.UTF8, "application/json");
39+
40+
_httpMessageHandlerMock
41+
.Protected()
42+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
43+
ItExpr.IsAny<CancellationToken>())
44+
.ReturnsAsync(httpResponse);
45+
46+
var authorization = new SdkAuthorization(PlatformType.Default, ValidDefaultSk);
47+
48+
// Act
49+
var result = await _apiClient.Get<FakeResponse>("/test", authorization);
50+
51+
// Assert
52+
Assert.NotNull(result);
53+
Assert.IsType<FakeResponse>(result);
54+
Assert.Equal("success", result.Message);
55+
}
56+
57+
[Fact]
58+
public async Task ShouldPostReturnDeserializedObject()
59+
{
60+
// Arrange
61+
var jsonResponse = "{\"message\":\"created\"}";
62+
using HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.Created)
63+
{
64+
Content = new StringContent(jsonResponse, Encoding.UTF8, "application/json")
65+
};
66+
67+
_httpMessageHandlerMock
68+
.Protected()
69+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
70+
ItExpr.IsAny<CancellationToken>())
71+
.ReturnsAsync(httpResponse);
72+
73+
var authorization = new SdkAuthorization(PlatformType.Default, ValidDefaultSk);
74+
var requestData = new { name = "new item" };
75+
76+
// Act
77+
var result = await _apiClient.Post<FakeResponse>("/test", authorization, requestData);
78+
79+
// Assert
80+
Assert.NotNull(result);
81+
Assert.IsType<FakeResponse>(result);
82+
Assert.Equal("created", result.Message);
83+
}
84+
85+
[Fact]
86+
public async Task ShouldDeleteReturnDeserializedObject()
87+
{
88+
// Arrange
89+
var jsonResponse = "{\"message\":\"deleted\"}";
90+
using HttpResponseMessage httpResponse = new HttpResponseMessage(HttpStatusCode.OK)
91+
{
92+
Content = new StringContent(jsonResponse, Encoding.UTF8, "application/json")
93+
};
94+
95+
_httpMessageHandlerMock
96+
.Protected()
97+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(),
98+
ItExpr.IsAny<CancellationToken>())
99+
.ReturnsAsync(httpResponse);
100+
101+
var authorization = new SdkAuthorization(PlatformType.Default, ValidDefaultSk);
102+
103+
// Act
104+
var result = await _apiClient.Delete<FakeResponse>("/test", authorization);
105+
106+
// Assert
107+
Assert.NotNull(result);
108+
Assert.IsType<FakeResponse>(result);
109+
Assert.Equal("deleted", result.Message);
110+
}
111+
112+
[Fact]
113+
public async Task ShouldGetHandleNoContentResponse()
114+
{
115+
// Arrange
116+
using var httpResponse = new HttpResponseMessage(HttpStatusCode.NoContent);
117+
httpResponse.Content = null;
118+
119+
_httpMessageHandlerMock
120+
.Protected()
121+
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
122+
.ReturnsAsync(httpResponse);
123+
124+
var authorization = new SdkAuthorization(PlatformType.Default, ValidDefaultSk);
125+
126+
// Act
127+
var result = await _apiClient.Get<HttpMetadata>("/test", authorization);
128+
129+
// Assert
130+
Assert.NotNull(result);
131+
Assert.IsType<HttpMetadata>(result);
132+
Assert.Equal(204, result.HttpStatusCode);
133+
Assert.NotNull(result.Body);
134+
Assert.NotNull(result.ResponseHeaders);
135+
}
136+
137+
public void Dispose()
138+
{
139+
_httpClient.Dispose();
140+
}
141+
}
142+
143+
public class FakeResponse : HttpMetadata
144+
{
145+
public string Message { get; set; }
146+
}
147+
}

test/CheckoutSdkTest/UnitTestFixture.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ namespace Checkout
33
public abstract class UnitTestFixture
44
{
55
// Previous
6-
protected const string ValidPreviousSk = "sk_test_fde517a8-3f01-41ef-b4bd-4282384b0a64";
7-
protected const string ValidPreviousPk = "pk_test_fe70ff27-7c32-4ce1-ae90-5691a188ee7b";
8-
protected const string InvalidPreviousSk = "sk_test_asdsad3q4dq";
6+
protected static readonly string ValidPreviousPk = System.Environment.GetEnvironmentVariable("CHECKOUT_PREVIOUS_PUBLIC_KEY");
7+
protected static readonly string ValidPreviousSk = System.Environment.GetEnvironmentVariable("CHECKOUT_PREVIOUS_SECRET_KEY");
98
protected const string InvalidPreviousPk = "pk_test_q414dasds";
9+
protected const string InvalidPreviousSk = "sk_test_asdsad3q4dq";
1010

1111
// Default
12-
protected const string ValidDefaultSk = "sk_sbox_m73dzbpy7cf3gfd46xr4yj5xo4e";
13-
protected const string ValidDefaultPk = "pk_sbox_sderftvmkgf7hdnpwnbhw7r2uic";
14-
protected const string InvalidDefaultSk = "sk_sbox_m73dzbpy7c-f3gfd46xr4yj5xo4e";
12+
protected static readonly string ValidDefaultPk = System.Environment.GetEnvironmentVariable("CHECKOUT_DEFAULT_PUBLIC_KEY");
13+
protected static readonly string ValidDefaultSk = System.Environment.GetEnvironmentVariable("CHECKOUT_DEFAULT_SECRET_KEY");
1514
protected const string InvalidDefaultPk = "pk_sbox_pkh";
15+
protected const string InvalidDefaultSk = "sk_sbox_m73dzbpy7c-f3gfd46xr4yj5xo4e";
1616
}
1717
}

0 commit comments

Comments
 (0)