OSDN Git Service

Merge pull request #307 from opentween/detect-rate-limits
[opentween/open-tween.git] / OpenTween.Tests / TwitterTest.cs
index 3455131..89d6b78 100644 (file)
@@ -24,6 +24,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Text.RegularExpressions;
+using OpenTween.Api;
 using OpenTween.Api.DataModel;
 using OpenTween.Models;
 using OpenTween.Setting;
@@ -37,10 +38,10 @@ namespace OpenTween
         [Theory]
         [InlineData("https://twitter.com/twitterapi/status/22634515958",
             new[] { "22634515958" })]
-        [InlineData("<a target=\"_self\" href=\"https://t.co/aaaaaaaa\" title=\"https://twitter.com/twitterapi/status/22634515958\">twitter.com/twitterapi/stat…</a>",
+        [InlineData("""<a target="_self" href="https://t.co/aaaaaaaa" title="https://twitter.com/twitterapi/status/22634515958">twitter.com/twitterapi/stat…</a>""",
             new[] { "22634515958" })]
-        [InlineData("<a target=\"_self\" href=\"https://t.co/bU3oR95KIy\" title=\"https://twitter.com/haru067/status/224782458816692224\">https://t.co/bU3oR95KIy</a>" +
-            "<a target=\"_self\" href=\"https://t.co/bbbbbbbb\" title=\"https://twitter.com/karno/status/311081657790771200\">https://t.co/bbbbbbbb</a>",
+        [InlineData("""<a target="_self" href="https://t.co/bU3oR95KIy" title="https://twitter.com/haru067/status/224782458816692224">https://t.co/bU3oR95KIy</a>""" +
+            """<a target="_self" href="https://t.co/bbbbbbbb" title="https://twitter.com/karno/status/311081657790771200">https://t.co/bbbbbbbb</a>""",
             new[] { "224782458816692224", "311081657790771200" })]
         [InlineData("https://mobile.twitter.com/muji_net/status/21984934471",
             new[] { "21984934471" })]
@@ -66,9 +67,7 @@ namespace OpenTween
         [InlineData("http://twitter.com/messages/compose?recipient_id=514241801", true)]
         [InlineData("https://twitter.com/messages/compose?recipient_id=514241801&text=%E3%81%BB%E3%81%92", true)]
         public void AttachmentUrlRegexTest(string url, bool isMatch)
-        {
-            Assert.Equal(isMatch, Twitter.AttachmentUrlRegex.IsMatch(url));
-        }
+            => Assert.Equal(isMatch, Twitter.AttachmentUrlRegex.IsMatch(url));
 
         [Theory]
         [InlineData("http://favstar.fm/users/twitterapi/status/22634515958", new[] { "22634515958" })]
@@ -87,132 +86,26 @@ namespace OpenTween
         [Fact]
         public void FindTopOfReplyChainTest()
         {
-            var posts = new Dictionary<long, PostClass>
-            {
-                [950L] = new PostClass { StatusId = 950L, InReplyToStatusId = null }, // このツイートが末端
-                [987L] = new PostClass { StatusId = 987L, InReplyToStatusId = 950L },
-                [999L] = new PostClass { StatusId = 999L, InReplyToStatusId = 987L },
-                [1000L] = new PostClass { StatusId = 1000L, InReplyToStatusId = 999L },
-            };
-            Assert.Equal(950L, Twitter.FindTopOfReplyChain(posts, 1000L).StatusId);
-            Assert.Equal(950L, Twitter.FindTopOfReplyChain(posts, 950L).StatusId);
-            Assert.Throws<ArgumentException>(() => Twitter.FindTopOfReplyChain(posts, 500L));
-
-            posts = new Dictionary<long, PostClass>
-            {
-                // 1200L は posts の中に存在しない
-                [1210L] = new PostClass { StatusId = 1210L, InReplyToStatusId = 1200L },
-                [1220L] = new PostClass { StatusId = 1220L, InReplyToStatusId = 1210L },
-                [1230L] = new PostClass { StatusId = 1230L, InReplyToStatusId = 1220L },
-            };
-            Assert.Equal(1210L, Twitter.FindTopOfReplyChain(posts, 1230L).StatusId);
-            Assert.Equal(1210L, Twitter.FindTopOfReplyChain(posts, 1210L).StatusId);
-        }
-
-        [Fact]
-        public void ParseSource_Test()
-        {
-            var sourceHtml = "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>";
-
-            var expected = ("Twitter Web Client", new Uri("http://twitter.com/"));
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_PlainTextTest()
-        {
-            var sourceHtml = "web";
-
-            var expected = ("web", (Uri)null);
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_RelativeUriTest()
-        {
-            // 参照: https://twitter.com/kim_upsilon/status/477796052049752064
-            var sourceHtml = "<a href=\"erased_45416\" rel=\"nofollow\">erased_45416</a>";
-
-            var expected = ("erased_45416", new Uri("https://twitter.com/erased_45416"));
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_EmptyTest()
-        {
-            // 参照: https://twitter.com/kim_upsilon/status/595156014032244738
-            var sourceHtml = "";
-
-            var expected = ("", (Uri)null);
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_NullTest()
-        {
-            string sourceHtml = null;
-
-            var expected = ("", (Uri)null);
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_UnescapeTest()
-        {
-            string sourceHtml = "<a href=\"http://example.com/?aaa=123&amp;bbb=456\" rel=\"nofollow\">&lt;&lt;hogehoge&gt;&gt;</a>";
-
-            var expected = ("<<hogehoge>>", new Uri("http://example.com/?aaa=123&bbb=456"));
-            Assert.Equal(expected, Twitter.ParseSource(sourceHtml));
-        }
-
-        [Fact]
-        public void ParseSource_UnescapeNoUriTest()
-        {
-            string sourceHtml = "&lt;&lt;hogehoge&gt;&gt;";
-
-            var expected = ("<<hogehoge>>", (Uri)null);
-            Assert.Equal(expected, Twitter.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 = Twitter.GetQuoteTweetStatusIds(entities);
-            Assert.Equal(new[] { 599261132361072640L }, statusIds);
-        }
-
-        [Fact]
-        public void GetQuoteTweetStatusIds_UrlStringTest()
-        {
-            var urls = new[]
+            var posts = new Dictionary<PostId, PostClass>
             {
-                "https://twitter.com/kim_upsilon/status/599261132361072640",
+                [new TwitterStatusId("950")] = new PostClass { StatusId = new TwitterStatusId("950"), InReplyToStatusId = null }, // このツイートが末端
+                [new TwitterStatusId("987")] = new PostClass { StatusId = new TwitterStatusId("987"), InReplyToStatusId = new TwitterStatusId("950") },
+                [new TwitterStatusId("999")] = new PostClass { StatusId = new TwitterStatusId("999"), InReplyToStatusId = new TwitterStatusId("987") },
+                [new TwitterStatusId("1000")] = new PostClass { StatusId = new TwitterStatusId("1000"), InReplyToStatusId = new TwitterStatusId("999") },
             };
+            Assert.Equal(new TwitterStatusId("950"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1000")).StatusId);
+            Assert.Equal(new TwitterStatusId("950"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("950")).StatusId);
+            Assert.Throws<ArgumentException>(() => Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("500")));
 
-            var statusIds = Twitter.GetQuoteTweetStatusIds(urls);
-            Assert.Equal(new[] { 599261132361072640L }, statusIds);
-        }
-
-        [Fact]
-        public void GetQuoteTweetStatusIds_OverflowTest()
-        {
-            var urls = new[]
+            posts = new Dictionary<PostId, PostClass>
             {
-                // 符号付き 64 ビット整数の範囲を超える値
-                "https://twitter.com/kim_upsilon/status/9999999999999999999",
+                // new TwitterStatusId("1200") は posts の中に存在しない
+                [new TwitterStatusId("1210")] = new PostClass { StatusId = new TwitterStatusId("1210"), InReplyToStatusId = new TwitterStatusId("1200") },
+                [new TwitterStatusId("1220")] = new PostClass { StatusId = new TwitterStatusId("1220"), InReplyToStatusId = new TwitterStatusId("1210") },
+                [new TwitterStatusId("1230")] = new PostClass { StatusId = new TwitterStatusId("1230"), InReplyToStatusId = new TwitterStatusId("1220") },
             };
-
-            var statusIds = Twitter.GetQuoteTweetStatusIds(urls);
-            Assert.Empty(statusIds);
+            Assert.Equal(new TwitterStatusId("1210"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1230")).StatusId);
+            Assert.Equal(new TwitterStatusId("1210"), Twitter.FindTopOfReplyChain(posts, new TwitterStatusId("1210")).StatusId);
         }
 
         [Fact]
@@ -221,18 +114,17 @@ namespace OpenTween
             var oldInstance = SettingManagerTest.Common;
             SettingManagerTest.Common = new SettingCommon();
 
-            var timeline = SettingManager.Common.CountApi;
-            var reply = SettingManager.Common.CountApiReply;
-            var dm = 20;  // DMは固定値
-            var more = SettingManager.Common.MoreCountApi;
-            var startup = SettingManager.Common.FirstCountApi;
-            var favorite = SettingManager.Common.FavoritesCountApi;
-            var list = SettingManager.Common.ListCountApi;
-            var search = SettingManager.Common.SearchCountApi;
-            var usertl = SettingManager.Common.UserTimelineCountApi;
+            var timeline = SettingManager.Instance.Common.CountApi;
+            var reply = SettingManager.Instance.Common.CountApiReply;
+            var more = SettingManager.Instance.Common.MoreCountApi;
+            var startup = SettingManager.Instance.Common.FirstCountApi;
+            var favorite = SettingManager.Instance.Common.FavoritesCountApi;
+            var list = SettingManager.Instance.Common.ListCountApi;
+            var search = SettingManager.Instance.Common.SearchCountApi;
+            var usertl = SettingManager.Instance.Common.UserTimelineCountApi;
 
             // デフォルト値チェック
-            Assert.Equal(false, SettingManager.Common.UseAdditionalCount);
+            Assert.False(SettingManager.Instance.Common.UseAdditionalCount);
             Assert.Equal(60, timeline);
             Assert.Equal(40, reply);
             Assert.Equal(200, more);
@@ -246,10 +138,6 @@ namespace OpenTween
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, false, false));
             Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, false));
 
-            // DM
-            Assert.Equal(dm, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.DirectMessegeRcv, false, false));
-            Assert.Equal(dm, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.DirectMessegeSnt, false, false));
-
             // その他はTimelineと同値になる
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, false));
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, false, false));
@@ -265,38 +153,33 @@ namespace OpenTween
             var oldInstance = SettingManagerTest.Common;
             SettingManagerTest.Common = new SettingCommon();
 
-            var timeline = SettingManager.Common.CountApi;
-            var reply = SettingManager.Common.CountApiReply;
-            var dm = 20;  // DMは固定値
-            var more = SettingManager.Common.MoreCountApi;
-            var startup = SettingManager.Common.FirstCountApi;
-            var favorite = SettingManager.Common.FavoritesCountApi;
-            var list = SettingManager.Common.ListCountApi;
-            var search = SettingManager.Common.SearchCountApi;
-            var usertl = SettingManager.Common.UserTimelineCountApi;
+            var timeline = SettingManager.Instance.Common.CountApi;
+            var reply = SettingManager.Instance.Common.CountApiReply;
+            var more = SettingManager.Instance.Common.MoreCountApi;
+            var startup = SettingManager.Instance.Common.FirstCountApi;
+            var favorite = SettingManager.Instance.Common.FavoritesCountApi;
+            var list = SettingManager.Instance.Common.ListCountApi;
+            var search = SettingManager.Instance.Common.SearchCountApi;
+            var usertl = SettingManager.Instance.Common.UserTimelineCountApi;
 
-            SettingManager.Common.UseAdditionalCount = true;
+            SettingManager.Instance.Common.UseAdditionalCount = true;
 
             // Timeline
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, false, false));
-            Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, true, false));
+            Assert.Equal(100, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, true, false)); // 100 件が上限
             Assert.Equal(startup, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Timeline, false, true));
 
             // Reply
             Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, false));
             Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, true, false));
