1 // OpenTween - Client of Twitter
2 // Copyright (c) 2016 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
23 using System.Collections.Generic;
27 using System.Net.Http;
28 using System.Net.Http.Headers;
29 using System.Reflection;
30 using System.Runtime.InteropServices;
32 using System.Threading;
33 using System.Threading.Tasks;
37 using OpenTween.Api.DataModel;
40 namespace OpenTween.Connection
42 public class TwitterApiConnectionTest
44 public TwitterApiConnectionTest()
45 => this.MyCommonSetup();
47 private void MyCommonSetup()
49 var mockAssembly = new Mock<_Assembly>();
50 mockAssembly.Setup(m => m.GetName()).Returns(new AssemblyName("OpenTween"));
52 MyCommon.EntryAssembly = mockAssembly.Object;
56 public async Task GetAsync_Test()
58 using var mockHandler = new HttpMessageHandlerMock();
59 using var http = new HttpClient(mockHandler);
60 using var apiConnection = new TwitterApiConnection();
61 apiConnection.Http = http;
63 mockHandler.Enqueue(x =>
65 Assert.Equal(HttpMethod.Get, x.Method);
66 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
67 x.RequestUri.GetLeftPart(UriPartial.Path));
69 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
71 Assert.Equal("1111", query["aaaa"]);
72 Assert.Equal("2222", query["bbbb"]);
74 return new HttpResponseMessage(HttpStatusCode.OK)
76 Content = new StringContent("\"hogehoge\""),
80 var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
81 var param = new Dictionary<string, string>
87 var result = await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete");
88 Assert.Equal("hogehoge", result);
90 Assert.Equal(0, mockHandler.QueueCount);
94 public async Task GetAsync_AbsoluteUriTest()
96 using var mockHandler = new HttpMessageHandlerMock();
97 using var http = new HttpClient(mockHandler);
98 using var apiConnection = new TwitterApiConnection();
99 apiConnection.Http = http;
101 mockHandler.Enqueue(x =>
103 Assert.Equal(HttpMethod.Get, x.Method);
104 Assert.Equal("http://example.com/hoge/tetete.json",
105 x.RequestUri.GetLeftPart(UriPartial.Path));
107 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
109 Assert.Equal("1111", query["aaaa"]);
110 Assert.Equal("2222", query["bbbb"]);
112 return new HttpResponseMessage(HttpStatusCode.OK)
114 Content = new StringContent("\"hogehoge\""),
118 var endpoint = new Uri("http://example.com/hoge/tetete.json", UriKind.Absolute);
119 var param = new Dictionary<string, string>
125 await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete");
127 Assert.Equal(0, mockHandler.QueueCount);
131 public async Task SendAsync_Test()
133 using var mockHandler = new HttpMessageHandlerMock();
134 using var http = new HttpClient(mockHandler);
135 using var apiConnection = new TwitterApiConnection();
136 apiConnection.Http = http;
138 mockHandler.Enqueue(x =>
140 Assert.Equal(HttpMethod.Get, x.Method);
141 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
142 x.RequestUri.GetLeftPart(UriPartial.Path));
144 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
146 Assert.Equal("1111", query["aaaa"]);
147 Assert.Equal("2222", query["bbbb"]);
149 return new HttpResponseMessage(HttpStatusCode.OK)
151 Content = new StringContent("\"hogehoge\""),
155 var request = new GetRequest
157 RequestUri = new("hoge/tetete.json", UriKind.Relative),
158 Query = new Dictionary<string, string>
163 EndpointName = "/hoge/tetete",
166 using var response = await apiConnection.SendAsync(request);
168 Assert.Equal("hogehoge", await response.ReadAsJson<string>());
170 Assert.Equal(0, mockHandler.QueueCount);
174 public async Task SendAsync_UpdateRateLimitTest()
176 using var mockHandler = new HttpMessageHandlerMock();
177 using var http = new HttpClient(mockHandler);
178 using var apiConnection = new TwitterApiConnection();
179 apiConnection.Http = http;
181 mockHandler.Enqueue(x =>
183 Assert.Equal(HttpMethod.Get, x.Method);
184 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
185 x.RequestUri.GetLeftPart(UriPartial.Path));
187 return new HttpResponseMessage(HttpStatusCode.OK)
191 { "X-Rate-Limit-Limit", "150" },
192 { "X-Rate-Limit-Remaining", "100" },
193 { "X-Rate-Limit-Reset", "1356998400" },
194 { "X-Access-Level", "read-write-directmessages" },
196 Content = new StringContent("\"hogehoge\""),
200 var apiStatus = new TwitterApiStatus();
201 MyCommon.TwitterApiInfo = apiStatus;
203 var request = new GetRequest
205 RequestUri = new("hoge/tetete.json", UriKind.Relative),
206 EndpointName = "/hoge/tetete",
209 using var response = await apiConnection.SendAsync(request);
211 Assert.Equal(TwitterApiAccessLevel.ReadWriteAndDirectMessage, apiStatus.AccessLevel);
212 Assert.Equal(new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), apiStatus.AccessLimit["/hoge/tetete"]);
214 Assert.Equal(0, mockHandler.QueueCount);
218 public async Task SendAsync_ErrorStatusTest()
220 using var mockHandler = new HttpMessageHandlerMock();
221 using var http = new HttpClient(mockHandler);
222 using var apiConnection = new TwitterApiConnection();
223 apiConnection.Http = http;
225 mockHandler.Enqueue(x =>
227 return new HttpResponseMessage(HttpStatusCode.BadGateway)
229 Content = new StringContent("### Invalid JSON Response ###"),
233 var request = new GetRequest
235 RequestUri = new("hoge/tetete.json", UriKind.Relative),
238 var exception = await Assert.ThrowsAsync<TwitterApiException>(
239 () => apiConnection.SendAsync(request)
242 // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する
243 Assert.Equal("BadGateway", exception.Message);
244 Assert.Null(exception.ErrorResponse);
246 Assert.Equal(0, mockHandler.QueueCount);
250 public async Task SendAsync_ErrorJsonTest()
252 using var mockHandler = new HttpMessageHandlerMock();
253 using var http = new HttpClient(mockHandler);
254 using var apiConnection = new TwitterApiConnection();
255 apiConnection.Http = http;
257 mockHandler.Enqueue(x =>
259 return new HttpResponseMessage(HttpStatusCode.Forbidden)
261 Content = new StringContent("""{"errors":[{"code":187,"message":"Status is a duplicate."}]}"""),
265 var request = new GetRequest
267 RequestUri = new("hoge/tetete.json", UriKind.Relative),
270 var exception = await Assert.ThrowsAsync<TwitterApiException>(
271 () => apiConnection.SendAsync(request)
274 // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する
275 Assert.Equal("DuplicateStatus", exception.Message);
277 Assert.Equal(TwitterErrorCode.DuplicateStatus, exception.Errors[0].Code);
278 Assert.Equal("Status is a duplicate.", exception.Errors[0].Message);
280 Assert.Equal(0, mockHandler.QueueCount);
284 public async Task GetStreamAsync_Test()
286 using var mockHandler = new HttpMessageHandlerMock();
287 using var http = new HttpClient(mockHandler);
288 using var apiConnection = new TwitterApiConnection();
289 using var image = TestUtils.CreateDummyImage();
290 apiConnection.Http = http;
292 mockHandler.Enqueue(x =>
294 Assert.Equal(HttpMethod.Get, x.Method);
295 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
296 x.RequestUri.GetLeftPart(UriPartial.Path));
298 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
300 Assert.Equal("1111", query["aaaa"]);
301 Assert.Equal("2222", query["bbbb"]);
303 return new HttpResponseMessage(HttpStatusCode.OK)
305 Content = new ByteArrayContent(image.Stream.ToArray()),
309 var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
310 var param = new Dictionary<string, string>
316 var stream = await apiConnection.GetStreamAsync(endpoint, param);
318 using (var memoryStream = new MemoryStream())
320 // 内容の比較のために MemoryStream にコピー
321 await stream.CopyToAsync(memoryStream);
323 Assert.Equal(image.Stream.ToArray(), memoryStream.ToArray());
326 Assert.Equal(0, mockHandler.QueueCount);
330 public async Task PostLazyAsync_Test()
332 using var mockHandler = new HttpMessageHandlerMock();
333 using var http = new HttpClient(mockHandler);
334 using var apiConnection = new TwitterApiConnection();
335 apiConnection.Http = http;
337 mockHandler.Enqueue(async x =>
339 Assert.Equal(HttpMethod.Post, x.Method);
340 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
341 x.RequestUri.AbsoluteUri);
343 var body = await x.Content.ReadAsStringAsync();
344 var query = HttpUtility.ParseQueryString(body);
346 Assert.Equal("1111", query["aaaa"]);
347 Assert.Equal("2222", query["bbbb"]);
349 return new HttpResponseMessage(HttpStatusCode.OK)
351 Content = new StringContent("\"hogehoge\""),
355 var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
356 var param = new Dictionary<string, string>
362 var result = await apiConnection.PostLazyAsync<string>(endpoint, param);
364 Assert.Equal("hogehoge", await result.LoadJsonAsync());
366 Assert.Equal(0, mockHandler.QueueCount);
370 public async Task PostLazyAsync_MultipartTest()
372 using var mockHandler = new HttpMessageHandlerMock();
373 using var http = new HttpClient(mockHandler);
374 using var apiConnection = new TwitterApiConnection();
375 apiConnection.HttpUpload = http;
377 using var image = TestUtils.CreateDummyImage();
378 using var media = new MemoryImageMediaItem(image);
380 mockHandler.Enqueue(async x =>
382 Assert.Equal(HttpMethod.Post, x.Method);
383 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
384 x.RequestUri.AbsoluteUri);
386 Assert.IsType<MultipartFormDataContent>(x.Content);
388 var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
389 .First(y => y.Name == "boundary").Value;
392 boundary = boundary.Substring(1, boundary.Length - 2);
395 $"--{boundary}\r\n" +
396 "Content-Type: text/plain; charset=utf-8\r\n" +
397 "Content-Disposition: form-data; name=aaaa\r\n" +
400 $"--{boundary}\r\n" +
401 "Content-Type: text/plain; charset=utf-8\r\n" +
402 "Content-Disposition: form-data; name=bbbb\r\n" +
405 $"--{boundary}\r\n" +
406 $"Content-Disposition: form-data; name=media1; filename={media.Name}; filename*=utf-8''{media.Name}\r\n" +
409 var expected = Encoding.UTF8.GetBytes(expectedText)
410 .Concat(image.Stream.ToArray())
411 .Concat(Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n"));
413 Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync());
415 return new HttpResponseMessage(HttpStatusCode.OK)
417 Content = new StringContent("\"hogehoge\""),
421 var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
422 var param = new Dictionary<string, string>
427 var mediaParam = new Dictionary<string, IMediaItem>
432 var result = await apiConnection.PostLazyAsync<string>(endpoint, param, mediaParam);
434 Assert.Equal("hogehoge", await result.LoadJsonAsync());
436 Assert.Equal(0, mockHandler.QueueCount);
440 public async Task PostLazyAsync_Multipart_NullTest()
442 using var mockHandler = new HttpMessageHandlerMock();
443 using var http = new HttpClient(mockHandler);
444 using var apiConnection = new TwitterApiConnection();
445 apiConnection.HttpUpload = http;
447 mockHandler.Enqueue(async x =>
449 Assert.Equal(HttpMethod.Post, x.Method);
450 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
451 x.RequestUri.AbsoluteUri);
453 Assert.IsType<MultipartFormDataContent>(x.Content);
455 var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
456 .First(y => y.Name == "boundary").Value;
459 boundary = boundary.Substring(1, boundary.Length - 2);
462 $"--{boundary}\r\n" +
463 $"\r\n--{boundary}--\r\n";
465 var expected = Encoding.UTF8.GetBytes(expectedText);
467 Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync());
469 return new HttpResponseMessage(HttpStatusCode.OK)
471 Content = new StringContent("\"hogehoge\""),
475 var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
477 var result = await apiConnection.PostLazyAsync<string>(endpoint, param: null, media: null);
479 Assert.Equal("hogehoge", await result.LoadJsonAsync());
481 Assert.Equal(0, mockHandler.QueueCount);
485 public async Task HandleTimeout_SuccessTest()
487 static async Task<int> AsyncFunc(CancellationToken token)
489 await Task.Delay(10);
490 token.ThrowIfCancellationRequested();
494 var timeout = TimeSpan.FromMilliseconds(200);
495 var ret = await TwitterApiConnection.HandleTimeout(AsyncFunc, timeout);
497 Assert.Equal(1, ret);
501 public async Task HandleTimeout_TimeoutTest()
503 var tcs = new TaskCompletionSource<bool>();
505 async Task<int> AsyncFunc(CancellationToken token)
507 await Task.Delay(200);
508 tcs.SetResult(token.IsCancellationRequested);
512 var timeout = TimeSpan.FromMilliseconds(10);
513 await Assert.ThrowsAsync<OperationCanceledException>(
514 () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
517 var cancelRequested = await tcs.Task;
518 Assert.True(cancelRequested);
522 public async Task HandleTimeout_ThrowExceptionAfterTimeoutTest()
524 var tcs = new TaskCompletionSource<int>();
526 async Task<int> AsyncFunc(CancellationToken token)
528 await Task.Delay(100);
530 throw new Exception();
533 var timeout = TimeSpan.FromMilliseconds(10);
534 await Assert.ThrowsAsync<OperationCanceledException>(
535 () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
538 // キャンセル後に AsyncFunc で発生した例外が無視される(UnobservedTaskException イベントを発生させない)ことをチェックする
540 void UnobservedExceptionHandler(object s, UnobservedTaskExceptionEventArgs e)
543 TaskScheduler.UnobservedTaskException += UnobservedExceptionHandler;
546 await Task.Delay(10);
547 GC.Collect(); // UnobservedTaskException は Task のデストラクタで呼ばれるため強制的に GC を実行する
548 await Task.Delay(10);
552 TaskScheduler.UnobservedTaskException -= UnobservedExceptionHandler;