diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 5b94876bc..57496d110 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,5 +1,10 @@
更新履歴
+==== Ver 3.11.0(2024/01/07)
+ * NEW: Cookie使用時の関連発言表示に対応
+ * FIX: APIリクエストのタイムアウト時に接続が切断されない場合がある不具合を修正
+ * FIX: 存在しないユーザーのプロフィールを取得しようとした場合のエラーが適切に処理されない不具合を修正
+
==== Ver 3.10.1(2023/12/23)
* FIX: OAuth 1.0a によるAPIアクセスに失敗する不具合を修正
diff --git a/OpenTween.Tests/Api/GraphQL/TimelineResponseTest.cs b/OpenTween.Tests/Api/GraphQL/TimelineResponseTest.cs
new file mode 100644
index 000000000..4a793df84
--- /dev/null
+++ b/OpenTween.Tests/Api/GraphQL/TimelineResponseTest.cs
@@ -0,0 +1,41 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.Threading.Tasks;
+using Xunit;
+
+namespace OpenTween.Api.GraphQL
+{
+ public class TimelineResponseTest
+ {
+ [Fact]
+ public async Task ToTwitterStatuses_Test()
+ {
+ using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/SearchTimeline_SimpleTweet.json");
+ var tweets = TimelineTweet.ExtractTimelineTweets(await apiResponse.ReadAsJsonXml());
+ var timelineResponse = new TimelineResponse(tweets, "", "");
+
+ var statuses = timelineResponse.ToTwitterStatuses();
+ Assert.Single(statuses);
+ Assert.Equal("1619433164757413894", statuses[0].IdStr);
+ }
+ }
+}
diff --git a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
index 9c4e287d9..292fbadad 100644
--- a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
+++ b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs
@@ -81,5 +81,30 @@ public async Task Send_UserUnavailableTest()
mock.VerifyAll();
}
+
+ [Fact]
+ public async Task Send_EmptyTest()
+ {
+ // ユーザーが存在しない場合にエラー情報を含まない空のオブジェクトが返されることがある
+ using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserByScreenName_Empty.json");
+
+ var mock = new Mock();
+ mock.Setup(x =>
+ x.SendAsync(It.IsAny())
+ )
+ .ReturnsAsync(apiResponse);
+
+ var request = new UserByScreenNameRequest
+ {
+ ScreenName = "==INVALID==",
+ };
+
+ var ex = await Assert.ThrowsAsync(
+ () => request.Send(mock.Object)
+ );
+ Assert.Equal("User is not available.", ex.Message);
+
+ mock.VerifyAll();
+ }
}
}
diff --git a/OpenTween.Tests/AsyncExceptionBoundaryTest.cs b/OpenTween.Tests/AsyncExceptionBoundaryTest.cs
new file mode 100644
index 000000000..f17e34b4d
--- /dev/null
+++ b/OpenTween.Tests/AsyncExceptionBoundaryTest.cs
@@ -0,0 +1,128 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2024 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.IO;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace OpenTween
+{
+ public class AsyncExceptionBoundaryTest
+ {
+ [Fact]
+ public async Task Wrap_SynchronousTest()
+ {
+ static Task AsyncFunc()
+ => throw new OperationCanceledException();
+
+ // 例外を無視して終了する
+ await AsyncExceptionBoundary.Wrap(AsyncFunc);
+ }
+
+ [Fact]
+ public async Task Wrap_AsynchronousTest()
+ {
+ static async Task AsyncFunc()
+ {
+ await Task.Yield();
+ throw new OperationCanceledException();
+ }
+
+ // 例外を無視して終了する
+ await AsyncExceptionBoundary.Wrap(AsyncFunc);
+ }
+
+ [Fact]
+ public async Task Wrap_IgnoredTest()
+ {
+ static Task AsyncFunc()
+ => throw new OperationCanceledException();
+
+ static bool IgnoreException(Exception ex)
+ => ex is OperationCanceledException;
+
+ // 例外を無視して終了する
+ await AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException);
+ }
+
+ [Fact]
+ public async Task Wrap_NotIgnoredTest()
+ {
+ static Task AsyncFunc()
+ => throw new IOException();
+
+ static bool IgnoreException(Exception ex)
+ => ex is OperationCanceledException;
+
+ // 例外を返して終了する
+ await Assert.ThrowsAsync(
+ () => AsyncExceptionBoundary.Wrap(AsyncFunc, IgnoreException)
+ );
+ }
+
+ [Fact]
+ public async Task IgnoreException_Test()
+ {
+ var task = Task.FromException(new OperationCanceledException());
+
+ // 例外を無視して終了する
+ await AsyncExceptionBoundary.IgnoreException(task);
+ }
+
+ [Fact]
+ public async Task IgnoreExceptionAndDispose_Test()
+ {
+ var task = Task.FromException(new OperationCanceledException());
+
+ // 例外を無視して終了する
+ await AsyncExceptionBoundary.IgnoreExceptionAndDispose(task);
+ }
+
+ [Fact]
+ public async Task IgnoreExceptionAndDispose_DisposeTest()
+ {
+ using var image = TestUtils.CreateDummyImage();
+ var task = Task.FromResult