-            Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, true));  //Replyの値が使われる
-
-            // DM
-            Assert.Equal(dm, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.DirectMessegeRcv, false, false));
-            Assert.Equal(dm, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.DirectMessegeSnt, false, false));
+            Assert.Equal(reply, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Reply, false, true));  // Replyの値が使われる
 
             // Favorites
             Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, false));
             Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, true, false));
             Assert.Equal(favorite, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, true));
 
-            SettingManager.Common.FavoritesCountApi = 0;
+            SettingManager.Instance.Common.FavoritesCountApi = 0;
 
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, false, false));
             Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.Favorites, true, false));
@@ -307,7 +190,7 @@ namespace OpenTween
             Assert.Equal(list, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, true, false));
             Assert.Equal(list, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, false, true));
 
-            SettingManager.Common.ListCountApi = 0;
+            SettingManager.Instance.Common.ListCountApi = 0;
 
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, false, false));
             Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.List, true, false));
@@ -318,10 +201,10 @@ namespace OpenTween
             Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false));
             Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, true));
 
-            SettingManager.Common.SearchCountApi = 0;
+            SettingManager.Instance.Common.SearchCountApi = 0;
 
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, false));
-            Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false));  //MoreCountApiの値がPublicSearchの最大値に制限される
+            Assert.Equal(search, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, true, false));  // MoreCountApiの値がPublicSearchの最大値に制限される
             Assert.Equal(startup, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, false, true));
 
             // UserTimeline
