OSDN Git Service

TwitterApiConnection.DeleteAsyncメソッドを削除
[opentween/open-tween.git] / OpenTween.Tests / Connection / TwitterApiConnectionTest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2016 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
3 // All rights reserved.
4 //
5 // This file is part of OpenTween.
6 //
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)
10 // any later version.
11 //
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
15 // for more details.
16 //
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.
21
22 using System;
23 using System.Collections.Generic;
24 using System.IO;
25 using System.Linq;
26 using System.Net;
27 using System.Net.Http;
28 using System.Net.Http.Headers;
29 using System.Reflection;
30 using System.Runtime.InteropServices;
31 using System.Text;
32 using System.Threading;
33 using System.Threading.Tasks;
34 using System.Web;
35 using Moq;
36 using OpenTween.Api;
37 using OpenTween.Api.DataModel;
38 using Xunit;
39
40 namespace OpenTween.Connection
41 {
42     public class TwitterApiConnectionTest
43     {
44         public TwitterApiConnectionTest()
45             => this.MyCommonSetup();
46
47         private void MyCommonSetup()
48         {
49             var mockAssembly = new Mock<_Assembly>();
50             mockAssembly.Setup(m => m.GetName()).Returns(new AssemblyName("OpenTween"));
51
52             MyCommon.EntryAssembly = mockAssembly.Object;
53         }
54
55         [Fact]
56         public async Task GetAsync_Test()
57         {
58             using var mockHandler = new HttpMessageHandlerMock();
59             using var http = new HttpClient(mockHandler);
60             using var apiConnection = new TwitterApiConnection();
61             apiConnection.Http = http;
62
63             mockHandler.Enqueue(x =>
64             {
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));
68
69                 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
70
71                 Assert.Equal("1111", query["aaaa"]);
72                 Assert.Equal("2222", query["bbbb"]);
73
74                 return new HttpResponseMessage(HttpStatusCode.OK)
75                 {
76                     Content = new StringContent("\"hogehoge\""),
77                 };
78             });
79
80             var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
81             var param = new Dictionary<string, string>
82             {
83                 ["aaaa"] = "1111",
84                 ["bbbb"] = "2222",
85             };
86
87             var result = await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete");
88             Assert.Equal("hogehoge", result);
89
90             Assert.Equal(0, mockHandler.QueueCount);
91         }
92
93         [Fact]
94         public async Task GetAsync_AbsoluteUriTest()
95         {
96             using var mockHandler = new HttpMessageHandlerMock();
97             using var http = new HttpClient(mockHandler);
98             using var apiConnection = new TwitterApiConnection();
99             apiConnection.Http = http;
100
101             mockHandler.Enqueue(x =>
102             {
103                 Assert.Equal(HttpMethod.Get, x.Method);
104                 Assert.Equal("http://example.com/hoge/tetete.json",
105                     x.RequestUri.GetLeftPart(UriPartial.Path));
106
107                 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
108
109                 Assert.Equal("1111", query["aaaa"]);
110                 Assert.Equal("2222", query["bbbb"]);
111
112                 return new HttpResponseMessage(HttpStatusCode.OK)
113                 {
114                     Content = new StringContent("\"hogehoge\""),
115                 };
116             });
117
118             var endpoint = new Uri("http://example.com/hoge/tetete.json", UriKind.Absolute);
119             var param = new Dictionary<string, string>
120             {
121                 ["aaaa"] = "1111",
122                 ["bbbb"] = "2222",
123             };
124
125             await apiConnection.GetAsync<string>(endpoint, param, endpointName: "/hoge/tetete");
126
127             Assert.Equal(0, mockHandler.QueueCount);
128         }
129
130         [Fact]
131         public async Task SendAsync_Test()
132         {
133             using var mockHandler = new HttpMessageHandlerMock();
134             using var http = new HttpClient(mockHandler);
135             using var apiConnection = new TwitterApiConnection();
136             apiConnection.Http = http;
137
138             mockHandler.Enqueue(x =>
139             {
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));
143
144                 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
145
146                 Assert.Equal("1111", query["aaaa"]);
147                 Assert.Equal("2222", query["bbbb"]);
148
149                 return new HttpResponseMessage(HttpStatusCode.OK)
150                 {
151                     Content = new StringContent("\"hogehoge\""),
152                 };
153             });
154
155             var request = new GetRequest
156             {
157                 RequestUri = new("hoge/tetete.json", UriKind.Relative),
158                 Query = new Dictionary<string, string>
159                 {
160                     ["aaaa"] = "1111",
161                     ["bbbb"] = "2222",
162                 },
163                 EndpointName = "/hoge/tetete",
164             };
165
166             using var response = await apiConnection.SendAsync(request);
167
168             Assert.Equal("hogehoge", await response.ReadAsJson<string>());
169
170             Assert.Equal(0, mockHandler.QueueCount);
171         }
172
173         [Fact]
174         public async Task SendAsync_UpdateRateLimitTest()
175         {
176             using var mockHandler = new HttpMessageHandlerMock();
177             using var http = new HttpClient(mockHandler);
178             using var apiConnection = new TwitterApiConnection();
179             apiConnection.Http = http;
180
181             mockHandler.Enqueue(x =>
182             {
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));
186
187                 return new HttpResponseMessage(HttpStatusCode.OK)
188                 {
189                     Headers =
190                     {
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" },
195                     },
196                     Content = new StringContent("\"hogehoge\""),
197                 };
198             });
199
200             var apiStatus = new TwitterApiStatus();
201             MyCommon.TwitterApiInfo = apiStatus;
202
203             var request = new GetRequest
204             {
205                 RequestUri = new("hoge/tetete.json", UriKind.Relative),
206                 EndpointName = "/hoge/tetete",
207             };
208
209             using var response = await apiConnection.SendAsync(request);
210
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"]);
213
214             Assert.Equal(0, mockHandler.QueueCount);
215         }
216
217         [Fact]
218         public async Task SendAsync_ErrorStatusTest()
219         {
220             using var mockHandler = new HttpMessageHandlerMock();
221             using var http = new HttpClient(mockHandler);
222             using var apiConnection = new TwitterApiConnection();
223             apiConnection.Http = http;
224
225             mockHandler.Enqueue(x =>
226             {
227                 return new HttpResponseMessage(HttpStatusCode.BadGateway)
228                 {
229                     Content = new StringContent("### Invalid JSON Response ###"),
230                 };
231             });
232
233             var request = new GetRequest
234             {
235                 RequestUri = new("hoge/tetete.json", UriKind.Relative),
236             };
237
238             var exception = await Assert.ThrowsAsync<TwitterApiException>(
239                 () => apiConnection.SendAsync(request)
240             );
241
242             // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する
243             Assert.Equal("BadGateway", exception.Message);
244             Assert.Null(exception.ErrorResponse);
245
246             Assert.Equal(0, mockHandler.QueueCount);
247         }
248
249         [Fact]
250         public async Task SendAsync_ErrorJsonTest()
251         {
252             using var mockHandler = new HttpMessageHandlerMock();
253             using var http = new HttpClient(mockHandler);
254             using var apiConnection = new TwitterApiConnection();
255             apiConnection.Http = http;
256
257             mockHandler.Enqueue(x =>
258             {
259                 return new HttpResponseMessage(HttpStatusCode.Forbidden)
260                 {
261                     Content = new StringContent("""{"errors":[{"code":187,"message":"Status is a duplicate."}]}"""),
262                 };
263             });
264
265             var request = new GetRequest
266             {
267                 RequestUri = new("hoge/tetete.json", UriKind.Relative),
268             };
269
270             var exception = await Assert.ThrowsAsync<TwitterApiException>(
271                 () => apiConnection.SendAsync(request)
272             );
273
274             // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する
275             Assert.Equal("DuplicateStatus", exception.Message);
276
277             Assert.Equal(TwitterErrorCode.DuplicateStatus, exception.Errors[0].Code);
278             Assert.Equal("Status is a duplicate.", exception.Errors[0].Message);
279
280             Assert.Equal(0, mockHandler.QueueCount);
281         }
282
283         [Fact]
284         public async Task GetStreamAsync_Test()
285         {
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;
291
292             mockHandler.Enqueue(x =>
293             {
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));
297
298                 var query = HttpUtility.ParseQueryString(x.RequestUri.Query);
299
300                 Assert.Equal("1111", query["aaaa"]);
301                 Assert.Equal("2222", query["bbbb"]);
302
303                 return new HttpResponseMessage(HttpStatusCode.OK)
304                 {
305                     Content = new ByteArrayContent(image.Stream.ToArray()),
306                 };
307             });
308
309             var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
310             var param = new Dictionary<string, string>
311             {
312                 ["aaaa"] = "1111",
313                 ["bbbb"] = "2222",
314             };
315
316             var stream = await apiConnection.GetStreamAsync(endpoint, param);
317
318             using (var memoryStream = new MemoryStream())
319             {
320                 // 内容の比較のために MemoryStream にコピー
321                 await stream.CopyToAsync(memoryStream);
322
323                 Assert.Equal(image.Stream.ToArray(), memoryStream.ToArray());
324             }
325
326             Assert.Equal(0, mockHandler.QueueCount);
327         }
328
329         [Fact]
330         public async Task PostLazyAsync_Test()
331         {
332             using var mockHandler = new HttpMessageHandlerMock();
333             using var http = new HttpClient(mockHandler);
334             using var apiConnection = new TwitterApiConnection();
335             apiConnection.Http = http;
336
337             mockHandler.Enqueue(async x =>
338             {
339                 Assert.Equal(HttpMethod.Post, x.Method);
340                 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
341                     x.RequestUri.AbsoluteUri);
342
343                 var body = await x.Content.ReadAsStringAsync();
344                 var query = HttpUtility.ParseQueryString(body);
345
346                 Assert.Equal("1111", query["aaaa"]);
347                 Assert.Equal("2222", query["bbbb"]);
348
349                 return new HttpResponseMessage(HttpStatusCode.OK)
350                 {
351                     Content = new StringContent("\"hogehoge\""),
352                 };
353             });
354
355             var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
356             var param = new Dictionary<string, string>
357             {
358                 ["aaaa"] = "1111",
359                 ["bbbb"] = "2222",
360             };
361
362             var result = await apiConnection.PostLazyAsync<string>(endpoint, param);
363
364             Assert.Equal("hogehoge", await result.LoadJsonAsync());
365
366             Assert.Equal(0, mockHandler.QueueCount);
367         }
368
369         [Fact]
370         public async Task PostLazyAsync_MultipartTest()
371         {
372             using var mockHandler = new HttpMessageHandlerMock();
373             using var http = new HttpClient(mockHandler);
374             using var apiConnection = new TwitterApiConnection();
375             apiConnection.HttpUpload = http;
376
377             using var image = TestUtils.CreateDummyImage();
378             using var media = new MemoryImageMediaItem(image);
379
380             mockHandler.Enqueue(async x =>
381             {
382                 Assert.Equal(HttpMethod.Post, x.Method);
383                 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
384                     x.RequestUri.AbsoluteUri);
385
386                 Assert.IsType<MultipartFormDataContent>(x.Content);
387
388                 var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
389                     .First(y => y.Name == "boundary").Value;
390
391                 // 前後のダブルクオーテーションを除去
392                 boundary = boundary.Substring(1, boundary.Length - 2);
393
394                 var expectedText =
395                     $"--{boundary}\r\n" +
396                     "Content-Type: text/plain; charset=utf-8\r\n" +
397                     "Content-Disposition: form-data; name=aaaa\r\n" +
398                     "\r\n" +
399                     "1111\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" +
403                     "\r\n" +
404                     "2222\r\n" +
405                     $"--{boundary}\r\n" +
406                     $"Content-Disposition: form-data; name=media1; filename={media.Name}; filename*=utf-8''{media.Name}\r\n" +
407                     "\r\n";
408
409                 var expected = Encoding.UTF8.GetBytes(expectedText)
410                     .Concat(image.Stream.ToArray())
411                     .Concat(Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n"));
412
413                 Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync());
414
415                 return new HttpResponseMessage(HttpStatusCode.OK)
416                 {
417                     Content = new StringContent("\"hogehoge\""),
418                 };
419             });
420
421             var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
422             var param = new Dictionary<string, string>
423             {
424                 ["aaaa"] = "1111",
425                 ["bbbb"] = "2222",
426             };
427             var mediaParam = new Dictionary<string, IMediaItem>
428             {
429                 ["media1"] = media,
430             };
431
432             var result = await apiConnection.PostLazyAsync<string>(endpoint, param, mediaParam);
433
434             Assert.Equal("hogehoge", await result.LoadJsonAsync());
435
436             Assert.Equal(0, mockHandler.QueueCount);
437         }
438
439         [Fact]
440         public async Task PostLazyAsync_Multipart_NullTest()
441         {
442             using var mockHandler = new HttpMessageHandlerMock();
443             using var http = new HttpClient(mockHandler);
444             using var apiConnection = new TwitterApiConnection();
445             apiConnection.HttpUpload = http;
446
447             mockHandler.Enqueue(async x =>
448             {
449                 Assert.Equal(HttpMethod.Post, x.Method);
450                 Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json",
451                     x.RequestUri.AbsoluteUri);
452
453                 Assert.IsType<MultipartFormDataContent>(x.Content);
454
455                 var boundary = x.Content.Headers.ContentType.Parameters.Cast<NameValueHeaderValue>()
456                     .First(y => y.Name == "boundary").Value;
457
458                 // 前後のダブルクオーテーションを除去
459                 boundary = boundary.Substring(1, boundary.Length - 2);
460
461                 var expectedText =
462                     $"--{boundary}\r\n" +
463                     $"\r\n--{boundary}--\r\n";
464
465                 var expected = Encoding.UTF8.GetBytes(expectedText);
466
467                 Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync());
468
469                 return new HttpResponseMessage(HttpStatusCode.OK)
470                 {
471                     Content = new StringContent("\"hogehoge\""),
472                 };
473             });
474
475             var endpoint = new Uri("hoge/tetete.json", UriKind.Relative);
476
477             var result = await apiConnection.PostLazyAsync<string>(endpoint, param: null, media: null);
478
479             Assert.Equal("hogehoge", await result.LoadJsonAsync());
480
481             Assert.Equal(0, mockHandler.QueueCount);
482         }
483
484         [Fact]
485         public async Task HandleTimeout_SuccessTest()
486         {
487             static async Task<int> AsyncFunc(CancellationToken token)
488             {
489                 await Task.Delay(10);
490                 token.ThrowIfCancellationRequested();
491                 return 1;
492             }
493
494             var timeout = TimeSpan.FromMilliseconds(200);
495             var ret = await TwitterApiConnection.HandleTimeout(AsyncFunc, timeout);
496
497             Assert.Equal(1, ret);
498         }
499
500         [Fact]
501         public async Task HandleTimeout_TimeoutTest()
502         {
503             var tcs = new TaskCompletionSource<bool>();
504
505             async Task<int> AsyncFunc(CancellationToken token)
506             {
507                 await Task.Delay(200);
508                 tcs.SetResult(token.IsCancellationRequested);
509                 return 1;
510             }
511
512             var timeout = TimeSpan.FromMilliseconds(10);
513             await Assert.ThrowsAsync<OperationCanceledException>(
514                 () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
515             );
516
517             var cancelRequested = await tcs.Task;
518             Assert.True(cancelRequested);
519         }
520
521         [Fact]
522         public async Task HandleTimeout_ThrowExceptionAfterTimeoutTest()
523         {
524             var tcs = new TaskCompletionSource<int>();
525
526             async Task<int> AsyncFunc(CancellationToken token)
527             {
528                 await Task.Delay(100);
529                 tcs.SetResult(1);
530                 throw new Exception();
531             }
532
533             var timeout = TimeSpan.FromMilliseconds(10);
534             await Assert.ThrowsAsync<OperationCanceledException>(
535                 () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout)
536             );
537
538             // キャンセル後に AsyncFunc で発生した例外が無視される(UnobservedTaskException イベントを発生させない)ことをチェックする
539             var error = false;
540             void UnobservedExceptionHandler(object s, UnobservedTaskExceptionEventArgs e)
541                 => error = true;
542
543             TaskScheduler.UnobservedTaskException += UnobservedExceptionHandler;
544
545             await tcs.Task;
546             await Task.Delay(10);
547             GC.Collect(); // UnobservedTaskException は Task のデストラクタで呼ばれるため強制的に GC を実行する
548             await Task.Delay(10);
549
550             Assert.False(error);
551
552             TaskScheduler.UnobservedTaskException -= UnobservedExceptionHandler;
553         }
554     }
555 }