OSDN Git Service

ツイートに含まれる短縮URLの展開を非同期に行い、ツイートの受信を遅延させないようにする
[opentween/open-tween.git] / OpenTween.Tests / PostClassTest.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2012 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.Linq;
25 using System.Reflection;
26 using System.Text;
27 using System.Threading.Tasks;
28 using Xunit;
29 using Xunit.Extensions;
30
31 namespace OpenTween
32 {
33     public class PostClassTest
34     {
35         class TestPostClass : PostClass
36         {
37             protected override PostClass RetweetSource
38             {
39                 get
40                 {
41                     var retweetedId = this.RetweetedId.Value;
42
43                     return PostClassTest.TestCases.ContainsKey(retweetedId) ?
44                         PostClassTest.TestCases[retweetedId] :
45                         null;
46                 }
47             }
48         }
49
50         private static Dictionary<long, PostClass> TestCases;
51
52         public PostClassTest()
53         {
54             PostClassTest.TestCases = new Dictionary<long, PostClass>
55             {
56                 [1L] = new TestPostClass { StatusId = 1L },
57                 [2L] = new TestPostClass { StatusId = 2L, IsFav = true },
58                 [3L] = new TestPostClass { StatusId = 3L, IsFav = false, RetweetedId = 2L },
59             };
60         }
61
62         [Fact]
63         public void CloneTest()
64         {
65             var post = new PostClass();
66             var clonePost = post.Clone();
67
68             TestUtils.CheckDeepCloning(post, clonePost);
69         }
70
71         [Theory]
72         [InlineData(null,  null)]
73         [InlineData("", "")]
74         [InlineData("aaa\nbbb", "aaa bbb")]
75         public void TextSingleLineTest(string text, string expected)
76         {
77             var post = new PostClass { TextFromApi = text };
78
79             Assert.Equal(expected, post.TextSingleLine);
80         }
81
82         [Theory]
83         [InlineData(1L, false)]
84         [InlineData(2L, true)]
85         [InlineData(3L, true)]
86         public void GetIsFavTest(long statusId, bool expected)
87         {
88             Assert.Equal(expected, PostClassTest.TestCases[statusId].IsFav);
89         }
90
91         [Theory]
92         [InlineData(2L, true)]
93         [InlineData(2L, false)]
94         [InlineData(3L, true)]
95         [InlineData(3L, false)]
96         public void SetIsFavTest(long statusId, bool isFav)
97         {
98             var post = PostClassTest.TestCases[statusId];
99
100             post.IsFav = isFav;
101             Assert.Equal(isFav, post.IsFav);
102
103             if (post.RetweetedId != null)
104                 Assert.Equal(isFav, PostClassTest.TestCases[post.RetweetedId.Value].IsFav);
105         }
106
107         [Theory]
108         [InlineData(false, false, false, false, -0x01)]
109         [InlineData( true, false, false, false, 0x00)]
110         [InlineData(false,  true, false, false, 0x01)]
111         [InlineData( true,  true, false, false, 0x02)]
112         [InlineData(false, false,  true, false, 0x03)]
113         [InlineData( true, false,  true, false, 0x04)]
114         [InlineData(false,  true,  true, false, 0x05)]
115         [InlineData( true,  true,  true, false, 0x06)]
116         [InlineData(false, false, false,  true, 0x07)]
117         [InlineData( true, false, false,  true, 0x08)]
118         [InlineData(false,  true, false,  true, 0x09)]
119         [InlineData( true,  true, false,  true, 0x0A)]
120         [InlineData(false, false,  true,  true, 0x0B)]
121         [InlineData( true, false,  true,  true, 0x0C)]
122         [InlineData(false,  true,  true,  true, 0x0D)]
123         [InlineData( true,  true,  true,  true, 0x0E)]
124         public void StateIndexTest(bool protect, bool mark, bool reply, bool geo, int expected)
125         {
126             var post = new TestPostClass
127             {
128                 IsProtect = protect,
129                 IsMark = mark,
130                 InReplyToStatusId = reply ? (long?)100L : null,
131                 PostGeo = geo ? new PostClass.StatusGeo(-126.716667, -47.15) : (PostClass.StatusGeo?)null,
132             };
133
134             Assert.Equal(expected, post.StateIndex);
135         }
136
137         [Fact]
138         public void SourceHtml_Test()
139         {
140             var post = new TestPostClass
141             {
142                 Source = "Twitter Web Client",
143                 SourceUri = new Uri("http://twitter.com/"),
144             };
145
146             Assert.Equal("<a href=\"http://twitter.com/\" rel=\"nofollow\">Twitter Web Client</a>", post.SourceHtml);
147         }
148
149         [Fact]
150         public void SourceHtml_PlainTextTest()
151         {
152             var post = new TestPostClass
153             {
154                 Source = "web",
155                 SourceUri = null,
156             };
157
158             Assert.Equal("web", post.SourceHtml);
159         }
160
161         [Fact]
162         public void SourceHtml_EscapeTest()
163         {
164             var post = new TestPostClass
165             {
166                 Source = "<script>alert(1)</script>",
167                 SourceUri = new Uri("http://example.com/?aaa=123&bbb=456"),
168             };
169
170             Assert.Equal("<a href=\"http://example.com/?aaa=123&amp;bbb=456\" rel=\"nofollow\">&lt;script&gt;alert(1)&lt;/script&gt;</a>", post.SourceHtml);
171         }
172
173         [Fact]
174         public void SourceHtml_EscapePlainTextTest()
175         {
176             var post = new TestPostClass
177             {
178                 Source = "<script>alert(1)</script>",
179                 SourceUri = null,
180             };
181
182             Assert.Equal("&lt;script&gt;alert(1)&lt;/script&gt;", post.SourceHtml);
183         }
184
185         [Fact]
186         public void DeleteTest()
187         {
188             var post = new TestPostClass
189             {
190                 InReplyToStatusId = 10L,
191                 InReplyToUser = "hogehoge",
192                 InReplyToUserId = 100L,
193                 IsReply = true,
194                 ReplyToList = new List<string> {"hogehoge"},
195             };
196
197             post.IsDeleted = true;
198
199             Assert.Null(post.InReplyToStatusId);
200             Assert.Equal("", post.InReplyToUser);
201             Assert.Null(post.InReplyToUserId);
202             Assert.False(post.IsReply);
203             Assert.Empty(post.ReplyToList);
204             Assert.Equal(-1, post.StateIndex);
205         }
206
207         [Fact]
208         public void CanDeleteBy_SentDMTest()
209         {
210             var post = new TestPostClass
211             {
212                 IsDm = true,
213                 IsMe = true, // 自分が送信した DM
214                 UserId = 222L, // 送信先ユーザーID
215             };
216
217             Assert.True(post.CanDeleteBy(selfUserId: 111L));
218         }
219
220         [Fact]
221         public void CanDeleteBy_ReceivedDMTest()
222         {
223             var post = new TestPostClass
224             {
225                 IsDm = true,
226                 IsMe = false, // 自分が受け取った DM
227                 UserId = 222L, // 送信元ユーザーID
228             };
229
230             Assert.True(post.CanDeleteBy(selfUserId: 111L));
231         }
232
233         [Fact]
234         public void CanDeleteBy_MyTweetTest()
235         {
236             var post = new TestPostClass
237             {
238                 UserId = 111L, // 自分のツイート
239             };
240
241             Assert.True(post.CanDeleteBy(selfUserId: 111L));
242         }
243
244         [Fact]
245         public void CanDeleteBy_OthersTweetTest()
246         {
247             var post = new TestPostClass
248             {
249                 UserId = 222L, // 他人のツイート
250             };
251
252             Assert.False(post.CanDeleteBy(selfUserId: 111L));
253         }
254
255         [Fact]
256         public void CanDeleteBy_RetweetedByMeTest()
257         {
258             var post = new TestPostClass
259             {
260                 RetweetedByUserId = 111L, // 自分がリツイートした
261                 UserId = 222L, // 他人のツイート
262             };
263
264             Assert.True(post.CanDeleteBy(selfUserId: 111L));
265         }
266
267         [Fact]
268         public void CanDeleteBy_RetweetedByOthersTest()
269         {
270             var post = new TestPostClass
271             {
272                 RetweetedByUserId = 333L, // 他人がリツイートした
273                 UserId = 222L, // 他人のツイート
274             };
275
276             Assert.False(post.CanDeleteBy(selfUserId: 111L));
277         }
278
279         [Fact]
280         public void CanDeleteBy_MyTweetHaveBeenRetweetedByOthersTest()
281         {
282             var post = new TestPostClass
283             {
284                 RetweetedByUserId = 222L, // 他人がリツイートした
285                 UserId = 111L, // 自分のツイート
286             };
287
288             Assert.True(post.CanDeleteBy(selfUserId: 111L));
289         }
290
291         [Fact]
292         public void ConvertToOriginalPost_Test()
293         {
294             var retweetPost = new PostClass
295             {
296                 StatusId = 100L,
297                 ScreenName = "@aaa",
298                 UserId = 1L,
299
300                 RetweetedId = 50L,
301                 RetweetedBy = "@bbb",
302                 RetweetedByUserId = 2L,
303                 RetweetedCount = 0,
304             };
305
306             var originalPost = retweetPost.ConvertToOriginalPost();
307
308             Assert.Equal(50L, originalPost.StatusId);
309             Assert.Equal("@aaa", originalPost.ScreenName);
310             Assert.Equal(1L, originalPost.UserId);
311
312             Assert.Equal(null, originalPost.RetweetedId);
313             Assert.Equal("", originalPost.RetweetedBy);
314             Assert.Equal(null, originalPost.RetweetedByUserId);
315             Assert.Equal(1, originalPost.RetweetedCount);
316         }
317
318         [Fact]
319         public void ConvertToOriginalPost_ErrorTest()
320         {
321             // 公式 RT でないツイート
322             var post = new PostClass { StatusId = 100L, RetweetedId = null };
323
324             Assert.Throws<InvalidOperationException>(() => post.ConvertToOriginalPost());
325         }
326
327         class FakeExpandedUrlInfo : PostClass.ExpandedUrlInfo
328         {
329             public TaskCompletionSource<string> fakeResult;
330
331             public FakeExpandedUrlInfo(string url, string expandedUrl, bool deepExpand)
332                 : base(url, expandedUrl, deepExpand)
333             {
334             }
335
336             protected override async Task DeepExpandAsync()
337             {
338                 this.fakeResult = new TaskCompletionSource<string>();
339                 this._expandedUrl = await this.fakeResult.Task;
340             }
341         }
342
343         [Fact]
344         public async Task ExpandedUrls_BasicScenario()
345         {
346             var post = new PostClass
347             {
348                 Text = "<a href=\"http://t.co/aaaaaaa\" title=\"http://t.co/aaaaaaa\">bit.ly/abcde</a>",
349                 ExpandedUrls = new Dictionary<string, PostClass.ExpandedUrlInfo>
350                 {
351                     ["http://t.co/aaaaaaa"] = new FakeExpandedUrlInfo(
352                         // 展開前の t.co ドメインの URL
353                         url:  "http://t.co/aaaaaaa",
354
355                         // Entity の expanded_url に含まれる URL
356                         expandedUrl: "http://bit.ly/abcde",
357
358                         // expandedUrl をさらに ShortUrl クラスで再帰的に展開する
359                         deepExpand: true
360                     ),
361                 },
362             };
363
364             var urlInfo = (FakeExpandedUrlInfo)post.ExpandedUrls.Values.Single();
365
366             // ExpandedUrlInfo による展開が完了していない状態
367             //   → この段階では Entity に含まれる expanded_url の URL が使用される
368             Assert.False(urlInfo.ExpandedCompleted);
369             Assert.Equal("http://bit.ly/abcde", urlInfo.ExpandedUrl);
370             Assert.Equal("http://bit.ly/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa"));
371             Assert.Equal(new[] { "http://bit.ly/abcde" }, post.GetExpandedUrls());
372             Assert.Equal("<a href=\"http://t.co/aaaaaaa\" title=\"http://bit.ly/abcde\">bit.ly/abcde</a>", post.Text);
373
374             // bit.ly 展開後の URL は「http://example.com/abcde」
375             urlInfo.fakeResult.SetResult("http://example.com/abcde");
376             await urlInfo.ExpandTask;
377
378             // ExpandedUrlInfo による展開が完了した後の状態
379             //   → 再帰的な展開後の URL が使用される
380             Assert.True(urlInfo.ExpandedCompleted);
381             Assert.Equal("http://example.com/abcde", urlInfo.ExpandedUrl);
382             Assert.Equal("http://example.com/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa"));
383             Assert.Equal(new[] { "http://example.com/abcde" }, post.GetExpandedUrls());
384             Assert.Equal("<a href=\"http://t.co/aaaaaaa\" title=\"http://example.com/abcde\">bit.ly/abcde</a>", post.Text);
385         }
386     }
387 }