// OpenTween - Client of Twitter // Copyright (c) 2013 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. using System; using System.Collections.Generic; using OpenTween.Api.DataModel; using Xunit; namespace OpenTween.Models { public class TwitterPostFactoryTest { private static readonly ISet EmptyIdSet = new HashSet(); private readonly Random random = new(); public TwitterPostFactoryTest() => PostClass.ExpandedUrlInfo.AutoExpand = false; private TabInformations CreateTabinfo() { var tabinfo = new TabInformations(); tabinfo.AddDefaultTabs(); return tabinfo; } private TwitterStatus CreateStatus() { var statusId = this.random.Next(10000); return new() { Id = statusId, IdStr = statusId.ToString(), CreatedAt = "Sat Jan 01 00:00:00 +0000 2022", FullText = "hoge", Source = "OpenTween", Entities = new(), User = this.CreateUser(), }; } private TwitterUser CreateUser() { var userId = this.random.Next(10000); return new() { Id = userId, IdStr = userId.ToString(), ScreenName = "tetete", Name = "ててて", ProfileImageUrlHttps = "https://example.com/profile.png", }; } [Fact] public void CreateFromStatus_Test() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds: EmptyIdSet); Assert.Equal(status.Id, post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); Assert.Equal("hoge", post.Text); Assert.Equal("hoge", post.TextFromApi); Assert.Equal("hoge", post.TextSingleLine); Assert.Equal("hoge", post.AccessibleText); Assert.Empty(post.ReplyToList); Assert.Empty(post.QuoteStatusIds); Assert.Empty(post.ExpandedUrls); Assert.Empty(post.Media); Assert.Null(post.PostGeo); Assert.Equal("OpenTween", post.Source); Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); Assert.Equal(0, post.FavoritedCount); Assert.False(post.IsFav); Assert.False(post.IsDm); Assert.False(post.IsDeleted); Assert.False(post.IsRead); Assert.False(post.IsExcludeReply); Assert.False(post.FilterHit); Assert.False(post.IsMark); Assert.False(post.IsReply); Assert.Null(post.InReplyToStatusId); Assert.Null(post.InReplyToUserId); Assert.Null(post.InReplyToUser); Assert.Null(post.RetweetedId); Assert.Null(post.RetweetedBy); Assert.Null(post.RetweetedByUserId); Assert.Equal(status.User.Id, post.UserId); Assert.Equal("tetete", post.ScreenName); Assert.Equal("ててて", post.Nickname); Assert.Equal("https://example.com/profile.png", post.ImageUrl); Assert.False(post.IsProtect); Assert.False(post.IsOwl); Assert.False(post.IsMe); } [Fact] public void CreateFromStatus_AuthorTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var selfUserId = status.User.Id; var post = factory.CreateFromStatus(status, selfUserId, followerIds: EmptyIdSet); Assert.True(post.IsMe); } [Fact] public void CreateFromStatus_FollowerTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var followerIds = new HashSet { status.User.Id }; var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); Assert.False(post.IsOwl); } [Fact] public void CreateFromStatus_NotFollowerTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); var followerIds = new HashSet { 30000L }; var post = factory.CreateFromStatus(status, selfUserId: 20000L, followerIds); Assert.True(post.IsOwl); } [Fact] public void CreateFromStatus_RetweetTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var originalStatus = this.CreateStatus(); var retweetStatus = this.CreateStatus(); retweetStatus.RetweetedStatus = originalStatus; retweetStatus.Source = "Twitter Web App"; var post = factory.CreateFromStatus(retweetStatus, selfUserId: 20000L, followerIds: EmptyIdSet); Assert.Equal(retweetStatus.Id, post.StatusId); Assert.Equal(retweetStatus.User.Id, post.RetweetedByUserId); Assert.Equal(originalStatus.Id, post.RetweetedId); Assert.Equal(originalStatus.User.Id, post.UserId); Assert.Equal("OpenTween", post.Source); Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); } private TwitterMessageEvent CreateDirectMessage(string senderId, string recipientId) { var messageId = this.random.Next(10000); return new() { Type = "message_create", Id = messageId.ToString(), CreatedTimestamp = "1640995200000", MessageCreate = new() { SenderId = senderId, Target = new() { RecipientId = recipientId, }, MessageData = new() { Text = "hoge", Entities = new(), }, SourceAppId = "22519141", }, }; } private Dictionary CreateApps() { return new() { ["22519141"] = new() { Id = "22519141", Name = "OpenTween", Url = "https://www.opentween.org/", }, }; } [Fact] public void CreateFromDirectMessageEvent_Test() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var selfUser = this.CreateUser(); var otherUser = this.CreateUser(); var eventItem = this.CreateDirectMessage(senderId: otherUser.IdStr, recipientId: selfUser.IdStr); var users = new Dictionary() { [selfUser.IdStr] = selfUser, [otherUser.IdStr] = otherUser, }; var apps = this.CreateApps(); var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); Assert.Equal(long.Parse(eventItem.Id), post.StatusId); Assert.Equal(new DateTimeUtc(2022, 1, 1, 0, 0, 0), post.CreatedAt); Assert.Equal("hoge", post.Text); Assert.Equal("hoge", post.TextFromApi); Assert.Equal("hoge", post.TextSingleLine); Assert.Equal("hoge", post.AccessibleText); Assert.Empty(post.ReplyToList); Assert.Empty(post.QuoteStatusIds); Assert.Empty(post.ExpandedUrls); Assert.Empty(post.Media); Assert.Null(post.PostGeo); Assert.Equal("OpenTween", post.Source); Assert.Equal("https://www.opentween.org/", post.SourceUri?.OriginalString); Assert.Equal(0, post.FavoritedCount); Assert.False(post.IsFav); Assert.True(post.IsDm); Assert.False(post.IsDeleted); Assert.False(post.IsRead); Assert.False(post.IsExcludeReply); Assert.False(post.FilterHit); Assert.False(post.IsMark); Assert.False(post.IsReply); Assert.Null(post.InReplyToStatusId); Assert.Null(post.InReplyToUserId); Assert.Null(post.InReplyToUser); Assert.Null(post.RetweetedId); Assert.Null(post.RetweetedBy); Assert.Null(post.RetweetedByUserId); Assert.Equal(otherUser.Id, post.UserId); Assert.Equal("tetete", post.ScreenName); Assert.Equal("ててて", post.Nickname); Assert.Equal("https://example.com/profile.png", post.ImageUrl); Assert.False(post.IsProtect); Assert.True(post.IsOwl); Assert.False(post.IsMe); } [Fact] public void CreateFromDirectMessageEvent_SenderTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var selfUser = this.CreateUser(); var otherUser = this.CreateUser(); var eventItem = this.CreateDirectMessage(senderId: selfUser.IdStr, recipientId: otherUser.IdStr); var users = new Dictionary() { [selfUser.IdStr] = selfUser, [otherUser.IdStr] = otherUser, }; var apps = this.CreateApps(); var post = factory.CreateFromDirectMessageEvent(eventItem, users, apps, selfUserId: selfUser.Id); Assert.Equal(otherUser.Id, post.UserId); Assert.False(post.IsOwl); Assert.True(post.IsMe); } [Fact] public void CreateFromStatus_MediaAltTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); status.FullText = "https://t.co/hoge"; status.ExtendedEntities = new() { Media = new[] { new TwitterEntityMedia { Indices = new[] { 0, 17 }, Url = "https://t.co/hoge", DisplayUrl = "pic.twitter.com/hoge", ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", AltText = "代替テキスト", }, }, }; var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = string.Format(Properties.Resources.ImageAltText, "代替テキスト"); Assert.Equal(accessibleText, post.AccessibleText); Assert.Equal("pic.twitter.com/hoge", post.Text); Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); } [Fact] public void CreateFromStatus_MediaNoAltTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); status.FullText = "https://t.co/hoge"; status.ExtendedEntities = new() { Media = new[] { new TwitterEntityMedia { Indices = new[] { 0, 17 }, Url = "https://t.co/hoge", DisplayUrl = "pic.twitter.com/hoge", ExpandedUrl = "https://twitter.com/hoge/status/1234567890/photo/1", AltText = null, }, }, }; var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); Assert.Equal("pic.twitter.com/hoge", post.AccessibleText); Assert.Equal("pic.twitter.com/hoge", post.Text); Assert.Equal("pic.twitter.com/hoge", post.TextFromApi); Assert.Equal("pic.twitter.com/hoge", post.TextSingleLine); } [Fact] public void CreateFromStatus_QuotedUrlTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); status.FullText = "https://t.co/hoge"; status.Entities = new() { Urls = new[] { new TwitterEntityUrl { Indices = new[] { 0, 17 }, Url = "https://t.co/hoge", DisplayUrl = "twitter.com/hoge/status/1…", ExpandedUrl = "https://twitter.com/hoge/status/1234567890", }, }, }; status.QuotedStatus = new() { Id = 1234567890L, IdStr = "1234567890", User = new() { Id = 1111, IdStr = "1111", ScreenName = "foo", }, FullText = "test", }; var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); Assert.Equal("twitter.com/hoge/status/1…", post.Text); Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); } [Fact] public void CreateFromStatus_QuotedUrlWithPermelinkTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); status.FullText = "hoge"; status.QuotedStatus = new() { Id = 1234567890L, IdStr = "1234567890", User = new TwitterUser { Id = 1111, IdStr = "1111", ScreenName = "foo", }, FullText = "test", }; status.QuotedStatusPermalink = new() { Url = "https://t.co/hoge", Display = "twitter.com/hoge/status/1…", Expanded = "https://twitter.com/hoge/status/1234567890", }; var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = "hoge " + string.Format(Properties.Resources.QuoteStatus_AccessibleText, "foo", "test"); Assert.Equal(accessibleText, post.AccessibleText); Assert.Equal("hoge twitter.com/hoge/status/1…", post.Text); Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("hoge twitter.com/hoge/status/1…", post.TextSingleLine); } [Fact] public void CreateFromStatus_QuotedUrlNoReferenceTest() { var factory = new TwitterPostFactory(this.CreateTabinfo()); var status = this.CreateStatus(); status.FullText = "https://t.co/hoge"; status.Entities = new() { Urls = new[] { new TwitterEntityUrl { Indices = new[] { 0, 17 }, Url = "https://t.co/hoge", DisplayUrl = "twitter.com/hoge/status/1…", ExpandedUrl = "https://twitter.com/hoge/status/1234567890", }, }, }; status.QuotedStatus = null; var post = factory.CreateFromStatus(status, selfUserId: 100L, followerIds: EmptyIdSet); var accessibleText = "twitter.com/hoge/status/1…"; Assert.Equal(accessibleText, post.AccessibleText); Assert.Equal("twitter.com/hoge/status/1…", post.Text); Assert.Equal("twitter.com/hoge/status/1…", post.TextFromApi); Assert.Equal("twitter.com/hoge/status/1…", post.TextSingleLine); } [Fact] public void CreateHtmlAnchor_Test() { var text = "@twitterapi #BreakingMyTwitter https://t.co/mIJcSoVSK3"; var entities = new TwitterEntities { UserMentions = new[] { new TwitterEntityMention { Indices = new[] { 0, 11 }, ScreenName = "twitterapi" }, }, Hashtags = new[] { new TwitterEntityHashtag { Indices = new[] { 12, 30 }, Text = "BreakingMyTwitter" }, }, Urls = new[] { new TwitterEntityUrl { Indices = new[] { 31, 54 }, Url = "https://t.co/mIJcSoVSK3", DisplayUrl = "apps-of-a-feather.com", ExpandedUrl = "http://apps-of-a-feather.com/", }, }, }; var expectedHtml = @"@twitterapi" + @" #BreakingMyTwitter" + @" apps-of-a-feather.com"; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); } [Fact] public void CreateHtmlAnchor_NicovideoTest() { var text = "sm9"; var entities = new TwitterEntities(); var expectedHtml = @"sm9"; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink: null)); } [Fact] public void CreateHtmlAnchor_QuotedUrlWithPermelinkTest() { var text = "hoge"; var entities = new TwitterEntities(); var quotedStatusLink = new TwitterQuotedStatusPermalink { Url = "https://t.co/hoge", Display = "twitter.com/hoge/status/1…", Expanded = "https://twitter.com/hoge/status/1234567890", }; var expectedHtml = @"hoge" + @" twitter.com/hoge/status/1…"; Assert.Equal(expectedHtml, TwitterPostFactory.CreateHtmlAnchor(text, entities, quotedStatusLink)); } [Fact] public void ParseSource_Test() { var sourceHtml = "Twitter Web Client"; var expected = ("Twitter Web Client", new Uri("http://twitter.com/")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_PlainTextTest() { var sourceHtml = "web"; var expected = ("web", (Uri?)null); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_RelativeUriTest() { // 参照: https://twitter.com/kim_upsilon/status/477796052049752064 var sourceHtml = "erased_45416"; var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_EmptyTest() { // 参照: https://twitter.com/kim_upsilon/status/595156014032244738 var sourceHtml = ""; var expected = ("", (Uri?)null); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_NullTest() { string? sourceHtml = null; var expected = ("", (Uri?)null); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_UnescapeTest() { var sourceHtml = "<<hogehoge>>"; var expected = ("<>", new Uri("http://example.com/?aaa=123&bbb=456")); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void ParseSource_UnescapeNoUriTest() { var sourceHtml = "<<hogehoge>>"; var expected = ("<>", (Uri?)null); Assert.Equal(expected, TwitterPostFactory.ParseSource(sourceHtml)); } [Fact] public void GetQuoteTweetStatusIds_EntityTest() { var entities = new[] { new TwitterEntityUrl { Url = "https://t.co/3HXq0LrbJb", ExpandedUrl = "https://twitter.com/kim_upsilon/status/599261132361072640", }, }; var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink: null); Assert.Equal(new[] { 599261132361072640L }, statusIds); } [Fact] public void GetQuoteTweetStatusIds_QuotedStatusLinkTest() { var entities = new TwitterEntities(); var quotedStatusLink = new TwitterQuotedStatusPermalink { Url = "https://t.co/3HXq0LrbJb", Expanded = "https://twitter.com/kim_upsilon/status/599261132361072640", }; var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(entities, quotedStatusLink); Assert.Equal(new[] { 599261132361072640L }, statusIds); } [Fact] public void GetQuoteTweetStatusIds_UrlStringTest() { var urls = new[] { "https://twitter.com/kim_upsilon/status/599261132361072640", }; var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls); Assert.Equal(new[] { 599261132361072640L }, statusIds); } [Fact] public void GetQuoteTweetStatusIds_OverflowTest() { var urls = new[] { // 符号付き 64 ビット整数の範囲を超える値 "https://twitter.com/kim_upsilon/status/9999999999999999999", }; var statusIds = TwitterPostFactory.GetQuoteTweetStatusIds(urls); Assert.Empty(statusIds); } } }