1 // OpenTween - Client of Twitter
2 // Copyright (c) 2013 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;
24 using OpenTween.Api.DataModel;
27 namespace OpenTween.Models
29 public class TwitterPostFactoryTest
31 private static readonly ISet<long> EmptyIdSet = new HashSet<long>();
33 private readonly Random random = new();
35 public TwitterPostFactoryTest()
36 => PostClass.ExpandedUrlInfo.AutoExpand = false;
38 private TabInformations CreateTabinfo()
40 var tabinfo = new TabInformations();
41 tabinfo.AddDefaultTabs();
45 private TwitterStatus CreateStatus()
47 var statusId = this.random.Next(10000);
52 IdStr = statusId.ToString(),
53 CreatedAt = "Sat Jan 01 00:00:00 +0000 2022",
55 Source = "<a href=\"https://www.opentween.org/\" rel=\"nofollow\">OpenTween</a>",
57 User = this.CreateUser(),
61 private TwitterUser CreateUser()
63 var userId = this.random.Next(10000);
68 IdStr = userId.ToString(),
69 ScreenName = "tetete",
71 ProfileImageUrlHttps = "https://example.com/profile.png",
76 public void CreateFromStatus_Test()
78 var factory = new TwitterPostFactory(this.CreateTabinfo());
79 var status = this.CreateStatus();
80 var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet);
82 Assert.Equal(status.Id, post.StatusId);
83 Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt);
84 Assert.Equal("hoge", post.Text);
85 Assert.Equal("hoge", post.TextFromApi);
86 Assert.Equal("hoge", post.TextSingleLine);
87 Assert.Equal("hoge", post.AccessibleText);
88 Assert.Empty(post.ReplyToList);
89 Assert.Empty(post.QuoteStatusIds);
90 Assert.Empty(post.ExpandedUrls);
91 Assert.Empty(post.Media);
92 Assert.Null(post.PostGeo);
93 Assert.Equal("OpenTween", post.Source);
94 Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
95 Assert.Equal(0, post.FavoritedCount);
96 Assert.False(post.IsFav);
97 Assert.False(post.IsDm);
98 Assert.False(post.IsDeleted);
99 Assert.False(post.IsRead);
100 Assert.False(post.IsExcludeReply);
101 Assert.False(post.FilterHit);
102 Assert.False(post.IsMark);
104 Assert.False(post.IsReply);
105 Assert.Null(post.InReplyToStatusId);
106 Assert.Null(post.InReplyToUserId);
107 Assert.Null(post.InReplyToUser);
109 Assert.Null(post.RetweetedId);
110 Assert.Null(post.RetweetedBy);
111 Assert.Null(post.RetweetedByUserId);
113 Assert.Equal(status.User.Id, post.UserId);
114 Assert.Equal("tetete", post.ScreenName);
115 Assert.Equal("ててて", post.Nickname);
116 Assert.Equal("https://example.com/profile.png", post.ImageUrl);
117 Assert.False(post.IsProtect);
118 Assert.False(post.IsOwl);
119 Assert.False(post.IsMe);
123 public void CreateFromStatus_AuthorTest()
125 var factory = new TwitterPostFactory(this.CreateTabinfo());
126 var status = this.CreateStatus();
127 var selfUserId = status.User.Id;
128 var post = factory.CreateFromStatus(status, selfUserId, followerIds: EmptyIdSet);
130 Assert.True(post.IsMe);
134 public void CreateFromStatus_FollowerTest()
136 var factory = new TwitterPostFactory(this.CreateTabinfo());
137 var status = this.CreateStatus();
138 var followerIds = new HashSet<long> { status.User.Id };
139 var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds);
141 Assert.False(post.IsOwl);
145 public void CreateFromStatus_NotFollowerTest()
147 var factory = new TwitterPostFactory(this.CreateTabinfo());
148 var status = this.CreateStatus();
149 var followerIds = new HashSet<long> { 30000L };
150 var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds);
152 Assert.True(post.IsOwl);
156 public void CreateFromStatus_RetweetTest()
158 var factory = new TwitterPostFactory(this.CreateTabinfo());
159 var originalStatus = this.CreateStatus();
161 var retweetStatus = this.CreateStatus();
162 retweetStatus.RetweetedStatus = originalStatus;
163 retweetStatus.Source = "<a href=\"https://mobile.twitter.com\" rel=\"nofollow\">Twitter Web App</a>";
165 var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet);
167 Assert.Equal(retweetStatus.Id, post.StatusId);
168 Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId);
169 Assert.Equal(originalStatus.Id, post.RetweetedId);
170 Assert.Equal(originalStatus.User.Id, post.UserId);
172 Assert.Equal("OpenTween", post.Source);
173 Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
176 private TwitterMessageEvent CreateDirectMessage(string senderId, string recipientId)
178 var messageId = this.random.Next(10000);
182 Type = "message_create",
183 Id = messageId.ToString(),
184 CreatedTimestamp = "1640995200000",
185 MessageCreate = new()
190 RecipientId = recipientId,
197 SourceAppId = "22519141",
202 private Dictionary<string, TwitterMessageEventList.App> CreateApps()
210 Url = "https://www.opentween.org/",
216 public void CreateFromDirectMessageEvent_Test()
218 var factory = new TwitterPostFactory(this.CreateTabinfo());
220 var selfUser = this.CreateUser();
221 var otherUser = this.CreateUser();
222 var eventItem = this.CreateDirectMessage(senderId: otherUser.IdStr, recipientId: selfUser.IdStr);
223 var users = new Dictionary<string, TwitterUser>()
225 [selfUser.IdStr] = selfUser,
226 [otherUser.IdStr] = otherUser,
228 var apps = this.CreateApps();
229 var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id);
231 Assert.Equal(long.Parse(eventItem.Id), post.StatusId);
232 Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt);
233 Assert.Equal("hoge", post.Text);
234 Assert.Equal("hoge", post.TextFromApi);
235 Assert.Equal("hoge", post.TextSingleLine);
236 Assert.Equal("hoge", post.AccessibleText);
237 Assert.Empty(post.ReplyToList);
238 Assert.Empty(post.QuoteStatusIds);
239 Assert.Empty(post.ExpandedUrls);
240 Assert.Empty(post.Media);
241 Assert.Null(post.PostGeo);
242 Assert.Equal("OpenTween", post.Source);
243 Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString);
244 Assert.Equal(0, post.FavoritedCount);
245 Assert.False(post.IsFav);
246 Assert.True(post.IsDm);
247 Assert.False(post.IsDeleted);
248 Assert.False(post.IsRead);
249 Assert.False(post.IsExcludeReply);
250 Assert.False(post.FilterHit);
251 Assert.False(post.IsMark);
253 Assert.False(post.IsReply);
254 Assert.Null(post.InReplyToStatusId);
255 Assert.Null(post.InReplyToUserId);
256 Assert.Null(post.InReplyToUser);
258 Assert.Null(post.RetweetedId);
259 Assert.Null(post.RetweetedBy);
260 Assert.Null(post.RetweetedByUserId);
262 Assert.Equal(otherUser.Id, post.UserId);
263 Assert.Equal("tetete", post.ScreenName);
264 Assert.Equal("ててて", post.Nickname);
265 Assert.Equal("https://example.com/profile.png", post.ImageUrl);
266 Assert.False(post.IsProtect);
267 Assert.True(post.IsOwl);
268 Assert.False(post.IsMe);
272 public void CreateFromDirectMessageEvent_SenderTest()
274 var factory = new TwitterPostFactory(this.CreateTabinfo());
276 var selfUser = this.CreateUser();
277 var otherUser = this.CreateUser();
278 var eventItem = this.CreateDirectMessage(senderId: selfUser.IdStr, recipientId: otherUser.IdStr);
279 var users = new Dictionary<string, TwitterUser>()
281 [selfUser.IdStr] = selfUser,
282 [otherUser.IdStr] = otherUser,
284 var apps = this.CreateApps();
285 var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id);
287 Assert.Equal(otherUser.Id, post.UserId);
288 Assert.False(post.IsOwl);
289 Assert.True(post.IsMe);
293 public void CreateFromStatus_MediaAltTest()
295 var factory = new TwitterPostFactory(this.CreateTabinfo());
297 var status = this.CreateStatus();
298 status.FullText = "https://t.co/hoge";
299 status.ExtendedEntities = new()
303 new TwitterEntityMedia
305 Indices = new[] { 0, 17 },
306 Url = "https://t.co/hoge",
307 DisplayUrl = "pic.twitter.com/hoge",
308 ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
314 var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
316 var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト");
317 Assert.Equal(accessibleText, post.AccessibleText);
318 Assert.Equal("<a href=\"https://t.co/hoge\" title=\"代替テキスト\">pic.twitter.com/hoge</a>", post.Text);
319 Assert.Equal("pic.twitter.com/hoge", post.TextFromApi);
320 Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine);
324 public void CreateFromStatus_MediaNoAltTest()
326 var factory = new TwitterPostFactory(this.CreateTabinfo());
328 var status = this.CreateStatus();
329 status.FullText = "https://t.co/hoge";
330 status.ExtendedEntities = new()
334 new TwitterEntityMedia
336 Indices = new[] { 0, 17 },
337 Url = "https://t.co/hoge",
338 DisplayUrl = "pic.twitter.com/hoge",
339 ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1",
345 var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
347 Assert.Equal("pic.twitter.com/hoge", post.AccessibleText);
348 Assert.Equal("<a href=\"https://t.co/hoge\" title=\"https://twitter.com/hoge/status/1234567890/photo/1\">pic.twitter.com/hoge</a>", post.Text);
349 Assert.Equal("pic.twitter.com/hoge", post.TextFromApi);
350 Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine);
354 public void CreateFromStatus_QuotedUrlTest()
356 var factory = new TwitterPostFactory(this.CreateTabinfo());
358 var status = this.CreateStatus();
359 status.FullText = "https://t.co/hoge";
360 status.Entities = new()
366 Indices = new[] { 0, 17 },
367 Url = "https://t.co/hoge",
368 DisplayUrl = "twitter.com/hoge/status/1…",
369 ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
373 status.QuotedStatus = new()
376 IdStr = "1234567890",
386 var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
388 var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
389 Assert.Equal(accessibleText, post.AccessibleText);
390 Assert.Equal("<a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
391 Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi);
392 Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine);
396 public void CreateFromStatus_QuotedUrlWithPermelinkTest()
398 var factory = new TwitterPostFactory(this.CreateTabinfo());
400 var status = this.CreateStatus();
401 status.FullText = "hoge";
402 status.QuotedStatus = new()
405 IdStr = "1234567890",
406 User = new TwitterUser
414 status.QuotedStatusPermalink = new()
416 Url = "https://t.co/hoge",
417 Display = "twitter.com/hoge/status/1…",
418 Expanded = "https://twitter.com/hoge/status/1234567890",
421 var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
423 var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test");
424 Assert.Equal(accessibleText, post.AccessibleText);
425 Assert.Equal("hoge <a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
426 Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextFromApi);
427 Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextSingleLine);
431 public void CreateFromStatus_QuotedUrlNoReferenceTest()
433 var factory = new TwitterPostFactory(this.CreateTabinfo());
435 var status = this.CreateStatus();
436 status.FullText = "https://t.co/hoge";
437 status.Entities = new()
443 Indices = new[] { 0, 17 },
444 Url = "https://t.co/hoge",
445 DisplayUrl = "twitter.com/hoge/status/1…",
446 ExpandedUrl = "https://twitter.com/hoge/status/1234567890",
450 status.QuotedStatus = null;
452 var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet);
454 var accessibleText = "twitter.com/hoge/status/1…";
455 Assert.Equal(accessibleText, post.AccessibleText);
456 Assert.Equal("<a href=\"https://twitter.com/hoge/status/1234567890\" title=\"https://twitter.com/hoge/status/1234567890\">twitter.com/hoge/status/1…</a>", post.Text);
457 Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi);
458 Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine);
462 public void CreateHtmlAnchor_Test()
464 var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3";
465 var entities = new TwitterEntities
469 new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" },
473 new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" },
479 Indices = new[] { 31, 54 },
480 Url = "https://t.co/mIJcSoVSK3",
481 DisplayUrl = "apps-of-a-feather.com",
482 ExpandedUrl = "http://apps-of-a-feather.com/",
487 var expectedHtml = @"<a class=""mention"" href=""https://twitter.com/twitterapi"">@twitterapi</a>"
488 + @" <a class=""hashtag"" href=""https://twitter.com/search?q=%23BreakingMyTwitter"">#BreakingMyTwitter</a>"
489 + @" <a href=""https://t.co/mIJcSoVSK3"" title=""https://t.co/mIJcSoVSK3"">apps-of-a-feather.com</a>";
491 Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
495 public void CreateHtmlAnchor_NicovideoTest()
498 var entities = new TwitterEntities();
500 var expectedHtml = @"<a href=""https://www.nicovideo.jp/watch/sm9"">sm9</a>";
502 Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null));
506 public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest()
509 var entities = new TwitterEntities();
510 var quotedStatusLink = new TwitterQuotedStatusPermalink
512 Url = "https://t.co/hoge",
513 Display = "twitter.com/hoge/status/1…",
514 Expanded = "https://twitter.com/hoge/status/1234567890",
517 var expectedHtml = @"hoge"
518 + @" <a href=""https://twitter.com/hoge/status/1234567890"" title=""https://twitter.com/hoge/status/1234567890"">twitter.com/hoge/status/1…</a>";
520 Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink));
524 public void ParseSource_Test()
526 var sourceHtml = "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>";
528 var expected = ("Twitter Web Client", new Uri("http://twitter.com/"));
529 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
533 public void ParseSource_PlainTextTest()
535 var sourceHtml = "web";
537 var expected = ("web", (Uri?)null);
538 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
542 public void ParseSource_RelativeUriTest()
544 // 参照: https://twitter.com/kim_upsilon/status/477796052049752064
545 var sourceHtml = "<a href=\"erased_45416\" rel=\"nofollow\">erased_45416</a>";
547 var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416"));
548 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
552 public void ParseSource_EmptyTest()
554 // 参照: https://twitter.com/kim_upsilon/status/595156014032244738
557 var expected = ("", (Uri?)null);
558 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
562 public void ParseSource_NullTest()
564 string? sourceHtml = null;
566 var expected = ("", (Uri?)null);
567 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
571 public void ParseSource_UnescapeTest()
573 var sourceHtml = "<a href=\"http://example.com/?aaa=123&bbb=456\" rel=\"nofollow\"><<hogehoge>></a>";
575 var expected = ("<<hogehoge>>", new Uri("http://example.com/?aaa=123&bbb=456"));
576 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
580 public void ParseSource_UnescapeNoUriTest()
582 var sourceHtml = "<<hogehoge>>";
584 var expected = ("<<hogehoge>>", (Uri?)null);
585 Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml));
589 public void GetQuoteTweetStatusIds_EntityTest()
595 Url = "https://t.co/3HXq0LrbJb",
596 ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640",
600 var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink: null);
601 Assert.Equal(new[] { 599261132361072640L }, statusIds);
605 public void GetQuoteTweetStatusIds_QuotedStatusLinkTest()
607 var entities = new TwitterEntities();
608 var quotedStatusLink = new TwitterQuotedStatusPermalink
610 Url = "https://t.co/3HXq0LrbJb",
611 Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640",
614 var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink);
615 Assert.Equal(new[] { 599261132361072640L }, statusIds);
619 public void GetQuoteTweetStatusIds_UrlStringTest()
623 "https://twitter.com/kim_upsilon/status/599261132361072640",
626 var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls);
627 Assert.Equal(new[] { 599261132361072640L }, statusIds);
631 public void GetQuoteTweetStatusIds_OverflowTest()
635 // 符号付き 64 ビット整数の範囲を超える値
636 "https://twitter.com/kim_upsilon/status/9999999999999999999",
639 var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls);
640 Assert.Empty(statusIds);