diff --git a/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs index 418d38f7f..5fee38558 100644 --- a/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs @@ -38,7 +38,7 @@ public async Task Send_Test() { var responseText = File.ReadAllText("Resources/Responses/CreateRetweet.json"); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) diff --git a/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs index ef5d8e85d..ad08aec06 100644 --- a/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs @@ -38,7 +38,7 @@ public async Task Send_Test() { var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) @@ -67,7 +67,7 @@ public async Task Send_ReplyTest() { var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) @@ -92,7 +92,7 @@ public async Task Send_MediaTest() { var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) diff --git a/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs index fad2f630c..9a52489d3 100644 --- a/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs @@ -36,7 +36,7 @@ public class DeleteRetweetRequestTest [Fact] public async Task Send_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) diff --git a/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs index f8c1583e7..3db62ccec 100644 --- a/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs @@ -36,7 +36,7 @@ public class DeleteTweetRequestTest [Fact] public async Task Send_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync(It.IsAny(), It.IsAny()) ) diff --git a/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs b/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs index cf623b50c..4cb9c952d 100644 --- a/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs @@ -19,14 +19,8 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Moq; -using OpenTween.Api.TwitterV2; using OpenTween.Connection; using Xunit; @@ -37,21 +31,23 @@ public class ListLatestTweetsTimelineRequestTest [Fact] public async Task Send_Test() { - using var responseStream = File.OpenRead("Resources/Responses/ListLatestTweetsTimeline_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/ListLatestTweetsTimeline_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"listId":"1675863884757110790","count":20}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("ListLatestTweetsTimeline", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"listId":"1675863884757110790","count":20}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("ListLatestTweetsTimeline", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new ListLatestTweetsTimelineRequest(listId: "1675863884757110790") { @@ -69,21 +65,23 @@ public async Task Send_Test() [Fact] public async Task Send_RequestCursor_Test() { - using var responseStream = File.OpenRead("Resources/Responses/ListLatestTweetsTimeline_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/ListLatestTweetsTimeline_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"listId":"1675863884757110790","count":20,"cursor":"aaa"}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("ListLatestTweetsTimeline", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/6ClPnsuzQJ1p7-g32GQw9Q/ListLatestTweetsTimeline"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"listId":"1675863884757110790","count":20,"cursor":"aaa"}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("ListLatestTweetsTimeline", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new ListLatestTweetsTimelineRequest(listId: "1675863884757110790") { diff --git a/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs b/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs index f424861d8..7003b27c3 100644 --- a/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs @@ -19,11 +19,6 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Moq; using OpenTween.Connection; @@ -36,21 +31,23 @@ public class SearchTimelineRequestTest [Fact] public async Task Send_Test() { - using var responseStream = File.OpenRead("Resources/Responses/SearchTimeline_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/SearchTimeline_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest"}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("SearchTimeline", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest"}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("SearchTimeline", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new SearchTimelineRequest(rawQuery: "#OpenTween") { @@ -68,21 +65,23 @@ public async Task Send_Test() [Fact] public async Task Send_RequestCursor_Test() { - using var responseStream = File.OpenRead("Resources/Responses/SearchTimeline_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/SearchTimeline_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest","cursor":"aaa"}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("SearchTimeline", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/lZ0GCEojmtQfiUQa5oJSEw/SearchTimeline"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"rawQuery":"#OpenTween","count":20,"product":"Latest","cursor":"aaa"}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("SearchTimeline", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new SearchTimelineRequest(rawQuery: "#OpenTween") { diff --git a/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs b/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs index 93e857f20..e88f82311 100644 --- a/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs @@ -19,14 +19,9 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using System; -using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using Moq; -using OpenTween.Api.TwitterV2; using OpenTween.Connection; using Xunit; @@ -37,19 +32,21 @@ public class TweetDetailRequestTest [Fact] public async Task Send_Test() { - using var responseStream = File.OpenRead("Resources/Responses/TweetDetail.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/TweetDetail.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail"), url); - Assert.Contains(@"""focalTweetId"":""1619433164757413894""", param["variables"]); - Assert.Equal("TweetDetail", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail"), request.RequestUri); + var query = request.Query!; + Assert.Contains(@"""focalTweetId"":""1619433164757413894""", query["variables"]); + Assert.Equal("TweetDetail", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new TweetDetailRequest { diff --git a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs index 075ee3462..9c4e287d9 100644 --- a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs @@ -19,11 +19,6 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Moq; using OpenTween.Connection; @@ -36,19 +31,21 @@ public class UserByScreenNameRequestTest [Fact] public async Task Send_Test() { - using var responseStream = File.OpenRead("Resources/Responses/UserByScreenName.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserByScreenName.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName"), url); - Assert.Contains(@"""screen_name"":""opentween""", param["variables"]); - Assert.Equal("UserByScreenName", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName"), request.RequestUri); + var query = request.Query!; + Assert.Contains(@"""screen_name"":""opentween""", query["variables"]); + Assert.Equal("UserByScreenName", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new UserByScreenNameRequest { @@ -64,13 +61,13 @@ public async Task Send_Test() [Fact] public async Task Send_UserUnavailableTest() { - using var responseStream = File.OpenRead("Resources/Responses/UserByScreenName_Suspended.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserByScreenName_Suspended.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new UserByScreenNameRequest { diff --git a/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs b/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs index b829b193f..50264c609 100644 --- a/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/UserTweetsAndRepliesRequestTest.cs @@ -19,11 +19,6 @@ // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Moq; using OpenTween.Connection; @@ -36,21 +31,23 @@ public class UserTweetsAndRepliesRequestTest [Fact] public async Task Send_Test() { - using var responseStream = File.OpenRead("Resources/Responses/UserTweetsAndReplies_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserTweetsAndReplies_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("UserTweetsAndReplies", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("UserTweetsAndReplies", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new UserTweetsAndRepliesRequest(userId: "40480664") { @@ -68,21 +65,23 @@ public async Task Send_Test() [Fact] public async Task Send_RequestCursor_Test() { - using var responseStream = File.OpenRead("Resources/Responses/UserTweetsAndReplies_SimpleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/UserTweetsAndReplies_SimpleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.GetStreamAsync(It.IsAny(), It.IsAny>(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback, string>((url, param, endpointName) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), url); - Assert.Equal(2, param.Count); - Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true,"cursor":"aaa"}""", param["variables"]); - Assert.True(param.ContainsKey("features")); - Assert.Equal("UserTweetsAndReplies", endpointName); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/YlkSUg0mRBx7-EkxCvc-bw/UserTweetsAndReplies"), request.RequestUri); + var query = request.Query!; + Assert.Equal(2, query.Count); + Assert.Equal("""{"userId":"40480664","count":20,"includePromotedContent":true,"withCommunity":true,"withVoice":true,"withV2Timeline":true,"cursor":"aaa"}""", query["variables"]); + Assert.True(query.ContainsKey("features")); + Assert.Equal("UserTweetsAndReplies", request.EndpointName); }) - .ReturnsAsync(responseStream); + .ReturnsAsync(apiResponse); var request = new UserTweetsAndRepliesRequest(userId: "40480664") { diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 49ca6b9ff..d74088b2f 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -84,7 +84,7 @@ public void Initialize_Test() [Fact] public async Task StatusesHomeTimeline_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("statuses/home_timeline.json", UriKind.Relative), @@ -112,7 +112,7 @@ public async Task StatusesHomeTimeline_Test() [Fact] public async Task StatusesMentionsTimeline_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("statuses/mentions_timeline.json", UriKind.Relative), @@ -140,7 +140,7 @@ public async Task StatusesMentionsTimeline_Test() [Fact] public async Task StatusesUserTimeline_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("statuses/user_timeline.json", UriKind.Relative), @@ -170,7 +170,7 @@ public async Task StatusesUserTimeline_Test() [Fact] public async Task StatusesShow_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("statuses/show.json", UriKind.Relative), @@ -196,7 +196,7 @@ public async Task StatusesShow_Test() [Fact] public async Task StatusesLookup_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("statuses/lookup.json", UriKind.Relative), @@ -223,7 +223,7 @@ public async Task StatusesLookup_Test() [Fact] public async Task StatusesUpdate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("statuses/update.json", UriKind.Relative), @@ -261,7 +261,7 @@ await twitterApi.StatusesUpdate( [Fact] public async Task StatusesUpdate_ExcludeReplyUserIdsEmptyTest() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("statuses/update.json", UriKind.Relative), @@ -288,7 +288,7 @@ await twitterApi.StatusesUpdate("hogehoge", replyToId: null, mediaIds: null, exc [Fact] public async Task StatusesDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("statuses/destroy.json", UriKind.Relative), @@ -308,7 +308,7 @@ public async Task StatusesDestroy_Test() [Fact] public async Task StatusesRetweet_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("statuses/retweet.json", UriKind.Relative), @@ -334,7 +334,7 @@ await twitterApi.StatusesRetweet(new("100")) [Fact] public async Task SearchTweets_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("search/tweets.json", UriKind.Relative), @@ -365,7 +365,7 @@ public async Task SearchTweets_Test() [Fact] public async Task ListsOwnerships_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/ownerships.json", UriKind.Relative), @@ -390,7 +390,7 @@ public async Task ListsOwnerships_Test() [Fact] public async Task ListsSubscriptions_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/subscriptions.json", UriKind.Relative), @@ -415,7 +415,7 @@ public async Task ListsSubscriptions_Test() [Fact] public async Task ListsMemberships_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/memberships.json", UriKind.Relative), @@ -441,7 +441,7 @@ public async Task ListsMemberships_Test() [Fact] public async Task ListsCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("lists/create.json", UriKind.Relative), @@ -466,7 +466,7 @@ await twitterApi.ListsCreate("hogehoge", description: "aaaa", @private: true) [Fact] public async Task ListsUpdate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("lists/update.json", UriKind.Relative), @@ -492,7 +492,7 @@ await twitterApi.ListsUpdate(12345L, name: "hogehoge", description: "aaaa", @pri [Fact] public async Task ListsDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("lists/destroy.json", UriKind.Relative), @@ -515,7 +515,7 @@ await twitterApi.ListsDestroy(12345L) [Fact] public async Task ListsStatuses_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/statuses.json", UriKind.Relative), @@ -545,7 +545,7 @@ public async Task ListsStatuses_Test() [Fact] public async Task ListsMembers_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/members.json", UriKind.Relative), @@ -572,7 +572,7 @@ public async Task ListsMembers_Test() [Fact] public async Task ListsMembersShow_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("lists/members/show.json", UriKind.Relative), @@ -599,7 +599,7 @@ public async Task ListsMembersShow_Test() [Fact] public async Task ListsMembersCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("lists/members/create.json", UriKind.Relative), @@ -626,7 +626,7 @@ await twitterApi.ListsMembersCreate(12345L, "twitterapi") [Fact] public async Task ListsMembersDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("lists/members/destroy.json", UriKind.Relative), @@ -653,7 +653,7 @@ await twitterApi.ListsMembersDestroy(12345L, "twitterapi") [Fact] public async Task DirectMessagesEventsList_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("direct_messages/events/list.json", UriKind.Relative), @@ -677,7 +677,7 @@ public async Task DirectMessagesEventsList_Test() [Fact] public async Task DirectMessagesEventsNew_Test() { - var mock = new Mock(); + var mock = new Mock(); var responseText = """ { "event": { @@ -717,7 +717,7 @@ public async Task DirectMessagesEventsNew_Test() [Fact] public async Task DirectMessagesEventsDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.DeleteAsync( new Uri("direct_messages/events/destroy.json?id=100", UriKind.Relative)) @@ -735,7 +735,7 @@ public async Task DirectMessagesEventsDestroy_Test() [Fact] public async Task UsersShow_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("users/show.json", UriKind.Relative), @@ -761,7 +761,7 @@ public async Task UsersShow_Test() [Fact] public async Task UsersLookup_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("users/lookup.json", UriKind.Relative), @@ -787,7 +787,7 @@ public async Task UsersLookup_Test() [Fact] public async Task UsersReportSpam_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("users/report_spam.json", UriKind.Relative), @@ -811,7 +811,7 @@ await twitterApi.UsersReportSpam(screenName: "twitterapi") [Fact] public async Task FavoritesList_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("favorites/list.json", UriKind.Relative), @@ -839,7 +839,7 @@ public async Task FavoritesList_Test() [Fact] public async Task FavoritesCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("favorites/create.json", UriKind.Relative), @@ -863,7 +863,7 @@ public async Task FavoritesCreate_Test() [Fact] public async Task FavoritesDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("favorites/destroy.json", UriKind.Relative), @@ -887,7 +887,7 @@ public async Task FavoritesDestroy_Test() [Fact] public async Task FriendshipsShow_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("friendships/show.json", UriKind.Relative), @@ -907,7 +907,7 @@ public async Task FriendshipsShow_Test() [Fact] public async Task FriendshipsCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("friendships/create.json", UriKind.Relative), @@ -927,7 +927,7 @@ await twitterApi.FriendshipsCreate(screenName: "twitterapi") [Fact] public async Task FriendshipsDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("friendships/destroy.json", UriKind.Relative), @@ -947,7 +947,7 @@ await twitterApi.FriendshipsDestroy(screenName: "twitterapi") [Fact] public async Task NoRetweetIds_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("friendships/no_retweets/ids.json", UriKind.Relative), @@ -967,7 +967,7 @@ public async Task NoRetweetIds_Test() [Fact] public async Task FollowersIds_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("followers/ids.json", UriKind.Relative), @@ -987,7 +987,7 @@ public async Task FollowersIds_Test() [Fact] public async Task MutesUsersIds_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("mutes/users/ids.json", UriKind.Relative), @@ -1007,7 +1007,7 @@ public async Task MutesUsersIds_Test() [Fact] public async Task BlocksIds_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("blocks/ids.json", UriKind.Relative), @@ -1027,7 +1027,7 @@ public async Task BlocksIds_Test() [Fact] public async Task BlocksCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("blocks/create.json", UriKind.Relative), @@ -1051,7 +1051,7 @@ await twitterApi.BlocksCreate(screenName: "twitterapi") [Fact] public async Task BlocksDestroy_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("blocks/destroy.json", UriKind.Relative), @@ -1075,7 +1075,7 @@ await twitterApi.BlocksDestroy(screenName: "twitterapi") [Fact] public async Task AccountVerifyCredentials_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("account/verify_credentials.json", UriKind.Relative), @@ -1107,7 +1107,7 @@ public async Task AccountVerifyCredentials_Test() [Fact] public async Task AccountUpdateProfile_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("account/update_profile.json", UriKind.Relative), @@ -1138,7 +1138,7 @@ public async Task AccountUpdateProfileImage_Test() { using var image = TestUtils.CreateDummyImage(); using var media = new MemoryImageMediaItem(image); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("account/update_profile_image.json", UriKind.Relative), @@ -1164,7 +1164,7 @@ await twitterApi.AccountUpdateProfileImage(media) [Fact] public async Task ApplicationRateLimitStatus_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("application/rate_limit_status.json", UriKind.Relative), @@ -1184,7 +1184,7 @@ public async Task ApplicationRateLimitStatus_Test() [Fact] public async Task Configuration_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("help/configuration.json", UriKind.Relative), @@ -1204,7 +1204,7 @@ public async Task Configuration_Test() [Fact] public async Task MediaUploadInit_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), @@ -1232,7 +1232,7 @@ public async Task MediaUploadAppend_Test() { using var image = TestUtils.CreateDummyImage(); using var media = new MemoryImageMediaItem(image); - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), @@ -1257,7 +1257,7 @@ public async Task MediaUploadAppend_Test() [Fact] public async Task MediaUploadFinalize_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostLazyAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), @@ -1281,7 +1281,7 @@ await twitterApi.MediaUploadFinalize(mediaId: 11111L) [Fact] public async Task MediaUploadStatus_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), @@ -1305,7 +1305,7 @@ public async Task MediaUploadStatus_Test() [Fact] public async Task MediaMetadataCreate_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.PostJsonAsync( new Uri("https://upload.twitter.com/1.1/media/metadata/create.json", UriKind.Absolute), diff --git a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs index fcf5adfdd..a2b00fd96 100644 --- a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs +++ b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs @@ -34,7 +34,7 @@ public class GetTimelineRequestTest [Fact] public async Task StatusesMentionsTimeline_Test() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(x => x.GetAsync( new Uri("/2/users/100/timelines/reverse_chronological", UriKind.Relative), diff --git a/OpenTween.Tests/Connection/ApiResponseTest.cs b/OpenTween.Tests/Connection/ApiResponseTest.cs new file mode 100644 index 000000000..a1ff5eee1 --- /dev/null +++ b/OpenTween.Tests/Connection/ApiResponseTest.cs @@ -0,0 +1,119 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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.Net; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Linq; +using OpenTween.Api; +using Xunit; + +namespace OpenTween.Connection +{ + public class ApiResponseTest + { + [Fact] + public async Task ReadAsBytes_Test() + { + using var responseContent = new ByteArrayContent(new byte[] { 1, 2, 3 }); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + Assert.Equal(new byte[] { 1, 2, 3 }, await response.ReadAsBytes()); + } + + [DataContract] + public struct TestJson + { + [DataMember(Name = "foo")] + public int Foo { get; set; } + } + + [Fact] + public async Task ReadAsJson_Test() + { + using var responseContent = new StringContent("""{"foo":123}"""); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + Assert.Equal(new() { Foo = 123 }, await response.ReadAsJson()); + } + + [Fact] + public async Task ReadAsJson_InvalidJsonTest() + { + using var responseContent = new StringContent("### Invalid JSON Response ###"); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + var ex = await Assert.ThrowsAsync( + () => response.ReadAsJson() + ); + Assert.Equal("### Invalid JSON Response ###", ex.ResponseText); + } + + [Fact] + public async Task ReadAsJsonXml_Test() + { + using var responseContent = new StringContent("""{"foo":123}"""); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + var rootElm = await response.ReadAsJsonXml(); + var xmlString = rootElm.ToString(SaveOptions.DisableFormatting); + Assert.Equal("""123""", xmlString); + } + + [Fact] + public async Task ReadAsJsonXml_InvalidJsonTest() + { + using var responseContent = new StringContent("### Invalid JSON Response ###"); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + var ex = await Assert.ThrowsAsync( + () => response.ReadAsJsonXml() + ); + Assert.Equal("### Invalid JSON Response ###", ex.ResponseText); + } + } +} diff --git a/OpenTween.Tests/Connection/GetRequestTest.cs b/OpenTween.Tests/Connection/GetRequestTest.cs new file mode 100644 index 000000000..db738b66f --- /dev/null +++ b/OpenTween.Tests/Connection/GetRequestTest.cs @@ -0,0 +1,81 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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 System.Net.Http; +using Xunit; + +namespace OpenTween.Connection +{ + public class GetRequestTest + { + [Fact] + public void CreateMessage_Test() + { + var request = new GetRequest + { + RequestUri = new("statuses/show.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = "12345", + }, + }; + + var baseUri = new Uri("https://api.twitter.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Get, requestMessage.Method); + Assert.Equal(new("https://api.twitter.com/v1/statuses/show.json?id=12345"), requestMessage.RequestUri); + } + + [Fact] + public void BuildUriWithQuery_Test() + { + var uri = new Uri("https://example.com/hoge"); + var query = new Dictionary + { + ["foo"] = "bar", + }; + Assert.Equal(new("https://example.com/hoge?foo=bar"), GetRequest.BuildUriWithQuery(uri, query)); + } + + [Fact] + public void BuildUriWithQuery_NullTest() + { + var uri = new Uri("https://example.com/hoge"); + Assert.Equal(new("https://example.com/hoge"), GetRequest.BuildUriWithQuery(uri, null)); + } + + [Fact] + public void BuildUriWithQuery_CannotMergeTest() + { + var uri = new Uri("https://example.com/hoge?aaa=111"); + var query = new Dictionary + { + ["bbb"] = "222", + }; + Assert.Throws( + () => GetRequest.BuildUriWithQuery(uri, query) + ); + } + } +} diff --git a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs index 929f45eb1..1c7b53cdf 100644 --- a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs +++ b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs @@ -29,6 +29,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Web; using Moq; @@ -127,7 +128,50 @@ public async Task GetAsync_AbsoluteUriTest() } [Fact] - public async Task GetAsync_UpdateRateLimitTest() + public async Task SendAsync_Test() + { + using var mockHandler = new HttpMessageHandlerMock(); + using var http = new HttpClient(mockHandler); + using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); + apiConnection.Http = http; + + mockHandler.Enqueue(x => + { + Assert.Equal(HttpMethod.Get, x.Method); + Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", + x.RequestUri.GetLeftPart(UriPartial.Path)); + + var query = HttpUtility.ParseQueryString(x.RequestUri.Query); + + Assert.Equal("1111", query["aaaa"]); + Assert.Equal("2222", query["bbbb"]); + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("\"hogehoge\""), + }; + }); + + var request = new GetRequest + { + RequestUri = new("hoge/tetete.json", UriKind.Relative), + Query = new Dictionary + { + ["aaaa"] = "1111", + ["bbbb"] = "2222", + }, + EndpointName = "/hoge/tetete", + }; + + using var response = await apiConnection.SendAsync(request); + + Assert.Equal("hogehoge", await response.ReadAsJson()); + + Assert.Equal(0, mockHandler.QueueCount); + } + + [Fact] + public async Task SendAsync_UpdateRateLimitTest() { using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); @@ -144,10 +188,10 @@ public async Task GetAsync_UpdateRateLimitTest() { Headers = { - { "X-Rate-Limit-Limit", "150" }, - { "X-Rate-Limit-Remaining", "100" }, - { "X-Rate-Limit-Reset", "1356998400" }, - { "X-Access-Level", "read-write-directmessages" }, + { "X-Rate-Limit-Limit", "150" }, + { "X-Rate-Limit-Remaining", "100" }, + { "X-Rate-Limit-Reset", "1356998400" }, + { "X-Access-Level", "read-write-directmessages" }, }, Content = new StringContent("\"hogehoge\""), }; @@ -156,9 +200,13 @@ public async Task GetAsync_UpdateRateLimitTest() var apiStatus = new TwitterApiStatus(); MyCommon.TwitterApiInfo = apiStatus; - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("hoge/tetete.json", UriKind.Relative), + EndpointName = "/hoge/tetete", + }; - await apiConnection.GetAsync(endpoint, null, endpointName: "/hoge/tetete"); + using var response = await apiConnection.SendAsync(request); Assert.Equal(TwitterApiAccessLevel.ReadWriteAndDirectMessage, apiStatus.AccessLevel); Assert.Equal(new ApiLimit(150, 100, new DateTimeUtc(2013, 1, 1, 0, 0, 0)), apiStatus.AccessLimit["/hoge/tetete"]); @@ -167,7 +215,7 @@ public async Task GetAsync_UpdateRateLimitTest() } [Fact] - public async Task GetAsync_ErrorStatusTest() + public async Task SendAsync_ErrorStatusTest() { using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); @@ -182,10 +230,13 @@ public async Task GetAsync_ErrorStatusTest() }; }); - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("hoge/tetete.json", UriKind.Relative), + }; var exception = await Assert.ThrowsAsync( - () => apiConnection.GetAsync(endpoint, null, endpointName: "/hoge/tetete") + () => apiConnection.SendAsync(request) ); // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する @@ -196,7 +247,7 @@ public async Task GetAsync_ErrorStatusTest() } [Fact] - public async Task GetAsync_ErrorJsonTest() + public async Task SendAsync_ErrorJsonTest() { using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); @@ -211,10 +262,13 @@ public async Task GetAsync_ErrorJsonTest() }; }); - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("hoge/tetete.json", UriKind.Relative), + }; var exception = await Assert.ThrowsAsync( - () => apiConnection.GetAsync(endpoint, null, endpointName: "/hoge/tetete") + () => apiConnection.SendAsync(request) ); // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する @@ -519,5 +573,76 @@ public async Task DeleteAsync_Test() Assert.Equal(0, mockHandler.QueueCount); } + + [Fact] + public async Task HandleTimeout_SuccessTest() + { + static async Task AsyncFunc(CancellationToken token) + { + await Task.Delay(10); + token.ThrowIfCancellationRequested(); + return 1; + } + + var timeout = TimeSpan.FromMilliseconds(200); + var ret = await TwitterApiConnection.HandleTimeout(AsyncFunc, timeout); + + Assert.Equal(1, ret); + } + + [Fact] + public async Task HandleTimeout_TimeoutTest() + { + var tcs = new TaskCompletionSource(); + + async Task AsyncFunc(CancellationToken token) + { + await Task.Delay(200); + tcs.SetResult(token.IsCancellationRequested); + return 1; + } + + var timeout = TimeSpan.FromMilliseconds(10); + await Assert.ThrowsAsync( + () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout) + ); + + var cancelRequested = await tcs.Task; + Assert.True(cancelRequested); + } + + [Fact] + public async Task HandleTimeout_ThrowExceptionAfterTimeoutTest() + { + var tcs = new TaskCompletionSource(); + + async Task AsyncFunc(CancellationToken token) + { + await Task.Delay(100); + tcs.SetResult(1); + throw new Exception(); + } + + var timeout = TimeSpan.FromMilliseconds(10); + await Assert.ThrowsAsync( + () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout) + ); + + // キャンセル後に AsyncFunc で発生した例外が無視される(UnobservedTaskException イベントを発生させない)ことをチェックする + var error = false; + void UnobservedExceptionHandler(object s, UnobservedTaskExceptionEventArgs e) + => error = true; + + TaskScheduler.UnobservedTaskException += UnobservedExceptionHandler; + + await tcs.Task; + await Task.Delay(10); + GC.Collect(); // UnobservedTaskException は Task のデストラクタで呼ばれるため強制的に GC を実行する + await Task.Delay(10); + + Assert.False(error); + + TaskScheduler.UnobservedTaskException -= UnobservedExceptionHandler; + } } } diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index 560c6a82b..1261d4730 100644 --- a/OpenTween.Tests/MyCommonTest.cs +++ b/OpenTween.Tests/MyCommonTest.cs @@ -135,9 +135,17 @@ public struct JsonData [Theory] [MemberData(nameof(CreateDataFromJsonTestCase))] - public void CreateDataFromJsonTest(string json, T expected) + public void CreateDataFromJson_StringTest(string json, T expected) => Assert.Equal(expected, MyCommon.CreateDataFromJson(json)); + [Theory] + [MemberData(nameof(CreateDataFromJsonTestCase))] + public void CreateDataFromJson_BytesTest(string json, T expected) + { + var jsonBytes = Encoding.UTF8.GetBytes(json); + Assert.Equal(expected, MyCommon.CreateDataFromJson(jsonBytes)); + } + [Theory] [InlineData("hoge123@example.com", true)] [InlineData("hogehoge", false)] diff --git a/OpenTween.Tests/TestUtils.cs b/OpenTween.Tests/TestUtils.cs index c1c4e8869..9d5848a84 100644 --- a/OpenTween.Tests/TestUtils.cs +++ b/OpenTween.Tests/TestUtils.cs @@ -20,16 +20,16 @@ // Boston, MA 02110-1301, USA. using System; -using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Imaging; using System.IO; -using System.Linq; +using System.Net; +using System.Net.Http; using System.Reflection; -using System.Text; using System.Threading.Tasks; using System.Windows.Forms; +using OpenTween.Connection; using Xunit; namespace OpenTween @@ -152,6 +152,22 @@ public void Dispose() public static DateTimeUtc LocalTime(int year, int month, int day, int hour, int minute, int second) => new(new DateTimeOffset(year, month, day, hour, minute, second, TimeZoneInfo.Local.BaseUtcOffset)); + + public static async Task CreateApiResponse(string path) + { + byte[] buffer; + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + buffer = new byte[stream.Length]; + await stream.ReadAsync(buffer, 0, buffer.Length); + } + var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new ByteArrayContent(buffer), + }; + return new ApiResponse(responseMessage); + } } } diff --git a/OpenTween.Tests/Thumbnail/Services/TonTwitterComTest.cs b/OpenTween.Tests/Thumbnail/Services/TonTwitterComTest.cs new file mode 100644 index 000000000..235c9c823 --- /dev/null +++ b/OpenTween.Tests/Thumbnail/Services/TonTwitterComTest.cs @@ -0,0 +1,135 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using OpenTween.Connection; +using Xunit; + +namespace OpenTween.Thumbnail.Services +{ + public class TonTwitterComTest + { + [Fact] + public async Task GetThumbnailInfoAsync_Test() + { + var mock = new Mock(); + TonTwitterCom.GetApiConnection = () => mock.Object; + + var service = new TonTwitterCom(); + var thumb = await service.GetThumbnailInfoAsync( + "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg", + new(), + CancellationToken.None + ); + + Assert.NotNull(thumb!); + Assert.Equal( + "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg:large", + thumb.MediaPageUrl + ); + Assert.Equal( + "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg:large", + thumb.FullSizeImageUrl + ); + Assert.Equal( + "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg", + thumb.ThumbnailImageUrl + ); + + TonTwitterCom.GetApiConnection = null; + } + + [Fact] + public async Task GetThumbnailInfoAsync_ApiConnectionIsNotSetTest() + { + TonTwitterCom.GetApiConnection = null; + + var service = new TonTwitterCom(); + var thumb = await service.GetThumbnailInfoAsync( + "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg", + new(), + CancellationToken.None + ); + + Assert.Null(thumb); + } + + [Fact] + public async Task GetThumbnailInfoAsync_NotMatchedTest() + { + var mock = new Mock(); + TonTwitterCom.GetApiConnection = () => mock.Object; + + var service = new TonTwitterCom(); + var thumb = await service.GetThumbnailInfoAsync( + "https://example.com/abcdef.jpg", + new(), + CancellationToken.None + ); + + Assert.Null(thumb); + + TonTwitterCom.GetApiConnection = null; + } + + [Fact] + public async Task LoadThumbnailImageAsync_Test() + { + using var image = TestUtils.CreateDummyImage(); + using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new ByteArrayContent(image.Stream.ToArray()), + }; + using var response = new ApiResponse(responseMessage); + + var mock = new Mock(); + mock.Setup( + x => x.SendAsync(It.IsAny()) + ) + .Callback(x => + { + var request = Assert.IsType(x); + Assert.Equal( + new("https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg"), + request.RequestUri + ); + }) + .ReturnsAsync(response); + + var apiConnection = mock.Object; + var thumb = new TonTwitterCom.Thumbnail(apiConnection) + { + MediaPageUrl = "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg:large", + FullSizeImageUrl = "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg:large", + ThumbnailImageUrl = "https://ton.twitter.com/1.1/ton/data/dm/123456/123456/abcdef.jpg", + }; + + var result = await thumb.LoadThumbnailImageAsync(CancellationToken.None); + Assert.Equal(image, result); + + mock.VerifyAll(); + } + } +} diff --git a/OpenTween/Api/GraphQL/CreateRetweetRequest.cs b/OpenTween/Api/GraphQL/CreateRetweetRequest.cs index 33c579df2..35b5060e2 100644 --- a/OpenTween/Api/GraphQL/CreateRetweetRequest.cs +++ b/OpenTween/Api/GraphQL/CreateRetweetRequest.cs @@ -49,7 +49,7 @@ public string CreateRequestBody() """; } - public async Task Send(IApiConnection apiConnection) + public async Task Send(IApiConnectionLegacy apiConnection) { var json = this.CreateRequestBody(); var response = await apiConnection.PostJsonAsync(EndpointUri, json); diff --git a/OpenTween/Api/GraphQL/CreateTweetRequest.cs b/OpenTween/Api/GraphQL/CreateTweetRequest.cs index d2b0f229c..46e1075ac 100644 --- a/OpenTween/Api/GraphQL/CreateTweetRequest.cs +++ b/OpenTween/Api/GraphQL/CreateTweetRequest.cs @@ -153,7 +153,7 @@ public string CreateRequestBody() return JsonUtils.SerializeJsonByDataContract(body); } - public async Task Send(IApiConnection apiConnection) + public async Task Send(IApiConnectionLegacy apiConnection) { var json = this.CreateRequestBody(); var response = await apiConnection.PostJsonAsync(EndpointUri, json); diff --git a/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs b/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs index ab5b0aedc..00a0b8368 100644 --- a/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs +++ b/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs @@ -45,7 +45,7 @@ public string CreateRequestBody() """; } - public async Task Send(IApiConnection apiConnection) + public async Task Send(IApiConnectionLegacy apiConnection) { var json = this.CreateRequestBody(); var responseText = await apiConnection.PostJsonAsync(EndpointUri, json); diff --git a/OpenTween/Api/GraphQL/DeleteTweetRequest.cs b/OpenTween/Api/GraphQL/DeleteTweetRequest.cs index feeee14eb..adccacec0 100644 --- a/OpenTween/Api/GraphQL/DeleteTweetRequest.cs +++ b/OpenTween/Api/GraphQL/DeleteTweetRequest.cs @@ -45,7 +45,7 @@ public string CreateRequestBody() """; } - public async Task Send(IApiConnection apiConnection) + public async Task Send(IApiConnectionLegacy apiConnection) { var json = this.CreateRequestBody(); var responseText = await apiConnection.PostJsonAsync(EndpointUri, json); diff --git a/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs b/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs index a241e4a86..3aa306f6f 100644 --- a/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs +++ b/OpenTween/Api/GraphQL/ListLatestTweetsTimelineRequest.cs @@ -23,13 +23,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; using System.Xml.XPath; using OpenTween.Connection; @@ -86,24 +80,18 @@ public Dictionary CreateParameters() public async Task Send(IApiConnection apiConnection) { - var param = this.CreateParameters(); - - XElement rootElm; - try - { - using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max); - rootElm = XElement.Load(jsonReader); - } - catch (IOException ex) - { - throw new WebApiException("IO Error", ex); - } - catch (NotSupportedException ex) + var request = new GetRequest { - // NotSupportedException: Stream does not support reading. のエラーが時々報告される - throw new WebApiException("Stream Error", ex); - } + RequestUri = EndpointUri, + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); ErrorResponse.ThrowIfError(rootElm); diff --git a/OpenTween/Api/GraphQL/SearchTimelineRequest.cs b/OpenTween/Api/GraphQL/SearchTimelineRequest.cs index 9e76ca05b..568371806 100644 --- a/OpenTween/Api/GraphQL/SearchTimelineRequest.cs +++ b/OpenTween/Api/GraphQL/SearchTimelineRequest.cs @@ -23,13 +23,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; using System.Xml.XPath; using OpenTween.Connection; @@ -88,24 +82,18 @@ public Dictionary CreateParameters() public async Task Send(IApiConnection apiConnection) { - var param = this.CreateParameters(); - - XElement rootElm; - try - { - using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max); - rootElm = XElement.Load(jsonReader); - } - catch (IOException ex) - { - throw new WebApiException("IO Error", ex); - } - catch (NotSupportedException ex) + var request = new GetRequest { - // NotSupportedException: Stream does not support reading. のエラーが時々報告される - throw new WebApiException("Stream Error", ex); - } + RequestUri = EndpointUri, + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); ErrorResponse.ThrowIfError(rootElm); diff --git a/OpenTween/Api/GraphQL/TweetDetailRequest.cs b/OpenTween/Api/GraphQL/TweetDetailRequest.cs index 496e2f47c..b8c4151d3 100644 --- a/OpenTween/Api/GraphQL/TweetDetailRequest.cs +++ b/OpenTween/Api/GraphQL/TweetDetailRequest.cs @@ -23,14 +23,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; -using System.Xml.XPath; using OpenTween.Connection; using OpenTween.Models; @@ -62,24 +55,18 @@ public Dictionary CreateParameters() public async Task Send(IApiConnection apiConnection) { - var param = this.CreateParameters(); - - XElement rootElm; - try - { - using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max); - rootElm = XElement.Load(jsonReader); - } - catch (IOException ex) - { - throw new WebApiException("IO Error", ex); - } - catch (NotSupportedException ex) + var request = new GetRequest { - // NotSupportedException: Stream does not support reading. のエラーが時々報告される - throw new WebApiException("Stream Error", ex); - } + RequestUri = EndpointUri, + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); ErrorResponse.ThrowIfError(rootElm); diff --git a/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs b/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs index 8d5607ef5..4b3c8ccad 100644 --- a/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs +++ b/OpenTween/Api/GraphQL/UserByScreenNameRequest.cs @@ -23,12 +23,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading.Tasks; -using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using OpenTween.Connection; @@ -61,24 +56,18 @@ public Dictionary CreateParameters() public async Task Send(IApiConnection apiConnection) { - var param = this.CreateParameters(); - - XElement rootElm; - try - { - using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max); - rootElm = XElement.Load(jsonReader); - } - catch (IOException ex) - { - throw new WebApiException("IO Error", ex); - } - catch (NotSupportedException ex) + var request = new GetRequest { - // NotSupportedException: Stream does not support reading. のエラーが時々報告される - throw new WebApiException("Stream Error", ex); - } + RequestUri = EndpointUri, + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); ErrorResponse.ThrowIfError(rootElm); diff --git a/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs b/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs index 916a914b8..fff651342 100644 --- a/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs +++ b/OpenTween/Api/GraphQL/UserTweetsAndRepliesRequest.cs @@ -23,13 +23,7 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Serialization.Json; -using System.Text; using System.Threading.Tasks; -using System.Xml; -using System.Xml.Linq; using System.Xml.XPath; using OpenTween.Connection; @@ -71,24 +65,18 @@ public Dictionary CreateParameters() public async Task Send(IApiConnection apiConnection) { - var param = this.CreateParameters(); - - XElement rootElm; - try - { - using var stream = await apiConnection.GetStreamAsync(EndpointUri, param, EndpointName); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(stream, XmlDictionaryReaderQuotas.Max); - rootElm = XElement.Load(jsonReader); - } - catch (IOException ex) - { - throw new WebApiException("IO Error", ex); - } - catch (NotSupportedException ex) + var request = new GetRequest { - // NotSupportedException: Stream does not support reading. のエラーが時々報告される - throw new WebApiException("Stream Error", ex); - } + RequestUri = EndpointUri, + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); ErrorResponse.ThrowIfError(rootElm); diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 1f3b68e63..abce76331 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -41,9 +41,9 @@ public sealed class TwitterApi : IDisposable public string CurrentScreenName { get; private set; } = ""; - public IApiConnection Connection => this.ApiConnection ?? throw new InvalidOperationException(); + public IApiConnectionLegacy Connection => this.ApiConnection ?? throw new InvalidOperationException(); - internal IApiConnection? ApiConnection; + internal IApiConnectionLegacy? ApiConnection; public TwitterAppToken AppToken { get; private set; } = TwitterAppToken.GetDefault(); diff --git a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs index 6ff98cf55..677d167ae 100644 --- a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs +++ b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs @@ -69,7 +69,7 @@ private Dictionary CreateParameters() return param; } - public Task Send(IApiConnection apiConnection) + public Task Send(IApiConnectionLegacy apiConnection) { var uri = this.CreateEndpointUri(); var param = this.CreateParameters(); diff --git a/OpenTween/Connection/ApiResponse.cs b/OpenTween/Connection/ApiResponse.cs new file mode 100644 index 000000000..3eabcb661 --- /dev/null +++ b/OpenTween/Connection/ApiResponse.cs @@ -0,0 +1,99 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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. + +#nullable enable + +using System; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using OpenTween.Api; + +namespace OpenTween.Connection +{ + public sealed class ApiResponse : IDisposable + { + public bool IsDisposed { get; private set; } + + private readonly HttpResponseMessage responseMessage; + + public ApiResponse(HttpResponseMessage responseMessage) + => this.responseMessage = responseMessage; + + public void Dispose() + { + if (this.IsDisposed) + return; + + this.responseMessage.Dispose(); + this.IsDisposed = true; + } + + public async Task ReadAsBytes() + { + using var content = this.responseMessage.Content; + + return await content.ReadAsByteArrayAsync() + .ConfigureAwait(false); + } + + public async Task ReadAsJson() + { + var responseBytes = await this.ReadAsBytes() + .ConfigureAwait(false); + + try + { + return MyCommon.CreateDataFromJson(responseBytes); + } + catch (SerializationException ex) + { + var responseText = Encoding.UTF8.GetString(responseBytes); + throw TwitterApiException.CreateFromException(ex, responseText); + } + } + + public async Task ReadAsJsonXml() + { + var responseBytes = await this.ReadAsBytes() + .ConfigureAwait(false); + + using var jsonReader = JsonReaderWriterFactory.CreateJsonReader( + responseBytes, + XmlDictionaryReaderQuotas.Max + ); + + try + { + return XElement.Load(jsonReader); + } + catch (XmlException ex) + { + var responseText = Encoding.UTF8.GetString(responseBytes); + throw new TwitterApiException("Invalid JSON", ex) { ResponseText = responseText }; + } + } + } +} diff --git a/OpenTween/Connection/GetRequest.cs b/OpenTween/Connection/GetRequest.cs new file mode 100644 index 000000000..018354a13 --- /dev/null +++ b/OpenTween/Connection/GetRequest.cs @@ -0,0 +1,56 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Net.Http; + +namespace OpenTween.Connection +{ + public class GetRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public IDictionary? Query { get; set; } + + public string? EndpointName { get; set; } + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Get, + RequestUri = BuildUriWithQuery(new(baseUri, this.RequestUri), this.Query), + }; + + public static Uri BuildUriWithQuery(Uri uri, IEnumerable>? query) + { + if (query == null) + return uri; + + if (!MyCommon.IsNullOrEmpty(uri.Query)) + throw new NotSupportedException("Merging uri query is not supported"); + + return new Uri(uri, "?" + MyCommon.BuildQueryString(query)); + } + } +} diff --git a/OpenTween/Connection/IApiConnection.cs b/OpenTween/Connection/IApiConnection.cs index b7c866f4c..16e816668 100644 --- a/OpenTween/Connection/IApiConnection.cs +++ b/OpenTween/Connection/IApiConnection.cs @@ -1,5 +1,5 @@ // OpenTween - Client of Twitter -// Copyright (c) 2016 kim_upsilon (@kim_upsilon) +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. @@ -22,34 +22,12 @@ #nullable enable using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace OpenTween.Connection { public interface IApiConnection : IDisposable { - Task GetAsync(Uri uri, IDictionary? param, string? endpointName); - - Task GetStreamAsync(Uri uri, IDictionary? param); - - Task GetStreamAsync(Uri uri, IDictionary? param, string? endpointName); - - Task GetStreamingStreamAsync(Uri uri, IDictionary? param); - - Task> PostLazyAsync(Uri uri, IDictionary? param); - - Task> PostLazyAsync(Uri uri, IDictionary? param, IDictionary? media); - - Task PostAsync(Uri uri, IDictionary? param, IDictionary? media); - - Task PostJsonAsync(Uri uri, string json); - - Task> PostJsonAsync(Uri uri, string json); - - Task DeleteAsync(Uri uri); + Task SendAsync(IHttpRequest request); } } diff --git a/OpenTween/Connection/IApiConnectionLegacy.cs b/OpenTween/Connection/IApiConnectionLegacy.cs new file mode 100644 index 000000000..4175e589d --- /dev/null +++ b/OpenTween/Connection/IApiConnectionLegacy.cs @@ -0,0 +1,55 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2016 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. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace OpenTween.Connection +{ + public interface IApiConnectionLegacy : IApiConnection, IDisposable + { + Task GetAsync(Uri uri, IDictionary? param, string? endpointName); + + Task GetStreamAsync(Uri uri, IDictionary? param); + + Task GetStreamAsync(Uri uri, IDictionary? param, string? endpointName); + + Task GetStreamingStreamAsync(Uri uri, IDictionary? param); + + Task> PostLazyAsync(Uri uri, IDictionary? param); + + Task> PostLazyAsync(Uri uri, IDictionary? param, IDictionary? media); + + Task PostAsync(Uri uri, IDictionary? param, IDictionary? media); + + Task PostJsonAsync(Uri uri, string json); + + Task> PostJsonAsync(Uri uri, string json); + + Task DeleteAsync(Uri uri); + } +} diff --git a/OpenTween/Connection/IHttpRequest.cs b/OpenTween/Connection/IHttpRequest.cs new file mode 100644 index 000000000..55dd9e3b4 --- /dev/null +++ b/OpenTween/Connection/IHttpRequest.cs @@ -0,0 +1,35 @@ +// OpenTween - Client of Twitter +// Copyright (c) 2023 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. + +#nullable enable + +using System; +using System.Net.Http; + +namespace OpenTween.Connection +{ + public interface IHttpRequest + { + string? EndpointName { get; } + + HttpRequestMessage CreateMessage(Uri baseUri); + } +} diff --git a/OpenTween/Connection/TwitterApiConnection.cs b/OpenTween/Connection/TwitterApiConnection.cs index b8bd085e1..091a32032 100644 --- a/OpenTween/Connection/TwitterApiConnection.cs +++ b/OpenTween/Connection/TwitterApiConnection.cs @@ -39,7 +39,7 @@ namespace OpenTween.Connection { - public class TwitterApiConnection : IApiConnection, IDisposable + public class TwitterApiConnection : IApiConnection, IApiConnectionLegacy, IDisposable { public static Uri RestApiBase { get; set; } = new("https://api.twitter.com/1.1/"); @@ -98,42 +98,34 @@ private void InitializeHttpClients() this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan; } - public async Task GetAsync(Uri uri, IDictionary? param, string? endpointName) + public async Task SendAsync(IHttpRequest request) { + var endpointName = request.EndpointName; + // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる if (endpointName != null) this.ThrowIfRateLimitExceeded(endpointName); - var requestUri = new Uri(RestApiBase, uri); - - if (param != null) - requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); - - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + using var requestMessage = request.CreateMessage(RestApiBase); + HttpResponseMessage? responseMessage = null; try { - using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); + responseMessage = await HandleTimeout( + (token) => this.Http.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, token), + Networking.DefaultTimeout + ); if (endpointName != null) - MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName); + MyCommon.TwitterApiInfo.UpdateFromHeader(responseMessage.Headers, endpointName); - await TwitterApiConnection.CheckStatusCode(response) + await TwitterApiConnection.CheckStatusCode(responseMessage) .ConfigureAwait(false); - using var content = response.Content; - var responseText = await content.ReadAsStringAsync() - .ConfigureAwait(false); + var response = new ApiResponse(responseMessage); + responseMessage = null; // responseMessage は ApiResponse で使用するため破棄されないようにする - try - { - return MyCommon.CreateDataFromJson(responseText); - } - catch (SerializationException ex) - { - throw TwitterApiException.CreateFromException(ex, responseText); - } + return response; } catch (HttpRequestException ex) { @@ -143,6 +135,26 @@ await TwitterApiConnection.CheckStatusCode(response) { throw TwitterApiException.CreateFromException(ex); } + finally + { + responseMessage?.Dispose(); + } + } + + public async Task GetAsync(Uri uri, IDictionary? param, string? endpointName) + { + var request = new GetRequest + { + RequestUri = uri, + Query = param, + EndpointName = endpointName, + }; + + using var response = await this.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } /// @@ -442,6 +454,38 @@ await TwitterApiConnection.CheckStatusCode(response) } } + public static async Task HandleTimeout(Func> func, TimeSpan timeout) + { + using var cts = new CancellationTokenSource(); + var cancellactionToken = cts.Token; + + var task = Task.Run(() => func(cancellactionToken), cancellactionToken); + var timeoutTask = Task.Delay(timeout); + + if (await Task.WhenAny(task, timeoutTask) == timeoutTask) + { + // タイムアウト + + // キャンセル後のタスクで発生した例外は無視する + static async Task IgnoreExceptions(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch + { + } + } + _ = IgnoreExceptions(task); + cts.Cancel(); + + throw new OperationCanceledException("Timeout", cancellactionToken); + } + + return await task; + } + protected static async Task CheckStatusCode(HttpResponseMessage response) { var statusCode = response.StatusCode; diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index 5e51960fe..e6b1f3d7c 100644 --- a/OpenTween/MyCommon.cs +++ b/OpenTween/MyCommon.cs @@ -719,9 +719,11 @@ public static DateTimeUtc DateTimeParse(string input) } public static T CreateDataFromJson(string content) + => MyCommon.CreateDataFromJson(Encoding.UTF8.GetBytes(content)); + + public static T CreateDataFromJson(byte[] bytes) { - var buf = Encoding.Unicode.GetBytes(content); - using var stream = new MemoryStream(buf); + using var stream = new MemoryStream(bytes); var settings = new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true, diff --git a/OpenTween/Thumbnail/Services/TonTwitterCom.cs b/OpenTween/Thumbnail/Services/TonTwitterCom.cs index 80fe74fe3..4e55b97f6 100644 --- a/OpenTween/Thumbnail/Services/TonTwitterCom.cs +++ b/OpenTween/Thumbnail/Services/TonTwitterCom.cs @@ -53,8 +53,9 @@ public class TonTwitterCom : IThumbnailService return null; var largeUrl = url + ":large"; + var apiConnection = GetApiConnection(); - return new TonTwitterCom.Thumbnail + return new TonTwitterCom.Thumbnail(apiConnection) { MediaPageUrl = largeUrl, ThumbnailImageUrl = url, @@ -67,22 +68,31 @@ public class TonTwitterCom : IThumbnailService public class Thumbnail : ThumbnailInfo { + private readonly IApiConnection apiConnection; + + public Thumbnail(IApiConnection apiConnection) + => this.apiConnection = apiConnection; + public override Task LoadThumbnailImageAsync(HttpClient http, CancellationToken cancellationToken) { - return Task.Run( - async () => + return Task.Run(async () => + { + var request = new GetRequest { - var apiConnection = TonTwitterCom.GetApiConnection!(); + RequestUri = new(this.ThumbnailImageUrl), + }; + + using var response = await this.apiConnection.SendAsync(request) + .ConfigureAwait(false); - using var imageStream = await apiConnection.GetStreamAsync(new Uri(this.ThumbnailImageUrl), null) - .ConfigureAwait(false); + var imageBytes = await response.ReadAsBytes() + .ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - return await MemoryImage.CopyFromStreamAsync(imageStream) - .ConfigureAwait(false); - }, - cancellationToken); + return MemoryImage.CopyFromBytes(imageBytes); + }, + cancellationToken); } } }