A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Diagnostics;
7using System.Net;
8using System.Net.Http;
9using System.Reflection;
10using System.Threading;
11using System.Threading.Tasks;
12using Newtonsoft.Json;
13using NUnit.Framework;
14using osu.Framework.Graphics;
15using osu.Framework.IO.Network;
16using WebRequest = osu.Framework.IO.Network.WebRequest;
17
18namespace osu.Framework.Tests.IO
19{
20 [TestFixture]
21 [Category("httpbin")]
22 public class TestWebRequest
23 {
24 private const string default_protocol = "http";
25 private const string invalid_get_url = "a.ppy.shhhhh";
26
27 private static readonly string host;
28 private static readonly IEnumerable<string> protocols;
29
30 static TestWebRequest()
31 {
32 bool localHttpBin = Environment.GetEnvironmentVariable("LocalHttpBin")?.Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
33
34 if (localHttpBin)
35 {
36 // httpbin very frequently falls over and causes random tests to fail
37 // Thus appveyor builds rely on a local httpbin instance to run the tests
38
39 host = "127.0.0.1";
40 protocols = new[] { default_protocol };
41 }
42 else
43 {
44 host = "httpbin.org";
45 protocols = new[] { default_protocol, "https" };
46 }
47 }
48
49 [Test, Retry(5)]
50 public void TestValidGet([ValueSource(nameof(protocols))] string protocol, [Values(true, false)] bool async)
51 {
52 var url = $"{protocol}://{host}/get";
53 var request = new JsonWebRequest<HttpBinGetResponse>(url)
54 {
55 Method = HttpMethod.Get,
56 AllowInsecureRequests = true
57 };
58
59 testValidGetInternal(async, request, "osu-framework");
60 }
61
62 [Test, Retry(5)]
63 public void TestCustomUserAgent([ValueSource(nameof(protocols))] string protocol, [Values(true, false)] bool async)
64 {
65 var url = $"{protocol}://{host}/get";
66 var request = new CustomUserAgentWebRequest(url)
67 {
68 Method = HttpMethod.Get,
69 AllowInsecureRequests = true
70 };
71
72 testValidGetInternal(async, request, "custom-ua");
73 }
74
75 private static void testValidGetInternal(bool async, JsonWebRequest<HttpBinGetResponse> request, string expectedUserAgent)
76 {
77 bool hasThrown = false;
78 request.Failed += exception => hasThrown = exception != null;
79
80 if (async)
81 Assert.DoesNotThrowAsync(request.PerformAsync);
82 else
83 Assert.DoesNotThrow(request.Perform);
84
85 Assert.IsTrue(request.Completed);
86 Assert.IsFalse(request.Aborted);
87
88 var responseObject = request.ResponseObject;
89
90 Assert.IsTrue(responseObject != null);
91 Assert.IsTrue(responseObject.Headers.UserAgent == expectedUserAgent);
92
93 // disabled due to hosted version returning incorrect response (https://github.com/postmanlabs/httpbin/issues/545)
94 // Assert.AreEqual(url, responseObject.Url);
95
96 Assert.IsFalse(hasThrown);
97 }
98
99 /// <summary>
100 /// Tests async execution is correctly yielding during IO wait time.
101 /// </summary>
102 [Test]
103 public void TestConcurrency()
104 {
105 const int request_count = 10;
106 const int induced_delay = 5;
107
108 int finished = 0;
109 int failed = 0;
110 int started = 0;
111
112 Stopwatch sw = new Stopwatch();
113 sw.Start();
114
115 List<long> startTimes = new List<long>();
116
117 List<Task> running = new List<Task>();
118
119 for (int i = 0; i < request_count; i++)
120 {
121 var request = new DelayedWebRequest
122 {
123 Method = HttpMethod.Get,
124 AllowInsecureRequests = true,
125 Delay = induced_delay
126 };
127
128 request.Started += () =>
129 {
130 Interlocked.Increment(ref started);
131 lock (startTimes)
132 startTimes.Add(sw.ElapsedMilliseconds);
133 };
134 request.Finished += () => Interlocked.Increment(ref finished);
135 request.Failed += _ =>
136 {
137 Interlocked.Increment(ref failed);
138 Interlocked.Increment(ref finished);
139 };
140
141 running.Add(request.PerformAsync());
142 }
143
144 Task.WaitAll(running.ToArray());
145
146 Assert.Zero(failed);
147
148 // in the case threads are not yielding, the time taken will be greater than double the induced delay (after considering latency).
149 Assert.Less(sw.ElapsedMilliseconds, induced_delay * 2 * 1000);
150
151 Assert.AreEqual(request_count, started);
152
153 Assert.AreEqual(request_count, finished);
154
155 Assert.AreEqual(request_count, startTimes.Count);
156
157 // another case would be requests starting too late into the test. just to make sure.
158 for (int i = 0; i < request_count; i++)
159 Assert.Less(startTimes[i] - startTimes[0], induced_delay * 1000);
160 }
161
162 [Test, Retry(5)]
163 public void TestInvalidGetExceptions([ValueSource(nameof(protocols))] string protocol, [Values(true, false)] bool async)
164 {
165 var request = new WebRequest($"{protocol}://{invalid_get_url}")
166 {
167 Method = HttpMethod.Get,
168 AllowInsecureRequests = true
169 };
170
171 Exception finishedException = null;
172 request.Failed += exception => finishedException = exception;
173
174 if (async)
175 Assert.ThrowsAsync<HttpRequestException>(request.PerformAsync);
176 else
177 Assert.Throws<HttpRequestException>(request.Perform);
178
179 Assert.IsTrue(request.Completed);
180 Assert.IsTrue(request.Aborted);
181
182 Assert.IsTrue(request.GetResponseString() == null);
183 Assert.IsNotNull(finishedException);
184 }
185
186 [Test, Retry(5)]
187 public void TestBadStatusCode([Values(true, false)] bool async)
188 {
189 var request = new WebRequest($"{default_protocol}://{host}/hidden-basic-auth/user/passwd")
190 {
191 AllowInsecureRequests = true,
192 };
193
194 bool hasThrown = false;
195 request.Failed += exception => hasThrown = exception != null;
196
197 if (async)
198 Assert.ThrowsAsync<WebException>(request.PerformAsync);
199 else
200 Assert.Throws<WebException>(request.Perform);
201
202 Assert.IsTrue(request.Completed);
203 Assert.IsTrue(request.Aborted);
204
205 Assert.IsEmpty(request.GetResponseString());
206
207 Assert.IsTrue(hasThrown);
208 }
209
210 [Test, Retry(5)]
211 public void TestJsonWebRequestThrowsCorrectlyOnMultipleErrors([Values(true, false)] bool async)
212 {
213 var request = new JsonWebRequest<Drawable>("badrequest://www.google.com")
214 {
215 AllowInsecureRequests = true,
216 };
217
218 bool hasThrown = false;
219 request.Failed += exception => hasThrown = exception != null;
220
221 if (async)
222 Assert.ThrowsAsync<ArgumentException>(request.PerformAsync);
223 else
224 Assert.Throws<ArgumentException>(request.Perform);
225
226 Assert.IsTrue(request.Completed);
227 Assert.IsTrue(request.Aborted);
228
229 Assert.IsNull(request.GetResponseString());
230 Assert.IsNull(request.ResponseObject);
231
232 Assert.IsTrue(hasThrown);
233 }
234
235 /// <summary>
236 /// Tests aborting the <see cref="WebRequest"/> after response has been received from the server
237 /// but before data has been read.
238 /// </summary>
239 [Test, Retry(5)]
240 public void TestAbortReceive([Values(true, false)] bool async)
241 {
242 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
243 {
244 Method = HttpMethod.Get,
245 AllowInsecureRequests = true,
246 };
247
248 bool hasThrown = false;
249 request.Failed += exception => hasThrown = exception != null;
250 request.Started += () => request.Abort();
251
252 if (async)
253 Assert.DoesNotThrowAsync(request.PerformAsync);
254 else
255 Assert.DoesNotThrow(request.Perform);
256
257 Assert.IsTrue(request.Completed);
258 Assert.IsTrue(request.Aborted);
259
260 Assert.IsTrue(request.ResponseObject == null);
261
262 Assert.IsFalse(hasThrown);
263 }
264
265 /// <summary>
266 /// Tests aborting the <see cref="WebRequest"/> before the request is sent to the server.
267 /// </summary>
268 [Test, Retry(5)]
269 public void TestAbortRequest()
270 {
271 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
272 {
273 Method = HttpMethod.Get,
274 AllowInsecureRequests = true,
275 };
276
277 bool hasThrown = false;
278 request.Failed += exception => hasThrown = exception != null;
279
280#pragma warning disable 4014
281 request.PerformAsync();
282#pragma warning restore 4014
283
284 Assert.DoesNotThrow(request.Abort);
285
286 Assert.IsTrue(request.Completed);
287 Assert.IsTrue(request.Aborted);
288
289 Assert.IsTrue(request.ResponseObject == null);
290
291 Assert.IsFalse(hasThrown);
292 }
293
294 /// <summary>
295 /// Tests being able to abort + restart a request.
296 /// </summary>
297 [Test, Retry(5)]
298 public void TestRestartAfterAbort([Values(true, false)] bool async)
299 {
300 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
301 {
302 Method = HttpMethod.Get,
303 AllowInsecureRequests = true,
304 };
305
306 bool hasThrown = false;
307 request.Failed += exception => hasThrown = exception != null;
308
309#pragma warning disable 4014
310 request.PerformAsync();
311#pragma warning restore 4014
312
313 Assert.DoesNotThrow(request.Abort);
314
315 if (async)
316 Assert.ThrowsAsync<InvalidOperationException>(request.PerformAsync);
317 else
318 Assert.Throws<InvalidOperationException>(request.Perform);
319
320 Assert.IsTrue(request.Completed);
321 Assert.IsTrue(request.Aborted);
322
323 var responseObject = request.ResponseObject;
324
325 Assert.IsTrue(responseObject == null);
326 Assert.IsFalse(hasThrown);
327 }
328
329 /// <summary>
330 /// Tests cancelling the <see cref="WebRequest"/> after response has been received from the server
331 /// but before data has been read.
332 /// </summary>
333 [Test, Retry(5)]
334 public void TestCancelReceive()
335 {
336 var cancellationSource = new CancellationTokenSource();
337 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
338 {
339 Method = HttpMethod.Get,
340 AllowInsecureRequests = true,
341 };
342
343 bool hasThrown = false;
344 request.Failed += exception => hasThrown = exception != null;
345 request.Started += () => cancellationSource.Cancel();
346
347 Assert.DoesNotThrowAsync(() => request.PerformAsync(cancellationSource.Token));
348
349 Assert.IsTrue(request.Completed);
350 Assert.IsTrue(request.Aborted);
351
352 Assert.IsTrue(request.ResponseObject == null);
353 Assert.IsFalse(hasThrown);
354 }
355
356 /// <summary>
357 /// Tests aborting the <see cref="WebRequest"/> before the request is sent to the server.
358 /// </summary>
359 [Test, Retry(5)]
360 public async Task TestCancelRequest()
361 {
362 var cancellationSource = new CancellationTokenSource();
363 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
364 {
365 Method = HttpMethod.Get,
366 AllowInsecureRequests = true,
367 };
368
369 bool hasThrown = false;
370 request.Failed += exception => hasThrown = exception != null;
371
372 cancellationSource.Cancel();
373 await request.PerformAsync(cancellationSource.Token).ConfigureAwait(false);
374
375 Assert.IsTrue(request.Completed);
376 Assert.IsTrue(request.Aborted);
377
378 Assert.IsTrue(request.ResponseObject == null);
379
380 Assert.IsFalse(hasThrown);
381 }
382
383 /// <summary>
384 /// Tests being able to cancel + restart a request.
385 /// </summary>
386 [Test, Retry(5)]
387 public void TestRestartAfterAbort()
388 {
389 var cancellationSource = new CancellationTokenSource();
390 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
391 {
392 Method = HttpMethod.Get,
393 AllowInsecureRequests = true,
394 };
395
396 bool hasThrown = false;
397 request.Failed += exception => hasThrown = exception != null;
398
399 cancellationSource.Cancel();
400 request.PerformAsync(cancellationSource.Token);
401
402 Assert.ThrowsAsync<InvalidOperationException>(request.PerformAsync);
403
404 Assert.IsTrue(request.Completed);
405 Assert.IsTrue(request.Aborted);
406
407 var responseObject = request.ResponseObject;
408
409 Assert.IsTrue(responseObject == null);
410 Assert.IsFalse(hasThrown);
411 }
412
413 /// <summary>
414 /// Tests that specifically-crafted <see cref="WebRequest"/> is completed after one timeout.
415 /// </summary>
416 [Test, Retry(5)]
417 public void TestOneTimeout()
418 {
419 var request = new DelayedWebRequest
420 {
421 Method = HttpMethod.Get,
422 AllowInsecureRequests = true,
423 Timeout = 1000,
424 Delay = 2
425 };
426
427 Exception thrownException = null;
428 request.Failed += e => thrownException = e;
429 request.CompleteInvoked = () => request.Delay = 0;
430
431 Assert.DoesNotThrow(request.Perform);
432
433 Assert.IsTrue(request.Completed);
434 Assert.IsFalse(request.Aborted);
435
436 Assert.IsTrue(thrownException == null);
437 Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount);
438 }
439
440 /// <summary>
441 /// Tests that a <see cref="WebRequest"/> will only timeout a maximum of <see cref="WebRequest.MAX_RETRIES"/> times before being aborted.
442 /// </summary>
443 [Test, Retry(5)]
444 public void TestFailTimeout()
445 {
446 var request = new WebRequest($"{default_protocol}://{host}/delay/4")
447 {
448 Method = HttpMethod.Get,
449 AllowInsecureRequests = true,
450 Timeout = 1000
451 };
452
453 Exception thrownException = null;
454 request.Failed += e => thrownException = e;
455
456 Assert.Throws<WebException>(request.Perform);
457
458 Assert.IsTrue(request.Completed);
459 Assert.IsTrue(request.Aborted);
460
461 Assert.IsTrue(thrownException != null);
462 Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount);
463 Assert.AreEqual(typeof(WebException), thrownException.GetType());
464 }
465
466 /// <summary>
467 /// Tests being able to abort + restart a request.
468 /// </summary>
469 [Test, Retry(5)]
470 public void TestEventUnbindOnCompletion([Values(true, false)] bool async)
471 {
472 var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
473 {
474 Method = HttpMethod.Get,
475 AllowInsecureRequests = true,
476 };
477
478 request.Started += () => { };
479 request.Failed += e => { };
480 request.DownloadProgress += (l1, l2) => { };
481 request.UploadProgress += (l1, l2) => { };
482
483 Assert.DoesNotThrow(request.Perform);
484
485 var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
486
487 foreach (var e in events)
488 {
489 var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
490 Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0);
491 }
492 }
493
494 /// <summary>
495 /// Tests being able to abort + restart a request.
496 /// </summary>
497 [Test, Retry(5)]
498 public void TestUnbindOnDispose([Values(true, false)] bool async)
499 {
500 WebRequest request;
501
502 using (request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
503 {
504 Method = HttpMethod.Get,
505 AllowInsecureRequests = true,
506 })
507 {
508 request.Started += () => { };
509 request.Failed += e => { };
510 request.DownloadProgress += (l1, l2) => { };
511 request.UploadProgress += (l1, l2) => { };
512
513 Assert.DoesNotThrow(request.Perform);
514 }
515
516 var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
517
518 foreach (var e in events)
519 {
520 var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
521 Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0);
522 }
523 }
524
525 [Test, Retry(5)]
526 public void TestGetWithQueryStringParameters()
527 {
528 const string test_key_1 = "testkey1";
529 const string test_val_1 = "testval1 that ends with a #";
530
531 const string test_key_2 = "testkey2";
532 const string test_val_2 = "testval2 that ends with a space ";
533
534 var request = new JsonWebRequest<HttpBinGetResponse>($@"{default_protocol}://{host}/get")
535 {
536 Method = HttpMethod.Get,
537 AllowInsecureRequests = true
538 };
539
540 request.AddParameter(test_key_1, test_val_1);
541 request.AddParameter(test_key_2, test_val_2);
542
543 Assert.DoesNotThrow(request.Perform);
544
545 var responseObject = request.ResponseObject;
546
547 Assert.IsTrue(request.Completed);
548 Assert.IsFalse(request.Aborted);
549
550 Assert.NotNull(responseObject.Arguments);
551
552 Assert.True(responseObject.Arguments.ContainsKey(test_key_1));
553 Assert.AreEqual(test_val_1, responseObject.Arguments[test_key_1]);
554
555 Assert.True(responseObject.Arguments.ContainsKey(test_key_2));
556 Assert.AreEqual(test_val_2, responseObject.Arguments[test_key_2]);
557 }
558
559 [Test, Retry(5)]
560 public void TestPostWithJsonResponse([Values(true, false)] bool async)
561 {
562 var request = new JsonWebRequest<HttpBinPostResponse>($"{default_protocol}://{host}/post")
563 {
564 Method = HttpMethod.Post,
565 AllowInsecureRequests = true,
566 };
567
568 request.AddParameter("testkey1", "testval1");
569 request.AddParameter("testkey2", "testval2");
570
571 if (async)
572 Assert.DoesNotThrowAsync(request.PerformAsync);
573 else
574 Assert.DoesNotThrow(request.Perform);
575
576 var responseObject = request.ResponseObject;
577
578 Assert.IsTrue(request.Completed);
579 Assert.IsFalse(request.Aborted);
580
581 Assert.IsTrue(responseObject.Form != null);
582 Assert.IsTrue(responseObject.Form.Count == 2);
583
584 Assert.IsTrue(responseObject.Headers.ContentLength > 0);
585
586 Assert.IsTrue(responseObject.Form.ContainsKey("testkey1"));
587 Assert.IsTrue(responseObject.Form["testkey1"] == "testval1");
588
589 Assert.IsTrue(responseObject.Form.ContainsKey("testkey2"));
590 Assert.IsTrue(responseObject.Form["testkey2"] == "testval2");
591
592 Assert.IsTrue(responseObject.Headers.ContentType.StartsWith("multipart/form-data; boundary=", StringComparison.Ordinal));
593 }
594
595 [Test, Retry(5)]
596 public void TestPostWithJsonRequest([Values(true, false)] bool async)
597 {
598 var request = new JsonWebRequest<HttpBinPostResponse>($"{default_protocol}://{host}/post")
599 {
600 Method = HttpMethod.Post,
601 AllowInsecureRequests = true,
602 };
603
604 var testObject = new TestObject();
605 request.AddRaw(JsonConvert.SerializeObject(testObject));
606
607 if (async)
608 Assert.DoesNotThrowAsync(request.PerformAsync);
609 else
610 Assert.DoesNotThrow(request.Perform);
611
612 var responseObject = request.ResponseObject;
613
614 Assert.IsTrue(request.Completed);
615 Assert.IsFalse(request.Aborted);
616
617 Assert.IsTrue(responseObject.Headers.ContentLength > 0);
618 Assert.IsTrue(responseObject.Json != null);
619 Assert.AreEqual(testObject.TestString, responseObject.Json.TestString);
620
621 Assert.IsTrue(responseObject.Headers.ContentType == null);
622 }
623
624 [Test, Retry(5)]
625 public void TestNoContentPost([Values(true, false)] bool async)
626 {
627 var request = new WebRequest($"{default_protocol}://{host}/anything")
628 {
629 Method = HttpMethod.Post,
630 AllowInsecureRequests = true,
631 };
632
633 if (async)
634 Assert.DoesNotThrowAsync(request.PerformAsync);
635 else
636 Assert.DoesNotThrow(request.Perform);
637
638 var responseJson = JsonConvert.DeserializeObject<HttpBinPostResponse>(request.GetResponseString());
639
640 Assert.IsTrue(request.Completed);
641 Assert.IsFalse(request.Aborted);
642 Assert.AreEqual(0, responseJson?.Headers.ContentLength);
643 }
644
645 [Test, Retry(5)]
646 public void TestPutWithQueryAndFormParams()
647 {
648 const string test_key_1 = "param1";
649 const string test_val_1 = "in query! ";
650
651 const string test_key_2 = "param2";
652 const string test_val_2 = "in form!";
653
654 const string test_key_3 = "param3";
655 const string test_val_3 = "in form by default!";
656
657 var request = new JsonWebRequest<HttpBinPutResponse>($"{default_protocol}://{host}/put")
658 {
659 Method = HttpMethod.Put,
660 AllowInsecureRequests = true,
661 };
662
663 request.AddParameter(test_key_1, test_val_1, RequestParameterType.Query);
664 request.AddParameter(test_key_2, test_val_2, RequestParameterType.Form);
665 request.AddParameter(test_key_3, test_val_3);
666
667 Assert.DoesNotThrow(request.Perform);
668
669 Assert.IsTrue(request.Completed);
670 Assert.IsFalse(request.Aborted);
671
672 var response = request.ResponseObject;
673
674 Assert.NotNull(response.Arguments);
675 Assert.True(response.Arguments.ContainsKey(test_key_1));
676 Assert.AreEqual(test_val_1, response.Arguments[test_key_1]);
677
678 Assert.NotNull(response.Form);
679 Assert.True(response.Form.ContainsKey(test_key_2));
680 Assert.AreEqual(test_val_2, response.Form[test_key_2]);
681
682 Assert.NotNull(response.Form);
683 Assert.True(response.Form.ContainsKey(test_key_3));
684 Assert.AreEqual(test_val_3, response.Form[test_key_3]);
685 }
686
687 [Test]
688 public void TestFormParamsNotSupportedForGet()
689 {
690 var request = new JsonWebRequest<HttpBinPutResponse>($"{default_protocol}://{host}/get")
691 {
692 Method = HttpMethod.Get,
693 AllowInsecureRequests = true,
694 };
695
696 Assert.Throws<ArgumentException>(() => request.AddParameter("cannot", "work", RequestParameterType.Form));
697 }
698
699 [Test, Retry(5)]
700 public void TestGetBinaryData([Values(true, false)] bool async, [Values(true, false)] bool chunked)
701 {
702 const int bytes_count = 65536;
703 const int chunk_size = 1024;
704
705 string endpoint = chunked ? "stream-bytes" : "bytes";
706
707 WebRequest request = new WebRequest($"{default_protocol}://{host}/{endpoint}/{bytes_count}")
708 {
709 Method = HttpMethod.Get,
710 AllowInsecureRequests = true,
711 };
712 if (chunked)
713 request.AddParameter("chunk_size", chunk_size.ToString());
714
715 if (async)
716 Assert.DoesNotThrowAsync(request.PerformAsync);
717 else
718 Assert.DoesNotThrow(request.Perform);
719
720 Assert.IsTrue(request.Completed);
721 Assert.IsFalse(request.Aborted);
722
723 Assert.AreEqual(bytes_count, request.ResponseStream.Length);
724 }
725
726 [Serializable]
727 private class HttpBinGetResponse
728 {
729 [JsonProperty("args")]
730 public Dictionary<string, string> Arguments { get; set; }
731
732 [JsonProperty("headers")]
733 public HttpBinHeaders Headers { get; set; }
734
735 [JsonProperty("url")]
736 public string Url { get; set; }
737 }
738
739 [Serializable]
740 private class HttpBinPostResponse
741 {
742 [JsonProperty("data")]
743 public string Data { get; set; }
744
745 [JsonProperty("form")]
746 public IDictionary<string, string> Form { get; set; }
747
748 [JsonProperty("headers")]
749 public HttpBinHeaders Headers { get; set; }
750
751 [JsonProperty("json")]
752 public TestObject Json { get; set; }
753 }
754
755 [Serializable]
756 private class HttpBinPutResponse
757 {
758 [JsonProperty("args")]
759 public Dictionary<string, string> Arguments { get; set; }
760
761 [JsonProperty("form")]
762 public Dictionary<string, string> Form { get; set; }
763 }
764
765 [Serializable]
766 public class HttpBinHeaders
767 {
768 [JsonProperty("Content-Length")]
769 public int ContentLength { get; set; }
770
771 [JsonProperty("Content-Type")]
772 public string ContentType { get; set; }
773
774 [JsonProperty("User-Agent")]
775 public string UserAgent { get; set; }
776 }
777
778 [Serializable]
779 public class TestObject
780 {
781 public string TestString = "readable";
782 }
783
784 private class CustomUserAgentWebRequest : JsonWebRequest<HttpBinGetResponse>
785 {
786 public CustomUserAgentWebRequest(string url)
787 : base(url)
788 {
789 }
790
791 protected override string UserAgent => "custom-ua";
792 }
793
794 private class DelayedWebRequest : WebRequest
795 {
796 public Action CompleteInvoked;
797
798 private int delay;
799
800 public int Delay
801 {
802 get => delay;
803 set
804 {
805 delay = value;
806 Url = $"{default_protocol}://{host}/delay/{delay}";
807 }
808 }
809
810 public DelayedWebRequest()
811 : base($"{default_protocol}://{host}/delay/0")
812 {
813 }
814
815 protected override void Complete(Exception e = null)
816 {
817 CompleteInvoked?.Invoke();
818 base.Complete(e);
819 }
820 }
821 }
822}