using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Moq;
public class TwitterApiConnectionTest
{
public TwitterApiConnectionTest()
- {
- this.MyCommonSetup();
- }
+ => this.MyCommonSetup();
private void MyCommonSetup()
{
}
[Fact]
- public async Task GetAsync_Test()
+ public async Task SendAsync_Test()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
-
- mockHandler.Enqueue(x =>
- {
- Assert.Equal(HttpMethod.Get, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.GetLeftPart(UriPartial.Path));
+ using var mockHandler = new HttpMessageHandlerMock();
+ using var http = new HttpClient(mockHandler);
+ using var apiConnection = new TwitterApiConnection();
+ apiConnection.Http = http;
- var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
+ mockHandler.Enqueue(x =>
+ {
+ Assert.Equal(HttpMethod.Get, x.Method);
+ Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
+ x.RequestUri.GetLeftPart(UriPartial.Path));
- Assert.Equal("1111", query["aaaa"]);
- Assert.Equal("2222", query["bbbb"]);
+ var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
+ Assert.Equal("1111", query["aaaa"]);
+ Assert.Equal("2222", query["bbbb"]);
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
- var param = new Dictionary<string, string>
+ return new HttpResponseMessage(HttpStatusCode.OK)
{
- ["aaaa"] = "1111",
- ["bbbb"] = "2222",
+ Content = new StringContent("\"hogehoge\""),
};
+ });
- var result = await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete")
- .ConfigureAwait(false);
- Assert.Equal("hogehoge", result);
-
- Assert.Equal(0, mockHandler.QueueCount);
- }
- }
-
- [Fact]
- public async Task GetAsync_AbsoluteUriTest()
- {
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
+ var request = new GetRequest
{
- apiConnection.http = http;
-
- mockHandler.Enqueue(x =>
- {
- Assert.Equal(HttpMethod.Get, x.Method);
- Assert.Equal("http://example.com/hoge/tetete.json",
- x.RequestUri.GetLeftPart(UriPartial.Path));
-
- var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
-
- Assert.Equal("1111", query["aaaa"]);
- Assert.Equal("2222", query["bbbb"]);
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var endpoint = new Uri("http://example.com/hoge/tetete.json", UriKind.Absolute);
- var param = new Dictionary<string, string>
+ RequestUri = new("hoge/tetete.json", UriKind.Relative),
+ Query = new Dictionary<string, string>
{
["aaaa"] = "1111",
["bbbb"] = "2222",
- };
-
- await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete")
- .ConfigureAwait(false);
-
- Assert.Equal(0, mockHandler.QueueCount);
- }
- }
-
- [Fact]
- public async Task GetAsync_UpdateRateLimitTest()
- {
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
+ },
+ EndpointName = "/hoge/tetete",
+ };
- mockHandler.Enqueue(x =>
- {
- Assert.Equal(HttpMethod.Get, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.GetLeftPart(UriPartial.Path));
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Headers =
- {
- { "X-Rate-Limit-Limit", "150" },
- { "X-Rate-Limit-Remaining", "100" },
- { "X-Rate-Limit-Reset", "1356998400" },
- { "X-Access-Level", "read-write-directmessages" },
- },
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var apiStatus = new TwitterApiStatus();
- MyCommon.TwitterApiInfo = apiStatus;
+ using var response = await apiConnection.SendAsync(request);
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
+ Assert.Equal("hogehoge", await response.ReadAsJson<string>());
- await apiConnection.GetAsync<string>(endpoint, null, endpointName: "/hoge/tetete")
- .ConfigureAwait(false);
-
- Assert.Equal(TwitterApiAccessLevel.ReadWriteAndDirectMessage, apiStatus.AccessLevel);
- Assert.Equal(new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), apiStatus.AccessLimit["/hoge/tetete"]);
-
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ Assert.Equal(0, mockHandler.QueueCount);
}
[Fact]
- public async Task GetAsync_ErrorStatusTest()
+ public async Task SendAsync_UpdateRateLimitTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
+ using var mockHandler = new HttpMessageHandlerMock();
+ using var http = new HttpClient(mockHandler);
+ using var apiConnection = new TwitterApiConnection();
+ apiConnection.Http = http;
- mockHandler.Enqueue(x =>
- {
- return new HttpResponseMessage(HttpStatusCode.BadGateway)
- {
- Content = new StringContent("### Invalid JSON Response ###"),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
-
- var exception = await Assert.ThrowsAsync<TwitterApiException>(() => apiConnection.GetAsync<string>(endpoint, null, endpointName: "/hoge/tetete"))
- .ConfigureAwait(false);
-
- // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する
- Assert.Equal("BadGateway", exception.Message);
- Assert.Null(exception.ErrorResponse);
-
- Assert.Equal(0, mockHandler.QueueCount);
- }
- }
-
- [Fact]
- public async Task GetAsync_ErrorJsonTest()
- {
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
+ mockHandler.Enqueue(x =>
{
- apiConnection.http = http;
+ Assert.Equal(HttpMethod.Get, x.Method);
+ Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
+ x.RequestUri.GetLeftPart(UriPartial.Path));
- mockHandler.Enqueue(x =>
+ return new HttpResponseMessage(HttpStatusCode.OK)
{
- return new HttpResponseMessage(HttpStatusCode.Forbidden)
+ Headers =
{
- Content = new StringContent("{\"errors\":[{\"code\":187,\"message\":\"Status is a duplicate.\"}]}"),
- };
- });
+ { "X-Rate-Limit-Limit", "150" },
+ { "X-Rate-Limit-Remaining", "100" },
+ { "X-Rate-Limit-Reset", "1356998400" },
+ { "X-Access-Level", "read-write-directmessages" },
+ },
+ Content = new StringContent("\"hogehoge\""),
+ };
+ });
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
+ var apiStatus = new TwitterApiStatus();
+ MyCommon.TwitterApiInfo = apiStatus;
- var exception = await Assert.ThrowsAsync<TwitterApiException>(() => apiConnection.GetAsync<string>(endpoint, null, endpointName: "/hoge/tetete"))
- .ConfigureAwait(false);
+ var request = new GetRequest
+ {
+ RequestUri = new("hoge/tetete.json", UriKind.Relative),
+ EndpointName = "/hoge/tetete",
+ };
- // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する
- Assert.Equal("DuplicateStatus", exception.Message);
+ using var response = await apiConnection.SendAsync(request);
- Assert.Equal(TwitterErrorCode.DuplicateStatus, exception.ErrorResponse.Errors[0].Code);
- Assert.Equal("Status is a duplicate.", exception.ErrorResponse.Errors[0].Message);
+ Assert.Equal(TwitterApiAccessLevel.ReadWriteAndDirectMessage, apiStatus.AccessLevel);
+ Assert.Equal(new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), apiStatus.AccessLimit["/hoge/tetete"]);
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ Assert.Equal(0, mockHandler.QueueCount);
}
[Fact]
- public async Task GetStreamAsync_Test()
+ public async Task SendAsync_ErrorStatusTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- using (var image = TestUtils.CreateDummyImage())
- {
- apiConnection.http = http;
+ using var mockHandler = new HttpMessageHandlerMock();
+ using var http = new HttpClient(mockHandler);
+ using var apiConnection = new TwitterApiConnection();
+ apiConnection.Http = http;
- mockHandler.Enqueue(x =>
- {
- Assert.Equal(HttpMethod.Get, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.GetLeftPart(UriPartial.Path));
-
- var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
-
- Assert.Equal("1111", query["aaaa"]);
- Assert.Equal("2222", query["bbbb"]);
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new ByteArrayContent(image.Stream.ToArray()),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
- var param = new Dictionary<string, string>
+ mockHandler.Enqueue(x =>
+ {
+ return new HttpResponseMessage(HttpStatusCode.BadGateway)
{
- ["aaaa"] = "1111",
- ["bbbb"] = "2222",
+ Content = new StringContent("### Invalid JSON Response ###"),
};
+ });
- var stream = await apiConnection.GetStreamAsync(endpoint, param)
- .ConfigureAwait(false);
+ var request = new GetRequest
+ {
+ RequestUri = new("hoge/tetete.json", UriKind.Relative),
+ };
- using (var memoryStream = new MemoryStream())
- {
- // 内容の比較のために MemoryStream にコピー
- await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
+ var exception = await Assert.ThrowsAsync<TwitterApiException>(
+ () => apiConnection.SendAsync(request)
+ );
- Assert.Equal(image.Stream.ToArray(), memoryStream.ToArray());
- }
+ // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する
+ Assert.Equal("BadGateway", exception.Message);
+ Assert.Null(exception.ErrorResponse);
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ Assert.Equal(0, mockHandler.QueueCount);
}
[Fact]
- public async Task PostLazyAsync_Test()
+ public async Task SendAsync_ErrorJsonTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
-
- mockHandler.Enqueue(async x =>
- {
- Assert.Equal(HttpMethod.Post, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.AbsoluteUri);
-
- var body = await x.Content.ReadAsStringAsync()
- .ConfigureAwait(false);
- var query = HttpUtility.ParseQueryString(body);
+ using var mockHandler = new HttpMessageHandlerMock();
+ using var http = new HttpClient(mockHandler);
+ using var apiConnection = new TwitterApiConnection();
+ apiConnection.Http = http;
- Assert.Equal("1111", query["aaaa"]);
- Assert.Equal("2222", query["bbbb"]);
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
- var param = new Dictionary<string, string>
+ mockHandler.Enqueue(x =>
+ {
+ return new HttpResponseMessage(HttpStatusCode.Forbidden)
{
- ["aaaa"] = "1111",
- ["bbbb"] = "2222",
+ Content = new StringContent("""{"errors":[{"code":187,"message":"Status is a duplicate."}]}"""),
};
+ });
- var result = await apiConnection.PostLazyAsync<string>(endpoint, param)
- .ConfigureAwait(false);
-
- Assert.Equal("hogehoge", await result.LoadJsonAsync()
- .ConfigureAwait(false));
-
- Assert.Equal(0, mockHandler.QueueCount);
- }
- }
-
- [Fact]
- public async Task PostLazyAsync_MultipartTest()
- {
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
+ var request = new GetRequest
{
- apiConnection.httpUpload = http;
+ RequestUri = new("hoge/tetete.json", UriKind.Relative),
+ };
- using (var image = TestUtils.CreateDummyImage())
- using (var media = new MemoryImageMediaItem(image))
- {
- mockHandler.Enqueue(async x =>
- {
- Assert.Equal(HttpMethod.Post, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.AbsoluteUri);
-
- Assert.IsType<MultipartFormDataContent>(x.Content);
-
- var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
- .First(y => y.Name == "boundary").Value;
-
- // 前後のダブルクオーテーションを除去
- boundary = boundary.Substring(1, boundary.Length - 2);
-
- var expectedText =
- $"--{boundary}\r\n" +
- "Content-Type: text/plain; charset=utf-8\r\n" +
- "Content-Disposition: form-data; name=aaaa\r\n" +
- "\r\n" +
- "1111\r\n"+
- $"--{boundary}\r\n" +
- "Content-Type: text/plain; charset=utf-8\r\n" +
- "Content-Disposition: form-data; name=bbbb\r\n" +
- "\r\n" +
- "2222\r\n" +
- $"--{boundary}\r\n" +
- $"Content-Disposition: form-data; name=media1; filename={media.Name}; filename*=utf-8''{media.Name}\r\n" +
- "\r\n";
-
- var expected = Encoding.UTF8.GetBytes(expectedText)
- .Concat(image.Stream.ToArray())
- .Concat(Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n"));
-
- Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync().ConfigureAwait(false));
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
- var param = new Dictionary<string, string>
- {
- ["aaaa"] = "1111",
- ["bbbb"] = "2222",
- };
- var mediaParam = new Dictionary<string, IMediaItem>
- {
- ["media1"] = media,
- };
+ var exception = await Assert.ThrowsAsync<TwitterApiException>(
+ () => apiConnection.SendAsync(request)
+ );
- var result = await apiConnection.PostLazyAsync<string>(endpoint, param, mediaParam)
- .ConfigureAwait(false);
+ // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する
+ Assert.Equal("DuplicateStatus", exception.Message);
- Assert.Equal("hogehoge", await result.LoadJsonAsync()
- .ConfigureAwait(false));
+ Assert.Equal(TwitterErrorCode.DuplicateStatus, exception.Errors[0].Code);
+ Assert.Equal("Status is a duplicate.", exception.Errors[0].Message);
- Assert.Equal(0, mockHandler.QueueCount);
- }
- }
+ Assert.Equal(0, mockHandler.QueueCount);
}
[Fact]
- public async Task PostLazyAsync_Multipart_NullTest()
+ public async Task HandleTimeout_SuccessTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
+ static async Task<int> AsyncFunc(CancellationToken token)
{
- apiConnection.httpUpload = http;
-
- mockHandler.Enqueue(async x =>
- {
- Assert.Equal(HttpMethod.Post, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.AbsoluteUri);
-
- Assert.IsType<MultipartFormDataContent>(x.Content);
-
- var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
- .First(y => y.Name == "boundary").Value;
-
- // 前後のダブルクオーテーションを除去
- boundary = boundary.Substring(1, boundary.Length - 2);
-
- var expectedText =
- $"--{boundary}\r\n" +
- $"\r\n--{boundary}--\r\n";
-
- var expected = Encoding.UTF8.GetBytes(expectedText);
-
- Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync().ConfigureAwait(false));
-
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
-
- var result = await apiConnection.PostLazyAsync<string>(endpoint, param: null, media: null)
- .ConfigureAwait(false);
+ await Task.Delay(10);
+ token.ThrowIfCancellationRequested();
+ return 1;
+ }
- Assert.Equal("hogehoge", await result.LoadJsonAsync()
- .ConfigureAwait(false));
+ var timeout = TimeSpan.FromMilliseconds(200);
+ var ret = await TwitterApiConnection.HandleTimeout(AsyncFunc, timeout);
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ Assert.Equal(1, ret);
}
[Fact]
- public async Task PostJsonAsync_Test()
+ public async Task HandleTimeout_TimeoutTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
-
- mockHandler.Enqueue(async x =>
- {
- Assert.Equal(HttpMethod.Post, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.AbsoluteUri);
-
- Assert.Equal("application/json; charset=utf-8", x.Content.Headers.ContentType.ToString());
-
- var body = await x.Content.ReadAsStringAsync()
- .ConfigureAwait(false);
-
- Assert.Equal("{\"aaaa\": 1111}", body);
+ var tcs = new TaskCompletionSource<bool>();
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
-
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
+ async Task<int> AsyncFunc(CancellationToken token)
+ {
+ await Task.Delay(200);
+ tcs.SetResult(token.IsCancellationRequested);
+ return 1;
+ }
- await apiConnection.PostJsonAsync(endpoint, "{\"aaaa\": 1111}")
- .ConfigureAwait(false);
+ var timeout = TimeSpan.FromMilliseconds(10);
+ await Assert.ThrowsAsync<OperationCanceledException>(
+ () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
+ );
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ var cancelRequested = await tcs.Task;
+ Assert.True(cancelRequested);
}
[Fact]
- public async Task DeleteAsync_Test()
+ public async Task HandleTimeout_ThrowExceptionAfterTimeoutTest()
{
- using (var mockHandler = new HttpMessageHandlerMock())
- using (var http = new HttpClient(mockHandler))
- using (var apiConnection = new TwitterApiConnection("", ""))
- {
- apiConnection.http = http;
+ var tcs = new TaskCompletionSource<int>();
- mockHandler.Enqueue(async x =>
- {
- Assert.Equal(HttpMethod.Delete, x.Method);
- Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
- x.RequestUri.AbsoluteUri);
+ async Task<int> AsyncFunc(CancellationToken token)
+ {
+ await Task.Delay(100);
+ tcs.SetResult(1);
+ throw new Exception();
+ }
- var body = await x.Content.ReadAsStringAsync()
- .ConfigureAwait(false);
- var query = HttpUtility.ParseQueryString(body);
+ var timeout = TimeSpan.FromMilliseconds(10);
+ await Assert.ThrowsAsync<OperationCanceledException>(
+ () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
+ );
- Assert.Equal("1111", query["aaaa"]);
- Assert.Equal("2222", query["bbbb"]);
+ // キャンセル後に AsyncFunc で発生した例外が無視される(UnobservedTaskException イベントを発生させない)ことをチェックする
+ var error = false;
+ void UnobservedExceptionHandler(object s, UnobservedTaskExceptionEventArgs e)
+ => error = true;
- return new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent("\"hogehoge\""),
- };
- });
+ TaskScheduler.UnobservedTaskException += UnobservedExceptionHandler;
- var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
- var param = new Dictionary<string, string>
- {
- ["aaaa"] = "1111",
- ["bbbb"] = "2222",
- };
+ await tcs.Task;
+ await Task.Delay(10);
+ GC.Collect(); // UnobservedTaskException は Task のデストラクタで呼ばれるため強制的に GC を実行する
+ await Task.Delay(10);
- await apiConnection.DeleteAsync(endpoint, param)
- .ConfigureAwait(false);
+ Assert.False(error);
- Assert.Equal(0, mockHandler.QueueCount);
- }
+ TaskScheduler.UnobservedTaskException -= UnobservedExceptionHandler;
}
}
}