@@ -329,7 +212,7 @@ namespace OpenTween
             Assert.Equal(usertl, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, true, false));
             Assert.Equal(usertl, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, false, true));
 
-            SettingManager.Common.UserTimelineCountApi = 0;
+            SettingManager.Instance.Common.UserTimelineCountApi = 0;
 
             Assert.Equal(timeline, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, false, false));
             Assert.Equal(more, Twitter.GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, true, false));
@@ -341,88 +224,131 @@ namespace OpenTween
         [Fact]
         public void GetTextLengthRemain_Test()
         {
-            using (var twitter = new Twitter())
-            {
-                Assert.Equal(280, twitter.GetTextLengthRemain(""));
-                Assert.Equal(272, twitter.GetTextLengthRemain("hogehoge"));
-            }
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            Assert.Equal(280, twitter.GetTextLengthRemain(""));
+            Assert.Equal(272, twitter.GetTextLengthRemain("hogehoge"));
         }
 
         [Fact]
         public void GetTextLengthRemain_DirectMessageTest()
         {
-            using (var twitter = new Twitter())
-            {
-                // 2015年8月から DM の文字数上限が 10,000 文字に変更された
-                // https://twittercommunity.com/t/41348
-                twitter.Configuration.DmTextCharacterLimit = 10000;
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
 
-                Assert.Equal(10000, twitter.GetTextLengthRemain("D twitter "));
-                Assert.Equal(9992, twitter.GetTextLengthRemain("D twitter hogehoge"));
+            // 2015年8月から DM の文字数上限が 10,000 文字に変更された
+            // https://twittercommunity.com/t/41348
+            twitter.Configuration.DmTextCharacterLimit = 10000;
 
-                // t.co に短縮される分の文字数を考慮
-                twitter.Configuration.ShortUrlLength = 20;
-                Assert.Equal(9971, twitter.GetTextLengthRemain("D twitter hogehoge http://example.com/"));
+            Assert.Equal(10000, twitter.GetTextLengthRemain("D twitter "));
+            Assert.Equal(9992, twitter.GetTextLengthRemain("D twitter hogehoge"));
 
-                twitter.Configuration.ShortUrlLengthHttps = 21;
-                Assert.Equal(9970, twitter.GetTextLengthRemain("D twitter hogehoge https://example.com/"));
-            }
+            // t.co に短縮される分の文字数を考慮
+            twitter.Configuration.ShortUrlLength = 20;
+            Assert.Equal(9971, twitter.GetTextLengthRemain("D twitter hogehoge http://example.com/"));
+
+            twitter.Configuration.ShortUrlLengthHttps = 21;
+            Assert.Equal(9970, twitter.GetTextLengthRemain("D twitter hogehoge https://example.com/"));
         }
 
         [Fact]
         public void GetTextLengthRemain_UrlTest()
         {
-            using (var twitter = new Twitter())
-            {
-                // t.co に短縮される分の文字数を考慮
-                twitter.TextConfiguration.TransformedURLLength = 20;
-                Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/"));
-                Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/hogehoge"));
-                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge http://example.com/"));
-
-                Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/"));
-                Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/hogehoge"));
-                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge https://example.com/"));
-            }
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            // t.co に短縮される分の文字数を考慮
+            twitter.TextConfiguration.TransformedURLLength = 20;
+            Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/"));
+            Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/hogehoge"));
+            Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge http://example.com/"));
+
+            Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/"));
+            Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/hogehoge"));
+            Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge https://example.com/"));
         }
 
         [Fact]
         public void GetTextLengthRemain_UrlWithoutSchemeTest()
         {
-            using (var twitter = new Twitter())
-            {
-                // t.co に短縮される分の文字数を考慮
-                twitter.TextConfiguration.TransformedURLLength = 20;
-                Assert.Equal(260, twitter.GetTextLengthRemain("example.com"));
-                Assert.Equal(260, twitter.GetTextLengthRemain("example.com/hogehoge"));
-                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge example.com"));
-
-                // スキーム (http://) を省略かつ末尾が ccTLD の場合は t.co に短縮されない
-                Assert.Equal(270, twitter.GetTextLengthRemain("example.jp"));
-                // ただし、末尾にパスが続く場合は t.co に短縮される
-                Assert.Equal(260, twitter.GetTextLengthRemain("example.jp/hogehoge"));
-            }
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            // t.co に短縮される分の文字数を考慮
+            twitter.TextConfiguration.TransformedURLLength = 20;
+            Assert.Equal(260, twitter.GetTextLengthRemain("example.com"));
+            Assert.Equal(260, twitter.GetTextLengthRemain("example.com/hogehoge"));
+            Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge example.com"));
+
+            // スキーム (http://) を省略かつ末尾が ccTLD の場合は t.co に短縮されない
+            Assert.Equal(270, twitter.GetTextLengthRemain("example.jp"));
+            // ただし、末尾にパスが続く場合は t.co に短縮される
+            Assert.Equal(260, twitter.GetTextLengthRemain("example.jp/hogehoge"));
         }
 
         [Fact]
         public void GetTextLengthRemain_SurrogatePairTest()
         {
-            using (var twitter = new Twitter())
-            {
-                Assert.Equal(278, twitter.GetTextLengthRemain("🍣"));
-                Assert.Equal(267, twitter.GetTextLengthRemain("🔥🐔🔥 焼き鳥"));
-            }
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            Assert.Equal(278, twitter.GetTextLengthRemain("🍣"));
+            Assert.Equal(267, twitter.GetTextLengthRemain("🔥🐔🔥 焼き鳥"));
+        }
+
+        [Fact]
+        public void GetTextLengthRemain_EmojiTest()
+        {
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            // 絵文字の文字数カウントの仕様変更に対するテストケース
+            // https://twittercommunity.com/t/114607
+
+            Assert.Equal(279, twitter.GetTextLengthRemain("©")); // 基本多言語面の絵文字
+            Assert.Equal(277, twitter.GetTextLengthRemain("©\uFE0E")); // 異字体セレクタ付き (text style)
+            Assert.Equal(279, twitter.GetTextLengthRemain("©\uFE0F")); // 異字体セレクタ付き (emoji style)
+            Assert.Equal(278, twitter.GetTextLengthRemain("🍣")); // 拡張面の絵文字
+            Assert.Equal(279, twitter.GetTextLengthRemain("#⃣")); // 合字で表現される絵文字
+            Assert.Equal(278, twitter.GetTextLengthRemain("👦\U0001F3FF")); // Emoji modifier 付きの絵文字
+            Assert.Equal(278, twitter.GetTextLengthRemain("\U0001F3FF")); // Emoji modifier 単体
+            Assert.Equal(278, twitter.GetTextLengthRemain("👨\u200D🎨")); // ZWJ で結合された絵文字
+            Assert.Equal(278, twitter.GetTextLengthRemain("🏃\u200D♀\uFE0F")); // ZWJ と異字体セレクタを含む絵文字
         }
 
         [Fact]
         public void GetTextLengthRemain_BrokenSurrogateTest()
         {
-            using (var twitter = new Twitter())
-            {
-                // 投稿欄に IME から絵文字を入力すると HighSurrogate のみ入力された状態で TextChanged イベントが呼ばれることがある
-                Assert.Equal(278, twitter.GetTextLengthRemain("\ud83d"));
-                Assert.Equal(9999, twitter.GetTextLengthRemain("D twitter \ud83d"));
-            }
+            using var twitterApi = new TwitterApi();
+            using var twitter = new Twitter(twitterApi);
+
+            // 投稿欄に IME から絵文字を入力すると HighSurrogate のみ入力された状態で TextChanged イベントが呼ばれることがある
+            Assert.Equal(278, twitter.GetTextLengthRemain("\ud83d"));
+            Assert.Equal(9999, twitter.GetTextLengthRemain("D twitter \ud83d"));
         }
