-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathResponseExtensionsTests.cs
341 lines (289 loc) · 15.1 KB
/
ResponseExtensionsTests.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Xunit;
namespace Moq.Contrib.HttpClient.Test
{
public class ResponseExtensionsTests
{
private readonly Mock<HttpMessageHandler> handler;
private readonly System.Net.Http.HttpClient client;
public ResponseExtensionsTests()
{
handler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
client = handler.CreateClient();
// Setting a BaseAddress only affects HttpClient's methods; the handler doesn't know about the BaseAddress
// and will receive the resolved url, so setups still need to specify the full request url
client.BaseAddress = new Uri("https://example.com");
}
[Theory]
[InlineData(HttpStatusCode.OK)]
[InlineData(HttpStatusCode.NotFound)]
[InlineData(HttpStatusCode.Unauthorized)]
public async Task RespondsWithStatusCode(HttpStatusCode statusCode)
{
handler.SetupAnyRequest()
.ReturnsResponse(statusCode);
var response = await client.GetAsync("");
response.StatusCode.Should().Be(statusCode);
}
[Theory]
[InlineData(null, "foo", null, null)]
[InlineData(HttpStatusCode.OK, @"{ ""foo"": ""bar"" }", "application/json", null)]
[InlineData(HttpStatusCode.Created, "<foo>bar</foo>", "text/xml", 932 /* Shift JIS */)]
public async Task RespondsWithString(HttpStatusCode? statusCode, string content, string mediaType, int? encodingCodePage)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding encoding = encodingCodePage.HasValue ? Encoding.GetEncoding(encodingCodePage.Value) : null;
if (statusCode.HasValue)
{
// Media/content type and encoding are optional; StringContent defaults these to text/plain and UTF-8
handler.SetupAnyRequest()
.ReturnsResponse(statusCode.Value, content, mediaType, encoding);
}
else
{
// Status code can be omitted and defaults to OK
handler.SetupAnyRequest()
.ReturnsResponse(content, mediaType, encoding);
}
var response = await client.GetAsync("");
var responseString = await response.Content.ReadAsStringAsync();
response.Content.Should().BeOfType<StringContent>();
response.StatusCode.Should().Be(statusCode ?? HttpStatusCode.OK);
responseString.Should().Be(content);
var contentType = response.Content.Headers.ContentType;
contentType.MediaType.Should().Be(mediaType ?? "text/plain");
contentType.CharSet.Should().Be((encoding ?? Encoding.UTF8).WebName);
}
[Fact]
public async Task RespondsWithJson()
{
// Once again using our music API from MatchesCustomPredicate, this time fetching songs
var expected = new List<Song>()
{
new Song()
{
Title = "Lost One's Weeping",
Artist = "Neru feat. Kagamine Rin",
Album = "世界征服",
Url = "https://youtu.be/mF4KTG4c-Ic"
},
new Song()
{
Title = "Gimme×Gimme",
Artist = "八王子P, Giga feat. Hatsune Miku, Kagamine Rin",
Album = "Hatsune Miku Magical Mirai 2020",
Url = "https://youtu.be/IfEAtKW2qSI"
}
};
handler.SetupRequest(HttpMethod.Get, "https://example.com/api/songs")
.ReturnsJsonResponse(expected);
var actual = await client.GetFromJsonAsync<List<Song>>("api/songs");
actual.Should().BeEquivalentTo(expected);
}
[Fact]
public async Task RespondsWithJsonUsingCustomSerializerOptions()
{
var model = new Song()
{
Title = "Onegai Sekai",
Artist = "HitoshizukuP, yama△ feat. Kagamine Rin",
Album = "Mistletoe ~Kamigami no Yadorigi~",
Url = "https://youtu.be/CKvtM4DFkI0"
};
// By default, JsonContent uses JsonSerializerDefaults.Web which camel-cases property names
handler.SetupAnyRequest()
.ReturnsJsonResponse(HttpStatusCode.Created, model);
var json = await client.GetStringAsync("");
json.Should().Be(
@"{""title"":""Onegai Sekai"",""artist"":""HitoshizukuP, yama\u25B3 feat. Kagamine Rin"",""album"":""Mistletoe ~Kamigami no Yadorigi~"",""url"":""https://youtu.be/CKvtM4DFkI0""}");
// We can pass custom serializer options the same way as JsonContent, PostAsJsonAsync(), etc.
// See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to#serialization-behavior
var options = new JsonSerializerOptions() // Not using the Web defaults
{
WriteIndented = true
};
handler.SetupAnyRequest()
.ReturnsJsonResponse(HttpStatusCode.Created, model, options);
var pretty = await client.GetStringAsync("");
var expected = @"{
""Title"": ""Onegai Sekai"",
""Artist"": ""HitoshizukuP, yama\u25B3 feat. Kagamine Rin"",
""Album"": ""Mistletoe ~Kamigami no Yadorigi~"",
""Url"": ""https://youtu.be/CKvtM4DFkI0""
}";
Assert.Equal(expected, pretty, ignoreLineEndingDifferences: true);
}
[Theory]
[InlineData(null, new byte[] { 39, 39, 39, 39 }, "image/png")]
[InlineData(HttpStatusCode.BadRequest, new byte[] { }, null)]
public async Task RespondsWithBytes(HttpStatusCode? statusCode, byte[] content, string mediaType)
{
if (statusCode.HasValue)
{
handler.SetupAnyRequest()
.ReturnsResponse(statusCode.Value, content, mediaType);
}
else
{
// Status code can be omitted and defaults to OK
handler.SetupAnyRequest()
.ReturnsResponse(content, mediaType);
}
var response = await client.GetAsync("");
var responseBytes = await response.Content.ReadAsByteArrayAsync();
response.Content.Should().BeOfType<ByteArrayContent>();
response.StatusCode.Should().Be(statusCode ?? HttpStatusCode.OK);
responseBytes.Should().BeEquivalentTo(content);
var responseMediaType = response.Content.Headers.ContentType?.MediaType;
responseMediaType.Should().Be(mediaType);
}
[Theory]
[InlineData(null, new byte[] { 39, 39, 39, 39 }, "image/png")]
[InlineData(HttpStatusCode.BadRequest, new byte[] { }, null)]
public async Task RespondsWithStream(HttpStatusCode? statusCode, byte[] bytes, string mediaType)
{
using (MemoryStream stream = new MemoryStream(bytes))
{
if (statusCode.HasValue)
{
handler.SetupAnyRequest()
.ReturnsResponse(statusCode.Value, stream, mediaType);
}
else
{
// Status code can be omitted and defaults to OK
handler.SetupAnyRequest()
.ReturnsResponse(stream, mediaType);
}
var response = await client.GetAsync("");
var responseBytes = await response.Content.ReadAsByteArrayAsync();
response.Content.Should().BeOfType<StreamContent>();
response.StatusCode.Should().Be(statusCode ?? HttpStatusCode.OK);
responseBytes.Should().BeEquivalentTo(bytes);
var responseMediaType = response.Content.Headers.ContentType?.MediaType;
responseMediaType.Should().Be(mediaType);
}
}
[Fact]
public async Task RespondsWithProvidedHttpContent()
{
var content = new MultipartContent();
handler.SetupAnyRequest()
.ReturnsResponse(HttpStatusCode.OK, content);
var response = await client.GetAsync("");
response.Content.Should().BeSameAs(content);
}
[Fact]
public async Task CanSetHeaders()
{
// All overloads take an action to configure the response and set custom headers; this is more
// useful than a Dictionary as it allows for using the typed header properties
handler.SetupAnyRequest()
.ReturnsResponse("response body", configure: response =>
{
response.Headers.Server.Add(new ProductInfoHeaderValue("Nginx", null));
response.Headers.Add("X-Powered-By", "ASP.NET");
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
FileName = "example.txt"
};
});
var res = await client.GetAsync("");
var body = await res.Content.ReadAsStringAsync();
var server = res.Headers.Server?.ToString();
var poweredBy = res.Headers.GetValues("X-Powered-By").FirstOrDefault();
var contentDisposition = res.Content.Headers.ContentDisposition?.ToString();
body.Should().Be("response body");
server.Should().Be("Nginx");
poweredBy.Should().Be("ASP.NET");
contentDisposition.Should().Be("attachment; filename=example.txt");
}
[Fact]
public async Task CanSimulateNetworkErrors()
{
// Triggering a network error (e.g. connection refused) can be done using the standard Throws()
handler.SetupAnyRequest()
.Throws<HttpRequestException>();
// Fancier version:
//var inner = new System.Net.Sockets.SocketException((int)System.Net.Sockets.SocketError.ConnectionRefused);
//handler.SetupAnyRequest()
// .Throws(new HttpRequestException(inner.Message, inner));
Func<Task> attempt = () => client.GetAsync("http://example.com");
await attempt.Should().ThrowAsync<HttpRequestException>();
}
[Fact]
public async Task ReturnsNewResponseInstanceEachRequest()
{
handler.SetupRequest(HttpMethod.Get, "https://example.com/foo")
.ReturnsResponse("bar");
var response1 = await client.GetAsync("foo");
var response2 = await client.GetAsync("foo");
// New instances are returned for each request to ensure that subsequent requests don't receive a disposed
// HttpResponseMessage or HttpContent
response2.Should().NotBeSameAs(response1, "each request should get its own response object");
response2.Content.Should().NotBeSameAs(response1.Content, "each response should have its own content object");
// HttpClient.GetStringAsync() wraps the HttpResponseMessage in a `using` (up until at least .NET 5) which
// would cause the second attempt to read the content to throw if it were given the same response object
(await client.GetStringAsync("foo")).Should().Be("bar");
(await client.GetStringAsync("foo")).Should().Be("bar", "the HttpContent should not be disposed");
handler.VerifyRequest(HttpMethod.Get, "https://example.com/foo", Times.Exactly(4));
}
[Fact]
public async Task StreamsReadFromSamePositionEachRequest()
{
// ReturnsResponse includes overloads that take a stream. If the stream is seekable (such as a MemoryStream),
// it will be wrapped so that each request maintains an independent stream position. This allows multiple
// requests (and setups) to read from the same stream without interfering with each other. The wrapper also
// prevents a disposing HttpContent from closing the underlying stream. If you have a non-seekable stream
// that needs to be used in multiple responses, copy it to a MemoryStream or byte array first.
var bytes = new byte[]
{
0x79, 0x6F, 0x75, 0x74, 0x75, 0x62, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x2F, 0x70, 0x6C, 0x61, 0x79,
0x6C, 0x69, 0x73, 0x74, 0x3F, 0x6C, 0x69, 0x73, 0x74, 0x3D, 0x50, 0x4C, 0x59, 0x6F, 0x6F, 0x45,
0x41, 0x46, 0x55, 0x66, 0x68, 0x44, 0x66, 0x65, 0x76, 0x57, 0x46, 0x4B, 0x4C, 0x61, 0x37, 0x67,
0x68, 0x33, 0x42, 0x6F, 0x67, 0x42, 0x55, 0x41, 0x65, 0x62, 0x59, 0x4F
};
int offsetStreamPosition = 39;
byte[] expectedOffsetBytes = bytes.Skip(offsetStreamPosition).ToArray();
using (MemoryStream stream = new MemoryStream(bytes))
using (MemoryStream offsetStream = new MemoryStream(bytes))
{
handler.SetupRequest(HttpMethod.Get, "https://example.com/normal")
.ReturnsResponse(stream);
// Multiple setups can share the same stream as well
handler.SetupRequest(HttpMethod.Get, "https://example.com/normal2")
.ReturnsResponse(stream);
// This stream is the same but seeked forward; each request should read from this position rather than
// seeking back to the beginning
offsetStream.Seek(offsetStreamPosition, SeekOrigin.Begin);
handler.SetupRequest(HttpMethod.Get, "https://example.com/offset")
.ReturnsResponse(offsetStream);
var responseBytes1 = await client.GetByteArrayAsync("normal");
var responseBytes2 = await client.GetByteArrayAsync("normal");
var responseBytes3 = await client.GetByteArrayAsync("normal2");
var offsetResponseBytes1 = await client.GetByteArrayAsync("offset");
var offsetResponseBytes2 = await client.GetByteArrayAsync("offset");
responseBytes1.Should().BeEquivalentTo(bytes);
responseBytes2.Should().BeEquivalentTo(bytes,
"the stream should be returned to its original position after being read");
responseBytes3.Should().BeEquivalentTo(bytes,
"the stream should be reusable not just between requests to one setup but also between setups");
offsetResponseBytes1.Should().BeEquivalentTo(expectedOffsetBytes,
"the stream should read from its initial (offset) position, not necessarily the beginning");
offsetResponseBytes2.Should().BeEquivalentTo(expectedOffsetBytes,
"the stream should be returned to its original (offset, not zero) position after being read");
}
}
}
}