OSDN Git Service

Merge pull request #307 from opentween/detect-rate-limits
[opentween/open-tween.git] / OpenTween.Tests / TweenMainTest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 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.Reflection;
27 using System.Text;
28 using System.Text.RegularExpressions;
29 using System.Windows.Forms;
30 using OpenTween.Api.DataModel;
31 using OpenTween.Models;
32 using OpenTween.OpenTweenCustomControl;
33 using OpenTween.Setting;
34 using OpenTween.SocialProtocol;
35 using OpenTween.Thumbnail;
36 using Xunit;
37 using Xunit.Extensions;
38
39 namespace OpenTween
40 {
41     public class TweenMainTest
42     {
43         private record TestContext(
44             SettingManager Settings,
45             TabInformations TabInfo
46         );
47
48         private void UsingTweenMain(Action<TweenMain, TestContext> func)
49         {
50             var settings = new SettingManager("");
51             var tabinfo = new TabInformations();
52             using var accounts = new AccountCollection();
53             using var imageCache = new ImageCache();
54             using var iconAssets = new IconAssetsManager();
55             var thumbnailGenerator = new ThumbnailGenerator(new(autoupdate: false));
56
57             // TabInformation.GetInstance() で取得できるようにする
58             var field = typeof(TabInformations).GetField("Instance",
59                 BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.SetField);
60             field.SetValue(null, tabinfo);
61
62             using var tweenMain = new TweenMain(settings, tabinfo, accounts, imageCache, iconAssets, thumbnailGenerator);
63             var context = new TestContext(settings, tabinfo);
64
65             func(tweenMain, context);
66         }
67
68         [WinFormsFact]
69         public void Initialize_Test()
70             => this.UsingTweenMain((_, _) => { });
71
72         [WinFormsFact]
73         public void AddNewTab_FilterTabTest()
74         {
75             this.UsingTweenMain((tweenMain, context) =>
76             {
77                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
78
79                 var tab = new FilterTabModel("hoge");
80                 context.TabInfo.AddTab(tab);
81                 tweenMain.AddNewTab(tab, startup: false);
82
83                 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
84
85                 var tabPage = tweenMain.ListTab.TabPages[4];
86                 Assert.Equal("hoge", tabPage.Text);
87                 Assert.Single(tabPage.Controls);
88                 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
89             });
90         }
91
92         [WinFormsFact]
93         public void AddNewTab_UserTimelineTabTest()
94         {
95             this.UsingTweenMain((tweenMain, context) =>
96             {
97                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
98
99                 var tab = new UserTimelineTabModel("hoge", "twitterapi");
100                 context.TabInfo.AddTab(tab);
101                 tweenMain.AddNewTab(tab, startup: false);
102
103                 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
104
105                 var tabPage = tweenMain.ListTab.TabPages[4];
106                 Assert.Equal("hoge", tabPage.Text);
107                 Assert.Equal(2, tabPage.Controls.Count);
108                 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
109
110                 var label = Assert.IsType<Label>(tabPage.Controls[1]);
111                 Assert.Equal("twitterapi's Timeline", label.Text);
112             });
113         }
114
115         [WinFormsFact]
116         public void AddNewTab_ListTimelineTabTest()
117         {
118             this.UsingTweenMain((tweenMain, context) =>
119             {
120                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
121
122                 var list = new ListElement
123                 {
124                     Id = 12345L,
125                     Name = "tetete",
126                     Username = "opentween",
127                     IsPublic = false,
128                 };
129                 var tab = new ListTimelineTabModel("hoge", list);
130                 context.TabInfo.AddTab(tab);
131                 tweenMain.AddNewTab(tab, startup: false);
132
133                 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
134
135                 var tabPage = tweenMain.ListTab.TabPages[4];
136                 Assert.Equal("hoge", tabPage.Text);
137                 Assert.Equal(2, tabPage.Controls.Count);
138                 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
139
140                 var label = Assert.IsType<Label>(tabPage.Controls[1]);
141                 Assert.Equal("@opentween/tetete [Protected]", label.Text);
142             });
143         }
144
145         [WinFormsFact]
146         public void AddNewTab_PublicSearchTabTest()
147         {
148             this.UsingTweenMain((tweenMain, context) =>
149             {
150                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
151
152                 var tab = new PublicSearchTabModel("hoge")
153                 {
154                     SearchWords = "#OpenTween",
155                     SearchLang = "ja",
156                 };
157                 context.TabInfo.AddTab(tab);
158                 tweenMain.AddNewTab(tab, startup: false);
159
160                 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
161
162                 var tabPage = tweenMain.ListTab.TabPages[4];
163                 Assert.Equal("hoge", tabPage.Text);
164                 Assert.Equal(2, tabPage.Controls.Count);
165                 Assert.IsType<DetailsListView>(tabPage.Controls[0]);
166
167                 var panel = Assert.IsType<Panel>(tabPage.Controls[1]);
168                 Assert.Equal(4, panel.Controls.Count);
169
170                 var comboSearchWord = Assert.IsType<ComboBox>(panel.Controls[0]);
171                 Assert.Equal("#OpenTween", comboSearchWord.Text);
172
173                 var comboSearchLang = Assert.IsType<ComboBox>(panel.Controls[1]);
174                 Assert.Equal("ja", comboSearchLang.Text);
175
176                 Assert.IsType<Button>(panel.Controls[2]);
177                 Assert.IsType<Label>(panel.Controls[3]);
178             });
179         }
180
181         [WinFormsFact]
182         public void RemoveSpecifiedTab_Test()
183         {
184             this.UsingTweenMain((tweenMain, context) =>
185             {
186                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
187
188                 var tab = new PublicSearchTabModel("hoge")
189                 {
190                     SearchWords = "#OpenTween",
191                     SearchLang = "ja",
192                 };
193                 context.TabInfo.AddTab(tab);
194                 tweenMain.AddNewTab(tab, startup: false);
195                 Assert.Equal(5, tweenMain.ListTab.TabPages.Count);
196
197                 var tabPage = tweenMain.ListTab.TabPages[4];
198                 var listView = (DetailsListView)tabPage.Controls[0];
199                 var searchPanel = (Panel)tabPage.Controls[1];
200                 Assert.Equal("hoge", tabPage.Text);
201
202                 tweenMain.RemoveSpecifiedTab("hoge", confirm: false);
203
204                 Assert.Equal(4, tweenMain.ListTab.TabPages.Count);
205                 Assert.False(context.TabInfo.ContainsTab("hoge"));
206                 Assert.True(tabPage.IsDisposed);
207                 Assert.True(listView.IsDisposed);
208                 Assert.True(searchPanel.IsDisposed);
209             });
210         }
211
212         [WinFormsFact]
213         public void RefreshTimeline_Test()
214         {
215             this.UsingTweenMain((tweenMain, context) =>
216             {
217                 var tabPage = tweenMain.ListTab.TabPages[0];
218                 Assert.Equal("Recent", tabPage.Text);
219
220                 var listView = (DetailsListView)tabPage.Controls[0];
221                 Assert.Equal(0, listView.VirtualListSize);
222
223                 var post = new PostClass
224                 {
225                     StatusId = new TwitterStatusId("100"),
226                     Text = "hoge",
227                     UserId = 111L,
228                     ScreenName = "opentween",
229                     CreatedAt = new(2024, 1, 1, 0, 0, 0),
230                 };
231                 context.TabInfo.AddPost(post);
232                 context.TabInfo.DistributePosts();
233                 tweenMain.RefreshTimeline();
234
235                 Assert.Equal(1, listView.VirtualListSize);
236
237                 var listItem = listView.Items[0];
238                 Assert.Equal("opentween", listItem.SubItems[4].Text);
239             });
240         }
241
242         [WinFormsFact]
243         public void FormatStatusText_NewLineTest()
244         {
245             this.UsingTweenMain((tweenMain, _) =>
246             {
247                 Assert.Equal("aaa\nbbb", tweenMain.FormatStatusText("aaa\r\nbbb"));
248             });
249         }
250
251         [WinFormsFact]
252         public void FormatStatusText_NewLineInDMTest()
253         {
254             this.UsingTweenMain((tweenMain, _) =>
255             {
256                 // DM にも適用する
257                 Assert.Equal("D opentween aaa\nbbb", tweenMain.FormatStatusText("D opentween aaa\r\nbbb"));
258             });
259         }
260
261         [WinFormsFact]
262         public void FormatStatusText_SeparateUrlAndFullwidthCharacter_EnabledTest()
263         {
264             this.UsingTweenMain((tweenMain, context) =>
265             {
266                 tweenMain.SeparateUrlAndFullwidthCharacter = true;
267                 Assert.Equal("https://example.com/ あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
268             });
269         }
270
271         [WinFormsFact]
272         public void FormatStatusText_SeparateUrlAndFullwidthCharacter_DisabledTest()
273         {
274             this.UsingTweenMain((tweenMain, context) =>
275             {
276                 tweenMain.SeparateUrlAndFullwidthCharacter = false;
277                 Assert.Equal("https://example.com/あああ", tweenMain.FormatStatusText("https://example.com/あああ"));
278             });
279         }
280
281         [WinFormsFact]
282         public void FormatStatusText_ReplaceFullwidthSpace_EnabledTest()
283         {
284             this.UsingTweenMain((tweenMain, context) =>
285             {
286                 context.Settings.Common.WideSpaceConvert = true;
287                 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
288             });
289         }
290
291         [WinFormsFact]
292         public void FormatStatusText_ReplaceFullwidthSpaceInDM_EnabledTest()
293         {
294             this.UsingTweenMain((tweenMain, context) =>
295             {
296                 context.Settings.Common.WideSpaceConvert = true;
297
298                 // DM にも適用する
299                 Assert.Equal("D opentween aaa bbb", tweenMain.FormatStatusText("D opentween aaa bbb"));
300             });
301         }
302
303         [WinFormsFact]
304         public void FormatStatusText_ReplaceFullwidthSpace_DisabledTest()
305         {
306             this.UsingTweenMain((tweenMain, context) =>
307             {
308                 context.Settings.Common.WideSpaceConvert = false;
309                 Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb"));
310             });
311         }
312
313         [WinFormsFact]
314         public void FormatStatusText_UseRecommendedFooter_Test()
315         {
316             this.UsingTweenMain((tweenMain, context) =>
317             {
318                 context.Settings.Local.UseRecommendStatus = true;
319                 Assert.Matches(new Regex(@"^aaa \[TWNv\d+\]$"), tweenMain.FormatStatusText("aaa"));
320             });
321         }
322
323         [WinFormsFact]
324         public void FormatStatusText_CustomFooterText_Test()
325         {
326             this.UsingTweenMain((tweenMain, context) =>
327             {
328                 context.Settings.Local.StatusText = "foo";
329                 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa"));
330             });
331         }
332
333         [WinFormsFact]
334         public void FormatStatusText_DisableFooterIfSendingDM_Test()
335         {
336             this.UsingTweenMain((tweenMain, context) =>
337             {
338                 context.Settings.Local.StatusText = "foo";
339
340                 // DM の場合はフッターを無効化する
341                 Assert.Equal("D opentween aaa", tweenMain.FormatStatusText("D opentween aaa"));
342             });
343         }
344
345         [WinFormsFact]
346         public void FormatStatusText_DisableFooterIfContainsUnofficialRT_Test()
347         {
348             this.UsingTweenMain((tweenMain, context) =>
349             {
350                 context.Settings.Local.StatusText = "foo";
351
352                 // 非公式 RT を含む場合はフッターを無効化する
353                 Assert.Equal("aaa RT @foo: bbb", tweenMain.FormatStatusText("aaa RT @foo: bbb"));
354             });
355         }
356
357         [WinFormsFact]
358         public void FormatStatusText_DisableFooterIfPostByEnterAndPressedShiftKey_Test()
359         {
360             this.UsingTweenMain((tweenMain, context) =>
361             {
362                 context.Settings.Common.PostCtrlEnter = false;
363                 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
364                 context.Settings.Local.StatusText = "foo";
365                 context.Settings.Local.StatusMultiline = false; // 単一行モード
366
367                 // Shift キーが押されている場合はフッターを無効化する
368                 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
369             });
370         }
371
372         [WinFormsFact]
373         public void FormatStatusText_DisableFooterIfPostByEnterAndPressedCtrlKeyAndMultilineMode_Test()
374         {
375             this.UsingTweenMain((tweenMain, context) =>
376             {
377                 context.Settings.Common.PostCtrlEnter = false;
378                 context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定
379                 context.Settings.Local.StatusText = "foo";
380                 context.Settings.Local.StatusMultiline = true; // 複数行モード
381
382                 // Ctrl キーが押されている場合はフッターを無効化する
383                 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
384             });
385         }
386
387         [WinFormsFact]
388         public void FormatStatusText_DisableFooterIfPostByShiftEnterAndPressedControlKey_Test()
389         {
390             this.UsingTweenMain((tweenMain, context) =>
391             {
392                 context.Settings.Common.PostCtrlEnter = false;
393                 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
394                 context.Settings.Local.StatusText = "foo";
395
396                 // Ctrl キーが押されている場合はフッターを無効化する
397                 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
398             });
399         }
400
401         [WinFormsFact]
402         public void FormatStatusText_EnableFooterIfPostByShiftEnter_Test()
403         {
404             this.UsingTweenMain((tweenMain, context) =>
405             {
406                 context.Settings.Common.PostCtrlEnter = false;
407                 context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定
408                 context.Settings.Local.StatusText = "foo";
409
410                 // Shift+Enter で投稿する場合、Ctrl キーが押されていなければフッターを付ける
411                 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
412             });
413         }
414
415         [WinFormsFact]
416         public void FormatStatusText_DisableFooterIfPostByCtrlEnterAndPressedShiftKey_Test()
417         {
418             this.UsingTweenMain((tweenMain, context) =>
419             {
420                 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
421                 context.Settings.Common.PostShiftEnter = false;
422                 context.Settings.Local.StatusText = "foo";
423
424                 // Shift キーが押されている場合はフッターを無効化する
425                 Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift));
426             });
427         }
428
429         [WinFormsFact]
430         public void FormatStatusText_EnableFooterIfPostByCtrlEnter_Test()
431         {
432             this.UsingTweenMain((tweenMain, context) =>
433             {
434                 context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定
435                 context.Settings.Common.PostShiftEnter = false;
436                 context.Settings.Local.StatusText = "foo";
437
438                 // Ctrl+Enter で投稿する場合、Shift キーが押されていなければフッターを付ける
439                 Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control));
440             });
441         }
442
443         [WinFormsFact]
444         public void FormatStatusText_PreventSmsCommand_Test()
445         {
446             this.UsingTweenMain((tweenMain, context) =>
447             {
448                 // 「D+」などから始まる文字列をツイートしようとすると SMS コマンドと誤認されてエラーが返される問題の回避策
449                 Assert.Equal("\u200bd+aaaa", tweenMain.FormatStatusText("d+aaaa"));
450             });
451         }
452
453         [Fact]
454         public void GetUrlFromDataObject_XMozUrlTest()
455         {
456             var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\nTwitter\0");
457             using var memstream = new MemoryStream(dataBytes);
458             var data = new DataObject("text/x-moz-url", memstream);
459
460             var expected = ("https://twitter.com/", "Twitter");
461             Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
462         }
463
464         [Fact]
465         public void GetUrlFromDataObject_IESiteModeToUrlTest()
466         {
467             var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0Twitter\0");
468             using var memstream = new MemoryStream(dataBytes);
469             var data = new DataObject("IESiteModeToUrl", memstream);
470
471             var expected = ("https://twitter.com/", "Twitter");
472             Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
473         }
474
475         [Fact]
476         public void GetUrlFromDataObject_UniformResourceLocatorWTest()
477         {
478             var dataBytes = Encoding.Unicode.GetBytes("https://twitter.com/\0");
479             using var memstream = new MemoryStream(dataBytes);
480             var data = new DataObject("UniformResourceLocatorW", memstream);
481
482             var expected = ("https://twitter.com/", (string?)null);
483             Assert.Equal(expected, TweenMain.GetUrlFromDataObject(data));
484         }
485
486         [Fact]
487         public void GetUrlFromDataObject_UnknownFormatTest()
488         {
489             using var memstream = new MemoryStream(Array.Empty<byte>());
490             var data = new DataObject("application/x-hogehoge", memstream);
491
492             Assert.Throws<NotSupportedException>(() => TweenMain.GetUrlFromDataObject(data));
493         }
494
495         [Fact]
496         public void CreateRetweetUnofficial_UrlTest()
497         {
498             var statusText = """<a href="http://t.co/KYi7vMZzRt" title="http://twitter.com/">twitter.com</a>""";
499
500             Assert.Equal("http://twitter.com/", TweenMain.CreateRetweetUnofficial(statusText, false));
501         }
502
503         [Fact]
504         public void CreateRetweetUnofficial_MentionTest()
505         {
506             var statusText = """<a class="mention" href="https://twitter.com/twitterapi">@TwitterAPI</a>""";
507
508             Assert.Equal("@TwitterAPI", TweenMain.CreateRetweetUnofficial(statusText, false));
509         }
510
511         [Fact]
512         public void CreateRetweetUnofficial_HashtagTest()
513         {
514             var statusText = """<a class="hashtag" href="https://twitter.com/search?q=%23OpenTween">#OpenTween</a>""";
515
516             Assert.Equal("#OpenTween", TweenMain.CreateRetweetUnofficial(statusText, false));
517         }
518
519         [Fact]
520         public void CreateRetweetUnofficial_SingleLineTest()
521         {
522             var statusText = "123<br>456<br>789";
523
524             Assert.Equal("123 456 789", TweenMain.CreateRetweetUnofficial(statusText, false));
525         }
526
527         [Fact]
528         public void CreateRetweetUnofficial_MultiLineTest()
529         {
530             var statusText = "123<br>456<br>789";
531
532             Assert.Equal("123" + Environment.NewLine + "456" + Environment.NewLine + "789", TweenMain.CreateRetweetUnofficial(statusText, true));
533         }
534
535         [Fact]
536         public void CreateRetweetUnofficial_DecodeTest()
537         {
538             var statusText = "&lt;&gt;&quot;&#39;&nbsp;";
539
540             Assert.Equal("<>\"' ", TweenMain.CreateRetweetUnofficial(statusText, false));
541         }
542
543         [Fact]
544         public void CreateRetweetUnofficial_WithFormatterTest()
545         {
546             // TweetFormatterでHTMLに整形 → CreateRetweetUnofficialで復元 までの動作が正しく行えているか
547
548             var text = "#てすと @TwitterAPI \n http://t.co/KYi7vMZzRt";
549             var entities = new TwitterEntity[]
550             {
551                 new TwitterEntityHashtag
552                 {
553                     Indices = new[] { 0, 4 },
554                     Text = "てすと",
555                 },
556                 new TwitterEntityMention
557                 {
558                     Indices = new[] { 5, 16 },
559                     Id = 6253282L,
560                     Name = "Twitter API",
561                     ScreenName = "twitterapi",
562                 },
563                 new TwitterEntityUrl
564                 {
565                     Indices = new[] { 19, 41 },
566                     DisplayUrl = "twitter.com",
567                     ExpandedUrl = "http://twitter.com/",
568                     Url = "http://t.co/KYi7vMZzRt",
569                 },
570             };
571
572             var html = TweetFormatter.AutoLinkHtml(text, entities);
573
574             var expected = "#てすと @TwitterAPI " + Environment.NewLine + " http://twitter.com/";
575             Assert.Equal(expected, TweenMain.CreateRetweetUnofficial(html, true));
576         }
577
578         [Theory]
579         [InlineData("", true)]
580         [InlineData("hoge", false)]
581         [InlineData("@twitterapi ", true)]
582         [InlineData("@twitterapi @opentween ", true)]
583         [InlineData("@twitterapi @opentween hoge", false)]
584         public void TextContainsOnlyMentions_Test(string input, bool expected)
585             => Assert.Equal(expected, TweenMain.TextContainsOnlyMentions(input));
586     }
587 }