+
+        [Theory]
+        [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "normal", "https://pbs.twimg.com/profile_images/00000/foo_normal.jpg")]
+        [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "bigger", "https://pbs.twimg.com/profile_images/00000/foo_bigger.jpg")]
+        [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "mini", "https://pbs.twimg.com/profile_images/00000/foo_mini.jpg")]
+        [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "original", "https://pbs.twimg.com/profile_images/00000/foo.jpg")]
+        [InlineData("https://pbs.twimg.com/profile_images/00000/foo_normal_bar_normal.jpg", "original", "https://pbs.twimg.com/profile_images/00000/foo_normal_bar.jpg")]
+        public void CreateProfileImageUrl_Test(string normalUrl, string size, string expected)
+            => Assert.Equal(expected, Twitter.CreateProfileImageUrl(normalUrl, size));
+
+        [Fact]
+        public void CreateProfileImageUrl_InvalidSizeTest()
+            => Assert.Throws<ArgumentException>(() => Twitter.CreateProfileImageUrl("https://pbs.twimg.com/profile_images/00000/foo_normal.jpg", "INVALID"));
+
+        [Theory]
+        [InlineData(24, "mini")]
+        [InlineData(25, "normal")]
+        [InlineData(48, "normal")]
+        [InlineData(49, "bigger")]
+        [InlineData(73, "bigger")]
+        [InlineData(74, "original")]
+        public void DecideProfileImageSize_Test(int sizePx, string expected)
+            => Assert.Equal(expected, Twitter.DecideProfileImageSize(sizePx));
     }
 }