mirror of
https://github.com/SK-la/osu-framework.git
synced 2026-03-15 03:20:30 +00:00
542 lines
18 KiB
C#
542 lines
18 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Reflection;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using NUnit.Framework;
|
|
using osu.Framework.IO.Network;
|
|
using WebRequest = osu.Framework.IO.Network.WebRequest;
|
|
|
|
namespace osu.Framework.Tests.IO
|
|
{
|
|
[TestFixture]
|
|
public class TestWebRequest
|
|
{
|
|
private const string default_protocol = "http";
|
|
private const string invalid_get_url = "a.ppy.shhhhh";
|
|
|
|
private static readonly string host;
|
|
private static readonly IEnumerable<string> protocols;
|
|
|
|
static TestWebRequest()
|
|
{
|
|
bool isAppveyorBuild = Environment.GetEnvironmentVariable("APPVEYOR")?.ToLower().Equals("true") ?? false;
|
|
|
|
if (isAppveyorBuild)
|
|
{
|
|
// httpbin very frequently falls over and causes random tests to fail
|
|
// Thus appveyor builds rely on a local httpbin instance to run the tests
|
|
|
|
host = "127.0.0.1";
|
|
protocols = new[] { default_protocol };
|
|
}
|
|
else
|
|
{
|
|
host = "httpbin.org";
|
|
protocols = new[] { default_protocol, "https" };
|
|
}
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestValidGet([ValueSource(nameof(protocols))] string protocol, [Values(true, false)] bool async)
|
|
{
|
|
var url = $"{protocol}://{host}/get";
|
|
var request = new JsonWebRequest<HttpBinGetResponse>(url)
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true
|
|
};
|
|
|
|
bool hasThrown = false;
|
|
request.Failed += exception => hasThrown = exception != null;
|
|
|
|
if (async)
|
|
Assert.DoesNotThrowAsync(request.PerformAsync);
|
|
else
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsFalse(request.Aborted);
|
|
|
|
var responseObject = request.ResponseObject;
|
|
|
|
Assert.IsTrue(responseObject != null);
|
|
Assert.IsTrue(responseObject.Headers.UserAgent == "osu!");
|
|
Assert.IsTrue(responseObject.Url == url);
|
|
|
|
Assert.IsFalse(hasThrown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests async execution is correctly yielding during IO wait time.
|
|
/// </summary>
|
|
[Test]
|
|
[Ignore("failing too often on appveyor")]
|
|
public void TestConcurrency()
|
|
{
|
|
const int request_count = 10;
|
|
const int induced_delay = 5;
|
|
|
|
int finished = 0;
|
|
|
|
Stopwatch sw = new Stopwatch();
|
|
sw.Start();
|
|
|
|
List<long> startTimes = new List<long>();
|
|
|
|
List<Task> running = new List<Task>();
|
|
for (int i = 0; i < request_count; i++)
|
|
{
|
|
var request = new DelayedWebRequest
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
Delay = induced_delay
|
|
};
|
|
|
|
request.Started += () => startTimes.Add(sw.ElapsedMilliseconds);
|
|
request.Finished += () => Interlocked.Increment(ref finished);
|
|
request.Failed += _ => Interlocked.Increment(ref finished);
|
|
running.Add(request.PerformAsync());
|
|
}
|
|
|
|
Task.WaitAll(running.ToArray());
|
|
|
|
// in the case threads are not yielding, the time taken will be greater than double the induced delay (after considering latency).
|
|
Assert.Less(sw.ElapsedMilliseconds, induced_delay * 2 * 1000);
|
|
|
|
Assert.AreEqual(request_count, startTimes.Count);
|
|
|
|
// another case would be requests starting too late into the test. just to make sure.
|
|
for (int i = 0; i < request_count; i++)
|
|
Assert.Less(startTimes[i] - startTimes[0], induced_delay * 1000);
|
|
|
|
Assert.AreEqual(request_count, finished);
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestInvalidGetExceptions([ValueSource(nameof(protocols))] string protocol, [Values(true, false)] bool async)
|
|
{
|
|
var request = new WebRequest($"{protocol}://{invalid_get_url}")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true
|
|
};
|
|
|
|
Exception finishedException = null;
|
|
request.Failed += exception => finishedException = exception;
|
|
|
|
if (async)
|
|
Assert.ThrowsAsync<HttpRequestException>(request.PerformAsync);
|
|
else
|
|
Assert.Throws<HttpRequestException>(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
Assert.IsTrue(request.ResponseString == null);
|
|
Assert.IsNotNull(finishedException);
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestBadStatusCode([Values(true, false)] bool async)
|
|
{
|
|
var request = new WebRequest($"{default_protocol}://{host}/hidden-basic-auth/user/passwd")
|
|
{
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
bool hasThrown = false;
|
|
request.Failed += exception => hasThrown = exception != null;
|
|
|
|
if (async)
|
|
Assert.ThrowsAsync<WebException>(request.PerformAsync);
|
|
else
|
|
Assert.Throws<WebException>(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
Assert.IsEmpty(request.ResponseString);
|
|
|
|
Assert.IsTrue(hasThrown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests aborting the <see cref="WebRequest"/> after response has been received from the server
|
|
/// but before data has been read.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestAbortReceive([Values(true, false)] bool async)
|
|
{
|
|
var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
bool hasThrown = false;
|
|
request.Failed += exception => hasThrown = exception != null;
|
|
request.Started += () => request.Abort();
|
|
|
|
if (async)
|
|
Assert.DoesNotThrowAsync(request.PerformAsync);
|
|
else
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
Assert.IsTrue(request.ResponseObject == null);
|
|
|
|
Assert.IsFalse(hasThrown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests aborting the <see cref="WebRequest"/> before the request is sent to the server.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestAbortRequest()
|
|
{
|
|
var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
bool hasThrown = false;
|
|
request.Failed += exception => hasThrown = exception != null;
|
|
|
|
#pragma warning disable 4014
|
|
request.PerformAsync();
|
|
#pragma warning restore 4014
|
|
|
|
Assert.DoesNotThrow(request.Abort);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
Assert.IsTrue(request.ResponseObject == null);
|
|
|
|
Assert.IsFalse(hasThrown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests being able to abort + restart a request.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestRestartAfterAbort([Values(true, false)] bool async)
|
|
{
|
|
var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
bool hasThrown = false;
|
|
request.Failed += exception => hasThrown = exception != null;
|
|
|
|
#pragma warning disable 4014
|
|
request.PerformAsync();
|
|
#pragma warning restore 4014
|
|
|
|
Assert.DoesNotThrow(request.Abort);
|
|
|
|
if (async)
|
|
Assert.ThrowsAsync<InvalidOperationException>(request.PerformAsync);
|
|
else
|
|
Assert.Throws<InvalidOperationException>(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
var responseObject = request.ResponseObject;
|
|
|
|
Assert.IsTrue(responseObject == null);
|
|
Assert.IsFalse(hasThrown);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that specifically-crafted <see cref="WebRequest"/> is completed after one timeout.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestOneTimeout()
|
|
{
|
|
var request = new DelayedWebRequest
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
Timeout = 1000,
|
|
Delay = 2
|
|
};
|
|
|
|
Exception thrownException = null;
|
|
request.Failed += e => thrownException = e;
|
|
request.CompleteInvoked = () => request.Delay = 0;
|
|
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsFalse(request.Aborted);
|
|
|
|
Assert.IsTrue(thrownException == null);
|
|
Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that a <see cref="WebRequest"/> will only timeout a maximum of <see cref="WebRequest.MAX_RETRIES"/> times before being aborted.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestFailTimeout()
|
|
{
|
|
var request = new WebRequest($"{default_protocol}://{host}/delay/4")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
Timeout = 1000
|
|
};
|
|
|
|
Exception thrownException = null;
|
|
request.Failed += e => thrownException = e;
|
|
|
|
Assert.Throws<WebException>(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsTrue(request.Aborted);
|
|
|
|
Assert.IsTrue(thrownException != null);
|
|
Assert.AreEqual(WebRequest.MAX_RETRIES, request.RetryCount);
|
|
Assert.AreEqual(typeof(WebException), thrownException.GetType());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests being able to abort + restart a request.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestEventUnbindOnCompletion([Values(true, false)] bool async)
|
|
{
|
|
var request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
request.Started += () => { };
|
|
request.Failed += e => { };
|
|
request.DownloadProgress += (l1, l2) => { };
|
|
request.UploadProgress += (l1, l2) => { };
|
|
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
|
|
foreach (var e in events)
|
|
{
|
|
var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
|
|
Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests being able to abort + restart a request.
|
|
/// </summary>
|
|
[Test, Retry(5)]
|
|
public void TestUnbindOnDispose([Values(true, false)] bool async)
|
|
{
|
|
WebRequest request;
|
|
using (request = new JsonWebRequest<HttpBinGetResponse>($"{default_protocol}://{host}/get")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
})
|
|
{
|
|
request.Started += () => { };
|
|
request.Failed += e => { };
|
|
request.DownloadProgress += (l1, l2) => { };
|
|
request.UploadProgress += (l1, l2) => { };
|
|
|
|
Assert.DoesNotThrow(request.Perform);
|
|
}
|
|
|
|
var events = request.GetType().GetEvents(BindingFlags.Instance | BindingFlags.Public);
|
|
foreach (var e in events)
|
|
{
|
|
var field = request.GetType().GetField(e.Name, BindingFlags.Instance | BindingFlags.Public);
|
|
Assert.IsFalse(((Delegate)field?.GetValue(request))?.GetInvocationList().Length > 0);
|
|
}
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestPostWithJsonResponse([Values(true, false)] bool async)
|
|
{
|
|
var request = new JsonWebRequest<HttpBinPostResponse>($"{default_protocol}://{host}/post")
|
|
{
|
|
Method = HttpMethod.Post,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
request.AddParameter("testkey1", "testval1");
|
|
request.AddParameter("testkey2", "testval2");
|
|
|
|
if (async)
|
|
Assert.DoesNotThrowAsync(request.PerformAsync);
|
|
else
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
var responseObject = request.ResponseObject;
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsFalse(request.Aborted);
|
|
|
|
Assert.IsTrue(responseObject.Form != null);
|
|
Assert.IsTrue(responseObject.Form.Count == 2);
|
|
|
|
Assert.IsTrue(responseObject.Headers.ContentLength > 0);
|
|
|
|
Assert.IsTrue(responseObject.Form.ContainsKey("testkey1"));
|
|
Assert.IsTrue(responseObject.Form["testkey1"] == "testval1");
|
|
|
|
Assert.IsTrue(responseObject.Form.ContainsKey("testkey2"));
|
|
Assert.IsTrue(responseObject.Form["testkey2"] == "testval2");
|
|
|
|
Assert.IsTrue(responseObject.Headers.ContentType.StartsWith("multipart/form-data; boundary="));
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestPostWithJsonRequest([Values(true, false)] bool async)
|
|
{
|
|
var request = new JsonWebRequest<HttpBinPostResponse>($"{default_protocol}://{host}/post")
|
|
{
|
|
Method = HttpMethod.Post,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
|
|
var testObject = new TestObject();
|
|
request.AddRaw(JsonConvert.SerializeObject(testObject));
|
|
|
|
if (async)
|
|
Assert.DoesNotThrowAsync(request.PerformAsync);
|
|
else
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
var responseObject = request.ResponseObject;
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsFalse(request.Aborted);
|
|
|
|
Assert.IsTrue(responseObject.Headers.ContentLength > 0);
|
|
Assert.IsTrue(responseObject.Json != null);
|
|
Assert.AreEqual(testObject.TestString, responseObject.Json.TestString);
|
|
|
|
Assert.IsTrue(responseObject.Headers.ContentType == null);
|
|
}
|
|
|
|
[Test, Retry(5)]
|
|
public void TestGetBinaryData([Values(true, false)] bool async, [Values(true, false)] bool chunked)
|
|
{
|
|
const int bytes_count = 65536;
|
|
const int chunk_size = 1024;
|
|
|
|
string endpoint = chunked ? "stream-bytes" : "bytes";
|
|
|
|
WebRequest request = new WebRequest($"{default_protocol}://{host}/{endpoint}/{bytes_count}")
|
|
{
|
|
Method = HttpMethod.Get,
|
|
AllowInsecureRequests = true,
|
|
};
|
|
if (chunked)
|
|
request.AddParameter("chunk_size", chunk_size.ToString());
|
|
|
|
if (async)
|
|
Assert.DoesNotThrowAsync(request.PerformAsync);
|
|
else
|
|
Assert.DoesNotThrow(request.Perform);
|
|
|
|
Assert.IsTrue(request.Completed);
|
|
Assert.IsFalse(request.Aborted);
|
|
|
|
Assert.AreEqual(bytes_count, request.ResponseStream.Length);
|
|
}
|
|
|
|
[Serializable]
|
|
private class HttpBinGetResponse
|
|
{
|
|
[JsonProperty("headers")]
|
|
public HttpBinHeaders Headers { get; set; }
|
|
|
|
[JsonProperty("url")]
|
|
public string Url { get; set; }
|
|
}
|
|
|
|
|
|
[Serializable]
|
|
private class HttpBinPostResponse
|
|
{
|
|
[JsonProperty("data")]
|
|
public string Data { get; set; }
|
|
|
|
[JsonProperty("form")]
|
|
public IDictionary<string, string> Form { get; set; }
|
|
|
|
[JsonProperty("headers")]
|
|
public HttpBinHeaders Headers { get; set; }
|
|
|
|
[JsonProperty("json")]
|
|
public TestObject Json { get; set; }
|
|
}
|
|
|
|
[Serializable]
|
|
public class HttpBinHeaders
|
|
{
|
|
[JsonProperty("Content-Length")]
|
|
public int ContentLength { get; set; }
|
|
|
|
[JsonProperty("Content-Type")]
|
|
public string ContentType { get; set; }
|
|
|
|
[JsonProperty("User-Agent")]
|
|
public string UserAgent { get; set; }
|
|
}
|
|
|
|
[Serializable]
|
|
public class TestObject
|
|
{
|
|
public string TestString = "readable";
|
|
}
|
|
|
|
private class DelayedWebRequest : WebRequest
|
|
{
|
|
public Action CompleteInvoked;
|
|
|
|
private int delay;
|
|
|
|
public int Delay
|
|
{
|
|
get { return delay; }
|
|
set
|
|
{
|
|
delay = value;
|
|
Url = $"{default_protocol}://{host}/delay/{delay}";
|
|
}
|
|
}
|
|
|
|
public DelayedWebRequest()
|
|
: base($"{default_protocol}://{host}/delay/0")
|
|
{
|
|
}
|
|
|
|
protected override void Complete(Exception e = null)
|
|
{
|
|
CompleteInvoked?.Invoke();
|
|
base.Complete(e);
|
|
}
|
|
}
|
|
}
|
|
}
|