diff --git a/.gitattributes b/.gitattributes index 176a458f9..2e46fbac0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto +*.sh eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8ace1bd33..331746e00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,10 @@ -name: Build +name: Run msbuild on: - push: - branches: ['develop', 'release'] - pull_request: + workflow_call: + inputs: + msbuild_args: + type: string env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages @@ -14,6 +15,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.1 @@ -37,7 +40,7 @@ jobs: - name: Build shell: pwsh run: | - msbuild /target:restore,build "/p:Configuration=$($env:CONFIGURATION)" /p:DebugType=None /verbosity:minimal + msbuild /target:restore,build "/p:Configuration=$($env:CONFIGURATION)" /verbosity:minimal ${{ inputs.msbuild_args }} - name: Upload build result uses: actions/upload-artifact@v3 @@ -45,136 +48,5 @@ jobs: name: build path: | ./OpenTween/bin/ - ./OpenTween/obj/ ./OpenTween.Tests/bin/ retention-days: 1 - - test: - runs-on: windows-2022 - needs: [build] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1 - - - name: Set configuration env - shell: pwsh - run: | - if ($env:GITHUB_REF -eq 'refs/heads/release') { - echo 'CONFIGURATION=Release' >> $env:GITHUB_ENV - } else { - echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV - } - - - uses: actions/cache@v3 - with: - path: ${{ github.workspace }}/.nuget/packages - key: nuget-${{ hashFiles('*/*.csproj') }} - restore-keys: | - nuget- - - - name: Restore build result - uses: actions/download-artifact@v3 - with: - name: build - - - name: Run tests - shell: pwsh - run: | - $altCoverVersion = '8.6.61' - $xunitVersion = '2.4.2' - $targetFramework = 'net48' - $altCoverPath = "$($env:NUGET_PACKAGES)\altcover\$($altCoverVersion)\tools\net472\AltCover.exe" - $xunitPath = "$($env:NUGET_PACKAGES)\xunit.runner.console\$($xunitVersion)\tools\net472\xunit.console.exe" - - $p = Start-Process ` - -FilePath $altCoverPath ` - -ArgumentList ( - '--inputDirectory', - ".\OpenTween.Tests\bin\$($env:CONFIGURATION)\$($targetFramework)", - '--outputDirectory', - '.\__Instrumented\', - '--assemblyFilter', - '?^OpenTween(?!\.Tests)', - '--typeFilter', - '?^OpenTween\.', - '--fileFilter', - '\.Designer\.cs', - '--visibleBranches' - ) ` - -NoNewWindow ` - -PassThru ` - -Wait - - if ($p.ExitCode -ne 0) { - exit $p.ExitCode - } - - $p = Start-Process ` - -FilePath $altCoverPath ` - -ArgumentList ( - 'runner', - '--recorderDirectory', - '.\__Instrumented\', - '--executable', - $xunitPath, - '--', - '.\__Instrumented\OpenTween.Tests.dll' - ) ` - -NoNewWindow ` - -PassThru ` - -Wait - - if ($p.ExitCode -ne 0) { - exit $p.ExitCode - } - - - name: Upload test results to codecov - shell: pwsh - run: | - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - .\codecov.exe -f coverage.xml - - package: - runs-on: windows-2022 - needs: [build] - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - ref: '${{ github.event.pull_request.head.sha }}' - - - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1 - - - name: Set configuration env - shell: pwsh - run: | - if ($env:GITHUB_REF -eq 'refs/heads/release') { - echo 'CONFIGURATION=Release' >> $env:GITHUB_ENV - } else { - echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV - } - - - name: Restore build result - uses: actions/download-artifact@v3 - with: - name: build - - - name: Build package - shell: powershell # runtime-versionを取得するため従来のPowershellを使用する - run: | - $env:PATH = $env:PATH + ';C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\Roslyn\' - $binDir = '.\OpenTween\bin\' + $env:CONFIGURATION + '\net48\' - $destPath = 'OpenTween.zip' - $headCommit = '${{ github.event.pull_request.head.sha }}' - .\tools\build-zip-archive.ps1 -BinDir $binDir -DestPath $destPath -HeadCommit $headCommit - - - name: Upload build result - uses: actions/upload-artifact@v3 - with: - name: package - path: | - ./OpenTween.zip diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 000000000..aeda25940 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,57 @@ +name: Build reproducible zip archive + +on: + push: + branches: ['develop', 'release'] + pull_request: + +env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + +jobs: + build: + uses: ./.github/workflows/build.yml + with: + # package のビルド時は *.pdb を生成しない (https://github.com/opentween/OpenTween/pull/256) + msbuild_args: /p:DebugType=None + + package: + runs-on: windows-2022 + needs: [build] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: '${{ github.event.pull_request.head.sha }}' + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.1 + + - name: Set configuration env + shell: pwsh + run: | + if ($env:GITHUB_REF -eq 'refs/heads/release') { + echo 'CONFIGURATION=Release' >> $env:GITHUB_ENV + } else { + echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV + } + + - name: Restore build result + uses: actions/download-artifact@v3 + with: + name: build + + - name: Build package + shell: powershell # runtime-versionを取得するため従来のPowershellを使用する + run: | + $env:PATH = $env:PATH + ';C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Msbuild\Current\Bin\Roslyn\' + $binDir = '.\OpenTween\bin\' + $env:CONFIGURATION + '\net48\' + $destPath = 'OpenTween.zip' + .\tools\build-zip-archive.ps1 -BinDir $binDir -DestPath $destPath + + - name: Upload build result + uses: actions/upload-artifact@v3 + with: + name: package + path: | + ./OpenTween.zip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..dcfad8c23 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,102 @@ +name: Run tests + +on: + push: + branches: ['develop', 'release'] + pull_request: + +env: + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages + +jobs: + build: + uses: ./.github/workflows/build.yml + + test: + runs-on: windows-2022 + needs: [build] + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Add msbuild to PATH + uses: microsoft/setup-msbuild@v1.1 + + - name: Set configuration env + shell: pwsh + run: | + if ($env:GITHUB_REF -eq 'refs/heads/release') { + echo 'CONFIGURATION=Release' >> $env:GITHUB_ENV + } else { + echo 'CONFIGURATION=Debug' >> $env:GITHUB_ENV + } + + - uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.nuget/packages + key: nuget-${{ hashFiles('*/*.csproj') }} + restore-keys: | + nuget- + + - name: Restore build result + uses: actions/download-artifact@v3 + with: + name: build + + - name: Run tests + shell: pwsh + run: | + $altCoverVersion = '8.6.95' + $xunitVersion = '2.6.2' + $targetFramework = 'net48' + $altCoverPath = "$($env:NUGET_PACKAGES)\altcover\$($altCoverVersion)\tools\net472\AltCover.exe" + $xunitPath = "$($env:NUGET_PACKAGES)\xunit.runner.console\$($xunitVersion)\tools\net481\xunit.console.exe" + + $p = Start-Process ` + -FilePath $altCoverPath ` + -ArgumentList ( + '--inputDirectory', + ".\OpenTween.Tests\bin\$($env:CONFIGURATION)\$($targetFramework)", + '--outputDirectory', + '.\__Instrumented\', + '--assemblyFilter', + '?^OpenTween(?!\.Tests)', + '--typeFilter', + '?^OpenTween\.', + '--fileFilter', + '\.Designer\.cs', + '--visibleBranches' + ) ` + -NoNewWindow ` + -PassThru ` + -Wait + + if ($p.ExitCode -ne 0) { + exit $p.ExitCode + } + + $p = Start-Process ` + -FilePath $altCoverPath ` + -ArgumentList ( + 'runner', + '--recorderDirectory', + '.\__Instrumented\', + '--executable', + $xunitPath, + '--', + '.\__Instrumented\OpenTween.Tests.dll' + ) ` + -NoNewWindow ` + -PassThru ` + -Wait + + if ($p.ExitCode -ne 0) { + exit $p.ExitCode + } + + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ffc2bc540..51ceeb19e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,9 @@ 更新履歴 +==== Ver 3.10.0(2023/12/16) + * NEW: graphqlエンドポイント経由で取得した引用ツイートの表示に対応 + * FIX: APIリクエストがタイムアウトした場合のキャンセル処理を改善 + ==== Ver 3.9.0(2023/12/03) * NEW: graphqlエンドポイントに対するレートリミットの表示に対応 * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更 diff --git a/OpenTween.Tests/Api/BitlyApiTest.cs b/OpenTween.Tests/Api/BitlyApiTest.cs index b8c7f4a66..73c295e46 100644 --- a/OpenTween.Tests/Api/BitlyApiTest.cs +++ b/OpenTween.Tests/Api/BitlyApiTest.cs @@ -60,8 +60,7 @@ public async Task ShortenAsync_OAuth2Test() bitly.EndUserAccessToken = "hogehoge"; - var result = await bitly.ShortenAsync(new Uri("http://www.example.com/"), "bit.ly") - .ConfigureAwait(false); + var result = await bitly.ShortenAsync(new Uri("http://www.example.com/"), "bit.ly"); Assert.Equal("http://bit.ly/foo", result.OriginalString); Assert.Equal(0, mockHandler.QueueCount); @@ -96,8 +95,7 @@ public async Task ShortenAsync_LegacyApiKeyTest() bitly.EndUserLoginName = "username"; bitly.EndUserApiKey = "hogehoge"; - var result = await bitly.ShortenAsync(new Uri("http://www.example.com/"), "bit.ly") - .ConfigureAwait(false); + var result = await bitly.ShortenAsync(new Uri("http://www.example.com/"), "bit.ly"); Assert.Equal("http://bit.ly/foo", result.OriginalString); Assert.Equal(0, mockHandler.QueueCount); @@ -122,8 +120,7 @@ public async Task GetAccessTokenAsync_Test() x.Headers.Authorization.Parameter ); - var body = await x.Content.ReadAsStringAsync() - .ConfigureAwait(false); + var body = await x.Content.ReadAsStringAsync(); var query = HttpUtility.ParseQueryString(body); Assert.Equal("password", query["grant_type"]); @@ -136,8 +133,7 @@ public async Task GetAccessTokenAsync_Test() }; }); - var result = await bitly.GetAccessTokenAsync("hogehoge", "tetete") - .ConfigureAwait(false); + var result = await bitly.GetAccessTokenAsync("hogehoge", "tetete"); Assert.Equal("abcdefg", result); Assert.Equal(0, mockHandler.QueueCount); @@ -158,8 +154,7 @@ public async Task GetAccessTokenAsync_ErrorResponseTest() }; }); - await Assert.ThrowsAsync(() => bitly.GetAccessTokenAsync("hogehoge", "tetete")) - .ConfigureAwait(false); + await Assert.ThrowsAsync(() => bitly.GetAccessTokenAsync("hogehoge", "tetete")); Assert.Equal(0, mockHandler.QueueCount); } diff --git a/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs index 40f4db801..233fd0143 100644 --- a/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/CreateRetweetRequestTest.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,25 +31,26 @@ public class CreateRetweetRequestTest [Fact] public async Task Send_Test() { - var responseText = File.ReadAllText("Resources/Responses/CreateRetweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/CreateRetweet.json"); var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet"), url); - Assert.Contains(@"""tweet_id"":""12345""", json); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet"), request.RequestUri); + Assert.Contains(@"""tweet_id"":""12345""", request.JsonString); }) - .ReturnsAsync(responseText); + .ReturnsAsync(apiResponse); var request = new CreateRetweetRequest { TweetId = new("12345"), }; - var tweetId = await request.Send(mock.Object).ConfigureAwait(false); + var tweetId = await request.Send(mock.Object); Assert.Equal("1617128268548964354", tweetId.Id); mock.VerifyAll(); diff --git a/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs index 0565e4793..b01e95a7f 100644 --- a/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/CreateTweetRequestTest.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,27 +31,28 @@ public class CreateTweetRequestTest [Fact] public async Task Send_Test() { - var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/CreateTweet_CircleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet"), url); - Assert.Contains(@"""tweet_text"":""tetete""", json); - Assert.DoesNotContain(@"""reply"":", json); - Assert.DoesNotContain(@"""media"":", json); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet"), request.RequestUri); + Assert.Contains(@"""tweet_text"":""tetete""", request.JsonString); + Assert.DoesNotContain(@"""reply"":", request.JsonString); + Assert.DoesNotContain(@"""media"":", request.JsonString); }) - .ReturnsAsync(responseText); + .ReturnsAsync(apiResponse); var request = new CreateTweetRequest { TweetText = "tetete", }; - var status = await request.Send(mock.Object).ConfigureAwait(false); + var status = await request.Send(mock.Object); Assert.Equal("1680534146492317696", status.IdStr); mock.VerifyAll(); @@ -65,17 +61,18 @@ public async Task Send_Test() [Fact] public async Task Send_ReplyTest() { - var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/CreateTweet_CircleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Contains(@"""reply"":{""exclude_reply_user_ids"":[""11111"",""22222""],""in_reply_to_tweet_id"":""12345""}", json); + var request = Assert.IsType(x); + Assert.Contains(@"""reply"":{""exclude_reply_user_ids"":[""11111"",""22222""],""in_reply_to_tweet_id"":""12345""}", request.JsonString); }) - .ReturnsAsync(responseText); + .ReturnsAsync(apiResponse); var request = new CreateTweetRequest { @@ -83,31 +80,32 @@ public async Task Send_ReplyTest() InReplyToTweetId = new("12345"), ExcludeReplyUserIds = new[] { "11111", "22222" }, }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } [Fact] public async Task Send_MediaTest() { - var responseText = File.ReadAllText("Resources/Responses/CreateTweet_CircleTweet.json"); + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/CreateTweet_CircleTweet.json"); var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Contains(@"""media"":{""media_entities"":[{""media_id"":""11111"",""tagged_users"":[]},{""media_id"":""22222"",""tagged_users"":[]}],""possibly_sensitive"":false}", json); + var request = Assert.IsType(x); + Assert.Contains(@"""media"":{""media_entities"":[{""media_id"":""11111"",""tagged_users"":[]},{""media_id"":""22222"",""tagged_users"":[]}],""possibly_sensitive"":false}", request.JsonString); }) - .ReturnsAsync(responseText); + .ReturnsAsync(apiResponse); var request = new CreateTweetRequest { TweetText = "tetete", MediaIds = new[] { "11111", "22222" }, }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } } diff --git a/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs index 84f766410..eceafb83a 100644 --- a/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/DeleteRetweetRequestTest.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,22 +31,26 @@ public class DeleteRetweetRequestTest [Fact] public async Task Send_Test() { + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/DeleteRetweet.json"); + var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet"), url); - Assert.Contains(@"""source_tweet_id"":""12345""", json); - }); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet"), request.RequestUri); + Assert.Contains(@"""source_tweet_id"":""12345""", request.JsonString); + }) + .ReturnsAsync(apiResponse); var request = new DeleteRetweetRequest { SourceTweetId = new("12345"), }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } diff --git a/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs b/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs index e4ebd0c5e..6cd25806b 100644 --- a/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.cs +++ b/OpenTween.Tests/Api/GraphQL/DeleteTweetRequestTest.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,22 +31,26 @@ public class DeleteTweetRequestTest [Fact] public async Task Send_Test() { + using var apiResponse = await TestUtils.CreateApiResponse("Resources/Responses/DeleteTweet.json"); + var mock = new Mock(); mock.Setup(x => - x.PostJsonAsync(It.IsAny(), It.IsAny()) + x.SendAsync(It.IsAny()) ) - .Callback((url, json) => + .Callback(x => { - Assert.Equal(new("https://twitter.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet"), url); - Assert.Contains(@"""tweet_id"":""12345""", json); - }); + var request = Assert.IsType(x); + Assert.Equal(new("https://twitter.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet"), request.RequestUri); + Assert.Contains(@"""tweet_id"":""12345""", request.JsonString); + }) + .ReturnsAsync(apiResponse); var request = new DeleteTweetRequest { TweetId = new("12345"), }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } diff --git a/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs b/OpenTween.Tests/Api/GraphQL/ListLatestTweetsTimelineRequestTest.cs index dc24d7c7d..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,28 +31,30 @@ 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") { Count = 20, }; - var response = await request.Send(mock.Object).ConfigureAwait(false); + var response = await request.Send(mock.Object); Assert.Single(response.Tweets); Assert.Equal("DAABCgABF0HfRMjAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop); Assert.Equal("DAABCgABF0HfRMi__7QKAAIVAxUYmFWQAwgAAwAAAAIAAA", response.CursorBottom); @@ -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") { @@ -91,7 +89,7 @@ public async Task Send_RequestCursor_Test() Cursor = "aaa", }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } } diff --git a/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs b/OpenTween.Tests/Api/GraphQL/SearchTimelineRequestTest.cs index 97459b11d..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,28 +31,30 @@ 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") { Count = 20, }; - var response = await request.Send(mock.Object).ConfigureAwait(false); + var response = await request.Send(mock.Object); Assert.Single(response.Tweets); Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAABCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorTop); Assert.Equal("DAADDAABCgABFnlh4hraMAYKAAIOTm0DEhTAAQAIAAIAAAACCAADAAAAAAgABAAAAAAKAAUX8j3ezIAnEAoABhfyPd7Mf9jwAAA", response.CursorBottom); @@ -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") { @@ -90,7 +89,7 @@ public async Task Send_RequestCursor_Test() Cursor = "aaa", }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } } diff --git a/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs b/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs index 6747ad158..f1958321e 100644 --- a/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs +++ b/OpenTween.Tests/Api/GraphQL/TimelineTweetTest.cs @@ -139,6 +139,34 @@ public void ToStatus_WithTwitterPostFactory_SelfThread_Test() Assert.Equal(40480664L, post.UserId); } + [Fact] + public void ToStatus_WithTwitterPostFactory_QuotedTweet_Test() + { + var rootElm = this.LoadResponseDocument("TimelineTweet_QuotedTweet.json"); + var timelineTweet = new TimelineTweet(rootElm); + var status = timelineTweet.ToTwitterStatus(); + var postFactory = new TwitterPostFactory(this.CreateTabInfo()); + var post = postFactory.CreateFromStatus(status, selfUserId: 1L, new HashSet()); + + Assert.Equal("1588614645866147840", post.StatusId.Id); + var quotedPostId = Assert.Single(post.QuoteStatusIds); + Assert.Equal("1583108196868116480", quotedPostId.Id); + } + + [Fact] + public void ToStatus_WithTwitterPostFactory_QuotedTweet_Tombstone_Test() + { + var rootElm = this.LoadResponseDocument("TimelineTweet_QuotedTweet_Tombstone.json"); + var timelineTweet = new TimelineTweet(rootElm); + var status = timelineTweet.ToTwitterStatus(); + var postFactory = new TwitterPostFactory(this.CreateTabInfo()); + var post = postFactory.CreateFromStatus(status, selfUserId: 1L, new HashSet()); + + Assert.Equal("1614653321310253057", post.StatusId.Id); + var quotedPostId = Assert.Single(post.QuoteStatusIds); + Assert.Equal("1614650279194136576", quotedPostId.Id); + } + [Fact] public void ToStatus_WithTwitterPostFactory_PromotedTweet_Test() { diff --git a/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs b/OpenTween.Tests/Api/GraphQL/TweetDetailRequestTest.cs index 69b7c16a7..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,26 +32,28 @@ 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 { FocalTweetId = new("1619433164757413894"), }; - var tweets = await request.Send(mock.Object).ConfigureAwait(false); + var tweets = await request.Send(mock.Object); Assert.Equal("1619433164757413894", tweets.Single().ToTwitterStatus().IdStr); mock.VerifyAll(); diff --git a/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs b/OpenTween.Tests/Api/GraphQL/UserByScreenNameRequestTest.cs index 77786fe8e..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,26 +31,28 @@ 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 { ScreenName = "opentween", }; - var user = await request.Send(mock.Object).ConfigureAwait(false); + var user = await request.Send(mock.Object); Assert.Equal("514241801", user.ToTwitterUser().IdStr); mock.VerifyAll(); @@ -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 ea1c51ed5..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,28 +31,30 @@ 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") { Count = 20, }; - var response = await request.Send(mock.Object).ConfigureAwait(false); + var response = await request.Send(mock.Object); Assert.Single(response.Tweets); Assert.Equal("DAABCgABF_tTnZvAJxEKAAIWes8rE1oQAAgAAwAAAAEAAA", response.CursorTop); Assert.Equal("DAABCgABF_tTnZu__-0KAAIWZa6KTRoAAwgAAwAAAAIAAA", response.CursorBottom); @@ -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") { @@ -90,7 +89,7 @@ public async Task Send_RequestCursor_Test() Cursor = "aaa", }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } } diff --git a/OpenTween.Tests/Api/ImgurApiTest.cs b/OpenTween.Tests/Api/ImgurApiTest.cs index 0114f3cb4..b675a278d 100644 --- a/OpenTween.Tests/Api/ImgurApiTest.cs +++ b/OpenTween.Tests/Api/ImgurApiTest.cs @@ -86,8 +86,7 @@ public async Task UploadFileAsync_Test() var imgurApi = new ImgurApi(ApiKey.Create("fake_api_key"), http); using var mediaItem = TestUtils.CreateDummyMediaItem(); - var uploadedUrl = await imgurApi.UploadFileAsync(mediaItem, "てすと") - .ConfigureAwait(false); + var uploadedUrl = await imgurApi.UploadFileAsync(mediaItem, "てすと"); Assert.Equal("https://i.imgur.com/aaaaaaa.png", uploadedUrl); Assert.Equal(0, mockHandler.QueueCount); diff --git a/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs b/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs index 4587f02f5..47403f130 100644 --- a/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs +++ b/OpenTween.Tests/Api/MicrosoftTranslatorApiTest.cs @@ -60,8 +60,7 @@ public async Task TranslateAsync_Test() Assert.Equal("ja", query["to"]); Assert.Equal("en", query["from"]); - var requestBody = await x.Content.ReadAsByteArrayAsync() - .ConfigureAwait(false); + var requestBody = await x.Content.ReadAsByteArrayAsync(); using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(requestBody, XmlDictionaryReaderQuotas.Max)) { @@ -88,8 +87,7 @@ public async Task TranslateAsync_Test() }; }); - var result = await translateApi.TranslateAsync("hogehoge", langTo: "ja", langFrom: "en") - .ConfigureAwait(false); + var result = await translateApi.TranslateAsync("hogehoge", langTo: "ja", langFrom: "en"); Assert.Equal("ほげほげ", result); mock.Verify(x => x.GetAccessTokenAsync(), Times.Once()); @@ -116,14 +114,13 @@ await Assert.ThrowsAsync( [Fact] public async Task UpdateAccessTokenIfExpired_FirstCallTest() { - var mock = new Mock(ApiKey.Create("fake_api_key"), null); + var mock = new Mock(ApiKey.Create("fake_api_key"), null!); mock.Setup(x => x.GetAccessTokenAsync()) .ReturnsAsync(("1234abcd", TimeSpan.FromSeconds(1000))); var translateApi = mock.Object; - await translateApi.UpdateAccessTokenIfExpired() - .ConfigureAwait(false); + await translateApi.UpdateAccessTokenIfExpired(); Assert.Equal("1234abcd", translateApi.AccessToken); @@ -135,14 +132,13 @@ await translateApi.UpdateAccessTokenIfExpired() [Fact] public async Task UpdateAccessTokenIfExpired_NotExpiredTest() { - var mock = new Mock(ApiKey.Create("fake_api_key"), null); + var mock = new Mock(ApiKey.Create("fake_api_key"), null!); var translateApi = mock.Object; translateApi.AccessToken = "1234abcd"; translateApi.RefreshAccessTokenAt = DateTimeUtc.Now + TimeSpan.FromMinutes(3); - await translateApi.UpdateAccessTokenIfExpired() - .ConfigureAwait(false); + await translateApi.UpdateAccessTokenIfExpired(); // RefreshAccessTokenAt の時刻を過ぎるまでは GetAccessTokenAsync は呼ばれない mock.Verify(x => x.GetAccessTokenAsync(), Times.Never()); @@ -151,7 +147,7 @@ await translateApi.UpdateAccessTokenIfExpired() [Fact] public async Task UpdateAccessTokenIfExpired_ExpiredTest() { - var mock = new Mock(ApiKey.Create("fake_api_key"), null); + var mock = new Mock(ApiKey.Create("fake_api_key"), null!); mock.Setup(x => x.GetAccessTokenAsync()) .ReturnsAsync(("5678efgh", TimeSpan.FromSeconds(1000))); @@ -159,8 +155,7 @@ public async Task UpdateAccessTokenIfExpired_ExpiredTest() translateApi.AccessToken = "1234abcd"; translateApi.RefreshAccessTokenAt = DateTimeUtc.Now - TimeSpan.FromMinutes(3); - await translateApi.UpdateAccessTokenIfExpired() - .ConfigureAwait(false); + await translateApi.UpdateAccessTokenIfExpired(); Assert.Equal("5678efgh", translateApi.AccessToken); @@ -190,8 +185,7 @@ public async Task GetAccessTokenAsync_Test() }; }); - var result = await translateApi.GetAccessTokenAsync() - .ConfigureAwait(false); + var result = await translateApi.GetAccessTokenAsync(); var expectedToken = (@"ACCESS_TOKEN", TimeSpan.FromMinutes(10)); Assert.Equal(expectedToken, result); diff --git a/OpenTween.Tests/Api/MobypictureApiTest.cs b/OpenTween.Tests/Api/MobypictureApiTest.cs index dc0899dc7..af05cb96e 100644 --- a/OpenTween.Tests/Api/MobypictureApiTest.cs +++ b/OpenTween.Tests/Api/MobypictureApiTest.cs @@ -58,8 +58,7 @@ public async Task UploadFileAsync_Test() var mobypictureApi = new MobypictureApi(ApiKey.Create("fake_api_key"), http); using var mediaItem = TestUtils.CreateDummyMediaItem(); - var uploadedUrl = await mobypictureApi.UploadFileAsync(mediaItem, "てすと") - .ConfigureAwait(false); + var uploadedUrl = await mobypictureApi.UploadFileAsync(mediaItem, "てすと"); Assert.Equal("https://www.mobypicture.com/user/OpenTween/view/00000000", uploadedUrl); Assert.Equal(0, mockHandler.QueueCount); diff --git a/OpenTween.Tests/Api/TwitterApiTest.cs b/OpenTween.Tests/Api/TwitterApiTest.cs index 27ad41e79..ed2117170 100644 --- a/OpenTween.Tests/Api/TwitterApiTest.cs +++ b/OpenTween.Tests/Api/TwitterApiTest.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; @@ -51,61 +52,88 @@ private void MyCommonSetup() [Fact] public void Initialize_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); - Assert.Null(twitterApi.ApiConnection); + using var twitterApi = new TwitterApi(); + var apiConnection = Assert.IsType(twitterApi.Connection); + Assert.IsType(apiConnection.Credential); - twitterApi.Initialize("*** AccessToken ***", "*** AccessSecret ***", userId: 100L, screenName: "hogehoge"); + var credential = new TwitterCredentialOAuth1(TwitterAppToken.GetDefault(), "*** AccessToken ***", "*** AccessSecret ***"); + twitterApi.Initialize(credential, userId: 100L, screenName: "hogehoge"); - Assert.IsType(twitterApi.ApiConnection); - - var apiConnection = (TwitterApiConnection)twitterApi.ApiConnection!; - Assert.Equal("*** AccessToken ***", apiConnection.AccessToken); - Assert.Equal("*** AccessSecret ***", apiConnection.AccessSecret); + apiConnection = Assert.IsType(twitterApi.Connection); + Assert.Same(credential, apiConnection.Credential); Assert.Equal(100L, twitterApi.CurrentUserId); Assert.Equal("hogehoge", twitterApi.CurrentScreenName); // 複数回 Initialize を実行した場合は新たに TwitterApiConnection が生成される - twitterApi.Initialize("*** AccessToken2 ***", "*** AccessSecret2 ***", userId: 200L, screenName: "foobar"); + var credential2 = new TwitterCredentialOAuth1(TwitterAppToken.GetDefault(), "*** AccessToken2 ***", "*** AccessSecret2 ***"); + twitterApi.Initialize(credential2, userId: 200L, screenName: "foobar"); var oldApiConnection = apiConnection; Assert.True(oldApiConnection.IsDisposed); - Assert.IsType(twitterApi.ApiConnection); - - apiConnection = (TwitterApiConnection)twitterApi.ApiConnection!; - Assert.Equal("*** AccessToken2 ***", apiConnection.AccessToken); - Assert.Equal("*** AccessSecret2 ***", apiConnection.AccessSecret); + apiConnection = Assert.IsType(twitterApi.Connection); + Assert.Same(credential2, apiConnection.Credential); Assert.Equal(200L, twitterApi.CurrentUserId); Assert.Equal("foobar", twitterApi.CurrentScreenName); } - [Fact] - public async Task StatusesHomeTimeline_Test() + private Mock CreateApiConnectionMock(Action verifyRequest) + where T : IHttpRequest + => this.CreateApiConnectionMock(verifyRequest, ""); + + private Mock CreateApiConnectionMock(Action verifyRequest, string responseText) + where T : IHttpRequest { + Func verifyRequestWrapper = r => + { + verifyRequest(r); + // Assert メソッドを使用する想定のため、失敗した場合は例外が発生しここまで到達しない + return true; + }; + + var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseText), + }; var mock = new Mock(); mock.Setup(x => - x.GetAsync( - new Uri("statuses/home_timeline.json", UriKind.Relative), - new Dictionary - { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - }, - "/statuses/home_timeline") + x.SendAsync( + It.Is(r => verifyRequestWrapper(r)) + ) ) - .ReturnsAsync(Array.Empty()); + .ReturnsAsync(new ApiResponse(responseMessage)); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + return mock; + } + + [Fact] + public async Task StatusesHomeTimeline_Test() + { + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("statuses/home_timeline.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/statuses/home_timeline", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesHomeTimeline(200, maxId: new("900"), sinceId: new("100")) - .ConfigureAwait(false); + await twitterApi.StatusesHomeTimeline(200, maxId: new("900"), sinceId: new("100")); mock.VerifyAll(); } @@ -113,28 +141,29 @@ public async Task StatusesHomeTimeline_Test() [Fact] public async Task StatusesMentionsTimeline_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("statuses/mentions_timeline.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("statuses/mentions_timeline.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - }, - "/statuses/mentions_timeline") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/statuses/mentions_timeline", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesMentionsTimeline(200, maxId: new("900"), sinceId: new("100")) - .ConfigureAwait(false); + await twitterApi.StatusesMentionsTimeline(200, maxId: new("900"), sinceId: new("100")); mock.VerifyAll(); } @@ -142,30 +171,31 @@ public async Task StatusesMentionsTimeline_Test() [Fact] public async Task StatusesUserTimeline_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("statuses/user_timeline.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("statuses/user_timeline.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "screen_name", "twitterapi" }, - { "include_rts", "true" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - }, - "/statuses/user_timeline") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["screen_name"] = "twitterapi", + ["include_rts"] = "true", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/statuses/user_timeline", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesUserTimeline("twitterapi", count: 200, maxId: new("900"), sinceId: new("100")) - .ConfigureAwait(false); + await twitterApi.StatusesUserTimeline("twitterapi", count: 200, maxId: new("900"), sinceId: new("100")); mock.VerifyAll(); } @@ -173,26 +203,27 @@ public async Task StatusesUserTimeline_Test() [Fact] public async Task StatusesShow_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("statuses/show.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("statuses/show.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "id", "100" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/statuses/show/:id") - ) - .ReturnsAsync(new TwitterStatus { Id = 100L }); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["id"] = "100", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/statuses/show/:id", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterStatus()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesShow(statusId: new("100")) - .ConfigureAwait(false); + await twitterApi.StatusesShow(statusId: new("100")); mock.VerifyAll(); } @@ -200,27 +231,27 @@ public async Task StatusesShow_Test() [Fact] public async Task StatusesLookup_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("statuses/lookup.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("statuses/lookup.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "id", "100,200" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/statuses/lookup" - ) - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["id"] = "100,200", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/statuses/lookup", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.StatusesLookup(statusIds: new[] { "100", "200" }) - .ConfigureAwait(false); + await twitterApi.StatusesLookup(statusIds: new[] { "100", "200" }); mock.VerifyAll(); } @@ -228,26 +259,25 @@ await twitterApi.StatusesLookup(statusIds: new[] { "100", "200" }) [Fact] public async Task StatusesUpdate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("statuses/update.json", UriKind.Relative), - new Dictionary - { - { "status", "hogehoge" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "in_reply_to_status_id", "100" }, - { "media_ids", "10,20" }, - { "auto_populate_reply_metadata", "true" }, - { "exclude_reply_user_ids", "100,200" }, - { "attachment_url", "https://twitter.com/twitterapi/status/22634515958" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("statuses/update.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["status"] = "hogehoge", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["in_reply_to_status_id"] = "100", + ["media_ids"] = "10,20", + ["auto_populate_reply_metadata"] = "true", + ["exclude_reply_user_ids"] = "100,200", + ["attachment_url"] = "https://twitter.com/twitterapi/status/22634515958", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesUpdate( @@ -258,8 +288,7 @@ await twitterApi.StatusesUpdate( excludeReplyUserIds: new[] { 100L, 200L }, attachmentUrl: "https://twitter.com/twitterapi/status/22634515958" ) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -267,27 +296,25 @@ await twitterApi.StatusesUpdate( [Fact] public async Task StatusesUpdate_ExcludeReplyUserIdsEmptyTest() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("statuses/update.json", UriKind.Relative), - new Dictionary - { - { "status", "hogehoge" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - // exclude_reply_user_ids は空の場合には送信されない - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("statuses/update.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["status"] = "hogehoge", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + // exclude_reply_user_ids は空の場合には送信されない + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesUpdate("hogehoge", replyToId: null, mediaIds: null, excludeReplyUserIds: Array.Empty()) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -295,20 +322,21 @@ await twitterApi.StatusesUpdate("hogehoge", replyToId: null, mediaIds: null, exc [Fact] public async Task StatusesDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("statuses/destroy.json", UriKind.Relative), - new Dictionary { { "id", "100" } }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("statuses/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesDestroy(statusId: new("100")) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -316,26 +344,24 @@ public async Task StatusesDestroy_Test() [Fact] public async Task StatusesRetweet_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("statuses/retweet.json", UriKind.Relative), - new Dictionary - { - { "id", "100" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("statuses/retweet.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["id"] = "100", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.StatusesRetweet(new("100")) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -343,31 +369,32 @@ await twitterApi.StatusesRetweet(new("100")) [Fact] public async Task SearchTweets_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("search/tweets.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("search/tweets.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "q", "from:twitterapi" }, - { "result_type", "recent" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "lang", "en" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - }, - "/search/tweets") - ) - .ReturnsAsync(new TwitterSearchResult()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["q"] = "from:twitterapi", + ["result_type"] = "recent", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["lang"] = "en", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/search/tweets", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterSearchResult()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.SearchTweets("from:twitterapi", "en", count: 200, maxId: new("900"), sinceId: new("100")) - .ConfigureAwait(false); + await twitterApi.SearchTweets("from:twitterapi", "en", count: 200, maxId: new("900"), sinceId: new("100")); mock.VerifyAll(); } @@ -375,25 +402,26 @@ public async Task SearchTweets_Test() [Fact] public async Task ListsOwnerships_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/ownerships.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/ownerships.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "screen_name", "twitterapi" }, - { "cursor", "-1" }, - { "count", "100" }, - }, - "/lists/ownerships") - ) - .ReturnsAsync(new TwitterLists()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["screen_name"] = "twitterapi", + ["cursor"] = "-1", + ["count"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/ownerships", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterLists()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsOwnerships("twitterapi", cursor: -1L, count: 100) - .ConfigureAwait(false); + await twitterApi.ListsOwnerships("twitterapi", cursor: -1L, count: 100); mock.VerifyAll(); } @@ -401,25 +429,26 @@ await twitterApi.ListsOwnerships("twitterapi", cursor: -1L, count: 100) [Fact] public async Task ListsSubscriptions_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/subscriptions.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/subscriptions.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "screen_name", "twitterapi" }, - { "cursor", "-1" }, - { "count", "100" }, - }, - "/lists/subscriptions") - ) - .ReturnsAsync(new TwitterLists()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["screen_name"] = "twitterapi", + ["cursor"] = "-1", + ["count"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/subscriptions", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterLists()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsSubscriptions("twitterapi", cursor: -1L, count: 100) - .ConfigureAwait(false); + await twitterApi.ListsSubscriptions("twitterapi", cursor: -1L, count: 100); mock.VerifyAll(); } @@ -427,26 +456,27 @@ await twitterApi.ListsSubscriptions("twitterapi", cursor: -1L, count: 100) [Fact] public async Task ListsMemberships_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/memberships.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/memberships.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "screen_name", "twitterapi" }, - { "cursor", "-1" }, - { "count", "100" }, - { "filter_to_owned_lists", "true" }, - }, - "/lists/memberships") - ) - .ReturnsAsync(new TwitterLists()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["screen_name"] = "twitterapi", + ["cursor"] = "-1", + ["count"] = "100", + ["filter_to_owned_lists"] = "true", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/memberships", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterLists()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsMemberships("twitterapi", cursor: -1L, count: 100, filterToOwnedLists: true) - .ConfigureAwait(false); + await twitterApi.ListsMemberships("twitterapi", cursor: -1L, count: 100, filterToOwnedLists: true); mock.VerifyAll(); } @@ -454,25 +484,23 @@ await twitterApi.ListsMemberships("twitterapi", cursor: -1L, count: 100, filterT [Fact] public async Task ListsCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("lists/create.json", UriKind.Relative), - new Dictionary - { - { "name", "hogehoge" }, - { "description", "aaaa" }, - { "mode", "private" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterList())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("lists/create.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["name"] = "hogehoge", + ["description"] = "aaaa", + ["mode"] = "private", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.ListsCreate("hogehoge", description: "aaaa", @private: true) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -480,26 +508,24 @@ await twitterApi.ListsCreate("hogehoge", description: "aaaa", @private: true) [Fact] public async Task ListsUpdate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("lists/update.json", UriKind.Relative), - new Dictionary - { - { "list_id", "12345" }, - { "name", "hogehoge" }, - { "description", "aaaa" }, - { "mode", "private" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterList())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("lists/update.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["list_id"] = "12345", + ["name"] = "hogehoge", + ["description"] = "aaaa", + ["mode"] = "private", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.ListsUpdate(12345L, name: "hogehoge", description: "aaaa", @private: true) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -507,23 +533,21 @@ await twitterApi.ListsUpdate(12345L, name: "hogehoge", description: "aaaa", @pri [Fact] public async Task ListsDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("lists/destroy.json", UriKind.Relative), - new Dictionary - { - { "list_id", "12345" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterList())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("lists/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["list_id"] = "12345", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.ListsDestroy(12345L) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -531,30 +555,31 @@ await twitterApi.ListsDestroy(12345L) [Fact] public async Task ListsStatuses_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/statuses.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/statuses.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "list_id", "12345" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - { "include_rts", "true" }, - }, - "/lists/statuses") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["list_id"] = "12345", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + ["include_rts"] = "true", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/statuses", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsStatuses(12345L, count: 200, maxId: new("900"), sinceId: new("100"), includeRTs: true) - .ConfigureAwait(false); + await twitterApi.ListsStatuses(12345L, count: 200, maxId: new("900"), sinceId: new("100"), includeRTs: true); mock.VerifyAll(); } @@ -562,27 +587,28 @@ public async Task ListsStatuses_Test() [Fact] public async Task ListsMembers_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/members.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/members.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "list_id", "12345" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "cursor", "-1" }, - }, - "/lists/members") - ) - .ReturnsAsync(new TwitterUsers()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["list_id"] = "12345", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["cursor"] = "-1", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/members", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsMembers(12345L, cursor: -1) - .ConfigureAwait(false); + await twitterApi.ListsMembers(12345L, cursor: -1); mock.VerifyAll(); } @@ -590,27 +616,28 @@ await twitterApi.ListsMembers(12345L, cursor: -1) [Fact] public async Task ListsMembersShow_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("lists/members/show.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("lists/members/show.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "list_id", "12345" }, - { "screen_name", "twitterapi" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/lists/members/show") - ) - .ReturnsAsync(new TwitterUser()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["list_id"] = "12345", + ["screen_name"] = "twitterapi", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/lists/members/show", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterUser()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ListsMembersShow(12345L, "twitterapi") - .ConfigureAwait(false); + await twitterApi.ListsMembersShow(12345L, "twitterapi"); mock.VerifyAll(); } @@ -618,27 +645,25 @@ await twitterApi.ListsMembersShow(12345L, "twitterapi") [Fact] public async Task ListsMembersCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("lists/members/create.json", UriKind.Relative), - new Dictionary - { - { "list_id", "12345" }, - { "screen_name", "twitterapi" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("lists/members/create.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["list_id"] = "12345", + ["screen_name"] = "twitterapi", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembersCreate(12345L, "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -646,27 +671,25 @@ await twitterApi.ListsMembersCreate(12345L, "twitterapi") [Fact] public async Task ListsMembersDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("lists/members/destroy.json", UriKind.Relative), - new Dictionary - { - { "list_id", "12345" }, - { "screen_name", "twitterapi" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("lists/members/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["list_id"] = "12345", + ["screen_name"] = "twitterapi", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.ListsMembersDestroy(12345L, "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -674,24 +697,25 @@ await twitterApi.ListsMembersDestroy(12345L, "twitterapi") [Fact] public async Task DirectMessagesEventsList_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("direct_messages/events/list.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("direct_messages/events/list.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "count", "50" }, - { "cursor", "12345abcdefg" }, - }, - "/direct_messages/events/list") - ) - .ReturnsAsync(new TwitterMessageEventList()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["count"] = "50", + ["cursor"] = "12345abcdefg", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/direct_messages/events/list", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterMessageEventList()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg") - .ConfigureAwait(false); + await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg"); mock.VerifyAll(); } @@ -699,8 +723,7 @@ await twitterApi.DirectMessagesEventsList(count: 50, cursor: "12345abcdefg") [Fact] public async Task DirectMessagesEventsNew_Test() { - var mock = new Mock(); - var responseText = """ + var requestJson = """ { "event": { "type": "message_create", @@ -721,18 +744,17 @@ public async Task DirectMessagesEventsNew_Test() } } """; - mock.Setup(x => - x.PostJsonAsync( - new Uri("direct_messages/events/new.json", UriKind.Relative), - responseText) - ) - .ReturnsAsync(LazyJson.Create(new TwitterMessageEventSingle())); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("direct_messages/events/new.json", UriKind.Relative), r.RequestUri); + Assert.Equal(requestJson, r.JsonString); + }); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.DirectMessagesEventsNew(recipientId: 12345L, text: "hogehoge", mediaId: 67890L) - .ConfigureAwait(false); + await twitterApi.DirectMessagesEventsNew(recipientId: 12345L, text: "hogehoge", mediaId: 67890L); mock.VerifyAll(); } @@ -740,18 +762,20 @@ await twitterApi.DirectMessagesEventsNew(recipientId: 12345L, text: "hogehoge", [Fact] public async Task DirectMessagesEventsDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.DeleteAsync( - new Uri("direct_messages/events/destroy.json?id=100", UriKind.Relative)) - ) - .Returns(Task.CompletedTask); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("direct_messages/events/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.DirectMessagesEventsDestroy(eventId: new("100")) - .ConfigureAwait(false); + await twitterApi.DirectMessagesEventsDestroy(eventId: new("100")); mock.VerifyAll(); } @@ -759,26 +783,27 @@ public async Task DirectMessagesEventsDestroy_Test() [Fact] public async Task UsersShow_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("users/show.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("users/show.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "screen_name", "twitterapi" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/users/show/:id") - ) - .ReturnsAsync(new TwitterUser { ScreenName = "twitterapi" }); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["screen_name"] = "twitterapi", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/users/show/:id", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterUser()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.UsersShow(screenName: "twitterapi") - .ConfigureAwait(false); + await twitterApi.UsersShow(screenName: "twitterapi"); mock.VerifyAll(); } @@ -786,26 +811,27 @@ await twitterApi.UsersShow(screenName: "twitterapi") [Fact] public async Task UsersLookup_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("users/lookup.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("users/lookup.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "user_id", "11111,22222" }, - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/users/lookup") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["user_id"] = "11111,22222", + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/users/lookup", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.UsersLookup(userIds: new[] { "11111", "22222" }) - .ConfigureAwait(false); + await twitterApi.UsersLookup(userIds: new[] { "11111", "22222" }); mock.VerifyAll(); } @@ -813,24 +839,22 @@ await twitterApi.UsersLookup(userIds: new[] { "11111", "22222" }) [Fact] public async Task UsersReportSpam_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("users/report_spam.json", UriKind.Relative), - new Dictionary - { - { "screen_name", "twitterapi" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser { ScreenName = "twitterapi" })); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("users/report_spam.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["screen_name"] = "twitterapi", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.UsersReportSpam(screenName: "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -838,28 +862,29 @@ await twitterApi.UsersReportSpam(screenName: "twitterapi") [Fact] public async Task FavoritesList_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("favorites/list.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("favorites/list.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "count", "200" }, - { "max_id", "900" }, - { "since_id", "100" }, - }, - "/favorites/list") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["count"] = "200", + ["max_id"] = "900", + ["since_id"] = "100", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/favorites/list", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterStatus()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.FavoritesList(200, maxId: 900L, sinceId: 100L) - .ConfigureAwait(false); + await twitterApi.FavoritesList(200, maxId: 900L, sinceId: 100L); mock.VerifyAll(); } @@ -867,24 +892,22 @@ await twitterApi.FavoritesList(200, maxId: 900L, sinceId: 100L) [Fact] public async Task FavoritesCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("favorites/create.json", UriKind.Relative), - new Dictionary - { - { "id", "100" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("favorites/create.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["id"] = "100", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.FavoritesCreate(statusId: new("100")) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -892,24 +915,22 @@ public async Task FavoritesCreate_Test() [Fact] public async Task FavoritesDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("favorites/destroy.json", UriKind.Relative), - new Dictionary - { - { "id", "100" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterStatus { Id = 100L })); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("favorites/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["id"] = "100", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.FavoritesDestroy(statusId: new("100")) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -917,20 +938,25 @@ public async Task FavoritesDestroy_Test() [Fact] public async Task FriendshipsShow_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("friendships/show.json", UriKind.Relative), - new Dictionary { { "source_screen_name", "twitter" }, { "target_screen_name", "twitterapi" } }, - "/friendships/show") - ) - .ReturnsAsync(new TwitterFriendship()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("friendships/show.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["source_screen_name"] = "twitter", + ["target_screen_name"] = "twitterapi", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/friendships/show", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterFriendship()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.FriendshipsShow(sourceScreenName: "twitter", targetScreenName: "twitterapi") - .ConfigureAwait(false); + await twitterApi.FriendshipsShow(sourceScreenName: "twitter", targetScreenName: "twitterapi"); mock.VerifyAll(); } @@ -938,20 +964,21 @@ await twitterApi.FriendshipsShow(sourceScreenName: "twitter", targetScreenName: [Fact] public async Task FriendshipsCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("friendships/create.json", UriKind.Relative), - new Dictionary { { "screen_name", "twitterapi" } }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterFriendship())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("friendships/create.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["screen_name"] = "twitterapi", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.FriendshipsCreate(screenName: "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -959,20 +986,21 @@ await twitterApi.FriendshipsCreate(screenName: "twitterapi") [Fact] public async Task FriendshipsDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("friendships/destroy.json", UriKind.Relative), - new Dictionary { { "screen_name", "twitterapi" } }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterFriendship())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("friendships/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["screen_name"] = "twitterapi", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.FriendshipsDestroy(screenName: "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -980,20 +1008,20 @@ await twitterApi.FriendshipsDestroy(screenName: "twitterapi") [Fact] public async Task NoRetweetIds_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("friendships/no_retweets/ids.json", UriKind.Relative), - null, - "/friendships/no_retweets/ids") - ) - .ReturnsAsync(Array.Empty()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("friendships/no_retweets/ids.json", UriKind.Relative), r.RequestUri); + Assert.Null(r.Query); + Assert.Equal("/friendships/no_retweets/ids", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(Array.Empty()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.NoRetweetIds() - .ConfigureAwait(false); + await twitterApi.NoRetweetIds(); mock.VerifyAll(); } @@ -1001,20 +1029,24 @@ await twitterApi.NoRetweetIds() [Fact] public async Task FollowersIds_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("followers/ids.json", UriKind.Relative), - new Dictionary { { "cursor", "-1" } }, - "/followers/ids") - ) - .ReturnsAsync(new TwitterIds()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("followers/ids.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["cursor"] = "-1", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/followers/ids", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterIds()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.FollowersIds(cursor: -1L) - .ConfigureAwait(false); + await twitterApi.FollowersIds(cursor: -1L); mock.VerifyAll(); } @@ -1022,20 +1054,24 @@ await twitterApi.FollowersIds(cursor: -1L) [Fact] public async Task MutesUsersIds_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("mutes/users/ids.json", UriKind.Relative), - new Dictionary { { "cursor", "-1" } }, - "/mutes/users/ids") - ) - .ReturnsAsync(new TwitterIds()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("mutes/users/ids.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["cursor"] = "-1", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/mutes/users/ids", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterIds()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MutesUsersIds(cursor: -1L) - .ConfigureAwait(false); + await twitterApi.MutesUsersIds(cursor: -1L); mock.VerifyAll(); } @@ -1043,20 +1079,24 @@ await twitterApi.MutesUsersIds(cursor: -1L) [Fact] public async Task BlocksIds_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("blocks/ids.json", UriKind.Relative), - new Dictionary { { "cursor", "-1" } }, - "/blocks/ids") - ) - .ReturnsAsync(new TwitterIds()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("blocks/ids.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["cursor"] = "-1", + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/blocks/ids", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterIds()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.BlocksIds(cursor: -1L) - .ConfigureAwait(false); + await twitterApi.BlocksIds(cursor: -1L); mock.VerifyAll(); } @@ -1064,24 +1104,22 @@ await twitterApi.BlocksIds(cursor: -1L) [Fact] public async Task BlocksCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("blocks/create.json", UriKind.Relative), - new Dictionary - { - { "screen_name", "twitterapi" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("blocks/create.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["screen_name"] = "twitterapi", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.BlocksCreate(screenName: "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1089,24 +1127,22 @@ await twitterApi.BlocksCreate(screenName: "twitterapi") [Fact] public async Task BlocksDestroy_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("blocks/destroy.json", UriKind.Relative), - new Dictionary - { - { "screen_name", "twitterapi" }, - { "tweet_mode", "extended" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("blocks/destroy.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["screen_name"] = "twitterapi", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.BlocksDestroy(screenName: "twitterapi") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1114,29 +1150,30 @@ await twitterApi.BlocksDestroy(screenName: "twitterapi") [Fact] public async Task AccountVerifyCredentials_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("account/verify_credentials.json", UriKind.Relative), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("account/verify_credentials.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - "/account/verify_credentials") - ) - .ReturnsAsync(new TwitterUser - { - Id = 100L, - ScreenName = "opentween", - }); + { "include_entities", "true" }, + { "include_ext_alt_text", "true" }, + { "tweet_mode", "extended" }, + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/account/verify_credentials", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterUser + { + Id = 100L, + ScreenName = "opentween", + }) + ); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.AccountVerifyCredentials() - .ConfigureAwait(false); + await twitterApi.AccountVerifyCredentials(); Assert.Equal(100L, twitterApi.CurrentUserId); Assert.Equal("opentween", twitterApi.CurrentScreenName); @@ -1147,29 +1184,27 @@ await twitterApi.AccountVerifyCredentials() [Fact] public async Task AccountUpdateProfile_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("account/update_profile.json", UriKind.Relative), - new Dictionary - { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - { "name", "Name" }, - { "url", "http://example.com/" }, - { "location", "Location" }, - { "description", "<script>alert(1)</script>" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("account/update_profile.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + ["name"] = "Name", + ["url"] = "http://example.com/", + ["location"] = "Location", + ["description"] = "<script>alert(1)</script>", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.AccountUpdateProfile(name: "Name", url: "http://example.com/", location: "Location", description: "") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1179,26 +1214,29 @@ public async Task AccountUpdateProfileImage_Test() { using var image = TestUtils.CreateDummyImage(); using var media = new MemoryImageMediaItem(image); - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("account/update_profile_image.json", UriKind.Relative), - new Dictionary - { - { "include_entities", "true" }, - { "include_ext_alt_text", "true" }, - { "tweet_mode", "extended" }, - }, - new Dictionary { { "image", media } }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUser())); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("account/update_profile_image.json", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }; + Assert.Equal(expectedQuery, r.Query); + var expectedMedia = new Dictionary + { + ["image"] = media, + }; + Assert.Equal(expectedMedia, r.Media); + }); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.AccountUpdateProfileImage(media) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1206,20 +1244,20 @@ await twitterApi.AccountUpdateProfileImage(media) [Fact] public async Task ApplicationRateLimitStatus_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("application/rate_limit_status.json", UriKind.Relative), - null, - "/application/rate_limit_status") - ) - .ReturnsAsync(new TwitterRateLimits()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("application/rate_limit_status.json", UriKind.Relative), r.RequestUri); + Assert.Null(r.Query); + Assert.Equal("/application/rate_limit_status", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterRateLimits()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.ApplicationRateLimitStatus() - .ConfigureAwait(false); + await twitterApi.ApplicationRateLimitStatus(); mock.VerifyAll(); } @@ -1227,20 +1265,20 @@ await twitterApi.ApplicationRateLimitStatus() [Fact] public async Task Configuration_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("help/configuration.json", UriKind.Relative), - null, - "/help/configuration") - ) - .ReturnsAsync(new TwitterConfiguration()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("help/configuration.json", UriKind.Relative), r.RequestUri); + Assert.Null(r.Query); + Assert.Equal("/help/configuration", r.EndpointName); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterConfiguration()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.Configuration() - .ConfigureAwait(false); + await twitterApi.Configuration(); mock.VerifyAll(); } @@ -1248,26 +1286,24 @@ await twitterApi.Configuration() [Fact] public async Task MediaUploadInit_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary - { - { "command", "INIT" }, - { "total_bytes", "123456" }, - { "media_type", "image/png" }, - { "media_category", "dm_image" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUploadMediaInit())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("https://upload.twitter.com/1.1/media/upload.json"), r.RequestUri); + var expectedQuery = new Dictionary + { + ["command"] = "INIT", + ["total_bytes"] = "123456", + ["media_type"] = "image/png", + ["media_category"] = "dm_image", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadInit(totalBytes: 123456L, mediaType: "image/png", mediaCategory: "dm_image") - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1277,25 +1313,28 @@ public async Task MediaUploadAppend_Test() { using var image = TestUtils.CreateDummyImage(); using var media = new MemoryImageMediaItem(image); - var mock = new Mock(); - mock.Setup(x => - x.PostAsync( - new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary - { - { "command", "APPEND" }, - { "media_id", "11111" }, - { "segment_index", "1" }, - }, - new Dictionary { { "media", media } }) - ) - .Returns(Task.CompletedTask); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("https://upload.twitter.com/1.1/media/upload.json"), r.RequestUri); + var expectedQuery = new Dictionary + { + ["command"] = "APPEND", + ["media_id"] = "11111", + ["segment_index"] = "1", + }; + Assert.Equal(expectedQuery, r.Query); + var expectedMedia = new Dictionary + { + ["media"] = media, + }; + Assert.Equal(expectedMedia, r.Media); + }); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaUploadAppend(mediaId: 11111L, segmentIndex: 1, media: media) - .ConfigureAwait(false); + await twitterApi.MediaUploadAppend(mediaId: 11111L, segmentIndex: 1, media: media); mock.VerifyAll(); } @@ -1303,24 +1342,22 @@ await twitterApi.MediaUploadAppend(mediaId: 11111L, segmentIndex: 1, media: medi [Fact] public async Task MediaUploadFinalize_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostLazyAsync( - new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary - { - { "command", "FINALIZE" }, - { "media_id", "11111" }, - }) - ) - .ReturnsAsync(LazyJson.Create(new TwitterUploadMediaResult())); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("https://upload.twitter.com/1.1/media/upload.json"), r.RequestUri); + var expectedQuery = new Dictionary + { + ["command"] = "FINALIZE", + ["media_id"] = "11111", + }; + Assert.Equal(expectedQuery, r.Query); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; await twitterApi.MediaUploadFinalize(mediaId: 11111L) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); mock.VerifyAll(); } @@ -1328,24 +1365,24 @@ await twitterApi.MediaUploadFinalize(mediaId: 11111L) [Fact] public async Task MediaUploadStatus_Test() { - var mock = new Mock(); - mock.Setup(x => - x.GetAsync( - new Uri("https://upload.twitter.com/1.1/media/upload.json", UriKind.Absolute), - new Dictionary + var mock = this.CreateApiConnectionMock( + r => + { + Assert.Equal(new("https://upload.twitter.com/1.1/media/upload.json"), r.RequestUri); + var expectedQuery = new Dictionary { - { "command", "STATUS" }, - { "media_id", "11111" }, - }, - null) - ) - .ReturnsAsync(new TwitterUploadMediaResult()); - - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + ["command"] = "STATUS", + ["media_id"] = "11111", + }; + Assert.Equal(expectedQuery, r.Query); + }, + JsonUtils.SerializeJsonByDataContract(new TwitterUploadMediaResult()) + ); + + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaUploadStatus(mediaId: 11111L) - .ConfigureAwait(false); + await twitterApi.MediaUploadStatus(mediaId: 11111L); mock.VerifyAll(); } @@ -1353,19 +1390,16 @@ await twitterApi.MediaUploadStatus(mediaId: 11111L) [Fact] public async Task MediaMetadataCreate_Test() { - var mock = new Mock(); - mock.Setup(x => - x.PostJsonAsync( - new Uri("https://upload.twitter.com/1.1/media/metadata/create.json", UriKind.Absolute), - """{"media_id": "12345", "alt_text": {"text": "hogehoge"}}""") - ) - .ReturnsAsync(""); + var mock = this.CreateApiConnectionMock(r => + { + Assert.Equal(new("https://upload.twitter.com/1.1/media/metadata/create.json"), r.RequestUri); + Assert.Equal("""{"media_id": "12345", "alt_text": {"text": "hogehoge"}}""", r.JsonString); + }); - using var twitterApi = new TwitterApi(ApiKey.Create("fake_consumer_key"), ApiKey.Create("fake_consumer_secret")); + using var twitterApi = new TwitterApi(); twitterApi.ApiConnection = mock.Object; - await twitterApi.MediaMetadataCreate(mediaId: 12345L, altText: "hogehoge") - .ConfigureAwait(false); + await twitterApi.MediaMetadataCreate(mediaId: 12345L, altText: "hogehoge"); mock.VerifyAll(); } diff --git a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs index bf9900609..62e0fedab 100644 --- a/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs +++ b/OpenTween.Tests/Api/TwitterV2/GetTimelineRequestTest.cs @@ -21,6 +21,8 @@ using System; using System.Collections.Generic; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; using Moq; using OpenTween.Api.DataModel; @@ -32,23 +34,36 @@ namespace OpenTween.Api.TwitterV2 public class GetTimelineRequestTest { [Fact] - public async Task StatusesMentionsTimeline_Test() + public async Task Send_Test() { + Func verifyRequest = r => + { + Assert.Equal(new("/2/users/100/timelines/reverse_chronological", UriKind.Relative), r.RequestUri); + var expectedQuery = new Dictionary + { + { "tweet.fields", "id" }, + { "max_results", "200" }, + { "until_id", "900" }, + { "since_id", "100" }, + }; + Assert.Equal(expectedQuery, r.Query); + Assert.Equal("/2/users/:id/timelines/reverse_chronological", r.EndpointName); + return true; + }; + + using var responseMessage = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent( + JsonUtils.SerializeJsonByDataContract(new TwitterV2TweetIds()) + ), + }; var mock = new Mock(); mock.Setup(x => - x.GetAsync( - new Uri("/2/users/100/timelines/reverse_chronological", UriKind.Relative), - new Dictionary - { - { "tweet.fields", "id" }, - { "max_results", "200" }, - { "until_id", "900" }, - { "since_id", "100" }, - }, - "/2/users/:id/timelines/reverse_chronological" + x.SendAsync( + It.Is(r => verifyRequest(r)) ) ) - .ReturnsAsync(new TwitterV2TweetIds()); + .ReturnsAsync(new ApiResponse(responseMessage)); var request = new GetTimelineRequest(userId: 100L) { @@ -57,7 +72,7 @@ public async Task StatusesMentionsTimeline_Test() UntilId = new("900"), }; - await request.Send(mock.Object).ConfigureAwait(false); + await request.Send(mock.Object); mock.VerifyAll(); } diff --git a/OpenTween.Tests/ApiInfoDialogTest.cs b/OpenTween.Tests/ApiInfoDialogTest.cs new file mode 100644 index 000000000..4abc38a85 --- /dev/null +++ b/OpenTween.Tests/ApiInfoDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class ApiInfoDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new ApiInfoDialog(); + } + } +} diff --git a/OpenTween.Tests/AppendSettingDialogTest.cs b/OpenTween.Tests/AppendSettingDialogTest.cs new file mode 100644 index 000000000..1e604a72e --- /dev/null +++ b/OpenTween.Tests/AppendSettingDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class AppendSettingDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new AppendSettingDialog(); + } + } +} diff --git a/OpenTween.Tests/ApplicationContainerTest.cs b/OpenTween.Tests/ApplicationContainerTest.cs new file mode 100644 index 000000000..d88fd5fbe --- /dev/null +++ b/OpenTween.Tests/ApplicationContainerTest.cs @@ -0,0 +1,41 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Setting; +using Xunit; + +namespace OpenTween +{ + public class ApplicationContainerTest + { + [WinFormsFact] + public void Initialize_Test() + { + var settingManager = new SettingManager(""); + using var container = new ApplicationContainer(settingManager); + } + } +} diff --git a/OpenTween.Tests/AsyncTimerTest.cs b/OpenTween.Tests/AsyncTimerTest.cs index c3f1a785a..d897fe18a 100644 --- a/OpenTween.Tests/AsyncTimerTest.cs +++ b/OpenTween.Tests/AsyncTimerTest.cs @@ -86,7 +86,7 @@ void Handler(object sender, ThreadExceptionEventArgs ev) var timeout = Task.Delay(1000); Assert.NotEqual(timeout, await Task.WhenAny(tcs.Task, timeout)); - Assert.IsType(tcs.Task.Result); + Assert.IsType(await tcs.Task); } finally { diff --git a/OpenTween.Tests/AtIdSupplementTest.cs b/OpenTween.Tests/AtIdSupplementTest.cs new file mode 100644 index 000000000..0b76bf3d6 --- /dev/null +++ b/OpenTween.Tests/AtIdSupplementTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class AtIdSupplementTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new AtIdSupplement(itemList: new(), startCharacter: ""); + } + } +} diff --git a/OpenTween.Tests/AuthDialogTest.cs b/OpenTween.Tests/AuthDialogTest.cs new file mode 100644 index 000000000..58d5ce4f9 --- /dev/null +++ b/OpenTween.Tests/AuthDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class AuthDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new AuthDialog(); + } + } +} diff --git a/OpenTween.Tests/AuthTypeSelectDialogTest.cs b/OpenTween.Tests/AuthTypeSelectDialogTest.cs new file mode 100644 index 000000000..0e897711c --- /dev/null +++ b/OpenTween.Tests/AuthTypeSelectDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class AuthTypeSelectDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new AuthTypeSelectDialog(); + } + } +} diff --git a/OpenTween.Tests/BingTest.cs b/OpenTween.Tests/BingTest.cs index cb1aa9255..b446d028a 100644 --- a/OpenTween.Tests/BingTest.cs +++ b/OpenTween.Tests/BingTest.cs @@ -36,6 +36,10 @@ namespace OpenTween /// public class BingTest { + [Fact] + public void Initialize_Test() + => new Bing(); + [Theory] [InlineData("af", 0)] [InlineData("sq", 1)] diff --git a/OpenTween.Tests/Connection/ApiResponseTest.cs b/OpenTween.Tests/Connection/ApiResponseTest.cs new file mode 100644 index 000000000..ae3a8d8de --- /dev/null +++ b/OpenTween.Tests/Connection/ApiResponseTest.cs @@ -0,0 +1,164 @@ +// 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); + } + + [Fact] + public async Task ReadAsLazyJson_Test() + { + using var responseContent = new StringContent("""{"foo":123}"""); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + using var lazyJson = response.ReadAsLazyJson(); + Assert.Equal(new() { Foo = 123 }, await lazyJson.LoadJsonAsync()); + } + + [Fact] + public async Task ReadAsLazyJson_DisposeTest() + { + using var responseContent = new StringContent("""{"foo":123}"""); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + using var lazyJson = response.ReadAsLazyJson(); + response.Dispose(); // ApiResponse を先に破棄しても LazyJson に影響しないことをテストする + + Assert.Equal(new() { Foo = 123 }, await lazyJson.LoadJsonAsync()); + } + + [Fact] + public async Task ReadAsString_Test() + { + using var responseContent = new StringContent("foo"); + using var responseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = responseContent, + }; + using var response = new ApiResponse(responseMessage); + + Assert.Equal("foo", await response.ReadAsString()); + } + } +} diff --git a/OpenTween.Tests/Connection/DeleteRequestTest.cs b/OpenTween.Tests/Connection/DeleteRequestTest.cs new file mode 100644 index 000000000..3b72c5b0d --- /dev/null +++ b/OpenTween.Tests/Connection/DeleteRequestTest.cs @@ -0,0 +1,50 @@ +// 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 DeleteRequestTest + { + [Fact] + public void CreateMessage_Test() + { + var request = new DeleteRequest + { + RequestUri = new("hoge/aaa.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = "12345", + }, + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Delete, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/hoge/aaa.json?id=12345"), requestMessage.RequestUri); + } + } +} diff --git a/OpenTween.Tests/Connection/GetRequestTest.cs b/OpenTween.Tests/Connection/GetRequestTest.cs new file mode 100644 index 000000000..efad64638 --- /dev/null +++ b/OpenTween.Tests/Connection/GetRequestTest.cs @@ -0,0 +1,50 @@ +// 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); + } + } +} diff --git a/OpenTween.Tests/Connection/HttpClientBuilderTest.cs b/OpenTween.Tests/Connection/HttpClientBuilderTest.cs new file mode 100644 index 000000000..201784b25 --- /dev/null +++ b/OpenTween.Tests/Connection/HttpClientBuilderTest.cs @@ -0,0 +1,104 @@ +// 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.Net.Http; +using Moq; +using Xunit; + +namespace OpenTween.Connection +{ + public class HttpClientBuilderTest + { + [Fact] + public void Build_Test() + { + var builder = new HttpClientBuilder(); + using var client = builder.Build(); + } + + [Fact] + public void SetupHttpClientHandler_Test() + { + var builder = new HttpClientBuilder(); + builder.SetupHttpClientHandler(x => x.AllowAutoRedirect = true); + builder.AddHandler(x => + { + var httpClientHandler = (HttpClientHandler)x; + Assert.True(httpClientHandler.AllowAutoRedirect); + return x; + }); + using var client = builder.Build(); + } + + [Fact] + public void AddHandler_Test() + { + var count = 0; + + var builder = new HttpClientBuilder(); + builder.AddHandler(x => + { + count++; + Assert.IsType(x); + return x; + }); + using var client = builder.Build(); + + Assert.Equal(1, count); + } + + [Fact] + public void AddHandler_NestingTest() + { + var count = 0; + HttpMessageHandler? handler = null; + + var builder = new HttpClientBuilder(); + builder.AddHandler(x => + { + count++; + handler = Mock.Of(); + return handler; + }); + builder.AddHandler(x => + { + count++; + Assert.NotNull(x); + Assert.Same(handler, x); + return x; + }); + using var client = builder.Build(); + + Assert.Equal(2, count); + } + + [Fact] + public void SetupHttpClient_Test() + { + var builder = new HttpClientBuilder(); + builder.SetupHttpClient(x => x.Timeout = TimeSpan.FromSeconds(10)); + using var client = builder.Build(); + + Assert.Equal(TimeSpan.FromSeconds(10), client.Timeout); + } + } +} diff --git a/OpenTween.Tests/Connection/LazyJsonTest.cs b/OpenTween.Tests/Connection/LazyJsonTest.cs index 0c003aca9..6ebf63245 100644 --- a/OpenTween.Tests/Connection/LazyJsonTest.cs +++ b/OpenTween.Tests/Connection/LazyJsonTest.cs @@ -47,8 +47,7 @@ public async Task LoadJsonAsync_Test() // この時点ではまだレスポンスボディは読まれない Assert.Equal(0, bodyStream.Position); - var result = await lazyJson.LoadJsonAsync() - .ConfigureAwait(false); + var result = await lazyJson.LoadJsonAsync(); Assert.Equal("hogehoge", result); } @@ -66,8 +65,9 @@ public async Task LoadJsonAsync_InvalidJsonTest() // この時点ではまだレスポンスボディは読まれない Assert.Equal(0, bodyStream.Position); - var exception = await Assert.ThrowsAnyAsync(() => lazyJson.LoadJsonAsync()) - .ConfigureAwait(false); + var exception = await Assert.ThrowsAnyAsync( + () => lazyJson.LoadJsonAsync() + ); Assert.IsType(exception.InnerException); } @@ -86,8 +86,7 @@ public async Task IgnoreResponse_Test() // レスポンスボディを読まずに破棄 await Task.FromResult(lazyJson) - .IgnoreResponse() - .ConfigureAwait(false); + .IgnoreResponse(); Assert.True(bodyStream.IsDisposed); } diff --git a/OpenTween.Tests/Connection/OAuthHandlerTest.cs b/OpenTween.Tests/Connection/OAuthHandlerTest.cs index 51dd44dc1..db59819a9 100644 --- a/OpenTween.Tests/Connection/OAuthHandlerTest.cs +++ b/OpenTween.Tests/Connection/OAuthHandlerTest.cs @@ -36,8 +36,7 @@ public async Task GetParameter_UriQueryTest() { var requestUri = new Uri("http://example.com/api?aaa=1&bbb=2"); - var actual = await OAuthHandler.GetParameters(requestUri, content: null) - .ConfigureAwait(false); + var actual = await OAuthHandler.GetParameters(requestUri, content: null); var expected = new[] { new KeyValuePair("aaa", "1"), @@ -58,8 +57,7 @@ public async Task GetParameter_FormUrlEncodedTest() }; using var content = new FormUrlEncodedContent(formParams); - var actual = await OAuthHandler.GetParameters(requestUri, content) - .ConfigureAwait(false); + var actual = await OAuthHandler.GetParameters(requestUri, content); var expected = new[] { @@ -81,8 +79,7 @@ public async Task GetParameter_MultipartTest() content.Add(paramA, "aaa"); content.Add(paramB, "bbb"); - var actual = await OAuthHandler.GetParameters(requestUri, content) - .ConfigureAwait(false); + var actual = await OAuthHandler.GetParameters(requestUri, content); // multipart/form-data のリクエストではパラメータを署名対象にしない Assert.Empty(actual); diff --git a/OpenTween.Tests/Connection/PostJsonRequestTest.cs b/OpenTween.Tests/Connection/PostJsonRequestTest.cs new file mode 100644 index 000000000..d8b5f283e --- /dev/null +++ b/OpenTween.Tests/Connection/PostJsonRequestTest.cs @@ -0,0 +1,48 @@ +// 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.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween.Connection +{ + public class PostJsonRequestTest + { + [Fact] + public async Task CreateMessage_Test() + { + var request = new PostJsonRequest + { + RequestUri = new("aaa/bbb.json", UriKind.Relative), + JsonString = """{"foo":12345}""", + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Post, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/aaa/bbb.json"), requestMessage.RequestUri); + Assert.Equal("""{"foo":12345}""", await requestMessage.Content.ReadAsStringAsync()); + } + } +} diff --git a/OpenTween.Tests/Connection/PostMultipartRequestTest.cs b/OpenTween.Tests/Connection/PostMultipartRequestTest.cs new file mode 100644 index 000000000..043770d86 --- /dev/null +++ b/OpenTween.Tests/Connection/PostMultipartRequestTest.cs @@ -0,0 +1,90 @@ +// 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.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween.Connection +{ + public class PostMultipartRequestTest + { + [Fact] + public async Task CreateMessage_Test() + { + using var image = TestUtils.CreateDummyImage(); + using var media = new MemoryImageMediaItem(image); + + var request = new PostMultipartRequest + { + RequestUri = new("hoge/aaa.json", UriKind.Relative), + Query = new Dictionary + { + ["aaaa"] = "1111", + ["bbbb"] = "2222", + }, + Media = new Dictionary + { + ["media1"] = media, + }, + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Post, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/hoge/aaa.json"), requestMessage.RequestUri); + + using var requestContent = Assert.IsType(requestMessage.Content); + var boundary = requestContent.Headers.ContentType.Parameters.Cast() + .First(y => y.Name == "boundary").Value; + + // 前後のダブルクオーテーションを除去 + boundary = boundary.Substring(1, boundary.Length - 2); + + var expectedText = + $"--{boundary}\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Disposition: form-data; name=aaaa\r\n" + + "\r\n" + + "1111\r\n" + + $"--{boundary}\r\n" + + "Content-Type: text/plain; charset=utf-8\r\n" + + "Content-Disposition: form-data; name=bbbb\r\n" + + "\r\n" + + "2222\r\n" + + $"--{boundary}\r\n" + + $"Content-Disposition: form-data; name=media1; filename={media.Name}; filename*=utf-8''{media.Name}\r\n" + + "\r\n"; + + var expected = Encoding.UTF8.GetBytes(expectedText) + .Concat(image.Stream.ToArray()) + .Concat(Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n")); + + Assert.Equal(expected, await requestContent.ReadAsByteArrayAsync()); + } + } +} diff --git a/OpenTween.Tests/Connection/PostRequestTest.cs b/OpenTween.Tests/Connection/PostRequestTest.cs new file mode 100644 index 000000000..fc23f3a63 --- /dev/null +++ b/OpenTween.Tests/Connection/PostRequestTest.cs @@ -0,0 +1,70 @@ +// 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 System.Threading.Tasks; +using Xunit; + +namespace OpenTween.Connection +{ + public class PostRequestTest + { + [Fact] + public void CreateMessage_Test() + { + var request = new PostRequest + { + RequestUri = new("hoge/aaa.json", UriKind.Relative), + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Post, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/hoge/aaa.json"), requestMessage.RequestUri); + Assert.Null(requestMessage.Content); + } + + [Fact] + public async Task CreateMessage_WithQueryTest() + { + var request = new PostRequest + { + RequestUri = new("hoge/aaa.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = "12345", + }, + }; + + var baseUri = new Uri("https://example.com/v1/"); + using var requestMessage = request.CreateMessage(baseUri); + + Assert.Equal(HttpMethod.Post, requestMessage.Method); + Assert.Equal(new("https://example.com/v1/hoge/aaa.json"), requestMessage.RequestUri); + + using var requestContent = Assert.IsType(requestMessage.Content); + Assert.Equal("id=12345", await requestContent.ReadAsStringAsync()); + } + } +} diff --git a/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs b/OpenTween.Tests/Connection/TwitterApiConnectionTest.cs index e97548196..8960fce13 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; @@ -52,11 +53,11 @@ private void MyCommonSetup() } [Fact] - public async Task GetAsync_Test() + 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(""), "", ""); + using var apiConnection = new TwitterApiConnection(); apiConnection.Http = http; mockHandler.Enqueue(x => @@ -76,64 +77,30 @@ public async Task GetAsync_Test() }; }); - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["aaaa"] = "1111", - ["bbbb"] = "2222", - }; - - var result = await apiConnection.GetAsync(endpoint, param, endpointName: "/hoge/tetete") - .ConfigureAwait(false); - Assert.Equal("hogehoge", result); - - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task GetAsync_AbsoluteUriTest() - { - 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("http://example.com/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) + RequestUri = new("hoge/tetete.json", UriKind.Relative), + Query = new Dictionary { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("http://example.com/hoge/tetete.json", UriKind.Absolute); - var param = new Dictionary - { - ["aaaa"] = "1111", - ["bbbb"] = "2222", + ["aaaa"] = "1111", + ["bbbb"] = "2222", + }, + EndpointName = "/hoge/tetete", }; - await apiConnection.GetAsync(endpoint, param, endpointName: "/hoge/tetete") - .ConfigureAwait(false); + using var response = await apiConnection.SendAsync(request); + + Assert.Equal("hogehoge", await response.ReadAsJson()); Assert.Equal(0, mockHandler.QueueCount); } [Fact] - public async Task GetAsync_UpdateRateLimitTest() + public async Task SendAsync_UpdateRateLimitTest() { using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); + using var apiConnection = new TwitterApiConnection(); apiConnection.Http = http; mockHandler.Enqueue(x => @@ -146,10 +113,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\""), }; @@ -158,10 +125,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") - .ConfigureAwait(false); + 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"]); @@ -170,11 +140,11 @@ await apiConnection.GetAsync(endpoint, null, endpointName: "/hoge/tetete } [Fact] - public async Task GetAsync_ErrorStatusTest() + public async Task SendAsync_ErrorStatusTest() { using var mockHandler = new HttpMessageHandlerMock(); using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); + using var apiConnection = new TwitterApiConnection(); apiConnection.Http = http; mockHandler.Enqueue(x => @@ -185,10 +155,14 @@ 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")) - .ConfigureAwait(false); + var exception = await Assert.ThrowsAsync( + () => apiConnection.SendAsync(request) + ); // エラーレスポンスの読み込みに失敗した場合はステータスコードをそのままメッセージに使用する Assert.Equal("BadGateway", exception.Message); @@ -198,11 +172,11 @@ 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); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); + using var apiConnection = new TwitterApiConnection(); apiConnection.Http = http; mockHandler.Enqueue(x => @@ -213,10 +187,14 @@ 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")) - .ConfigureAwait(false); + var exception = await Assert.ThrowsAsync( + () => apiConnection.SendAsync(request) + ); // エラーレスポンスの JSON に含まれるエラーコードに基づいてメッセージを出力する Assert.Equal("DuplicateStatus", exception.Message); @@ -228,310 +206,74 @@ public async Task GetAsync_ErrorJsonTest() } [Fact] - public async Task GetStreamAsync_Test() + public async Task HandleTimeout_SuccessTest() { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - using var image = TestUtils.CreateDummyImage(); - apiConnection.Http = http; - - mockHandler.Enqueue(x => + static async Task AsyncFunc(CancellationToken token) { - 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 ByteArrayContent(image.Stream.ToArray()), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var param = new Dictionary - { - ["aaaa"] = "1111", - ["bbbb"] = "2222", - }; - - var stream = await apiConnection.GetStreamAsync(endpoint, param) - .ConfigureAwait(false); - - using (var memoryStream = new MemoryStream()) - { - // 内容の比較のために MemoryStream にコピー - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - - Assert.Equal(image.Stream.ToArray(), memoryStream.ToArray()); + await Task.Delay(10); + token.ThrowIfCancellationRequested(); + return 1; } - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task PostLazyAsync_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(async x => - { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - var body = await x.Content.ReadAsStringAsync() - .ConfigureAwait(false); - var query = HttpUtility.ParseQueryString(body); - - Assert.Equal("1111", query["aaaa"]); - Assert.Equal("2222", query["bbbb"]); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var param = new Dictionary - { - ["aaaa"] = "1111", - ["bbbb"] = "2222", - }; - - var result = await apiConnection.PostLazyAsync(endpoint, param) - .ConfigureAwait(false); - - Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); - - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task PostLazyAsync_MultipartTest() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.HttpUpload = http; - - using var image = TestUtils.CreateDummyImage(); - using var media = new MemoryImageMediaItem(image); + var timeout = TimeSpan.FromMilliseconds(200); + var ret = await TwitterApiConnection.HandleTimeout(AsyncFunc, timeout); - mockHandler.Enqueue(async x => - { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.IsType(x.Content); - - var boundary = x.Content.Headers.ContentType.Parameters.Cast() - .First(y => y.Name == "boundary").Value; - - // 前後のダブルクオーテーションを除去 - boundary = boundary.Substring(1, boundary.Length - 2); - - var expectedText = - $"--{boundary}\r\n" + - "Content-Type: text/plain; charset=utf-8\r\n" + - "Content-Disposition: form-data; name=aaaa\r\n" + - "\r\n" + - "1111\r\n" + - $"--{boundary}\r\n" + - "Content-Type: text/plain; charset=utf-8\r\n" + - "Content-Disposition: form-data; name=bbbb\r\n" + - "\r\n" + - "2222\r\n" + - $"--{boundary}\r\n" + - $"Content-Disposition: form-data; name=media1; filename={media.Name}; filename*=utf-8''{media.Name}\r\n" + - "\r\n"; - - var expected = Encoding.UTF8.GetBytes(expectedText) - .Concat(image.Stream.ToArray()) - .Concat(Encoding.UTF8.GetBytes($"\r\n--{boundary}--\r\n")); - - Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - var param = new Dictionary - { - ["aaaa"] = "1111", - ["bbbb"] = "2222", - }; - var mediaParam = new Dictionary - { - ["media1"] = media, - }; - - var result = await apiConnection.PostLazyAsync(endpoint, param, mediaParam) - .ConfigureAwait(false); - - Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); - - Assert.Equal(0, mockHandler.QueueCount); + Assert.Equal(1, ret); } [Fact] - public async Task PostLazyAsync_Multipart_NullTest() + public async Task HandleTimeout_TimeoutTest() { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.HttpUpload = http; + var tcs = new TaskCompletionSource(); - mockHandler.Enqueue(async x => + async Task AsyncFunc(CancellationToken token) { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.IsType(x.Content); - - var boundary = x.Content.Headers.ContentType.Parameters.Cast() - .First(y => y.Name == "boundary").Value; - - // 前後のダブルクオーテーションを除去 - boundary = boundary.Substring(1, boundary.Length - 2); - - var expectedText = - $"--{boundary}\r\n" + - $"\r\n--{boundary}--\r\n"; - - var expected = Encoding.UTF8.GetBytes(expectedText); - - Assert.Equal(expected, await x.Content.ReadAsByteArrayAsync().ConfigureAwait(false)); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - - var result = await apiConnection.PostLazyAsync(endpoint, param: null, media: null) - .ConfigureAwait(false); - - Assert.Equal("hogehoge", await result.LoadJsonAsync().ConfigureAwait(false)); - - Assert.Equal(0, mockHandler.QueueCount); - } - - [Fact] - public async Task PostJsonAsync_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(async x => - { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.Equal("application/json; charset=utf-8", x.Content.Headers.ContentType.ToString()); - - var body = await x.Content.ReadAsStringAsync() - .ConfigureAwait(false); - - Assert.Equal("""{"aaaa": 1111}""", body); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(@"{""ok"":true}"), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); + await Task.Delay(200); + tcs.SetResult(token.IsCancellationRequested); + return 1; + } - var response = await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}""") - .ConfigureAwait(false); + var timeout = TimeSpan.FromMilliseconds(10); + await Assert.ThrowsAsync( + () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout) + ); - Assert.Equal(@"{""ok"":true}", response); - Assert.Equal(0, mockHandler.QueueCount); + var cancelRequested = await tcs.Task; + Assert.True(cancelRequested); } [Fact] - public async Task PostJsonAsync_T_Test() + public async Task HandleTimeout_ThrowExceptionAfterTimeoutTest() { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.Http = http; + var tcs = new TaskCompletionSource(); - mockHandler.Enqueue(async x => + async Task AsyncFunc(CancellationToken token) { - Assert.Equal(HttpMethod.Post, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); - - Assert.Equal("application/json; charset=utf-8", x.Content.Headers.ContentType.ToString()); - - var body = await x.Content.ReadAsStringAsync() - .ConfigureAwait(false); - - Assert.Equal("""{"aaaa": 1111}""", body); - - return new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("\"hogehoge\""), - }; - }); - - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); - - var response = await apiConnection.PostJsonAsync(endpoint, """{"aaaa": 1111}""") - .ConfigureAwait(false); - - var result = await response.LoadJsonAsync() - .ConfigureAwait(false); - - Assert.Equal("hogehoge", result); - - Assert.Equal(0, mockHandler.QueueCount); - } + await Task.Delay(100); + tcs.SetResult(1); + throw new Exception(); + } - [Fact] - public async Task DeleteAsync_Test() - { - using var mockHandler = new HttpMessageHandlerMock(); - using var http = new HttpClient(mockHandler); - using var apiConnection = new TwitterApiConnection(ApiKey.Create(""), ApiKey.Create(""), "", ""); - apiConnection.Http = http; + var timeout = TimeSpan.FromMilliseconds(10); + await Assert.ThrowsAsync( + () => TwitterApiConnection.HandleTimeout(AsyncFunc, timeout) + ); - mockHandler.Enqueue(x => - { - Assert.Equal(HttpMethod.Delete, x.Method); - Assert.Equal("https://api.twitter.com/1.1/hoge/tetete.json", - x.RequestUri.AbsoluteUri); + // キャンセル後に AsyncFunc で発生した例外が無視される(UnobservedTaskException イベントを発生させない)ことをチェックする + var error = false; + void UnobservedExceptionHandler(object s, UnobservedTaskExceptionEventArgs e) + => error = true; - return new HttpResponseMessage(HttpStatusCode.NoContent); - }); + TaskScheduler.UnobservedTaskException += UnobservedExceptionHandler; - var endpoint = new Uri("hoge/tetete.json", UriKind.Relative); + await tcs.Task; + await Task.Delay(10); + GC.Collect(); // UnobservedTaskException は Task のデストラクタで呼ばれるため強制的に GC を実行する + await Task.Delay(10); - await apiConnection.DeleteAsync(endpoint) - .ConfigureAwait(false); + Assert.False(error); - Assert.Equal(0, mockHandler.QueueCount); + TaskScheduler.UnobservedTaskException -= UnobservedExceptionHandler; } } } diff --git a/OpenTween.Tests/Connection/TwitterCredentialCookieTest.cs b/OpenTween.Tests/Connection/TwitterCredentialCookieTest.cs new file mode 100644 index 000000000..938b8f553 --- /dev/null +++ b/OpenTween.Tests/Connection/TwitterCredentialCookieTest.cs @@ -0,0 +1,47 @@ +// 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.Http; +using Xunit; + +namespace OpenTween.Connection +{ + public class TwitterCredentialCookieTest + { + [Fact] + public void CreateHttpHandler_Test() + { + var appToken = new TwitterAppToken + { + AuthType = APIAuthType.TwitterComCookie, + TwitterComCookie = "aaa=bbb", + }; + var credential = new TwitterCredentialCookie(appToken); + + using var innerHandler = new HttpClientHandler(); + using var handler = credential.CreateHttpHandler(innerHandler); + + var cookieHandler = Assert.IsType(handler); + Assert.Equal("aaa=bbb", cookieHandler.RawCookie); + Assert.Same(innerHandler, cookieHandler.InnerHandler); + } + } +} diff --git a/OpenTween.Tests/Connection/TwitterCredentialOAuth1Test.cs b/OpenTween.Tests/Connection/TwitterCredentialOAuth1Test.cs new file mode 100644 index 000000000..5ede503b5 --- /dev/null +++ b/OpenTween.Tests/Connection/TwitterCredentialOAuth1Test.cs @@ -0,0 +1,51 @@ +// 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.Http; +using Xunit; + +namespace OpenTween.Connection +{ + public class TwitterCredentialOAuth1Test + { + [Fact] + public void CreateHttpHandler_Test() + { + var appToken = new TwitterAppToken + { + AuthType = APIAuthType.OAuth1, + OAuth1CustomConsumerKey = ApiKey.Create("consumer_key"), + OAuth1CustomConsumerSecret = ApiKey.Create("consumer_secret"), + }; + var credential = new TwitterCredentialOAuth1(appToken, "access_token", "access_secret"); + + using var innerHandler = new HttpClientHandler(); + using var handler = credential.CreateHttpHandler(innerHandler); + + var oauthHandler = Assert.IsType(handler); + Assert.Equal("consumer_key", oauthHandler.ConsumerKey.Value); + Assert.Equal("consumer_secret", oauthHandler.ConsumerSecret.Value); + Assert.Equal("access_token", oauthHandler.AccessToken); + Assert.Equal("access_secret", oauthHandler.AccessSecret); + Assert.Same(innerHandler, oauthHandler.InnerHandler); + } + } +} diff --git a/OpenTween.Tests/Connection/UriQueryBuilderTest.cs b/OpenTween.Tests/Connection/UriQueryBuilderTest.cs new file mode 100644 index 000000000..02eaca978 --- /dev/null +++ b/OpenTween.Tests/Connection/UriQueryBuilderTest.cs @@ -0,0 +1,61 @@ +// 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 Xunit; + +namespace OpenTween.Connection +{ + public class UriQueryBuilderTest + { + [Fact] + public void Build_Test() + { + var uri = new Uri("https://example.com/hoge"); + var query = new Dictionary + { + ["foo"] = "bar", + }; + Assert.Equal(new("https://example.com/hoge?foo=bar"), UriQueryBuilder.Build(uri, query)); + } + + [Fact] + public void Build_NullTest() + { + var uri = new Uri("https://example.com/hoge"); + Assert.Equal(new("https://example.com/hoge"), UriQueryBuilder.Build(uri, null)); + } + + [Fact] + public void Build_CannotMergeTest() + { + var uri = new Uri("https://example.com/hoge?aaa=111"); + var query = new Dictionary + { + ["bbb"] = "222", + }; + Assert.Throws( + () => UriQueryBuilder.Build(uri, query) + ); + } + } +} diff --git a/OpenTween.Tests/DebounceTimerTest.cs b/OpenTween.Tests/DebounceTimerTest.cs index aadda5b2c..cf570bf9e 100644 --- a/OpenTween.Tests/DebounceTimerTest.cs +++ b/OpenTween.Tests/DebounceTimerTest.cs @@ -62,6 +62,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -119,6 +121,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -159,6 +163,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -215,6 +221,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -272,6 +280,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -312,6 +322,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: true, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -385,6 +397,8 @@ Task Callback() using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); var mockTimer = debouncing.MockTimer; + debouncing.Enabled = true; + Assert.Equal(0, count); Assert.False(mockTimer.IsTimerRunning); @@ -418,5 +432,48 @@ Task Callback() Assert.False(mockTimer.IsTimerRunning); } } + + [Fact] + public async Task Call_DisabledTest() + { + using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) + { + static Task Callback() + => Task.CompletedTask; + + var interval = TimeSpan.FromMinutes(2); + var maxWait = TimeSpan.MaxValue; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; + + debouncing.Enabled = false; + + await debouncing.Call(); + Assert.False(mockTimer.IsTimerRunning); + } + } + + [Fact] + public async Task DisabledWhileTimerIsRunning() + { + using (TestUtils.FreezeTime(new DateTimeUtc(2022, 1, 1, 0, 0, 0))) + { + static Task Callback() + => Task.CompletedTask; + + var interval = TimeSpan.FromMinutes(2); + var maxWait = TimeSpan.MaxValue; + using var debouncing = new TestDebounceTimer(Callback, interval, leading: false, trailing: true); + var mockTimer = debouncing.MockTimer; + + debouncing.Enabled = true; + + await debouncing.Call(); + Assert.True(mockTimer.IsTimerRunning); + + debouncing.Enabled = false; + Assert.False(mockTimer.IsTimerRunning); + } + } } } diff --git a/OpenTween.Tests/DetailsListViewTest.cs b/OpenTween.Tests/DetailsListViewTest.cs new file mode 100644 index 000000000..d890a1499 --- /dev/null +++ b/OpenTween.Tests/DetailsListViewTest.cs @@ -0,0 +1,40 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.OpenTweenCustomControl; +using Xunit; + +namespace OpenTween +{ + public class DetailsListViewTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var listView = new DetailsListView(); + } + } +} diff --git a/OpenTween.Tests/EncryptApiKeyDialogTest.cs b/OpenTween.Tests/EncryptApiKeyDialogTest.cs new file mode 100644 index 000000000..cf11ebcce --- /dev/null +++ b/OpenTween.Tests/EncryptApiKeyDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class EncryptApiKeyDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new EncryptApiKeyDialog(); + } + } +} diff --git a/OpenTween.Tests/ErrorReportHandlerTest.cs b/OpenTween.Tests/ErrorReportHandlerTest.cs new file mode 100644 index 000000000..ae78cd620 --- /dev/null +++ b/OpenTween.Tests/ErrorReportHandlerTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class ErrorReportHandlerTest + { + [Fact] + public void Initialize_Test() + { + using var handler = new ErrorReportHandler(); + } + } +} diff --git a/OpenTween.Tests/ExtensionsTest.cs b/OpenTween.Tests/ExtensionsTest.cs index 7a6bbd90b..38f67ef61 100644 --- a/OpenTween.Tests/ExtensionsTest.cs +++ b/OpenTween.Tests/ExtensionsTest.cs @@ -26,6 +26,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Windows.Forms; using Moq; using Xunit; @@ -125,6 +126,60 @@ public void GetCodepointCount_ErrorTest() Assert.Throws(() => "abc".GetCodepointCount(2, 1)); } + [WinFormsFact] + public async Task TryInvoke_InvokeNotRequiredTest() + { + var tcs = new TaskCompletionSource(); + using var control = new Control(); + control.CreateControl(); + + var uiThreadId = Thread.CurrentThread.ManagedThreadId; + var ret = control.TryInvoke(() => + { + Assert.Equal(uiThreadId, Thread.CurrentThread.ManagedThreadId); + tcs.SetResult(1); + }); + Assert.True(ret); + + await tcs.Task; + } + + [WinFormsFact] + public async Task TryInvoke_InvokeRequiredTest() + { + var tcs = new TaskCompletionSource(); + using var control = new Control(); + control.CreateControl(); + + var uiThreadId = Thread.CurrentThread.ManagedThreadId; + await Task.Run(() => + { + var workerThreadId = Thread.CurrentThread.ManagedThreadId; + var ret = control.TryInvoke(() => + { + Assert.NotEqual(workerThreadId, Thread.CurrentThread.ManagedThreadId); + Assert.Equal(uiThreadId, Thread.CurrentThread.ManagedThreadId); + tcs.SetResult(1); + }); + Assert.True(ret); + }); + + await tcs.Task; + } + + [WinFormsFact] + public void TryInvoke_DisposedTest() + { + var control = new Control(); + control.CreateControl(); + control.Dispose(); + + var ret = control.TryInvoke( + () => Assert.Fail("should not be called") + ); + Assert.False(ret); + } + [Fact] public async Task ForEachAsync_Test() { diff --git a/OpenTween.Tests/FilterDialogTest.cs b/OpenTween.Tests/FilterDialogTest.cs new file mode 100644 index 000000000..fee1961b7 --- /dev/null +++ b/OpenTween.Tests/FilterDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class FilterDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new FilterDialog(); + } + } +} diff --git a/OpenTween.Tests/HookGlobalHotkeyTest.cs b/OpenTween.Tests/HookGlobalHotkeyTest.cs new file mode 100644 index 000000000..246b7fff0 --- /dev/null +++ b/OpenTween.Tests/HookGlobalHotkeyTest.cs @@ -0,0 +1,41 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Xunit; + +namespace OpenTween +{ + public class HookGlobalHotkeyTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var form = new Form(); + using var hook = new HookGlobalHotkey(form); + } + } +} diff --git a/OpenTween.Tests/IconAssetsManagerTest.cs b/OpenTween.Tests/IconAssetsManagerTest.cs new file mode 100644 index 000000000..ceb4c3a12 --- /dev/null +++ b/OpenTween.Tests/IconAssetsManagerTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class IconAssetsManagerTest + { + [Fact] + public void Initialize_Test() + { + using var assetsManager = new IconAssetsManager(); + } + } +} diff --git a/OpenTween.Tests/ImageCacheTest.cs b/OpenTween.Tests/ImageCacheTest.cs new file mode 100644 index 000000000..a707e14b9 --- /dev/null +++ b/OpenTween.Tests/ImageCacheTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class ImageCacheTest + { + [Fact] + public void Initialize_Test() + { + using var imageCache = new ImageCache(); + } + } +} diff --git a/OpenTween.Tests/InputTabNameTest.cs b/OpenTween.Tests/InputTabNameTest.cs new file mode 100644 index 000000000..559785a94 --- /dev/null +++ b/OpenTween.Tests/InputTabNameTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class InputTabNameTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new InputTabName(); + } + } +} diff --git a/OpenTween.Tests/InternetSecurityManagerTest.cs b/OpenTween.Tests/InternetSecurityManagerTest.cs new file mode 100644 index 000000000..d5cdb1196 --- /dev/null +++ b/OpenTween.Tests/InternetSecurityManagerTest.cs @@ -0,0 +1,41 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Xunit; + +namespace OpenTween +{ + public class InternetSecurityManagerTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var webBrowser = new WebBrowser(); + var securityManager = new InternetSecurityManager(webBrowser); + } + } +} diff --git a/OpenTween.Tests/LRUCacheDictionaryTest.cs b/OpenTween.Tests/LRUCacheDictionaryTest.cs index d9d56bd39..b921fcea3 100644 --- a/OpenTween.Tests/LRUCacheDictionaryTest.cs +++ b/OpenTween.Tests/LRUCacheDictionaryTest.cs @@ -223,10 +223,12 @@ public void ContainsTest() ["key3"] = "value3", }; - Assert.Contains(new KeyValuePair("key1", "value1"), dict); - Assert.DoesNotContain(new KeyValuePair("key3", "value2"), dict); - Assert.DoesNotContain(new KeyValuePair("value3", "key3"), dict); - Assert.DoesNotContain(new KeyValuePair("hogehoge", "hogehoge"), dict); +#pragma warning disable xUnit2017 + Assert.True(dict.Contains(new("key1", "value1"))); + Assert.False(dict.Contains(new("key3", "value2"))); + Assert.False(dict.Contains(new("value3", "key3"))); + Assert.False(dict.Contains(new("hogehoge", "hogehoge"))); +#pragma warning restore xUnit2017 } [Fact] diff --git a/OpenTween.Tests/ListAvailableTest.cs b/OpenTween.Tests/ListAvailableTest.cs new file mode 100644 index 000000000..b5110941a --- /dev/null +++ b/OpenTween.Tests/ListAvailableTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class ListAvailableTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new ListAvailable(); + } + } +} diff --git a/OpenTween.Tests/ListManageTest.cs b/OpenTween.Tests/ListManageTest.cs new file mode 100644 index 000000000..8f3c9aa0a --- /dev/null +++ b/OpenTween.Tests/ListManageTest.cs @@ -0,0 +1,42 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Api; +using Xunit; + +namespace OpenTween +{ + public class ListManageTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var twitterApi = new TwitterApi(); + using var twitter = new Twitter(twitterApi); + using var dialog = new ListManage(twitter); + } + } +} diff --git a/OpenTween.Tests/LoginDialogTest.cs b/OpenTween.Tests/LoginDialogTest.cs new file mode 100644 index 000000000..8b66cace3 --- /dev/null +++ b/OpenTween.Tests/LoginDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class LoginDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new LoginDialog(); + } + } +} diff --git a/OpenTween.Tests/MediaSelectorTest.cs b/OpenTween.Tests/MediaSelectorTest.cs index 2ad1e83f3..ffd650269 100644 --- a/OpenTween.Tests/MediaSelectorTest.cs +++ b/OpenTween.Tests/MediaSelectorTest.cs @@ -51,10 +51,9 @@ private void MyCommonSetup() [Fact] public void SelectedMediaServiceIndex_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); Assert.Equal("Twitter", mediaSelector.MediaServices[0].Key); @@ -70,10 +69,9 @@ public void SelectedMediaServiceIndex_Test() [Fact] public void SelectMediaService_TwitterTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -89,10 +87,9 @@ public void SelectMediaService_TwitterTest() [Fact] public void SelectMediaService_ImgurTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Imgur"); @@ -106,10 +103,9 @@ public void SelectMediaService_ImgurTest() [Fact] public void AddMediaItem_FilePath_SingleTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -132,10 +128,9 @@ public void AddMediaItem_FilePath_SingleTest() [Fact] public void AddMediaItem_MemoryImageTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -160,10 +155,9 @@ public void AddMediaItem_MemoryImageTest() [Fact] public void AddMediaItem_FilePath_MultipleTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -186,10 +180,9 @@ public void AddMediaItem_FilePath_MultipleTest() [Fact] public void ClearMediaItems_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -207,10 +200,9 @@ public void ClearMediaItems_Test() [Fact] public void DetachMediaItems_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -230,10 +222,9 @@ public void DetachMediaItems_Test() [Fact] public void SelectedMediaItemChange_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -268,10 +259,9 @@ public void SelectedMediaItemChange_Test() [Fact] public void SelectedMediaItemChange_DisposeTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -292,10 +282,9 @@ public void SelectedMediaItemChange_DisposeTest() [Fact] public void SetSelectedMediaAltText_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -317,10 +306,9 @@ public void SetSelectedMediaAltText_Test() [Fact] public void Validate_PassTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -333,10 +321,9 @@ public void Validate_PassTest() [Fact] public void Validate_EmptyErrorTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -350,10 +337,9 @@ public void Validate_EmptyErrorTest() [Fact] public void Validate_ServiceNotSelectedErrorTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); using var mediaItem = TestUtils.CreateDummyMediaItem(); @@ -368,10 +354,9 @@ public void Validate_ServiceNotSelectedErrorTest() [Fact] public void Validate_ExtensionErrorTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -391,10 +376,9 @@ public void Validate_ExtensionErrorTest() [Fact] public void Validate_FileSizeErrorTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); - twitter.Initialize("", "", "", 0L); mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); mediaSelector.SelectMediaService("Twitter"); @@ -414,7 +398,7 @@ public void Validate_FileSizeErrorTest() [Fact] public void MoveSelectedMediaItemToPrevious_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); @@ -430,7 +414,7 @@ public void MoveSelectedMediaItemToPrevious_Test() [Fact] public void MoveSelectedMediaItemToNext_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); @@ -446,7 +430,7 @@ public void MoveSelectedMediaItemToNext_Test() [Fact] public void RemoveSelectedMediaItem_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); using var mediaSelector = new MediaSelector(); diff --git a/OpenTween.Tests/MemoryImageListTest.cs b/OpenTween.Tests/MemoryImageListTest.cs new file mode 100644 index 000000000..167088d69 --- /dev/null +++ b/OpenTween.Tests/MemoryImageListTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class MemoryImageListTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var imageList = new MemoryImageList(); + } + } +} diff --git a/OpenTween.Tests/MemoryImageTest.cs b/OpenTween.Tests/MemoryImageTest.cs index b038455c0..bf29a2f70 100644 --- a/OpenTween.Tests/MemoryImageTest.cs +++ b/OpenTween.Tests/MemoryImageTest.cs @@ -37,7 +37,7 @@ public class MemoryImageTest public async Task ImageFormat_GifTest() { using var imgStream = File.OpenRead("Resources/re.gif"); - using var image = await MemoryImage.CopyFromStreamAsync(imgStream).ConfigureAwait(false); + using var image = await MemoryImage.CopyFromStreamAsync(imgStream); Assert.Equal(ImageFormat.Gif, image.ImageFormat); Assert.Equal(".gif", image.ImageFormatExt); } @@ -58,8 +58,7 @@ public async Task CopyFromStream_Test() { using var stream = File.OpenRead("Resources/re.gif"); using var memstream = new MemoryStream(); - await stream.CopyToAsync(memstream) - .ConfigureAwait(false); + await stream.CopyToAsync(memstream); stream.Seek(0, SeekOrigin.Begin); @@ -72,13 +71,11 @@ public async Task CopyFromStreamAsync_Test() { using var stream = File.OpenRead("Resources/re.gif"); using var memstream = new MemoryStream(); - await stream.CopyToAsync(memstream) - .ConfigureAwait(false); + await stream.CopyToAsync(memstream); stream.Seek(0, SeekOrigin.Begin); - using var image = await MemoryImage.CopyFromStreamAsync(stream) - .ConfigureAwait(false); + using var image = await MemoryImage.CopyFromStreamAsync(stream); Assert.Equal(memstream.ToArray(), image.Stream.ToArray()); } @@ -87,8 +84,7 @@ public async Task CopyFromBytes_Test() { using var stream = File.OpenRead("Resources/re.gif"); using var memstream = new MemoryStream(); - await stream.CopyToAsync(memstream) - .ConfigureAwait(false); + await stream.CopyToAsync(memstream); var imageBytes = memstream.ToArray(); using var image = MemoryImage.CopyFromBytes(imageBytes); @@ -121,10 +117,10 @@ public void Dispose_Test() public async Task Equals_Test() { using var imgStream1 = File.OpenRead("Resources/re.gif"); - using var image1 = await MemoryImage.CopyFromStreamAsync(imgStream1).ConfigureAwait(false); + using var image1 = await MemoryImage.CopyFromStreamAsync(imgStream1); using var imgStream2 = File.OpenRead("Resources/re.gif"); - using var image2 = await MemoryImage.CopyFromStreamAsync(imgStream2).ConfigureAwait(false); + using var image2 = await MemoryImage.CopyFromStreamAsync(imgStream2); Assert.True(image1.Equals(image2)); Assert.True(image2.Equals(image1)); diff --git a/OpenTween.Tests/Models/PostClassTest.cs b/OpenTween.Tests/Models/PostClassTest.cs index f4f32a7c6..c7bbfb100 100644 --- a/OpenTween.Tests/Models/PostClassTest.cs +++ b/OpenTween.Tests/Models/PostClassTest.cs @@ -305,64 +305,5 @@ public void ConvertToOriginalPost_ErrorTest() Assert.Throws(() => post.ConvertToOriginalPost()); } - - private class FakeExpandedUrlInfo : PostClass.ExpandedUrlInfo - { - public TaskCompletionSource FakeResult = new(); - - public FakeExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) - : base(url, expandedUrl, deepExpand) - { - } - - protected override async Task DeepExpandAsync() - => this.expandedUrl = await this.FakeResult.Task; - } - - [Fact] - public async Task ExpandedUrls_BasicScenario() - { - PostClass.ExpandedUrlInfo.AutoExpand = true; - - var post = new PostClass - { - Text = """bit.ly/abcde""", - ExpandedUrls = new[] - { - new FakeExpandedUrlInfo( - // 展開前の t.co ドメインの URL - url: "http://t.co/aaaaaaa", - - // Entity の expanded_url に含まれる URL - expandedUrl: "http://bit.ly/abcde", - - // expandedUrl をさらに ShortUrl クラスで再帰的に展開する - deepExpand: true - ), - }, - }; - - var urlInfo = (FakeExpandedUrlInfo)post.ExpandedUrls.Single(); - - // ExpandedUrlInfo による展開が完了していない状態 - // → この段階では Entity に含まれる expanded_url の URL が使用される - Assert.False(urlInfo.ExpandedCompleted); - Assert.Equal("http://bit.ly/abcde", urlInfo.ExpandedUrl); - Assert.Equal("http://bit.ly/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa")); - Assert.Equal(new[] { "http://bit.ly/abcde" }, post.GetExpandedUrls()); - Assert.Equal("""bit.ly/abcde""", post.Text); - - // bit.ly 展開後の URL は「http://example.com/abcde」 - urlInfo.FakeResult.SetResult("http://example.com/abcde"); - await urlInfo.ExpandTask; - - // ExpandedUrlInfo による展開が完了した後の状態 - // → 再帰的な展開後の URL が使用される - Assert.True(urlInfo.ExpandedCompleted); - Assert.Equal("http://example.com/abcde", urlInfo.ExpandedUrl); - Assert.Equal("http://example.com/abcde", post.GetExpandedUrl("http://t.co/aaaaaaa")); - Assert.Equal(new[] { "http://example.com/abcde" }, post.GetExpandedUrls()); - Assert.Equal("""bit.ly/abcde""", post.Text); - } } } diff --git a/OpenTween.Tests/Models/PostUrlExpanderTest.cs b/OpenTween.Tests/Models/PostUrlExpanderTest.cs new file mode 100644 index 000000000..e6620600d --- /dev/null +++ b/OpenTween.Tests/Models/PostUrlExpanderTest.cs @@ -0,0 +1,91 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween.Models +{ + public class PostUrlExpanderTest + { + [Fact] + public async Task Expand_Test() + { + var handler = new HttpMessageHandlerMock(); + using var http = new HttpClient(handler); + var shortUrl = new ShortUrl(http); + + // https://bit.ly/abcde -> https://example.com/abcde + handler.Enqueue(x => + { + Assert.Equal(HttpMethod.Head, x.Method); + Assert.Equal(new Uri("https://bit.ly/abcde"), x.RequestUri); + + return new HttpResponseMessage(HttpStatusCode.TemporaryRedirect) + { + Headers = { Location = new Uri("https://example.com/abcde") }, + }; + }); + + var post = new PostClass + { + Text = """bit.ly/abcde""", + ExpandedUrls = new[] + { + new PostClass.ExpandedUrlInfo( + // 展開前の t.co ドメインの URL + Url: "https://t.co/aaaaaaa", + + // Entity の expanded_url に含まれる URL + ExpandedUrl: "https://bit.ly/abcde" + ), + }, + }; + + var urlInfo = post.ExpandedUrls.Single(); + + // 短縮 URL の展開が完了していない状態 + // → この段階では Entity に含まれる expanded_url の URL が使用される + Assert.False(urlInfo.ExpandCompleted); + Assert.Equal("https://bit.ly/abcde", urlInfo.ExpandedUrl); + Assert.Equal("https://bit.ly/abcde", post.GetExpandedUrl("https://t.co/aaaaaaa")); + Assert.Equal(new[] { "https://bit.ly/abcde" }, post.GetExpandedUrls()); + Assert.Equal("""bit.ly/abcde""", post.Text); + + // bit.ly 展開後の URL は「https://example.com/abcde」 + var expander = new PostUrlExpander(shortUrl); + await expander.Expand(post); + + // 短縮 URL の展開が完了した後の状態 + // → 再帰的な展開後の URL が使用される + urlInfo = post.ExpandedUrls.Single(); + Assert.True(urlInfo.ExpandCompleted); + Assert.Equal("https://example.com/abcde", urlInfo.ExpandedUrl); + Assert.Equal("https://example.com/abcde", post.GetExpandedUrl("https://t.co/aaaaaaa")); + Assert.Equal(new[] { "https://example.com/abcde" }, post.GetExpandedUrls()); + Assert.Equal("""bit.ly/abcde""", post.Text); + } + } +} diff --git a/OpenTween.Tests/Models/TabInformationTest.cs b/OpenTween.Tests/Models/TabInformationTest.cs index f4ba637d9..56ee16757 100644 --- a/OpenTween.Tests/Models/TabInformationTest.cs +++ b/OpenTween.Tests/Models/TabInformationTest.cs @@ -310,6 +310,61 @@ public void SelectTab_Test() public void SelectTab_NotExistTest() => Assert.Throws(() => this.tabinfo.SelectTab("INVALID")); + [Fact] + public void LoadTabsFromSettings_Test() + { + var settingTabs = new SettingTabs + { + Tabs = + { + new() + { + TabName = "hoge", + TabType = MyCommon.TabUsageType.PublicSearch, + SearchWords = "aaa", + }, + }, + }; + var tabinfo = this.CreateInstance(); + tabinfo.LoadTabsFromSettings(settingTabs); + Assert.Single(tabinfo.Tabs); + + var tab = (PublicSearchTabModel)tabinfo.Tabs["hoge"]; + Assert.Equal("aaa", tab.SearchWords); + } + + [Fact] + public void LoadTabsFromSettings_DuplicateTabNameTest() + { + var settingTabs = new SettingTabs + { + Tabs = + { + new() + { + TabName = "hoge", + TabType = MyCommon.TabUsageType.PublicSearch, + SearchWords = "aaa", + }, + new() + { + TabName = "hoge", // 重複したタブ名 + TabType = MyCommon.TabUsageType.PublicSearch, + SearchWords = "bbb", + }, + }, + }; + var tabinfo = this.CreateInstance(); + tabinfo.LoadTabsFromSettings(settingTabs); + Assert.Equal(2, tabinfo.Tabs.Count); + + var tab1 = (PublicSearchTabModel)tabinfo.Tabs["hoge"]; + Assert.Equal("aaa", tab1.SearchWords); + + var tab2 = (PublicSearchTabModel)tabinfo.Tabs["hoge2"]; + Assert.Equal("bbb", tab2.SearchWords); + } + [Theory] [InlineData(MyCommon.TabUsageType.Home, typeof(HomeTabModel))] [InlineData(MyCommon.TabUsageType.Mentions, typeof(MentionsTabModel))] @@ -375,7 +430,7 @@ public void CreateTabFromSettings_PublicSearchTabTest() public void AddDefaultTabs_Test() { var tabinfo = this.CreateInstance(); - Assert.Equal(0, tabinfo.Tabs.Count); + Assert.Empty(tabinfo.Tabs); tabinfo.AddDefaultTabs(); diff --git a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs index c12a7d044..88718fd24 100644 --- a/OpenTween.Tests/Models/TwitterPostFactoryTest.cs +++ b/OpenTween.Tests/Models/TwitterPostFactoryTest.cs @@ -32,9 +32,6 @@ public class TwitterPostFactoryTest private readonly Random random = new(); - public TwitterPostFactoryTest() - => PostClass.ExpandedUrlInfo.AutoExpand = false; - private TabInformations CreateTabinfo() { var tabinfo = new TabInformations(); diff --git a/OpenTween.Tests/MouseWheelMessageFilterTest.cs b/OpenTween.Tests/MouseWheelMessageFilterTest.cs index 3d246ef5c..20c2e84da 100644 --- a/OpenTween.Tests/MouseWheelMessageFilterTest.cs +++ b/OpenTween.Tests/MouseWheelMessageFilterTest.cs @@ -31,6 +31,12 @@ namespace OpenTween { public class MouseWheelMessageFilterTest { + [WinFormsFact] + public void Initialize_Test() + { + using var filter = new MouseWheelMessageFilter(); + } + [Fact] public void ParseMessage_MinusTest() { diff --git a/OpenTween.Tests/MyCommonTest.cs b/OpenTween.Tests/MyCommonTest.cs index 71ffbf50b..8ebb2a1bc 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)] @@ -149,14 +157,14 @@ public void IsValidEmailTest(string email, bool expected) => Assert.Equal(expected, MyCommon.IsValidEmail(email)); [Theory] - [InlineData(Keys.Shift, new[] { Keys.Shift }, true)] - [InlineData(Keys.Shift, new[] { Keys.Control }, false)] - [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Control }, true)] - [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Alt }, true)] - [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Control, Keys.Alt }, true)] - [InlineData(Keys.Control | Keys.Alt, new[] { Keys.Shift }, false)] - public void IsKeyDownTest(Keys modifierKeys, Keys[] checkKeys, bool expected) - => Assert.Equal(expected, MyCommon.IsKeyDownInternal(modifierKeys, checkKeys)); + [InlineData(Keys.Shift, Keys.Shift, true)] + [InlineData(Keys.Shift, Keys.Control, false)] + [InlineData(Keys.Control | Keys.Alt, Keys.Control, true)] + [InlineData(Keys.Control | Keys.Alt, Keys.Alt, true)] + [InlineData(Keys.Control | Keys.Alt, Keys.Control | Keys.Alt, true)] + [InlineData(Keys.Control | Keys.Alt, Keys.Shift, false)] + public void IsKeyDownTest(Keys modifierKeys, Keys checkKeys, bool expected) + => Assert.Equal(expected, MyCommon.IsKeyDown(modifierKeys, checkKeys)); [Fact] public void GetAssemblyNameTest() @@ -346,7 +354,7 @@ public void CreateBrowserProcessStartInfo_QuotedBrowserPathWithArgsTest() Assert.False(startInfo.UseShellExecute); } - public static readonly TheoryData ToRangeChunkTestCase = new() + public static readonly TheoryData ToRangeChunkTestCase = new() { { new[] { 1 }, diff --git a/OpenTween.Tests/MyListsTest.cs b/OpenTween.Tests/MyListsTest.cs new file mode 100644 index 000000000..9d98e5945 --- /dev/null +++ b/OpenTween.Tests/MyListsTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class MyListsTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new MyLists(); + } + } +} diff --git a/OpenTween.Tests/OTBaseFormTest.cs b/OpenTween.Tests/OTBaseFormTest.cs index e89f26148..4311fcaaa 100644 --- a/OpenTween.Tests/OTBaseFormTest.cs +++ b/OpenTween.Tests/OTBaseFormTest.cs @@ -125,5 +125,28 @@ public void ScaleChildControl_VScrollBarTest() Assert.Equal(40, scrollBar.Width); } + + [WinFormsFact] + public void ScaleChildControl_ImageListTest() + { + using var imageList = new ImageList() { ImageSize = new(16, 16) }; + OTBaseForm.ScaleChildControl(imageList, new SizeF(2.0f, 2.0f)); + + Assert.Equal(new(32, 32), imageList.ImageSize); + } + + [Fact] + public void ScaleBy_SizeTest() + { + var factor = new SizeF(2.0f, 2.0f); + Assert.Equal(new(32, 32), OTBaseForm.ScaleBy(factor, new(16, 16))); + } + + [Fact] + public void ScaleBy_IntegerTest() + { + var factor = 2.0f; + Assert.Equal(32, OTBaseForm.ScaleBy(factor, 16)); + } } } diff --git a/OpenTween.Tests/OpenTween.Tests.csproj b/OpenTween.Tests/OpenTween.Tests.csproj index f9444e3b6..6d5f37fd7 100644 --- a/OpenTween.Tests/OpenTween.Tests.csproj +++ b/OpenTween.Tests/OpenTween.Tests.csproj @@ -15,6 +15,7 @@ + @@ -25,91 +26,25 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest diff --git a/OpenTween.Tests/OpenURLTest.cs b/OpenTween.Tests/OpenURLTest.cs new file mode 100644 index 000000000..523080388 --- /dev/null +++ b/OpenTween.Tests/OpenURLTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class OpenURLTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new OpenURL(); + } + } +} diff --git a/OpenTween.Tests/Resources/Responses/DeleteRetweet.json b/OpenTween.Tests/Resources/Responses/DeleteRetweet.json new file mode 100644 index 000000000..f125b0470 --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/DeleteRetweet.json @@ -0,0 +1,14 @@ +{ + "data": { + "unretweet": { + "source_tweet_results": { + "result": { + "rest_id": "1234567890123456789", + "legacy": { + "full_text": "foo" + } + } + } + } + } +} diff --git a/OpenTween.Tests/Resources/Responses/DeleteTweet.json b/OpenTween.Tests/Resources/Responses/DeleteTweet.json new file mode 100644 index 000000000..919d638ea --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/DeleteTweet.json @@ -0,0 +1,7 @@ +{ + "data": { + "delete_tweet": { + "tweet_results": {} + } + } +} diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json new file mode 100644 index 000000000..67e4fe360 --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet.json @@ -0,0 +1,252 @@ +{ + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1588614645866147840", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjo0MDQ4MDY2NA==", + "rest_id": "40480664", + "affiliates_highlighted_label": {}, + "has_graduated_access": false, + "is_blue_verified": false, + "profile_image_shape": "Circle", + "legacy": { + "can_dm": true, + "can_media_tag": true, + "created_at": "Sat May 16 15:20:01 +0000 2009", + "default_profile": false, + "default_profile_image": false, + "description": "", + "entities": { + "description": { + "urls": [] + }, + "url": { + "urls": [ + { + "display_url": "m.upsilo.net/@upsilon", + "expanded_url": "https://m.upsilo.net/@upsilon", + "url": "https://t.co/vNMmyHHh15", + "indices": [ + 0, + 23 + ] + } + ] + } + }, + "fast_followers_count": 0, + "favourites_count": 215369, + "followers_count": 1287, + "friends_count": 1, + "has_custom_timelines": false, + "is_translator": false, + "listed_count": 92, + "location": "Funabashi, Chiba, Japan", + "media_count": 876, + "name": "upsilon", + "needs_phone_verification": false, + "normal_followers_count": 1287, + "pinned_tweet_ids_str": [], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png", + "profile_interstitial_type": "", + "screen_name": "kim_upsilon", + "statuses_count": 10081, + "translator_type": "regular", + "url": "https://t.co/vNMmyHHh15", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + } + } + } + }, + "unmention_data": {}, + "unified_card": { + "card_fetch_state": "NoCard" + }, + "edit_control": { + "edit_tweet_ids": [ + "1588614645866147840" + ], + "editable_until_msecs": "1667592021000", + "is_edit_eligible": false, + "edits_remaining": "5" + }, + "is_translatable": true, + "views": { + "state": "Enabled" + }, + "source": "OpenTween (dev)", + "quoted_status_result": { + "result": { + "__typename": "Tweet", + "rest_id": "1583108196868116480", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjo0MDQ4MDY2NA==", + "rest_id": "40480664", + "affiliates_highlighted_label": {}, + "has_graduated_access": false, + "is_blue_verified": false, + "profile_image_shape": "Circle", + "legacy": { + "can_dm": true, + "can_media_tag": true, + "created_at": "Sat May 16 15:20:01 +0000 2009", + "default_profile": false, + "default_profile_image": false, + "description": "", + "entities": { + "description": { + "urls": [] + }, + "url": { + "urls": [ + { + "display_url": "m.upsilo.net/@upsilon", + "expanded_url": "https://m.upsilo.net/@upsilon", + "url": "https://t.co/vNMmyHHh15", + "indices": [ + 0, + 23 + ] + } + ] + } + }, + "fast_followers_count": 0, + "favourites_count": 215369, + "followers_count": 1287, + "friends_count": 1, + "has_custom_timelines": false, + "is_translator": false, + "listed_count": 92, + "location": "Funabashi, Chiba, Japan", + "media_count": 876, + "name": "upsilon", + "needs_phone_verification": false, + "normal_followers_count": 1287, + "pinned_tweet_ids_str": [], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png", + "profile_interstitial_type": "", + "screen_name": "kim_upsilon", + "statuses_count": 10081, + "translator_type": "regular", + "url": "https://t.co/vNMmyHHh15", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + } + } + } + }, + "unmention_data": {}, + "edit_control": { + "edit_tweet_ids": [ + "1583108196868116480" + ], + "editable_until_msecs": "1666279181000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "is_translatable": true, + "views": { + "state": "Enabled" + }, + "source": "OpenTween (dev)", + "legacy": { + "bookmark_count": 0, + "bookmarked": false, + "created_at": "Thu Oct 20 14:49:41 +0000 2022", + "conversation_id_str": "1583108196868116480", + "display_text_range": [ + 0, + 97 + ], + "entities": { + "user_mentions": [], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 2, + "favorited": false, + "full_text": "AppVeyorでビルドした時と自分の開発環境でビルドした時でなぜか sgen.exe の出力が異なって生成物のハッシュ値が一致しなくなる問題に悩み中。Reproducible Buildむずい", + "is_quote_status": false, + "lang": "ja", + "quote_count": 1, + "reply_count": 0, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "40480664", + "id_str": "1583108196868116480" + } + } + }, + "legacy": { + "bookmark_count": 0, + "bookmarked": false, + "created_at": "Fri Nov 04 19:30:21 +0000 2022", + "conversation_id_str": "1588614645866147840", + "display_text_range": [ + 0, + 63 + ], + "entities": { + "user_mentions": [], + "urls": [ + { + "display_url": "twitter.com/kim_upsilon/st…", + "expanded_url": "https://twitter.com/kim_upsilon/status/1583108196868116480", + "url": "https://t.co/mb89Ecojqd", + "indices": [ + 40, + 63 + ] + } + ], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 2, + "favorited": false, + "full_text": "これ結局原因が分からないまま sgen.exe を使うのを止めることで解決した https://t.co/mb89Ecojqd", + "is_quote_status": true, + "lang": "ja", + "possibly_sensitive": false, + "possibly_sensitive_editable": true, + "quote_count": 0, + "quoted_status_id_str": "1583108196868116480", + "quoted_status_permalink": { + "url": "https://t.co/mb89Ecojqd", + "expanded": "https://twitter.com/kim_upsilon/status/1583108196868116480", + "display": "twitter.com/kim_upsilon/st…" + }, + "reply_count": 1, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "40480664", + "id_str": "1588614645866147840" + }, + "quick_promote_eligibility": { + "eligibility": "IneligibleNotProfessional" + } + } + }, + "tweetDisplayType": "SelfThread", + "hasModeratedReplies": false +} diff --git a/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json new file mode 100644 index 000000000..d1de5794e --- /dev/null +++ b/OpenTween.Tests/Resources/Responses/TimelineTweet_QuotedTweet_Tombstone.json @@ -0,0 +1,159 @@ +{ + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1614653321310253057", + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjo0MDQ4MDY2NA==", + "rest_id": "40480664", + "affiliates_highlighted_label": {}, + "has_graduated_access": false, + "is_blue_verified": false, + "profile_image_shape": "Circle", + "legacy": { + "can_dm": false, + "can_media_tag": false, + "created_at": "Sat May 16 15:20:01 +0000 2009", + "default_profile": false, + "default_profile_image": false, + "description": "", + "entities": { + "description": { + "urls": [] + }, + "url": { + "urls": [ + { + "display_url": "m.upsilo.net/@upsilon", + "expanded_url": "https://m.upsilo.net/@upsilon", + "url": "https://t.co/vNMmyHHh15", + "indices": [ + 0, + 23 + ] + } + ] + } + }, + "fast_followers_count": 0, + "favourites_count": 215391, + "followers_count": 1287, + "friends_count": 1, + "has_custom_timelines": false, + "is_translator": false, + "listed_count": 92, + "location": "Funabashi, Chiba, Japan", + "media_count": 876, + "name": "upsilon", + "normal_followers_count": 1287, + "pinned_tweet_ids_str": [], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/40480664/1349188016", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/719076434/____normal.png", + "profile_interstitial_type": "", + "screen_name": "kim_upsilon", + "statuses_count": 10081, + "translator_type": "regular", + "url": "https://t.co/vNMmyHHh15", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + } + } + } + }, + "unmention_data": {}, + "unified_card": { + "card_fetch_state": "NoCard" + }, + "edit_control": { + "edit_tweet_ids": [ + "1614653321310253057" + ], + "editable_until_msecs": "1673800125000", + "is_edit_eligible": false, + "edits_remaining": "5" + }, + "is_translatable": true, + "views": { + "count": "1779", + "state": "EnabledWithCount" + }, + "source": "OpenTween (dev)", + "quoted_status_result": { + "result": { + "__typename": "TweetTombstone", + "tombstone": { + "__typename": "TextTombstone", + "text": { + "rtl": false, + "text": "This Post is from a suspended account. Learn more", + "entities": [ + { + "fromIndex": 39, + "toIndex": 49, + "ref": { + "type": "TimelineUrl", + "url": "https://help.twitter.com/rules-and-policies/notices-on-twitter", + "urlType": "ExternalUrl" + } + } + ] + } + } + } + }, + "legacy": { + "bookmark_count": 0, + "bookmarked": false, + "created_at": "Sun Jan 15 15:58:45 +0000 2023", + "conversation_id_str": "1614653321310253057", + "display_text_range": [ + 0, + 45 + ], + "entities": { + "user_mentions": [], + "urls": [ + { + "display_url": "twitter.com/omlll/status/1…", + "expanded_url": "https://twitter.com/omlll/status/1614650279194136576", + "url": "https://t.co/l1XzDghegz", + "indices": [ + 22, + 45 + ] + } + ], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 9, + "favorited": false, + "full_text": "これは間違いなくバカが作ったツールですね…\nhttps://t.co/l1XzDghegz", + "is_quote_status": true, + "lang": "ja", + "possibly_sensitive": false, + "possibly_sensitive_editable": true, + "quote_count": 0, + "quoted_status_id_str": "1614650279194136576", + "quoted_status_permalink": { + "url": "https://t.co/l1XzDghegz", + "expanded": "https://twitter.com/omlll/status/1614650279194136576", + "display": "twitter.com/omlll/status/1…" + }, + "reply_count": 1, + "retweet_count": 2, + "retweeted": false, + "user_id_str": "40480664", + "id_str": "1614653321310253057" + } + } + }, + "tweetDisplayType": "Tweet" +} diff --git a/OpenTween.Tests/SearchWordDialogTest.cs b/OpenTween.Tests/SearchWordDialogTest.cs new file mode 100644 index 000000000..3a03305b2 --- /dev/null +++ b/OpenTween.Tests/SearchWordDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class SearchWordDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new SearchWordDialog(); + } + } +} diff --git a/OpenTween.Tests/SendErrorReportFormTest.cs b/OpenTween.Tests/SendErrorReportFormTest.cs new file mode 100644 index 000000000..1911f6384 --- /dev/null +++ b/OpenTween.Tests/SendErrorReportFormTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class SendErrorReportFormTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var form = new SendErrorReportForm(); + } + } +} diff --git a/OpenTween.Tests/ShortcutCommandTest.cs b/OpenTween.Tests/ShortcutCommandTest.cs index f03c565e7..7e27c0d6e 100644 --- a/OpenTween.Tests/ShortcutCommandTest.cs +++ b/OpenTween.Tests/ShortcutCommandTest.cs @@ -139,7 +139,7 @@ public async Task RunCommand_Test() Assert.False(invoked); - await shortcut.RunCommand().ConfigureAwait(false); + await shortcut.RunCommand(); Assert.True(invoked); } @@ -152,13 +152,13 @@ public async Task RunCommand_AsyncTest() var shortcut = ShortcutCommand.Create(Keys.F5) .Do(async () => { - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100); invoked = true; }); Assert.False(invoked); - await shortcut.RunCommand().ConfigureAwait(false); + await shortcut.RunCommand(); Assert.True(invoked); } diff --git a/OpenTween.Tests/TestUtils.cs b/OpenTween.Tests/TestUtils.cs index e7fe87b8a..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 @@ -68,7 +68,7 @@ void Handler(object s, T e) await testCode().ConfigureAwait(false); if (raisedEvent != null) - throw new Xunit.Sdk.RaisesException(typeof(void), raisedEvent.GetType()); + throw Xunit.Sdk.RaisesException.ForIncorrectType(typeof(void), raisedEvent.GetType()); } finally { @@ -81,7 +81,7 @@ public static void NotPropertyChanged(INotifyPropertyChanged @object, string pro void Handler(object s, PropertyChangedEventArgs e) { if (s == @object && e.PropertyName == propertyName) - throw new Xunit.Sdk.PropertyChangedException(propertyName); + throw Xunit.Sdk.PropertyChangedException.ForUnsetProperty(propertyName); } try @@ -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/FoursquareCheckinTest.cs b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs index 37c92c702..982b3d2cd 100644 --- a/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/FoursquareCheckinTest.cs @@ -212,8 +212,7 @@ public async Task GetThumbnailInfoAsync_ApiKeyErrorTest() var service = new FoursquareCheckin(http, ApiKey.Create("%e%INVALID_API_KEY"), ApiKey.Create("%e%INVALID_API_KEY")); var post = new PostClass(); - var thumb = await service.GetThumbnailInfoAsync("https://www.swarmapp.com/c/xxxxxxxx", post, CancellationToken.None) - .ConfigureAwait(false); + var thumb = await service.GetThumbnailInfoAsync("https://www.swarmapp.com/c/xxxxxxxx", post, CancellationToken.None); Assert.Null(thumb); } diff --git a/OpenTween.Tests/Thumbnail/Services/PbsTwimgComTest.cs b/OpenTween.Tests/Thumbnail/Services/PbsTwimgComTest.cs index 2c70e8e5e..db950584e 100644 --- a/OpenTween.Tests/Thumbnail/Services/PbsTwimgComTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/PbsTwimgComTest.cs @@ -101,8 +101,7 @@ public async Task GetThumbnailInfoAsync_ModernUrlTest() var mediaUrl = "https://pbs.twimg.com/media/DYlFv51VwAUdqWr?format=jpg&name=large"; var service = new PbsTwimgCom(); - var thumb = await service.GetThumbnailInfoAsync(mediaUrl, new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + var thumb = await service.GetThumbnailInfoAsync(mediaUrl, new PostClass(), CancellationToken.None); Assert.NotNull(thumb); Assert.Equal("https://pbs.twimg.com/media/DYlFv51VwAUdqWr?format=jpg&name=large", thumb!.ThumbnailImageUrl); @@ -115,8 +114,7 @@ public async Task GetThumbnailInfoAsync_LegacyUrlTest() var mediaUrl = "https://pbs.twimg.com/media/DYlFv51VwAUdqWr.jpg"; var service = new PbsTwimgCom(); - var thumb = await service.GetThumbnailInfoAsync(mediaUrl, new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + var thumb = await service.GetThumbnailInfoAsync(mediaUrl, new PostClass(), CancellationToken.None); Assert.NotNull(thumb); Assert.Equal("https://pbs.twimg.com/media/DYlFv51VwAUdqWr?format=jpg&name=large", thumb!.ThumbnailImageUrl); diff --git a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs index 5a6db06fb..9d1455ea6 100644 --- a/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/TinamiTest.cs @@ -102,8 +102,7 @@ public async Task GetThumbnailInfoAsync_ApiKeyErrorTest() { var service = new Tinami(ApiKey.Create("%e%INVALID_API_KEY"), null); - var thumbinfo = await service.GetThumbnailInfoAsync("http://www.tinami.com/view/12345", new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + var thumbinfo = await service.GetThumbnailInfoAsync("http://www.tinami.com/view/12345", new PostClass(), CancellationToken.None); Assert.Null(thumbinfo); } 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.Tests/Thumbnail/Services/TumblrTest.cs b/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs index 3ff5aba00..45ab7a3e9 100644 --- a/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/TumblrTest.cs @@ -91,8 +91,7 @@ public async Task GetThumbnailInfoAsync_RequestTest() var service = new Tumblr(ApiKey.Create("fake_api_key"), http); var url = "http://hoge.tumblr.com/post/1234567/tetetete"; - await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None); } Assert.Equal(0, handler.QueueCount); @@ -131,8 +130,7 @@ public async Task GetThumbnailInfoAsync_CustomHostnameRequestTest() // Tumblrのカスタムドメイン名を使ってるっぽいURL var url = "http://tumblr.example.com/post/1234567/tetetete"; - await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None); } Assert.Equal(0, handler.QueueCount); @@ -147,8 +145,7 @@ public async Task GetThumbnailInfoAsync_ApiKeyErrorTest() var service = new Tumblr(ApiKey.Create("%e%INVALID_API_KEY"), http); var url = "http://hoge.tumblr.com/post/1234567/tetetete"; - var thumb = await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None) - .ConfigureAwait(false); + var thumb = await service.GetThumbnailInfoAsync(url, new PostClass(), CancellationToken.None); Assert.Null(thumb); } } diff --git a/OpenTween.Tests/Thumbnail/Services/YoutubeTest.cs b/OpenTween.Tests/Thumbnail/Services/YoutubeTest.cs index 4d4b481bb..1d3679cc0 100644 --- a/OpenTween.Tests/Thumbnail/Services/YoutubeTest.cs +++ b/OpenTween.Tests/Thumbnail/Services/YoutubeTest.cs @@ -42,7 +42,7 @@ public class YoutubeTest [InlineData("https://youtu.be/aaaaa", "aaaaa")] [InlineData("https://youtu.be/aaaaa?t=123", "aaaaa")] [InlineData("https://www.youtube.com/channel/aaaaa", null)] // チャンネルページ - public void UrlPatternRegex_Test(string testUrl, string expected) + public void UrlPatternRegex_Test(string testUrl, string? expected) { var match = Youtube.UrlPatternRegex.Match(testUrl); diff --git a/OpenTween.Tests/Thumbnail/ThumbnailGeneratorTest.cs b/OpenTween.Tests/Thumbnail/ThumbnailGeneratorTest.cs index 0c61bd71f..eb513712d 100644 --- a/OpenTween.Tests/Thumbnail/ThumbnailGeneratorTest.cs +++ b/OpenTween.Tests/Thumbnail/ThumbnailGeneratorTest.cs @@ -39,7 +39,7 @@ public class ThumbnailGeneratorTest [InlineData("https://www.instagram.com/hogehoge/p/aaaaaaaaaaa/", "aaaaaaaaaaa")] // ユーザー名付き [InlineData("https://www.instagram.com/p/aaaaaaaaaaa/?utm_medium=copy_link", "aaaaaaaaaaa")] // トラッキングパラメータ付き [InlineData("https://www.instagram.com/hogehoge/", null)] // プロフィールページ - public void InstagramPattern_IsMatchTest(string testUrl, string expected) + public void InstagramPattern_IsMatchTest(string testUrl, string? expected) { var match = ThumbnailGenerator.InstagramPattern.Match(testUrl); diff --git a/OpenTween.Tests/TimelineListViewDrawerTest.cs b/OpenTween.Tests/TimelineListViewDrawerTest.cs new file mode 100644 index 000000000..3ecbcafb4 --- /dev/null +++ b/OpenTween.Tests/TimelineListViewDrawerTest.cs @@ -0,0 +1,46 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Models; +using OpenTween.OpenTweenCustomControl; +using Xunit; + +namespace OpenTween +{ + public class TimelineListViewDrawerTest + { + [WinFormsFact] + public void Initialize_Test() + { + var tab = new PublicSearchTabModel("hoge"); + using var listView = new DetailsListView(); + var listViewCache = new TimelineListViewCache(listView, tab, new()); + using var imageCache = new ImageCache(); + using var theme = new ThemeManager(new()); + using var listViewDrawer = new TimelineListViewDrawer(listView, tab, listViewCache, imageCache, theme); + } + } +} diff --git a/OpenTween.Tests/TimelineListViewStateTest.cs b/OpenTween.Tests/TimelineListViewStateTest.cs new file mode 100644 index 000000000..eb4d8f4cd --- /dev/null +++ b/OpenTween.Tests/TimelineListViewStateTest.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.Models; +using OpenTween.OpenTweenCustomControl; +using Xunit; + +namespace OpenTween +{ + public class TimelineListViewStateTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var listView = new DetailsListView(); + var tab = new PublicSearchTabModel("hoge"); + var listViewState = new TimelineListViewState(listView, tab); + } + } +} diff --git a/OpenTween.Tests/ToolStripLabelHistoryTest.cs b/OpenTween.Tests/ToolStripLabelHistoryTest.cs new file mode 100644 index 000000000..50831fe8f --- /dev/null +++ b/OpenTween.Tests/ToolStripLabelHistoryTest.cs @@ -0,0 +1,40 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using OpenTween.OpenTweenCustomControl; +using Xunit; + +namespace OpenTween +{ + public class ToolStripLabelHistoryTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var toolStripLabelHistory = new ToolStripLabelHistory(); + } + } +} diff --git a/OpenTween.Tests/TweenAboutBoxTest.cs b/OpenTween.Tests/TweenAboutBoxTest.cs new file mode 100644 index 000000000..3deee155d --- /dev/null +++ b/OpenTween.Tests/TweenAboutBoxTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class TweenAboutBoxTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new TweenAboutBox(); + } + } +} diff --git a/OpenTween.Tests/TweenMainTest.cs b/OpenTween.Tests/TweenMainTest.cs index a7974618c..8beba3b2c 100644 --- a/OpenTween.Tests/TweenMainTest.cs +++ b/OpenTween.Tests/TweenMainTest.cs @@ -24,8 +24,14 @@ using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Windows.Forms; +using OpenTween.Api; using OpenTween.Api.DataModel; +using OpenTween.Connection; +using OpenTween.Models; +using OpenTween.Setting; +using OpenTween.Thumbnail; using Xunit; using Xunit.Extensions; @@ -33,6 +39,241 @@ namespace OpenTween { public class TweenMainTest { + private record TestContext( + SettingManager Settings + ); + + private void UsingTweenMain(Action func) + { + var settings = new SettingManager(""); + var tabinfo = new TabInformations(); + using var twitterApi = new TwitterApi(); + using var twitter = new Twitter(twitterApi); + using var imageCache = new ImageCache(); + using var iconAssets = new IconAssetsManager(); + var thumbnailGenerator = new ThumbnailGenerator(new(autoupdate: false)); + + using var tweenMain = new TweenMain(settings, tabinfo, twitter, imageCache, iconAssets, thumbnailGenerator); + var context = new TestContext(settings); + + func(tweenMain, context); + } + + [WinFormsFact] + public void Initialize_Test() + => this.UsingTweenMain((_, _) => { }); + + [WinFormsFact] + public void FormatStatusText_NewLineTest() + { + this.UsingTweenMain((tweenMain, _) => + { + Assert.Equal("aaa\nbbb", tweenMain.FormatStatusText("aaa\r\nbbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_NewLineInDMTest() + { + this.UsingTweenMain((tweenMain, _) => + { + // DM にも適用する + Assert.Equal("D opentween aaa\nbbb", tweenMain.FormatStatusText("D opentween aaa\r\nbbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_SeparateUrlAndFullwidthCharacter_EnabledTest() + { + this.UsingTweenMain((tweenMain, context) => + { + tweenMain.SeparateUrlAndFullwidthCharacter = true; + Assert.Equal("https://example.com/ あああ", tweenMain.FormatStatusText("https://example.com/あああ")); + }); + } + + [WinFormsFact] + public void FormatStatusText_SeparateUrlAndFullwidthCharacter_DisabledTest() + { + this.UsingTweenMain((tweenMain, context) => + { + tweenMain.SeparateUrlAndFullwidthCharacter = false; + Assert.Equal("https://example.com/あああ", tweenMain.FormatStatusText("https://example.com/あああ")); + }); + } + + [WinFormsFact] + public void FormatStatusText_ReplaceFullwidthSpace_EnabledTest() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.WideSpaceConvert = true; + Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_ReplaceFullwidthSpaceInDM_EnabledTest() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.WideSpaceConvert = true; + + // DM にも適用する + Assert.Equal("D opentween aaa bbb", tweenMain.FormatStatusText("D opentween aaa bbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_ReplaceFullwidthSpace_DisabledTest() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.WideSpaceConvert = false; + Assert.Equal("aaa bbb", tweenMain.FormatStatusText("aaa bbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_UseRecommendedFooter_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Local.UseRecommendStatus = true; + Assert.Matches(new Regex(@"^aaa \[TWNv\d+\]$"), tweenMain.FormatStatusText("aaa")); + }); + } + + [WinFormsFact] + public void FormatStatusText_CustomFooterText_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Local.StatusText = "foo"; + Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa")); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfSendingDM_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Local.StatusText = "foo"; + + // DM の場合はフッターを無効化する + Assert.Equal("D opentween aaa", tweenMain.FormatStatusText("D opentween aaa")); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfContainsUnofficialRT_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Local.StatusText = "foo"; + + // 非公式 RT を含む場合はフッターを無効化する + Assert.Equal("aaa RT @foo: bbb", tweenMain.FormatStatusText("aaa RT @foo: bbb")); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfPostByEnterAndPressedShiftKey_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = false; + context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定 + context.Settings.Local.StatusText = "foo"; + context.Settings.Local.StatusMultiline = false; // 単一行モード + + // Shift キーが押されている場合はフッターを無効化する + Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift)); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfPostByEnterAndPressedCtrlKeyAndMultilineMode_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = false; + context.Settings.Common.PostShiftEnter = false; // Enter で投稿する設定 + context.Settings.Local.StatusText = "foo"; + context.Settings.Local.StatusMultiline = true; // 複数行モード + + // Ctrl キーが押されている場合はフッターを無効化する + Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control)); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfPostByShiftEnterAndPressedControlKey_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = false; + context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定 + context.Settings.Local.StatusText = "foo"; + + // Ctrl キーが押されている場合はフッターを無効化する + Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control)); + }); + } + + [WinFormsFact] + public void FormatStatusText_EnableFooterIfPostByShiftEnter_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = false; + context.Settings.Common.PostShiftEnter = true; // Shift+Enter で投稿する設定 + context.Settings.Local.StatusText = "foo"; + + // Shift+Enter で投稿する場合、Ctrl キーが押されていなければフッターを付ける + Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift)); + }); + } + + [WinFormsFact] + public void FormatStatusText_DisableFooterIfPostByCtrlEnterAndPressedShiftKey_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定 + context.Settings.Common.PostShiftEnter = false; + context.Settings.Local.StatusText = "foo"; + + // Shift キーが押されている場合はフッターを無効化する + Assert.Equal("aaa", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Shift)); + }); + } + + [WinFormsFact] + public void FormatStatusText_EnableFooterIfPostByCtrlEnter_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + context.Settings.Common.PostCtrlEnter = true; // Ctrl+Enter で投稿する設定 + context.Settings.Common.PostShiftEnter = false; + context.Settings.Local.StatusText = "foo"; + + // Ctrl+Enter で投稿する場合、Shift キーが押されていなければフッターを付ける + Assert.Equal("aaa foo", tweenMain.FormatStatusText("aaa", modifierKeys: Keys.Control)); + }); + } + + [WinFormsFact] + public void FormatStatusText_PreventSmsCommand_Test() + { + this.UsingTweenMain((tweenMain, context) => + { + // 「D+」などから始まる文字列をツイートしようとすると SMS コマンドと誤認されてエラーが返される問題の回避策 + Assert.Equal("\u200bd+aaaa", tweenMain.FormatStatusText("d+aaaa")); + }); + } + [Fact] public void GetUrlFromDataObject_XMozUrlTest() { diff --git a/OpenTween.Tests/TweetDetailsViewTest.cs b/OpenTween.Tests/TweetDetailsViewTest.cs index 6aeceea2c..928c35c6b 100644 --- a/OpenTween.Tests/TweetDetailsViewTest.cs +++ b/OpenTween.Tests/TweetDetailsViewTest.cs @@ -32,6 +32,12 @@ namespace OpenTween { public class TweetDetailsViewTest { + [WinFormsFact] + public void Initialize_Test() + { + using var detailsView = new TweetDetailsView(); + } + [Fact] public void FormatQuoteTweetHtml_PostClassTest() { diff --git a/OpenTween.Tests/TweetThumbnailControlTest.cs b/OpenTween.Tests/TweetThumbnailControlTest.cs new file mode 100644 index 000000000..e58fbf326 --- /dev/null +++ b/OpenTween.Tests/TweetThumbnailControlTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class TweetThumbnailControlTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var control = new TweetThumbnailControl(); + } + } +} diff --git a/OpenTween.Tests/TweetThumbnailTest.cs b/OpenTween.Tests/TweetThumbnailTest.cs index 69d394913..635f8e847 100644 --- a/OpenTween.Tests/TweetThumbnailTest.cs +++ b/OpenTween.Tests/TweetThumbnailTest.cs @@ -83,8 +83,7 @@ public async Task PrepareThumbnails_Test() Media = new() { new("http://example.com/abcd") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.True(tweetThumbnail.ThumbnailAvailable); Assert.Single(tweetThumbnail.Thumbnails); @@ -108,8 +107,7 @@ public async Task PrepareThumbnails_NoThumbnailTest() Media = new() { new("http://hoge.example.com/") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.False(tweetThumbnail.ThumbnailAvailable); Assert.Throws(() => tweetThumbnail.Thumbnails); @@ -246,8 +244,7 @@ public async Task SelectedIndex_Test() Media = new() { new("http://example.com/abcd"), new("http://example.com/efgh") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.Equal(2, tweetThumbnail.Thumbnails.Length); Assert.Equal(0, tweetThumbnail.SelectedIndex); @@ -291,8 +288,7 @@ public async Task GetImageSearchUriGoogle_Test() Media = new() { new("http://example.com/abcd") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.Equal("http://img.example.com/abcd.png", tweetThumbnail.CurrentThumbnail.ThumbnailImageUrl); Assert.Equal( @@ -316,8 +312,7 @@ public async Task GetImageSearchUriSauceNao_Test() Media = new() { new("http://example.com/abcd") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.Equal("http://img.example.com/abcd.png", tweetThumbnail.CurrentThumbnail.ThumbnailImageUrl); Assert.Equal( @@ -341,8 +336,7 @@ public async Task Scroll_Test() Media = new() { new("http://example.com/abcd"), new("http://example.com/efgh") }, }; - await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None) - .ConfigureAwait(false); + await tweetThumbnail.PrepareThumbnails(post, CancellationToken.None); Assert.Equal(2, tweetThumbnail.Thumbnails.Length); Assert.Equal(0, tweetThumbnail.SelectedIndex); diff --git a/OpenTween.Tests/TwitterTest.cs b/OpenTween.Tests/TwitterTest.cs index 56bdf6c52..89d6b784d 100644 --- a/OpenTween.Tests/TwitterTest.cs +++ b/OpenTween.Tests/TwitterTest.cs @@ -224,7 +224,7 @@ public void GetApiResultCount_AdditionalCountTest() [Fact] public void GetTextLengthRemain_Test() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); Assert.Equal(280, twitter.GetTextLengthRemain("")); @@ -234,7 +234,7 @@ public void GetTextLengthRemain_Test() [Fact] public void GetTextLengthRemain_DirectMessageTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); // 2015年8月から DM の文字数上限が 10,000 文字に変更された @@ -255,7 +255,7 @@ public void GetTextLengthRemain_DirectMessageTest() [Fact] public void GetTextLengthRemain_UrlTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); // t.co に短縮される分の文字数を考慮 @@ -272,7 +272,7 @@ public void GetTextLengthRemain_UrlTest() [Fact] public void GetTextLengthRemain_UrlWithoutSchemeTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); // t.co に短縮される分の文字数を考慮 @@ -290,7 +290,7 @@ public void GetTextLengthRemain_UrlWithoutSchemeTest() [Fact] public void GetTextLengthRemain_SurrogatePairTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); Assert.Equal(278, twitter.GetTextLengthRemain("🍣")); @@ -300,7 +300,7 @@ public void GetTextLengthRemain_SurrogatePairTest() [Fact] public void GetTextLengthRemain_EmojiTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); // 絵文字の文字数カウントの仕様変更に対するテストケース @@ -320,7 +320,7 @@ public void GetTextLengthRemain_EmojiTest() [Fact] public void GetTextLengthRemain_BrokenSurrogateTest() { - using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); + using var twitterApi = new TwitterApi(); using var twitter = new Twitter(twitterApi); // 投稿欄に IME から絵文字を入力すると HighSurrogate のみ入力された状態で TextChanged イベントが呼ばれることがある diff --git a/OpenTween.Tests/UpdateDialogTest.cs b/OpenTween.Tests/UpdateDialogTest.cs new file mode 100644 index 000000000..12e6436f8 --- /dev/null +++ b/OpenTween.Tests/UpdateDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class UpdateDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new UpdateDialog(); + } + } +} diff --git a/OpenTween.Tests/WaitingDialogTest.cs b/OpenTween.Tests/WaitingDialogTest.cs new file mode 100644 index 000000000..2f19ce28e --- /dev/null +++ b/OpenTween.Tests/WaitingDialogTest.cs @@ -0,0 +1,39 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace OpenTween +{ + public class WaitingDialogTest + { + [WinFormsFact] + public void Initialize_Test() + { + using var dialog = new WaitingDialog(); + } + } +} diff --git a/OpenTween/Api/GraphQL/CreateRetweetRequest.cs b/OpenTween/Api/GraphQL/CreateRetweetRequest.cs index b04388e95..16df55b24 100644 --- a/OpenTween/Api/GraphQL/CreateRetweetRequest.cs +++ b/OpenTween/Api/GraphQL/CreateRetweetRequest.cs @@ -22,14 +22,7 @@ #nullable enable 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; @@ -40,7 +33,7 @@ public class CreateRetweetRequest { private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet"); - required public TwitterStatusId TweetId { get; set; } + public required TwitterStatusId TweetId { get; set; } public string CreateRequestBody() { @@ -51,12 +44,18 @@ public string CreateRequestBody() public async Task Send(IApiConnection apiConnection) { - var json = this.CreateRequestBody(); - var response = await apiConnection.PostJsonAsync(EndpointUri, json); - var responseBytes = Encoding.UTF8.GetBytes(response); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(responseBytes, XmlDictionaryReaderQuotas.Max); + var request = new PostJsonRequest + { + RequestUri = EndpointUri, + JsonString = this.CreateRequestBody(), + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); - var rootElm = XElement.Load(jsonReader); ErrorResponse.ThrowIfError(rootElm); var tweetIdStr = rootElm.XPathSelectElement("/data/create_retweet/retweet_results/result/rest_id")?.Value ?? throw CreateParseError(); diff --git a/OpenTween/Api/GraphQL/CreateTweetRequest.cs b/OpenTween/Api/GraphQL/CreateTweetRequest.cs index d0bff8655..5caf3ac22 100644 --- a/OpenTween/Api/GraphQL/CreateTweetRequest.cs +++ b/OpenTween/Api/GraphQL/CreateTweetRequest.cs @@ -23,14 +23,9 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Runtime.Serialization; -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.Api.DataModel; using OpenTween.Connection; @@ -42,7 +37,7 @@ public class CreateTweetRequest { private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/tTsjMKyhajZvK4q76mpIBg/CreateTweet"); - required public string TweetText { get; set; } + public required string TweetText { get; set; } public TwitterStatusId? InReplyToTweetId { get; set; } @@ -155,12 +150,18 @@ public string CreateRequestBody() public async Task Send(IApiConnection apiConnection) { - var json = this.CreateRequestBody(); - var response = await apiConnection.PostJsonAsync(EndpointUri, json); - var responseBytes = Encoding.UTF8.GetBytes(response); - using var jsonReader = JsonReaderWriterFactory.CreateJsonReader(responseBytes, XmlDictionaryReaderQuotas.Max); + var request = new PostJsonRequest + { + RequestUri = EndpointUri, + JsonString = this.CreateRequestBody(), + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); + + var rootElm = await response.ReadAsJsonXml() + .ConfigureAwait(false); - var rootElm = XElement.Load(jsonReader); ErrorResponse.ThrowIfError(rootElm); var tweetElm = rootElm.XPathSelectElement("/data/create_tweet/tweet_results/result") ?? throw CreateParseError(); diff --git a/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs b/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs index c2b61d34e..500ed372e 100644 --- a/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs +++ b/OpenTween/Api/GraphQL/DeleteRetweetRequest.cs @@ -22,10 +22,6 @@ #nullable enable using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using OpenTween.Connection; using OpenTween.Models; @@ -36,7 +32,7 @@ public class DeleteRetweetRequest { private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet"); - required public TwitterStatusId SourceTweetId { get; set; } + public required TwitterStatusId SourceTweetId { get; set; } public string CreateRequestBody() { @@ -47,9 +43,19 @@ public string CreateRequestBody() public async Task Send(IApiConnection apiConnection) { - var json = this.CreateRequestBody(); - var responseText = await apiConnection.PostJsonAsync(EndpointUri, json); - ErrorResponse.ThrowIfError(responseText); + var request = new PostJsonRequest + { + RequestUri = EndpointUri, + JsonString = this.CreateRequestBody(), + }; + + 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/DeleteTweetRequest.cs b/OpenTween/Api/GraphQL/DeleteTweetRequest.cs index c6962830f..db3b28c63 100644 --- a/OpenTween/Api/GraphQL/DeleteTweetRequest.cs +++ b/OpenTween/Api/GraphQL/DeleteTweetRequest.cs @@ -22,10 +22,6 @@ #nullable enable using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; using System.Threading.Tasks; using OpenTween.Connection; using OpenTween.Models; @@ -36,7 +32,7 @@ public class DeleteTweetRequest { private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet"); - required public TwitterStatusId TweetId { get; set; } + public required TwitterStatusId TweetId { get; set; } public string CreateRequestBody() { @@ -47,9 +43,19 @@ public string CreateRequestBody() public async Task Send(IApiConnection apiConnection) { - var json = this.CreateRequestBody(); - var responseText = await apiConnection.PostJsonAsync(EndpointUri, json); - ErrorResponse.ThrowIfError(responseText); + var request = new PostJsonRequest + { + RequestUri = EndpointUri, + JsonString = this.CreateRequestBody(), + }; + + 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/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/TimelineTweet.cs b/OpenTween/Api/GraphQL/TimelineTweet.cs index dd20e3401..af7b1764d 100644 --- a/OpenTween/Api/GraphQL/TimelineTweet.cs +++ b/OpenTween/Api/GraphQL/TimelineTweet.cs @@ -92,7 +92,7 @@ public void ThrowIfTweetIsTombstone() public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm) { - var tweetElm = tweetUnionElm.Element("__typename")?.Value switch + var tweetElm = GetTweetTypeName(tweetUnionElm) switch { "Tweet" => tweetUnionElm, "TweetWithVisibilityResults" => tweetUnionElm.Element("tweet") ?? throw CreateParseError(), @@ -102,12 +102,18 @@ public static TwitterStatus ParseTweetUnion(XElement tweetUnionElm) return TimelineTweet.ParseTweet(tweetElm); } + public static string GetTweetTypeName(XElement tweetUnionElm) + => tweetUnionElm.Element("__typename")?.Value ?? throw CreateParseError(); + public static TwitterStatus ParseTweet(XElement tweetElm) { var tweetLegacyElm = tweetElm.Element("legacy") ?? throw CreateParseError(); var userElm = tweetElm.Element("core")?.Element("user_results")?.Element("result") ?? throw CreateParseError(); var retweetedTweetElm = tweetLegacyElm.Element("retweeted_status_result")?.Element("result"); var user = new TwitterGraphqlUser(userElm); + var quotedTweetElm = tweetElm.Element("quoted_status_result")?.Element("result") ?? null; + var quotedStatusPermalink = tweetLegacyElm.Element("quoted_status_permalink") ?? null; + var isQuotedTweetTombstone = quotedTweetElm != null && GetTweetTypeName(quotedTweetElm) == "TweetTombstone"; static string GetText(XElement elm, string name) => elm.Element(name)?.Value ?? throw CreateParseError(); @@ -168,6 +174,15 @@ static string GetText(XElement elm, string name) }, User = user.ToTwitterUser(), RetweetedStatus = retweetedTweetElm != null ? TimelineTweet.ParseTweetUnion(retweetedTweetElm) : null, + IsQuoteStatus = GetTextOrNull(tweetLegacyElm, "is_quote_status") == "true", + QuotedStatus = quotedTweetElm != null && !isQuotedTweetTombstone ? TimelineTweet.ParseTweetUnion(quotedTweetElm) : null, + QuotedStatusIdStr = GetTextOrNull(tweetLegacyElm, "quoted_status_id_str"), + QuotedStatusPermalink = quotedStatusPermalink == null ? null : new() + { + Url = GetText(quotedStatusPermalink, "url"), + Expanded = GetText(quotedStatusPermalink, "expanded"), + Display = GetText(quotedStatusPermalink, "display"), + }, }; } diff --git a/OpenTween/Api/GraphQL/TweetDetailRequest.cs b/OpenTween/Api/GraphQL/TweetDetailRequest.cs index 009e1041f..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; @@ -42,7 +35,7 @@ public class TweetDetailRequest private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/-Ls3CrSQNo2fRKH6i6Na1A/TweetDetail"); - required public TwitterStatusId FocalTweetId { get; set; } + public required TwitterStatusId FocalTweetId { get; set; } public Dictionary CreateParameters() { @@ -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 f261924b6..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; @@ -41,7 +36,7 @@ public class UserByScreenNameRequest private static readonly Uri EndpointUri = new("https://twitter.com/i/api/graphql/xc8f1g7BYqr6VTzTbvNlGw/UserByScreenName"); - required public string ScreenName { get; set; } + public required string ScreenName { get; set; } public Dictionary CreateParameters() { @@ -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/ImgurApi.cs b/OpenTween/Api/ImgurApi.cs index 591bec315..60422bc5c 100644 --- a/OpenTween/Api/ImgurApi.cs +++ b/OpenTween/Api/ImgurApi.cs @@ -57,8 +57,10 @@ public ImgurApi(ApiKey clientId, HttpClient? http) } else { - this.http = Networking.CreateHttpClient(Networking.CreateHttpClientHandler()); - this.http.Timeout = Networking.UploadImageTimeout; + var builder = Networking.CreateHttpClientBuilder(); + builder.SetupHttpClient(x => x.Timeout = Networking.UploadImageTimeout); + + this.http = builder.Build(); } } diff --git a/OpenTween/Api/MobypictureApi.cs b/OpenTween/Api/MobypictureApi.cs index d76d4347c..02f36d427 100644 --- a/OpenTween/Api/MobypictureApi.cs +++ b/OpenTween/Api/MobypictureApi.cs @@ -53,9 +53,11 @@ public MobypictureApi(ApiKey apiKey, TwitterApi twitterApi) { this.apiKey = apiKey; - var handler = twitterApi.CreateOAuthEchoHandler(AuthServiceProvider, OAuthRealm); - this.http = Networking.CreateHttpClient(handler); - this.http.Timeout = Networking.UploadImageTimeout; + var builder = Networking.CreateHttpClientBuilder(); + builder.SetupHttpClient(x => x.Timeout = Networking.UploadImageTimeout); + builder.AddHandler(x => twitterApi.CreateOAuthEchoHandler(x, AuthServiceProvider, OAuthRealm)); + + this.http = builder.Build(); } public MobypictureApi(ApiKey apiKey, HttpClient http) diff --git a/OpenTween/Api/TwitterApi.cs b/OpenTween/Api/TwitterApi.cs index 157cc2d73..f1186b1e7 100644 --- a/OpenTween/Api/TwitterApi.cs +++ b/OpenTween/Api/TwitterApi.cs @@ -25,6 +25,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -40,34 +41,20 @@ public sealed class TwitterApi : IDisposable public string CurrentScreenName { get; private set; } = ""; - public IApiConnection Connection => this.ApiConnection ?? throw new InvalidOperationException(); + public IApiConnection Connection => this.ApiConnection; - internal IApiConnection? ApiConnection; + internal IApiConnection ApiConnection; - public TwitterAppToken AppToken { get; private set; } = TwitterAppToken.GetDefault(); + public APIAuthType AuthType { get; private set; } = APIAuthType.None; public TwitterApi() - { - } - - public TwitterApi(ApiKey consumerKey, ApiKey consumerSecret) - { - this.AppToken = new() - { - AuthType = APIAuthType.OAuth1, - OAuth1CustomConsumerKey = consumerKey, - OAuth1CustomConsumerSecret = consumerSecret, - }; - } - - public void Initialize(string accessToken, string accessSecret, long userId, string screenName) - => this.Initialize(this.AppToken, accessToken, accessSecret, userId, screenName); + => this.ApiConnection = new TwitterApiConnection(new TwitterCredentialNone()); - public void Initialize(TwitterAppToken appToken, string accessToken, string accessSecret, long userId, string screenName) + public void Initialize(ITwitterCredential credential, long userId, string screenName) { - this.AppToken = appToken; + this.AuthType = credential.AuthType; - var newInstance = new TwitterApiConnection(this.AppToken, accessToken, accessSecret); + var newInstance = new TwitterApiConnection(credential); var oldInstance = Interlocked.Exchange(ref this.ApiConnection, newInstance); oldInstance?.Dispose(); @@ -75,9 +62,8 @@ public void Initialize(TwitterAppToken appToken, string accessToken, string acce this.CurrentScreenName = screenName; } - public Task StatusesHomeTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) + public async Task StatusesHomeTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { - var endpoint = new Uri("statuses/home_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", @@ -92,12 +78,22 @@ public Task StatusesHomeTimeline(int? count = null, TwitterStat if (sinceId != null) param["since_id"] = sinceId.Id; - return this.Connection.GetAsync(endpoint, param, "/statuses/home_timeline"); + var request = new GetRequest + { + RequestUri = new("statuses/home_timeline.json", UriKind.Relative), + Query = param, + EndpointName = "/statuses/home_timeline", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task StatusesMentionsTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) + public async Task StatusesMentionsTimeline(int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { - var endpoint = new Uri("statuses/mentions_timeline.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", @@ -112,12 +108,22 @@ public Task StatusesMentionsTimeline(int? count = null, Twitter if (sinceId != null) param["since_id"] = sinceId.Id; - return this.Connection.GetAsync(endpoint, param, "/statuses/mentions_timeline"); + var request = new GetRequest + { + RequestUri = new("statuses/mentions_timeline.json", UriKind.Relative), + Query = param, + EndpointName = "/statuses/mentions_timeline", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task StatusesUserTimeline(string screenName, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) + public async Task StatusesUserTimeline(string screenName, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { - var endpoint = new Uri("statuses/user_timeline.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, @@ -134,38 +140,65 @@ public Task StatusesUserTimeline(string screenName, int? count if (sinceId != null) param["since_id"] = sinceId.Id; - return this.Connection.GetAsync(endpoint, param, "/statuses/user_timeline"); + var request = new GetRequest + { + RequestUri = new("statuses/user_timeline.json", UriKind.Relative), + Query = param, + EndpointName = "/statuses/user_timeline", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task StatusesShow(TwitterStatusId statusId) + public async Task StatusesShow(TwitterStatusId statusId) { - var endpoint = new Uri("statuses/show.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["id"] = statusId.Id, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("statuses/show.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = statusId.Id, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/statuses/show/:id", }; - return this.Connection.GetAsync(endpoint, param, "/statuses/show/:id"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task StatusesLookup(IReadOnlyList statusIds) + public async Task StatusesLookup(IReadOnlyList statusIds) { - var endpoint = new Uri("statuses/lookup.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["id"] = string.Join(",", statusIds), - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("statuses/lookup.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = string.Join(",", statusIds), + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/statuses/lookup", }; - return this.Connection.GetAsync(endpoint, param, "/statuses/lookup"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> StatusesUpdate( + public async Task> StatusesUpdate( string status, TwitterStatusId? replyToId, IReadOnlyList? mediaIds, @@ -173,7 +206,6 @@ public Task> StatusesUpdate( IReadOnlyList? excludeReplyUserIds = null, string? attachmentUrl = null) { - var endpoint = new Uri("statuses/update.json", UriKind.Relative); var param = new Dictionary { ["status"] = status, @@ -193,37 +225,57 @@ public Task> StatusesUpdate( if (attachmentUrl != null) param["attachment_url"] = attachmentUrl; - return this.Connection.PostLazyAsync(endpoint, param); + var request = new PostRequest + { + RequestUri = new("statuses/update.json", UriKind.Relative), + Query = param, + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> StatusesDestroy(TwitterStatusId statusId) + public async Task> StatusesDestroy(TwitterStatusId statusId) { - var endpoint = new Uri("statuses/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["id"] = statusId.Id, + RequestUri = new("statuses/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = statusId.Id, + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> StatusesRetweet(TwitterStatusId statusId) + public async Task> StatusesRetweet(TwitterStatusId statusId) { - var endpoint = new Uri("statuses/retweet.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["id"] = statusId.Id, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("statuses/retweet.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = statusId.Id, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task SearchTweets(string query, string? lang = null, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) + public async Task SearchTweets(string query, string? lang = null, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null) { - var endpoint = new Uri("search/tweets.json", UriKind.Relative); var param = new Dictionary { ["q"] = query, @@ -242,12 +294,22 @@ public Task SearchTweets(string query, string? lang = null, if (sinceId != null) param["since_id"] = sinceId.Id; - return this.Connection.GetAsync(endpoint, param, "/search/tweets"); + var request = new GetRequest + { + RequestUri = new("search/tweets.json", UriKind.Relative), + Query = param, + EndpointName = "/search/tweets", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task ListsOwnerships(string screenName, long? cursor = null, int? count = null) + public async Task ListsOwnerships(string screenName, long? cursor = null, int? count = null) { - var endpoint = new Uri("lists/ownerships.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, @@ -258,12 +320,22 @@ public Task ListsOwnerships(string screenName, long? cursor = null if (count != null) param["count"] = count.ToString(); - return this.Connection.GetAsync(endpoint, param, "/lists/ownerships"); + var request = new GetRequest + { + RequestUri = new("lists/ownerships.json", UriKind.Relative), + Query = param, + EndpointName = "/lists/ownerships", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task ListsSubscriptions(string screenName, long? cursor = null, int? count = null) + public async Task ListsSubscriptions(string screenName, long? cursor = null, int? count = null) { - var endpoint = new Uri("lists/subscriptions.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, @@ -274,12 +346,22 @@ public Task ListsSubscriptions(string screenName, long? cursor = n if (count != null) param["count"] = count.ToString(); - return this.Connection.GetAsync(endpoint, param, "/lists/subscriptions"); + var request = new GetRequest + { + RequestUri = new("lists/subscriptions.json", UriKind.Relative), + Query = param, + EndpointName = "/lists/subscriptions", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null) + public async Task ListsMemberships(string screenName, long? cursor = null, int? count = null, bool? filterToOwnedLists = null) { - var endpoint = new Uri("lists/memberships.json", UriKind.Relative); var param = new Dictionary { ["screen_name"] = screenName, @@ -292,12 +374,22 @@ public Task ListsMemberships(string screenName, long? cursor = nul if (filterToOwnedLists != null) param["filter_to_owned_lists"] = filterToOwnedLists.Value ? "true" : "false"; - return this.Connection.GetAsync(endpoint, param, "/lists/memberships"); + var request = new GetRequest + { + RequestUri = new("lists/memberships.json", UriKind.Relative), + Query = param, + EndpointName = "/lists/memberships", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> ListsCreate(string name, string? description = null, bool? @private = null) + public async Task> ListsCreate(string name, string? description = null, bool? @private = null) { - var endpoint = new Uri("lists/create.json", UriKind.Relative); var param = new Dictionary { ["name"] = name, @@ -308,12 +400,20 @@ public Task> ListsCreate(string name, string? description if (@private != null) param["mode"] = @private.Value ? "private" : "public"; - return this.Connection.PostLazyAsync(endpoint, param); + var request = new PostRequest + { + RequestUri = new("lists/create.json", UriKind.Relative), + Query = param, + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> ListsUpdate(long listId, string? name = null, string? description = null, bool? @private = null) + public async Task> ListsUpdate(long listId, string? name = null, string? description = null, bool? @private = null) { - var endpoint = new Uri("lists/update.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), @@ -326,23 +426,37 @@ public Task> ListsUpdate(long listId, string? name = null, if (@private != null) param["mode"] = @private.Value ? "private" : "public"; - return this.Connection.PostLazyAsync(endpoint, param); + var request = new PostRequest + { + RequestUri = new("lists/update.json", UriKind.Relative), + Query = param, + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> ListsDestroy(long listId) + public async Task> ListsDestroy(long listId) { - var endpoint = new Uri("lists/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["list_id"] = listId.ToString(), + RequestUri = new("lists/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["list_id"] = listId.ToString(), + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task ListsStatuses(long listId, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null, bool? includeRTs = null) + public async Task ListsStatuses(long listId, int? count = null, TwitterStatusId? maxId = null, TwitterStatusId? sinceId = null, bool? includeRTs = null) { - var endpoint = new Uri("lists/statuses.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), @@ -360,12 +474,22 @@ public Task ListsStatuses(long listId, int? count = null, Twitt if (includeRTs != null) param["include_rts"] = includeRTs.Value ? "true" : "false"; - return this.Connection.GetAsync(endpoint, param, "/lists/statuses"); + var request = new GetRequest + { + RequestUri = new("lists/statuses.json", UriKind.Relative), + Query = param, + EndpointName = "/lists/statuses", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task ListsMembers(long listId, long? cursor = null) + public async Task ListsMembers(long listId, long? cursor = null) { - var endpoint = new Uri("lists/members.json", UriKind.Relative); var param = new Dictionary { ["list_id"] = listId.ToString(), @@ -377,57 +501,87 @@ public Task ListsMembers(long listId, long? cursor = null) if (cursor != null) param["cursor"] = cursor.ToString(); - return this.Connection.GetAsync(endpoint, param, "/lists/members"); + var request = new GetRequest + { + RequestUri = new("lists/members.json", UriKind.Relative), + Query = param, + EndpointName = "/lists/members", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task ListsMembersShow(long listId, string screenName) + public async Task ListsMembersShow(long listId, string screenName) { - var endpoint = new Uri("lists/members/show.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["list_id"] = listId.ToString(), - ["screen_name"] = screenName, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("lists/members/show.json", UriKind.Relative), + Query = new Dictionary + { + ["list_id"] = listId.ToString(), + ["screen_name"] = screenName, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/lists/members/show", }; - return this.Connection.GetAsync(endpoint, param, "/lists/members/show"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> ListsMembersCreate(long listId, string screenName) + public async Task> ListsMembersCreate(long listId, string screenName) { - var endpoint = new Uri("lists/members/create.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["list_id"] = listId.ToString(), - ["screen_name"] = screenName, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("lists/members/create.json", UriKind.Relative), + Query = new Dictionary + { + ["list_id"] = listId.ToString(), + ["screen_name"] = screenName, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> ListsMembersDestroy(long listId, string screenName) + public async Task> ListsMembersDestroy(long listId, string screenName) { - var endpoint = new Uri("lists/members/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["list_id"] = listId.ToString(), - ["screen_name"] = screenName, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("lists/members/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["list_id"] = listId.ToString(), + ["screen_name"] = screenName, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task DirectMessagesEventsList(int? count = null, string? cursor = null) + public async Task DirectMessagesEventsList(int? count = null, string? cursor = null) { - var endpoint = new Uri("direct_messages/events/list.json", UriKind.Relative); var param = new Dictionary(); if (count != null) @@ -435,13 +589,22 @@ public Task DirectMessagesEventsList(int? count = null, if (cursor != null) param["cursor"] = cursor; - return this.Connection.GetAsync(endpoint, param, "/direct_messages/events/list"); + var request = new GetRequest + { + RequestUri = new("direct_messages/events/list.json", UriKind.Relative), + Query = param, + EndpointName = "/direct_messages/events/list", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null) + public async Task> DirectMessagesEventsNew(long recipientId, string text, long? mediaId = null) { - var endpoint = new Uri("direct_messages/events/new.json", UriKind.Relative); - var attachment = ""; if (mediaId != null) { @@ -471,66 +634,98 @@ public Task> DirectMessagesEventsNew(long re } """; - return this.Connection.PostJsonAsync(endpoint, json); + var request = new PostJsonRequest + { + RequestUri = new("direct_messages/events/new.json", UriKind.Relative), + JsonString = json, + }; + + var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) + public async Task DirectMessagesEventsDestroy(TwitterDirectMessageId eventId) { - var endpoint = new Uri("direct_messages/events/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new DeleteRequest { - ["id"] = eventId.Id, + RequestUri = new("direct_messages/events/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = eventId.Id, + }, }; - // なぜか application/x-www-form-urlencoded でパラメーターを送ると Bad Request になる謎仕様 - endpoint = new Uri(endpoint.OriginalString + "?" + MyCommon.BuildQueryString(param), UriKind.Relative); - - return this.Connection.DeleteAsync(endpoint); + await this.Connection.SendAsync(request) + .IgnoreResponse() + .ConfigureAwait(false); } - public Task UsersShow(string screenName) + public async Task UsersShow(string screenName) { - var endpoint = new Uri("users/show.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["screen_name"] = screenName, - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("users/show.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/users/show/:id", }; - return this.Connection.GetAsync(endpoint, param, "/users/show/:id"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task UsersLookup(IReadOnlyList userIds) + public async Task UsersLookup(IReadOnlyList userIds) { - var endpoint = new Uri("users/lookup.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["user_id"] = string.Join(",", userIds), - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("users/lookup.json", UriKind.Relative), + Query = new Dictionary + { + ["user_id"] = string.Join(",", userIds), + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/users/lookup", }; - return this.Connection.GetAsync(endpoint, param, "/users/lookup"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> UsersReportSpam(string screenName) + public async Task> UsersReportSpam(string screenName) { - var endpoint = new Uri("users/report_spam.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["screen_name"] = screenName, - ["tweet_mode"] = "extended", + RequestUri = new("users/report_spam.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task FavoritesList(int? count = null, long? maxId = null, long? sinceId = null) + public async Task FavoritesList(int? count = null, long? maxId = null, long? sinceId = null) { - var endpoint = new Uri("favorites/list.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", @@ -545,142 +740,242 @@ public Task FavoritesList(int? count = null, long? maxId = null if (sinceId != null) param["since_id"] = sinceId.ToString(); - return this.Connection.GetAsync(endpoint, param, "/favorites/list"); + var request = new GetRequest + { + RequestUri = new("favorites/list.json", UriKind.Relative), + Query = param, + EndpointName = "/favorites/list", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> FavoritesCreate(TwitterStatusId statusId) + public async Task> FavoritesCreate(TwitterStatusId statusId) { - var endpoint = new Uri("favorites/create.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["id"] = statusId.Id, - ["tweet_mode"] = "extended", + RequestUri = new("favorites/create.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = statusId.Id, + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> FavoritesDestroy(TwitterStatusId statusId) + public async Task> FavoritesDestroy(TwitterStatusId statusId) { - var endpoint = new Uri("favorites/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["id"] = statusId.Id, - ["tweet_mode"] = "extended", + RequestUri = new("favorites/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["id"] = statusId.Id, + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task FriendshipsShow(string sourceScreenName, string targetScreenName) + public async Task FriendshipsShow(string sourceScreenName, string targetScreenName) { - var endpoint = new Uri("friendships/show.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["source_screen_name"] = sourceScreenName, - ["target_screen_name"] = targetScreenName, + RequestUri = new("friendships/show.json", UriKind.Relative), + Query = new Dictionary + { + ["source_screen_name"] = sourceScreenName, + ["target_screen_name"] = targetScreenName, + }, + EndpointName = "/friendships/show", }; - return this.Connection.GetAsync(endpoint, param, "/friendships/show"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> FriendshipsCreate(string screenName) + public async Task> FriendshipsCreate(string screenName) { - var endpoint = new Uri("friendships/create.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["screen_name"] = screenName, + RequestUri = new("friendships/create.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> FriendshipsDestroy(string screenName) + public async Task> FriendshipsDestroy(string screenName) { - var endpoint = new Uri("friendships/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["screen_name"] = screenName, + RequestUri = new("friendships/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task NoRetweetIds() + public async Task NoRetweetIds() { - var endpoint = new Uri("friendships/no_retweets/ids.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("friendships/no_retweets/ids.json", UriKind.Relative), + EndpointName = "/friendships/no_retweets/ids", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); - return this.Connection.GetAsync(endpoint, null, "/friendships/no_retweets/ids"); + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task FollowersIds(long? cursor = null) + public async Task FollowersIds(long? cursor = null) { - var endpoint = new Uri("followers/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); - return this.Connection.GetAsync(endpoint, param, "/followers/ids"); + var request = new GetRequest + { + RequestUri = new("followers/ids.json", UriKind.Relative), + Query = param, + EndpointName = "/followers/ids", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task MutesUsersIds(long? cursor = null) + public async Task MutesUsersIds(long? cursor = null) { - var endpoint = new Uri("mutes/users/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); - return this.Connection.GetAsync(endpoint, param, "/mutes/users/ids"); + var request = new GetRequest + { + RequestUri = new("mutes/users/ids.json", UriKind.Relative), + Query = param, + EndpointName = "/mutes/users/ids", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task BlocksIds(long? cursor = null) + public async Task BlocksIds(long? cursor = null) { - var endpoint = new Uri("blocks/ids.json", UriKind.Relative); var param = new Dictionary(); if (cursor != null) param["cursor"] = cursor.ToString(); - return this.Connection.GetAsync(endpoint, param, "/blocks/ids"); + var request = new GetRequest + { + RequestUri = new("blocks/ids.json", UriKind.Relative), + Query = param, + EndpointName = "/blocks/ids", + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> BlocksCreate(string screenName) + public async Task> BlocksCreate(string screenName) { - var endpoint = new Uri("blocks/create.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["screen_name"] = screenName, - ["tweet_mode"] = "extended", + RequestUri = new("blocks/create.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> BlocksDestroy(string screenName) + public async Task> BlocksDestroy(string screenName) { - var endpoint = new Uri("blocks/destroy.json", UriKind.Relative); - var param = new Dictionary + var request = new PostRequest { - ["screen_name"] = screenName, - ["tweet_mode"] = "extended", + RequestUri = new("blocks/destroy.json", UriKind.Relative), + Query = new Dictionary + { + ["screen_name"] = screenName, + ["tweet_mode"] = "extended", + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } public async Task AccountVerifyCredentials() { - var endpoint = new Uri("account/verify_credentials.json", UriKind.Relative); - var param = new Dictionary + var request = new GetRequest { - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", + RequestUri = new("account/verify_credentials.json", UriKind.Relative), + Query = new Dictionary + { + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + EndpointName = "/account/verify_credentials", }; - var user = await this.Connection.GetAsync(endpoint, param, "/account/verify_credentials") + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + var user = await response.ReadAsJson() .ConfigureAwait(false); this.CurrentUserId = user.Id; @@ -689,9 +984,8 @@ public async Task AccountVerifyCredentials() return user; } - public Task> AccountUpdateProfile(string name, string url, string? location, string? description) + public async Task> AccountUpdateProfile(string name, string url, string? location, string? description) { - var endpoint = new Uri("account/update_profile.json", UriKind.Relative); var param = new Dictionary { ["include_entities"] = "true", @@ -714,43 +1008,73 @@ public Task> AccountUpdateProfile(string name, string url, param["description"] = escapedDescription; } - return this.Connection.PostLazyAsync(endpoint, param); + var request = new PostRequest + { + RequestUri = new("account/update_profile.json", UriKind.Relative), + Query = param, + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task> AccountUpdateProfileImage(IMediaItem image) + public async Task> AccountUpdateProfileImage(IMediaItem image) { - var endpoint = new Uri("account/update_profile_image.json", UriKind.Relative); - var param = new Dictionary + var request = new PostMultipartRequest { - ["include_entities"] = "true", - ["include_ext_alt_text"] = "true", - ["tweet_mode"] = "extended", - }; - var paramMedia = new Dictionary - { - ["image"] = image, + RequestUri = new("account/update_profile_image.json", UriKind.Relative), + Query = new Dictionary + { + ["include_entities"] = "true", + ["include_ext_alt_text"] = "true", + ["tweet_mode"] = "extended", + }, + Media = new Dictionary + { + ["image"] = image, + }, }; - return this.Connection.PostLazyAsync(endpoint, param, paramMedia); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task ApplicationRateLimitStatus() + public async Task ApplicationRateLimitStatus() { - var endpoint = new Uri("application/rate_limit_status.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("application/rate_limit_status.json", UriKind.Relative), + EndpointName = "/application/rate_limit_status", + }; - return this.Connection.GetAsync(endpoint, null, "/application/rate_limit_status"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task Configuration() + public async Task Configuration() { - var endpoint = new Uri("help/configuration.json", UriKind.Relative); + var request = new GetRequest + { + RequestUri = new("help/configuration.json", UriKind.Relative), + EndpointName = "/help/configuration", + }; - return this.Connection.GetAsync(endpoint, null, "/help/configuration"); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task> MediaUploadInit(long totalBytes, string mediaType, string? mediaCategory = null) + public async Task> MediaUploadInit(long totalBytes, string mediaType, string? mediaCategory = null) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); var param = new Dictionary { ["command"] = "INIT", @@ -761,62 +1085,93 @@ public Task> MediaUploadInit(long totalBytes, s if (mediaCategory != null) param["media_category"] = mediaCategory; - return this.Connection.PostLazyAsync(endpoint, param); + var request = new PostRequest + { + RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"), + Query = param, + }; + + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media) + public async Task MediaUploadAppend(long mediaId, int segmentIndex, IMediaItem media) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); - var param = new Dictionary + var request = new PostMultipartRequest { - ["command"] = "APPEND", - ["media_id"] = mediaId.ToString(), - ["segment_index"] = segmentIndex.ToString(), - }; - var paramMedia = new Dictionary - { - ["media"] = media, + RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"), + Query = new Dictionary + { + ["command"] = "APPEND", + ["media_id"] = mediaId.ToString(), + ["segment_index"] = segmentIndex.ToString(), + }, + Media = new Dictionary + { + ["media"] = media, + }, }; - return this.Connection.PostAsync(endpoint, param, paramMedia); + await this.Connection.SendAsync(request) + .IgnoreResponse() + .ConfigureAwait(false); } - public Task> MediaUploadFinalize(long mediaId) + public async Task> MediaUploadFinalize(long mediaId) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); - var param = new Dictionary + var request = new PostRequest { - ["command"] = "FINALIZE", - ["media_id"] = mediaId.ToString(), + RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"), + Query = new Dictionary + { + ["command"] = "FINALIZE", + ["media_id"] = mediaId.ToString(), + }, }; - return this.Connection.PostLazyAsync(endpoint, param); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return response.ReadAsLazyJson(); } - public Task MediaUploadStatus(long mediaId) + public async Task MediaUploadStatus(long mediaId) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/upload.json"); - var param = new Dictionary + var request = new GetRequest { - ["command"] = "STATUS", - ["media_id"] = mediaId.ToString(), + RequestUri = new("https://upload.twitter.com/1.1/media/upload.json"), + Query = new Dictionary + { + ["command"] = "STATUS", + ["media_id"] = mediaId.ToString(), + }, }; - return this.Connection.GetAsync(endpoint, param, endpointName: null); + using var response = await this.Connection.SendAsync(request) + .ConfigureAwait(false); + + return await response.ReadAsJson() + .ConfigureAwait(false); } - public Task MediaMetadataCreate(long mediaId, string altText) + public async Task MediaMetadataCreate(long mediaId, string altText) { - var endpoint = new Uri("https://upload.twitter.com/1.1/media/metadata/create.json"); - var escapedAltText = JsonUtils.EscapeJsonString(altText); - var json = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}"""; + var request = new PostJsonRequest + { + RequestUri = new("https://upload.twitter.com/1.1/media/metadata/create.json"), + JsonString = $$$"""{"media_id": "{{{mediaId}}}", "alt_text": {"text": "{{{escapedAltText}}}"}}""", + }; - return this.Connection.PostJsonAsync(endpoint, json); + await this.Connection.SendAsync(request) + .IgnoreResponse() + .ConfigureAwait(false); } - public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? realm = null) - => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(authServiceProvider, realm); + public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null) + => ((TwitterApiConnection)this.Connection).CreateOAuthEchoHandler(innerHandler, authServiceProvider, realm); public void Dispose() => this.ApiConnection?.Dispose(); diff --git a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs index 6ff98cf55..24faa6b23 100644 --- a/OpenTween/Api/TwitterV2/GetTimelineRequest.cs +++ b/OpenTween/Api/TwitterV2/GetTimelineRequest.cs @@ -69,12 +69,20 @@ private Dictionary CreateParameters() return param; } - public Task Send(IApiConnection apiConnection) + public async Task Send(IApiConnection apiConnection) { - var uri = this.CreateEndpointUri(); - var param = this.CreateParameters(); + var request = new GetRequest + { + RequestUri = this.CreateEndpointUri(), + Query = this.CreateParameters(), + EndpointName = EndpointName, + }; + + using var response = await apiConnection.SendAsync(request) + .ConfigureAwait(false); - return apiConnection.GetAsync(uri, param, EndpointName); + return await response.ReadAsJson() + .ConfigureAwait(false); } } } diff --git a/OpenTween/AppendSettingDialog.cs b/OpenTween/AppendSettingDialog.cs index 0803eb487..703cd51c0 100644 --- a/OpenTween/AppendSettingDialog.cs +++ b/OpenTween/AppendSettingDialog.cs @@ -195,7 +195,7 @@ private async void StartAuthButton_Click(object sender, EventArgs e) }; using var twitterApi = new TwitterApi(); - twitterApi.Initialize(appToken, "", "", 0L, ""); + twitterApi.Initialize(new TwitterCredentialCookie(appToken), 0L, ""); var twitterUser = await twitterApi.AccountVerifyCredentials(); newAccount.UserId = twitterUser.Id; newAccount.Username = twitterUser.ScreenName; @@ -294,7 +294,7 @@ public void ApplyNetworkSettings() if (MyCommon.IsNullOrEmpty(pin)) return null; // キャンセルされた場合 - var accessTokenResponse = await TwitterApiConnection.GetAccessTokenAsync(appToken, requestToken, pin); + var accessTokenResponse = await TwitterApiConnection.GetAccessTokenAsync(requestToken, pin); return new UserAccount { diff --git a/OpenTween/ApplicationEvents.cs b/OpenTween/ApplicationEvents.cs index 39e75494d..04d5a6946 100644 --- a/OpenTween/ApplicationEvents.cs +++ b/OpenTween/ApplicationEvents.cs @@ -94,6 +94,8 @@ public static int Main(string[] args) return 1; // 設定が完了しなかったため終了 } + SetupTwitter(container.Twitter, settings); + Application.Run(container.MainForm); return 0; @@ -136,5 +138,33 @@ private static bool ShowSettingsDialog(SettingManager settings, IconAssetsManage settings.SaveAll(); return true; } + + private static void SetupTwitter(Twitter tw, SettingManager settings) + { + var account = settings.Common.SelectedAccount; + if (account != null) + tw.Initialize(account.GetTwitterCredential(), account.Username, account.UserId); + else + tw.Initialize(new TwitterCredentialNone(), "", 0L); + + tw.RestrictFavCheck = settings.Common.RestrictFavCheck; + tw.ReadOwnPost = settings.Common.ReadOwnPost; + + // アクセストークンが有効であるか確認する + // ここが Twitter API への最初のアクセスになるようにすること + try + { + tw.VerifyCredentials(); + } + catch (WebApiException ex) + { + MessageBox.Show( + string.Format(Properties.Resources.StartupAuthError_Text, ex.Message), + ApplicationSettings.ApplicationName, + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); + } + } } } diff --git a/OpenTween/Connection/ApiResponse.cs b/OpenTween/Connection/ApiResponse.cs new file mode 100644 index 000000000..da03230e9 --- /dev/null +++ b/OpenTween/Connection/ApiResponse.cs @@ -0,0 +1,126 @@ +// 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; + private bool preventDisposeResponse; + + public ApiResponse(HttpResponseMessage responseMessage) + => this.responseMessage = responseMessage; + + public void Dispose() + { + if (this.IsDisposed) + return; + + if (!this.preventDisposeResponse) + 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 }; + } + } + + public LazyJson ReadAsLazyJson() + { + this.preventDisposeResponse = true; + + return new(this.responseMessage); + } + + public async Task ReadAsString() + { + using var content = this.responseMessage.Content; + + return await content.ReadAsStringAsync() + .ConfigureAwait(false); + } + } + + public static class ApiResponseTaskExtension + { + public static async Task IgnoreResponse(this Task task) + { + using var response = await task.ConfigureAwait(false); + // レスポンスボディを読み込まず破棄する + } + } +} diff --git a/OpenTween/Connection/DeleteRequest.cs b/OpenTween/Connection/DeleteRequest.cs new file mode 100644 index 000000000..60927f2e8 --- /dev/null +++ b/OpenTween/Connection/DeleteRequest.cs @@ -0,0 +1,47 @@ +// 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 DeleteRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public IDictionary? Query { get; set; } + + public string? EndpointName { get; set; } + + public TimeSpan Timeout { get; set; } = Networking.DefaultTimeout; + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Delete, + RequestUri = UriQueryBuilder.Build(new(baseUri, this.RequestUri), this.Query), + }; + } +} diff --git a/OpenTween/Connection/GetRequest.cs b/OpenTween/Connection/GetRequest.cs new file mode 100644 index 000000000..2a9791d03 --- /dev/null +++ b/OpenTween/Connection/GetRequest.cs @@ -0,0 +1,47 @@ +// 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 TimeSpan Timeout { get; set; } = Networking.DefaultTimeout; + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Get, + RequestUri = UriQueryBuilder.Build(new(baseUri, this.RequestUri), this.Query), + }; + } +} diff --git a/OpenTween/Connection/HttpClientBuilder.cs b/OpenTween/Connection/HttpClientBuilder.cs new file mode 100644 index 000000000..bb5c1cea9 --- /dev/null +++ b/OpenTween/Connection/HttpClientBuilder.cs @@ -0,0 +1,76 @@ +// 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 HttpClientBuilder + { + private readonly List> setupHttpClientHandler = new(); + private readonly List> customHandlers = new(); + private readonly List> setupHttpClient = new(); + + public void SetupHttpClientHandler(Action func) + => this.setupHttpClientHandler.Add(func); + + public void AddHandler(Func func) + => this.customHandlers.Add(func); + + public void SetupHttpClient(Action func) + => this.setupHttpClient.Add(func); + + public HttpClient Build() + { + WebRequestHandler? handler = null; + HttpMessageHandler? wrappedHandler = null; + HttpClient? client = null; + try + { + handler = new(); + foreach (var setupFunc in this.setupHttpClientHandler) + setupFunc(handler); + + wrappedHandler = handler; + foreach (var handlerFunc in this.customHandlers) + wrappedHandler = handlerFunc(wrappedHandler); + + client = new(wrappedHandler, disposeHandler: true); + + foreach (var setupFunc in this.setupHttpClient) + setupFunc(client); + + return client; + } + catch + { + client?.Dispose(); + wrappedHandler?.Dispose(); + handler?.Dispose(); + throw; + } + } + } +} 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/IHttpRequest.cs b/OpenTween/Connection/IHttpRequest.cs new file mode 100644 index 000000000..ab309cb4c --- /dev/null +++ b/OpenTween/Connection/IHttpRequest.cs @@ -0,0 +1,37 @@ +// 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; } + + TimeSpan Timeout { get; } + + HttpRequestMessage CreateMessage(Uri baseUri); + } +} diff --git a/OpenTween/Connection/ITwitterCredential.cs b/OpenTween/Connection/ITwitterCredential.cs new file mode 100644 index 000000000..fdad97079 --- /dev/null +++ b/OpenTween/Connection/ITwitterCredential.cs @@ -0,0 +1,34 @@ +// 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.Net.Http; + +namespace OpenTween.Connection +{ + public interface ITwitterCredential + { + APIAuthType AuthType { get; } + + HttpMessageHandler CreateHttpHandler(HttpMessageHandler innerHandler); + } +} diff --git a/OpenTween/Connection/LazyJson.cs b/OpenTween/Connection/LazyJson.cs index 2f0bc80e0..51a156cca 100644 --- a/OpenTween/Connection/LazyJson.cs +++ b/OpenTween/Connection/LazyJson.cs @@ -46,14 +46,6 @@ public sealed class LazyJson : IDisposable public LazyJson(HttpResponseMessage response) => this.Response = response; - internal LazyJson(T instance) - { - this.Response = null; - - this.instance = instance; - this.completed = true; - } - public async Task LoadJsonAsync() { if (this.completed) @@ -80,12 +72,6 @@ public void Dispose() => this.Response?.Dispose(); } - public static class LazyJson - { - public static LazyJson Create(T instance) - => new(instance); - } - public static class LazyJsonTaskExtension { public static async Task IgnoreResponse(this Task> task) diff --git a/OpenTween/Connection/Networking.cs b/OpenTween/Connection/Networking.cs index bdc0e47ad..b7511d0d9 100644 --- a/OpenTween/Connection/Networking.cs +++ b/OpenTween/Connection/Networking.cs @@ -87,7 +87,7 @@ static Networking() { DefaultTimeout = TimeSpan.FromSeconds(20); UploadImageTimeout = TimeSpan.FromSeconds(60); - globalHttpClient = CreateHttpClient(new HttpClientHandler()); + globalHttpClient = CreateHttpClientBuilder().Build(); } /// @@ -134,50 +134,39 @@ public static void SetWebProxy( } /// - /// OpenTween で必要な設定を施した HttpClientHandler インスタンスを生成します - /// - [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")] - public static WebRequestHandler CreateHttpClientHandler() - { - var handler = new WebRequestHandler - { - UseCookies = false, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - ReadWriteTimeout = (int)DefaultTimeout.TotalMilliseconds, - }; - - if (Networking.Proxy != null) - { - handler.UseProxy = true; - handler.Proxy = Networking.Proxy; - } - else - { - handler.UseProxy = false; - } - - return handler; - } - - /// - /// OpenTween で必要な設定を施した HttpClient インスタンスを生成します + /// OpenTween のユーザー設定を適用した を返します /// /// /// 通常は Networking.Http を使用すべきです。 /// このメソッドを使用する場合は、WebProxyChanged イベントが発生する度に HttpClient を生成し直すように実装してください。 /// - [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")] - public static HttpClient CreateHttpClient(HttpMessageHandler handler) + public static HttpClientBuilder CreateHttpClientBuilder() { - HttpClient client; - if (ForceIPv4) - client = new HttpClient(new ForceIPv4Handler(handler)); - else - client = new HttpClient(handler); + var builder = new HttpClientBuilder(); + + builder.SetupHttpClientHandler(x => + { + x.UseCookies = false; + x.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + x.ReadWriteTimeout = (int)DefaultTimeout.TotalMilliseconds; + + if (Networking.Proxy != null) + { + x.UseProxy = true; + x.Proxy = Networking.Proxy; + } + else + { + x.UseProxy = false; + } + }); + + builder.SetupHttpClient(x => x.Timeout = Networking.DefaultTimeout); - client.Timeout = Networking.DefaultTimeout; + if (forceIPv4) + builder.AddHandler(x => new ForceIPv4Handler(x)); - return client; + return builder; } public static string GetUserAgentString(bool fakeMSIE = false) @@ -200,7 +189,7 @@ internal static void CheckInitialized() [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")] private static void OnWebProxyChanged(EventArgs e) { - var newClient = Networking.CreateHttpClient(Networking.CreateHttpClientHandler()); + var newClient = Networking.CreateHttpClientBuilder().Build(); var oldClient = Interlocked.Exchange(ref globalHttpClient, newClient); oldClient.Dispose(); diff --git a/OpenTween/Connection/PostJsonRequest.cs b/OpenTween/Connection/PostJsonRequest.cs new file mode 100644 index 000000000..236bfe6d6 --- /dev/null +++ b/OpenTween/Connection/PostJsonRequest.cs @@ -0,0 +1,48 @@ +// 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.Text; + +namespace OpenTween.Connection +{ + public class PostJsonRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public required string JsonString { get; set; } + + public string? EndpointName { get; set; } + + public TimeSpan Timeout { get; set; } = Networking.DefaultTimeout; + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Post, + RequestUri = new(baseUri, this.RequestUri), + Content = new StringContent(this.JsonString, Encoding.UTF8, "application/json"), + }; + } +} diff --git a/OpenTween/Connection/PostMultipartRequest.cs b/OpenTween/Connection/PostMultipartRequest.cs new file mode 100644 index 000000000..6e27e36d2 --- /dev/null +++ b/OpenTween/Connection/PostMultipartRequest.cs @@ -0,0 +1,66 @@ +// 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 PostMultipartRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public IDictionary? Query { get; set; } + + public IDictionary? Media { get; set; } + + public string? EndpointName { get; set; } + + public TimeSpan Timeout { get; set; } = Networking.UploadImageTimeout; + + public HttpRequestMessage CreateMessage(Uri baseUri) + { + var content = new MultipartFormDataContent(); + + if (this.Query != null) + { + foreach (var (key, value) in this.Query) + content.Add(new StringContent(value), key); + } + + if (this.Media != null) + { + foreach (var (key, value) in this.Media) + content.Add(new StreamContent(value.OpenRead()), key, value.Name); + } + + return new() + { + Method = HttpMethod.Post, + RequestUri = new(baseUri, this.RequestUri), + Content = content, + }; + } + } +} diff --git a/OpenTween/Connection/PostRequest.cs b/OpenTween/Connection/PostRequest.cs new file mode 100644 index 000000000..122a90dfc --- /dev/null +++ b/OpenTween/Connection/PostRequest.cs @@ -0,0 +1,48 @@ +// 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 PostRequest : IHttpRequest + { + public required Uri RequestUri { get; set; } + + public IDictionary? Query { get; set; } + + public string? EndpointName { get; set; } + + public TimeSpan Timeout { get; set; } = Networking.DefaultTimeout; + + public HttpRequestMessage CreateMessage(Uri baseUri) + => new() + { + Method = HttpMethod.Post, + RequestUri = new(baseUri, this.RequestUri), + Content = this.Query != null ? new FormUrlEncodedContent(this.Query) : null, + }; + } +} diff --git a/OpenTween/Connection/TwitterApiConnection.cs b/OpenTween/Connection/TwitterApiConnection.cs index 16e2c342b..410359737 100644 --- a/OpenTween/Connection/TwitterApiConnection.cs +++ b/OpenTween/Connection/TwitterApiConnection.cs @@ -52,88 +52,60 @@ public static string RestApiHost public bool IsDisposed { get; private set; } = false; - public string AccessToken { get; } - - public string AccessSecret { get; } - internal HttpClient Http; - internal HttpClient HttpUpload; - internal HttpClient HttpStreaming; - private readonly TwitterAppToken appToken; + internal ITwitterCredential Credential { get; } - public TwitterApiConnection(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret) - : this( - new() - { - AuthType = APIAuthType.OAuth1, - OAuth1CustomConsumerKey = consumerKey, - OAuth1CustomConsumerSecret = consumerSecret, - }, - accessToken, - accessSecret - ) + public TwitterApiConnection() + : this(new TwitterCredentialNone()) { } - public TwitterApiConnection(TwitterAppToken appToken, string accessToken, string accessSecret) + public TwitterApiConnection(ITwitterCredential credential) { - this.appToken = appToken; - this.AccessToken = accessToken; - this.AccessSecret = accessSecret; + this.Credential = credential; this.InitializeHttpClients(); Networking.WebProxyChanged += this.Networking_WebProxyChanged; } - [MemberNotNull(nameof(Http), nameof(HttpUpload), nameof(HttpStreaming))] + [MemberNotNull(nameof(Http))] private void InitializeHttpClients() { - this.Http = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret); - - this.HttpUpload = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret); - this.HttpUpload.Timeout = Networking.UploadImageTimeout; + this.Http = InitializeHttpClient(this.Credential); - this.HttpStreaming = InitializeHttpClient(this.appToken, this.AccessToken, this.AccessSecret, disableGzip: true); - this.HttpStreaming.Timeout = Timeout.InfiniteTimeSpan; + // タイムアウト設定は IHttpRequest.Timeout でリクエスト毎に制御する + this.Http.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), + request.Timeout + ); 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 +115,10 @@ await TwitterApiConnection.CheckStatusCode(response) { throw TwitterApiException.CreateFromException(ex); } + finally + { + responseMessage?.Dispose(); + } } /// @@ -167,279 +143,36 @@ private void ThrowIfRateLimitExceeded(string endpointName) } } - public Task GetStreamAsync(Uri uri, IDictionary? param) - => this.GetStreamAsync(uri, param, null); - - public async Task GetStreamAsync(Uri uri, IDictionary? param, string? endpointName) - { - // レートリミット規制中はAPIリクエストを送信せずに直ちにエラーを発生させる - if (endpointName != null) - this.ThrowIfRateLimitExceeded(endpointName); - - var requestUri = new Uri(RestApiBase, uri); - - if (param != null) - requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); - - try - { - var response = await this.Http.GetAsync(requestUri) - .ConfigureAwait(false); - - if (endpointName != null) - MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - - return await response.Content.ReadAsStreamAsync() - .ConfigureAwait(false); - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - - public async Task GetStreamingStreamAsync(Uri uri, IDictionary? param) - { - var requestUri = new Uri(RestApiBase, uri); - - if (param != null) - requestUri = new Uri(requestUri, "?" + MyCommon.BuildQueryString(param)); - - try - { - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - - return await response.Content.ReadAsStreamAsync() - .ConfigureAwait(false); - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - - public async Task> PostLazyAsync(Uri uri, IDictionary? param) - { - var requestUri = new Uri(RestApiBase, uri); - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - using var postContent = new FormUrlEncodedContent(param); - request.Content = postContent; - - HttpResponseMessage? response = null; - try - { - response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - - var result = new LazyJson(response); - response = null; - - return result; - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - finally - { - response?.Dispose(); - } - } - - public async Task> PostLazyAsync(Uri uri, IDictionary? param, IDictionary? media) - { - var requestUri = new Uri(RestApiBase, uri); - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - using var postContent = new MultipartFormDataContent(); - if (param != null) - { - foreach (var (key, value) in param) - postContent.Add(new StringContent(value), key); - } - if (media != null) - { - foreach (var (key, value) in media) - postContent.Add(new StreamContent(value.OpenRead()), key, value.Name); - } - - request.Content = postContent; - - HttpResponseMessage? response = null; - try - { - response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - - var result = new LazyJson(response); - response = null; - - return result; - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - finally - { - response?.Dispose(); - } - } - - public async Task PostAsync(Uri uri, IDictionary? param, IDictionary? media) - { - var requestUri = new Uri(RestApiBase, uri); - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - using var postContent = new MultipartFormDataContent(); - if (param != null) - { - foreach (var (key, value) in param) - postContent.Add(new StringContent(value), key); - } - if (media != null) - { - foreach (var (key, value) in media) - postContent.Add(new StreamContent(value.OpenRead()), key, value.Name); - } - - request.Content = postContent; - - try - { - using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - - public async Task PostJsonAsync(Uri uri, string json) - { - var requestUri = new Uri(RestApiBase, uri); - using var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - - using var postContent = new StringContent(json, Encoding.UTF8, "application/json"); - request.Content = postContent; - - try - { - using var response = await this.Http.SendAsync(request) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - - return await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - } - - public async Task> PostJsonAsync(Uri uri, string json) + public static async Task HandleTimeout(Func> func, TimeSpan timeout) { - var requestUri = new Uri(RestApiBase, uri); - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + using var cts = new CancellationTokenSource(); + var cancellactionToken = cts.Token; - using var postContent = new StringContent(json, Encoding.UTF8, "application/json"); - request.Content = postContent; + var task = Task.Run(() => func(cancellactionToken), cancellactionToken); + var timeoutTask = Task.Delay(timeout); - HttpResponseMessage? response = null; - try + if (await Task.WhenAny(task, timeoutTask) == timeoutTask) { - response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); + // タイムアウト - var result = new LazyJson(response); - response = null; + // キャンセル後のタスクで発生した例外は無視する + static async Task IgnoreExceptions(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch + { + } + } + _ = IgnoreExceptions(task); + cts.Cancel(); - return result; - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); + throw new OperationCanceledException("Timeout", cancellactionToken); } - finally - { - response?.Dispose(); - } - } - - public async Task DeleteAsync(Uri uri) - { - var requestUri = new Uri(RestApiBase, uri); - using var request = new HttpRequestMessage(HttpMethod.Delete, requestUri); - - try - { - using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) - .ConfigureAwait(false); - await TwitterApiConnection.CheckStatusCode(response) - .ConfigureAwait(false); - } - catch (HttpRequestException ex) - { - throw TwitterApiException.CreateFromException(ex); - } - catch (OperationCanceledException ex) - { - throw TwitterApiException.CreateFromException(ex); - } + return await task; } protected static async Task CheckStatusCode(HttpResponseMessage response) @@ -488,18 +221,33 @@ protected static async Task CheckStatusCode(HttpResponseMessage response) } } - public OAuthEchoHandler CreateOAuthEchoHandler(Uri authServiceProvider, Uri? realm = null) + public OAuthEchoHandler CreateOAuthEchoHandler(HttpMessageHandler innerHandler, Uri authServiceProvider, Uri? realm = null) { var uri = new Uri(RestApiBase, authServiceProvider); - return OAuthEchoHandler.CreateHandler( - Networking.CreateHttpClientHandler(), - uri, - this.appToken.OAuth1ConsumerKey, - this.appToken.OAuth1ConsumerSecret, - this.AccessToken, - this.AccessSecret, - realm); + if (this.Credential is TwitterCredentialOAuth1 oauthCredential) + { + return OAuthEchoHandler.CreateHandler( + innerHandler, + uri, + oauthCredential.AppToken.OAuth1ConsumerKey, + oauthCredential.AppToken.OAuth1ConsumerSecret, + oauthCredential.Token, + oauthCredential.TokenSecret, + realm); + } + else + { + // MobipictureApi クラス向けの暫定対応 + return OAuthEchoHandler.CreateHandler( + innerHandler, + uri, + ApiKey.Create(""), + ApiKey.Create(""), + "", + "", + realm); + } } public void Dispose() @@ -519,8 +267,6 @@ protected virtual void Dispose(bool disposing) { Networking.WebProxyChanged -= this.Networking_WebProxyChanged; this.Http.Dispose(); - this.HttpUpload.Dispose(); - this.HttpStreaming.Dispose(); } } @@ -530,19 +276,20 @@ protected virtual void Dispose(bool disposing) private void Networking_WebProxyChanged(object sender, EventArgs e) => this.InitializeHttpClients(); - public static async Task<(string Token, string TokenSecret)> GetRequestTokenAsync(TwitterAppToken appToken) + public static async Task GetRequestTokenAsync(TwitterAppToken appToken) { + var emptyCredential = new TwitterCredentialOAuth1(appToken, "", ""); var param = new Dictionary { ["oauth_callback"] = "oob", }; - var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, appToken, oauthToken: null) + var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/request_token"), param, emptyCredential) .ConfigureAwait(false); - return (response["oauth_token"], response["oauth_token_secret"]); + return new(appToken, response["oauth_token"], response["oauth_token_secret"]); } - public static Uri GetAuthorizeUri((string Token, string TokenSecret) requestToken, string? screenName = null) + public static Uri GetAuthorizeUri(TwitterCredentialOAuth1 requestToken, string? screenName = null) { var param = new Dictionary { @@ -555,13 +302,13 @@ public static Uri GetAuthorizeUri((string Token, string TokenSecret) requestToke return new Uri("https://api.twitter.com/oauth/authorize?" + MyCommon.BuildQueryString(param)); } - public static async Task> GetAccessTokenAsync(TwitterAppToken appToken, (string Token, string TokenSecret) requestToken, string verifier) + public static async Task> GetAccessTokenAsync(TwitterCredentialOAuth1 credential, string verifier) { var param = new Dictionary { ["oauth_verifier"] = verifier, }; - var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, appToken, requestToken) + var response = await GetOAuthTokenAsync(new Uri("https://api.twitter.com/oauth/access_token"), param, credential) .ConfigureAwait(false); return response; @@ -570,14 +317,10 @@ public static async Task> GetAccessTokenAsync(Twitte private static async Task> GetOAuthTokenAsync( Uri uri, IDictionary param, - TwitterAppToken appToken, - (string Token, string TokenSecret)? oauthToken) + TwitterCredentialOAuth1 credential + ) { - HttpClient authorizeClient; - if (oauthToken != null) - authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, oauthToken.Value.Token, oauthToken.Value.TokenSecret); - else - authorizeClient = InitializeHttpClient(appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, "", ""); + using var authorizeClient = InitializeHttpClient(credential); var requestUri = new Uri(uri, "?" + MyCommon.BuildQueryString(param)); @@ -609,37 +352,17 @@ await TwitterApiConnection.CheckStatusCode(response) } } - private static HttpClient InitializeHttpClient(ApiKey consumerKey, ApiKey consumerSecret, string accessToken, string accessSecret, bool disableGzip = false) - { - var innerHandler = Networking.CreateHttpClientHandler(); - innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); - - if (disableGzip) - innerHandler.AutomaticDecompression = DecompressionMethods.None; - - var handler = new OAuthHandler(innerHandler, consumerKey, consumerSecret, accessToken, accessSecret); - - return Networking.CreateHttpClient(handler); - } - - private static HttpClient InitializeHttpClient(TwitterAppToken appToken, string accessToken, string accessSecret, bool disableGzip = false) + private static HttpClient InitializeHttpClient(ITwitterCredential credential) { - var innerHandler = Networking.CreateHttpClientHandler(); - innerHandler.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); + var builder = Networking.CreateHttpClientBuilder(); - if (disableGzip) - innerHandler.AutomaticDecompression = DecompressionMethods.None; + builder.SetupHttpClientHandler( + x => x.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache) + ); - HttpMessageHandler handler = appToken.AuthType switch - { - APIAuthType.OAuth1 - => new OAuthHandler(innerHandler, appToken.OAuth1ConsumerKey, appToken.OAuth1ConsumerSecret, accessToken, accessSecret), - APIAuthType.TwitterComCookie - => new TwitterComCookieHandler(innerHandler, appToken.TwitterComCookie), - _ => throw new NotImplementedException(), - }; + builder.AddHandler(x => credential.CreateHttpHandler(x)); - return Networking.CreateHttpClient(handler); + return builder.Build(); } } } diff --git a/OpenTween/Connection/TwitterCredentialCookie.cs b/OpenTween/Connection/TwitterCredentialCookie.cs new file mode 100644 index 000000000..ad0387cbc --- /dev/null +++ b/OpenTween/Connection/TwitterCredentialCookie.cs @@ -0,0 +1,41 @@ +// 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.Net.Http; + +namespace OpenTween.Connection +{ + public class TwitterCredentialCookie : ITwitterCredential + { + public APIAuthType AuthType + => APIAuthType.TwitterComCookie; + + public TwitterAppToken AppToken { get; } + + public TwitterCredentialCookie(TwitterAppToken appToken) + => this.AppToken = appToken; + + public HttpMessageHandler CreateHttpHandler(HttpMessageHandler innerHandler) + => new TwitterComCookieHandler(innerHandler, this.AppToken.TwitterComCookie); + } +} diff --git a/OpenTween/Connection/TwitterCredentialNone.cs b/OpenTween/Connection/TwitterCredentialNone.cs new file mode 100644 index 000000000..f061203f0 --- /dev/null +++ b/OpenTween/Connection/TwitterCredentialNone.cs @@ -0,0 +1,36 @@ +// 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.Net.Http; + +namespace OpenTween.Connection +{ + public class TwitterCredentialNone : ITwitterCredential + { + public APIAuthType AuthType + => APIAuthType.None; + + public HttpMessageHandler CreateHttpHandler(HttpMessageHandler innerHandler) + => innerHandler; + } +} diff --git a/OpenTween/Connection/TwitterCredentialOAuth1.cs b/OpenTween/Connection/TwitterCredentialOAuth1.cs new file mode 100644 index 000000000..9d2d15961 --- /dev/null +++ b/OpenTween/Connection/TwitterCredentialOAuth1.cs @@ -0,0 +1,55 @@ +// 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.Net.Http; + +namespace OpenTween.Connection +{ + public class TwitterCredentialOAuth1 : ITwitterCredential + { + public APIAuthType AuthType + => APIAuthType.OAuth1; + + public TwitterAppToken AppToken { get; } + + public string Token { get; } + + public string TokenSecret { get; } + + public TwitterCredentialOAuth1(TwitterAppToken appToken, string accessToken, string accessSecret) + { + this.AppToken = appToken; + this.Token = accessToken; + this.TokenSecret = accessSecret; + } + + public HttpMessageHandler CreateHttpHandler(HttpMessageHandler innerHandler) + => new OAuthHandler( + innerHandler, + this.AppToken.OAuth1ConsumerKey, + this.AppToken.OAuth1ConsumerSecret, + this.Token, + this.TokenSecret + ); + } +} diff --git a/OpenTween/Connection/UriQueryBuilder.cs b/OpenTween/Connection/UriQueryBuilder.cs new file mode 100644 index 000000000..2c30dd3ca --- /dev/null +++ b/OpenTween/Connection/UriQueryBuilder.cs @@ -0,0 +1,42 @@ +// 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; + +namespace OpenTween.Connection +{ + public static class UriQueryBuilder + { + public static Uri Build(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, "?" + MyCommon.BuildQueryString(query)); + } + } +} diff --git a/OpenTween/DebounceTimer.cs b/OpenTween/DebounceTimer.cs index cc728cbb0..033138be3 100644 --- a/OpenTween/DebounceTimer.cs +++ b/OpenTween/DebounceTimer.cs @@ -39,10 +39,26 @@ public class DebounceTimer : IDisposable private readonly Func timerCallback; private readonly object lockObject = new(); + private bool enabled = false; private DateTimeUtc lastCall; private bool calledSinceLastInvoke; private bool refreshTimerEnabled; + public bool Enabled + { + get => this.enabled; + set + { + if (value == this.enabled) + return; + + this.enabled = value; + + if (!value) + this.debouncingTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + } + public TimeSpan Interval { get; } public bool InvokeLeading { get; } @@ -66,6 +82,9 @@ protected virtual ITimer CreateTimer(Func callback) public async Task Call() { + if (!this.Enabled) + return; + bool startTimer, invoke; lock (this.lockObject) { diff --git a/OpenTween/ErrorReportHandler.cs b/OpenTween/ErrorReportHandler.cs index af9d572f6..166b1727b 100644 --- a/OpenTween/ErrorReportHandler.cs +++ b/OpenTween/ErrorReportHandler.cs @@ -106,18 +106,6 @@ public static bool IsExceptionIgnorable(Exception ex) return true; } - if (ex is TaskCanceledException cancelEx) - { - // ton.twitter.com の画像でタイムアウトした場合、try-catch で例外がキャッチできない - // https://osdn.net/ticket/browse.php?group_id=6526&tid=37433 - var stackTrace = new StackTrace(cancelEx); - var lastFrameMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod(); - var matchClass = lastFrameMethod.ReflectedType == typeof(TwitterApiConnection); - var matchMethod = lastFrameMethod.Name == nameof(TwitterApiConnection.GetStreamAsync); - if (matchClass && matchMethod) - return true; - } - return false; } } diff --git a/OpenTween/Models/PostClass.cs b/OpenTween/Models/PostClass.cs index d73455fde..b1791c80c 100644 --- a/OpenTween/Models/PostClass.cs +++ b/OpenTween/Models/PostClass.cs @@ -117,60 +117,12 @@ public string Text public bool IsPromoted { get; set; } - /// - /// に含まれる t.co の展開後の URL を保持するクラス - /// - public class ExpandedUrlInfo : ICloneable + public record ExpandedUrlInfo( + string Url, + string ExpandedUrl + ) { - public static bool AutoExpand { get; set; } = true; - - /// 展開前の t.co ドメインの URL - public string Url { get; } - - /// 展開後の URL - /// - /// による展開が完了するまでは Entity に含まれる expanded_url の値を返します - /// - public string ExpandedUrl => this.expandedUrl; - - /// による展開を行うタスク - public Task ExpandTask { get; private set; } - - /// による展開が完了したか否か - public bool ExpandedCompleted => this.ExpandTask.IsCompleted; - - protected string expandedUrl; - - public ExpandedUrlInfo(string url, string expandedUrl) - : this(url, expandedUrl, deepExpand: true) - { - } - - public ExpandedUrlInfo(string url, string expandedUrl, bool deepExpand) - { - this.Url = url; - this.expandedUrl = expandedUrl; - - if (AutoExpand && deepExpand) - this.ExpandTask = this.DeepExpandAsync(); - else - this.ExpandTask = Task.CompletedTask; - } - - protected virtual async Task DeepExpandAsync() - { - var origUrl = this.expandedUrl; - var newUrl = await ShortUrl.Instance.ExpandUrlAsync(origUrl) - .ConfigureAwait(false); - - Interlocked.CompareExchange(ref this.expandedUrl, newUrl, origUrl); - } - - public ExpandedUrlInfo Clone() - => new(this.Url, this.ExpandedUrl, deepExpand: false); - - object ICloneable.Clone() - => this.Clone(); + public bool ExpandCompleted { get; init; } } [Flags] @@ -333,7 +285,7 @@ private string ReplaceToExpandedUrl(string html, out bool completedAll) foreach (var urlInfo in this.ExpandedUrls) { - if (!urlInfo.ExpandedCompleted) + if (!urlInfo.ExpandCompleted) completedAll = false; var tcoUrl = urlInfo.Url; diff --git a/OpenTween/Models/PostUrlExpander.cs b/OpenTween/Models/PostUrlExpander.cs new file mode 100644 index 000000000..7ee9a208f --- /dev/null +++ b/OpenTween/Models/PostUrlExpander.cs @@ -0,0 +1,65 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenTween.Models +{ + public class PostUrlExpander + { + private readonly ShortUrl shortUrl; + + public PostUrlExpander(ShortUrl shortUrl) + => this.shortUrl = shortUrl; + + public async Task Expand(PostClass post) + { + var urls = post.ExpandedUrls; + if (urls.Length == 0) + return; + + var tasks = MyCommon.CountUp(0, urls.Length - 1) + .Select(i => this.UpdateUrlItem(urls, i)); + + await Task.WhenAll(tasks); + } + + public async Task UpdateUrlItem(PostClass.ExpandedUrlInfo[] urls, int index) + { + var urlItem = urls[index]; + + var expandedUrl = await this.shortUrl.ExpandUrlAsync(urlItem.ExpandedUrl) + .ConfigureAwait(false); + + var newUrlItem = urlItem with + { + ExpandedUrl = expandedUrl, + ExpandCompleted = true, + }; + Interlocked.Exchange(ref urls[index], newUrlItem); + } + } +} diff --git a/OpenTween/Models/TabInformations.cs b/OpenTween/Models/TabInformations.cs index 7b338462a..d3c2d61b3 100644 --- a/OpenTween/Models/TabInformations.cs +++ b/OpenTween/Models/TabInformations.cs @@ -230,7 +230,7 @@ public void LoadTabsFromSettings(SettingTabs settingTabs) continue; if (this.ContainsTab(tab.TabName)) - tab.TabName = this.MakeTabName("MyTab"); + tab.TabName = this.MakeTabName(tab.TabName); this.AddTab(tab); } diff --git a/OpenTween/MyCommon.cs b/OpenTween/MyCommon.cs index 5e51960fe..0732c2e2b 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, @@ -755,20 +757,11 @@ public static bool IsValidEmail(string strIn) /// /// 状態を調べるキー /// で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。 - public static bool IsKeyDown(params Keys[] keys) - => MyCommon.IsKeyDownInternal(Control.ModifierKeys, keys); + public static bool IsKeyDown(Keys keys) + => MyCommon.IsKeyDown(Control.ModifierKeys, keys); - internal static bool IsKeyDownInternal(Keys modifierKeys, Keys[] targetKeys) - { - foreach (var key in targetKeys) - { - if ((modifierKeys & key) != key) - { - return false; - } - } - return true; - } + public static bool IsKeyDown(Keys modifierKeys, Keys targetKeys) + => (modifierKeys & targetKeys) == targetKeys; /// /// アプリケーションのアセンブリ名を取得します。 diff --git a/OpenTween/OpenTween.csproj b/OpenTween/OpenTween.csproj index fa782f423..688af6852 100644 --- a/OpenTween/OpenTween.csproj +++ b/OpenTween/OpenTween.csproj @@ -642,7 +642,7 @@ 1.0.1 - 1.2.0-beta.406 + 1.2.0-beta.507 runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/OpenTween/Properties/AssemblyInfo.cs b/OpenTween/Properties/AssemblyInfo.cs index b7e69474f..7046ae2b8 100644 --- a/OpenTween/Properties/AssemblyInfo.cs +++ b/OpenTween/Properties/AssemblyInfo.cs @@ -22,7 +22,7 @@ // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")] -[assembly: AssemblyVersion("3.9.0.0")] +[assembly: AssemblyVersion("3.10.0.0")] [assembly: InternalsVisibleTo("OpenTween.Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq diff --git a/OpenTween/Properties/Resources.Designer.cs b/OpenTween/Properties/Resources.Designer.cs index 0201229ab..aebbd9b49 100644 --- a/OpenTween/Properties/Resources.Designer.cs +++ b/OpenTween/Properties/Resources.Designer.cs @@ -580,6 +580,10 @@ internal static string ChangeIconToolStripMenuItem_Confirm { /// /// 更新履歴 /// + ///==== Ver 3.10.0(2023/12/16) + /// * NEW: graphqlエンドポイント経由で取得した引用ツイートの表示に対応 + /// * FIX: APIリクエストがタイムアウトした場合のキャンセル処理を改善 + /// ///==== Ver 3.9.0(2023/12/03) /// * NEW: graphqlエンドポイントに対するレートリミットの表示に対応 /// * CHG: タイムライン更新時に全件ではなく新着投稿のみ差分を取得する動作に変更 @@ -590,9 +594,7 @@ internal static string ChangeIconToolStripMenuItem_Confirm { ///==== Ver 3.8.0(2023/11/29) /// * NEW: graphqlエンドポイントを使用した検索タイムラインの取得に対応 /// * NEW: graphqlエンドポイントを使用したプロフィール情報の取得に対応 - /// * NEW: graphqlエンドポイントを使用したユーザータイムラインの取得に対応 - /// * CHG: タイムライン更新が停止する不具合が報告される件への暫定的な対処 - /// - タイムライン更新に30秒以上掛かっている場合は完了を待機せず次のタイマーを [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 + /// * NEW: graphq [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 /// internal static string ChangeLog { get { diff --git a/OpenTween/Setting/SettingCommon.cs b/OpenTween/Setting/SettingCommon.cs index 3e9f65684..488d926b2 100644 --- a/OpenTween/Setting/SettingCommon.cs +++ b/OpenTween/Setting/SettingCommon.cs @@ -398,6 +398,20 @@ public TwitterAppToken GetTwitterAppToken() }; } + public ITwitterCredential GetTwitterCredential() + { + var appToken = this.GetTwitterAppToken(); + + return appToken.AuthType switch + { + APIAuthType.OAuth1 + => new TwitterCredentialOAuth1(appToken, this.TwitterOAuth1ConsumerKey, this.TwitterOAuth1ConsumerSecret), + APIAuthType.TwitterComCookie + => new TwitterCredentialCookie(appToken), + _ => new TwitterCredentialNone(), + }; + } + private string Encrypt(string password) { if (MyCommon.IsNullOrEmpty(password)) password = ""; @@ -441,6 +455,7 @@ public override string ToString() public enum APIAuthType { + None, OAuth1, TwitterComCookie, } diff --git a/OpenTween/ShortUrl.cs b/OpenTween/ShortUrl.cs index b6dab0e06..e417022b6 100644 --- a/OpenTween/ShortUrl.cs +++ b/OpenTween/ShortUrl.cs @@ -515,13 +515,12 @@ private Uri UpgradeToHttpsIfAvailable(Uri original) [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")] private static HttpClient CreateDefaultHttpClient() { - var handler = Networking.CreateHttpClientHandler(); - handler.AllowAutoRedirect = false; + var builder = Networking.CreateHttpClientBuilder(); - var http = Networking.CreateHttpClient(handler); - http.Timeout = TimeSpan.FromSeconds(30); + builder.SetupHttpClientHandler(x => x.AllowAutoRedirect = false); + builder.SetupHttpClient(x => x.Timeout = TimeSpan.FromSeconds(30)); - return http; + return builder.Build(); } } } 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); } } } diff --git a/OpenTween/Tween.cs b/OpenTween/Tween.cs index 969462a2d..ae3c66027 100644 --- a/OpenTween/Tween.cs +++ b/OpenTween/Tween.cs @@ -208,7 +208,9 @@ public partial class TweenMain : OTBaseForm private readonly DebounceTimer saveConfigDebouncer; private readonly string recommendedStatusFooter; - private bool urlMultibyteSplit = false; + + internal bool SeparateUrlAndFullwidthCharacter { get; set; } = false; + private bool preventSmsCommand = true; // URL短縮のUndo用 @@ -264,202 +266,6 @@ private readonly record struct StatusTextHistory( private readonly HookGlobalHotkey hookGlobalHotkey; - private void TweenMain_Activated(object sender, EventArgs e) - { - // 画面がアクティブになったら、発言欄の背景色戻す - if (this.StatusText.Focused) - { - this.StatusText_Enter(this.StatusText, System.EventArgs.Empty); - } - } - - private bool disposed = false; - - /// - /// 使用中のリソースをすべてクリーンアップします。 - /// - /// マネージ リソースが破棄される場合 true、破棄されない場合は false です。 - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (this.disposed) - return; - - if (disposing) - { - this.components?.Dispose(); - - // 後始末 - this.SearchDialog.Dispose(); - this.urlDialog.Dispose(); - this.themeManager.Dispose(); - this.sfTab.Dispose(); - - this.timelineScheduler.Dispose(); - this.workerCts.Cancel(); - this.thumbnailTokenSource?.Dispose(); - - this.hookGlobalHotkey.Dispose(); - } - - // 終了時にRemoveHandlerしておかないとメモリリークする - // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx - Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged; - Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged; - - this.disposed = true; - } - - private void InitColumns(ListView list, bool startup) - { - this.InitColumnText(); - - ColumnHeader[]? columns = null; - try - { - if (this.Use2ColumnsMode) - { - columns = new[] - { - new ColumnHeader(), // アイコン - new ColumnHeader(), // 本文 - }; - - columns[0].Text = this.columnText[0]; - columns[1].Text = this.columnText[2]; - - if (startup) - { - var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; - - columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]); - columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]); - columns[0].DisplayIndex = 0; - columns[1].DisplayIndex = 1; - } - else - { - var idx = 0; - foreach (var curListColumn in this.CurrentListView.Columns.Cast()) - { - columns[idx].Width = curListColumn.Width; - columns[idx].DisplayIndex = curListColumn.DisplayIndex; - idx++; - } - } - } - else - { - columns = new[] - { - new ColumnHeader(), // アイコン - new ColumnHeader(), // ニックネーム - new ColumnHeader(), // 本文 - new ColumnHeader(), // 日付 - new ColumnHeader(), // ユーザID - new ColumnHeader(), // 未読 - new ColumnHeader(), // マーク&プロテクト - new ColumnHeader(), // ソース - }; - - foreach (var i in Enumerable.Range(0, columns.Length)) - columns[i].Text = this.columnText[i]; - - if (startup) - { - var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; - - foreach (var (column, index) in columns.WithIndex()) - { - column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]); - column.DisplayIndex = this.settings.Local.ColumnsOrder[index]; - } - } - else - { - var idx = 0; - foreach (var curListColumn in this.CurrentListView.Columns.Cast()) - { - columns[idx].Width = curListColumn.Width; - columns[idx].DisplayIndex = curListColumn.DisplayIndex; - idx++; - } - } - } - - list.Columns.AddRange(columns); - - columns = null; - } - finally - { - if (columns != null) - { - foreach (var column in columns) - column?.Dispose(); - } - } - } - - private void InitColumnText() - { - this.columnText[0] = ""; - this.columnText[1] = Properties.Resources.AddNewTabText2; - this.columnText[2] = Properties.Resources.AddNewTabText3; - this.columnText[3] = Properties.Resources.AddNewTabText4_2; - this.columnText[4] = Properties.Resources.AddNewTabText5; - this.columnText[5] = ""; - this.columnText[6] = ""; - this.columnText[7] = "Source"; - - this.columnOrgText[0] = ""; - this.columnOrgText[1] = Properties.Resources.AddNewTabText2; - this.columnOrgText[2] = Properties.Resources.AddNewTabText3; - this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2; - this.columnOrgText[4] = Properties.Resources.AddNewTabText5; - this.columnOrgText[5] = ""; - this.columnOrgText[6] = ""; - this.columnOrgText[7] = "Source"; - - var c = this.statuses.SortMode switch - { - ComparerMode.Nickname => 1, // ニックネーム - ComparerMode.Data => 2, // 本文 - ComparerMode.Id => 3, // 時刻=発言Id - ComparerMode.Name => 4, // 名前 - ComparerMode.Source => 7, // Source - _ => 0, - }; - - if (this.Use2ColumnsMode) - { - if (this.statuses.SortOrder == SortOrder.Descending) - { - // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE - this.columnText[2] = this.columnOrgText[2] + "▾"; - } - else - { - // U+25B4 BLACK UP-POINTING SMALL TRIANGLE - this.columnText[2] = this.columnOrgText[2] + "▴"; - } - } - else - { - if (this.statuses.SortOrder == SortOrder.Descending) - { - // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE - this.columnText[c] = this.columnOrgText[c] + "▾"; - } - else - { - // U+25B4 BLACK UP-POINTING SMALL TRIANGLE - this.columnText[c] = this.columnOrgText[c] + "▴"; - } - } - } - public TweenMain( SettingManager settingManager, TabInformations tabInfo, @@ -502,7 +308,6 @@ ThumbnailGenerator thumbGenerator this.InitializeShortcuts(); this.ignoreConfigSave = true; - this.Visible = false; this.TraceOutToolStripMenuItem.Checked = MyCommon.TraceFlag; @@ -522,40 +327,13 @@ ThumbnailGenerator thumbGenerator // 現在の DPI と設定保存時の DPI との比を取得する var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); - // 認証関連 - var account = this.settings.Common.SelectedAccount; - if (account != null) - this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId); - else - this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L); - this.initial = true; - this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; - this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; - - // アクセストークンが有効であるか確認する - // ここが Twitter API への最初のアクセスになるようにすること - try - { - this.tw.VerifyCredentials(); - } - catch (WebApiException ex) - { - MessageBox.Show( - this, - string.Format(Properties.Resources.StartupAuthError_Text, ex.Message), - ApplicationSettings.ApplicationName, - MessageBoxButtons.OK, - MessageBoxIcon.Warning); - } - // サムネイル関連の初期化 // プロキシ設定等の通信まわりの初期化が済んでから処理する var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet; imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet; imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM; - imgazyobizinet.AutoUpdate = true; Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection; @@ -745,9 +523,9 @@ ThumbnailGenerator thumbGenerator this.SetMainWindowTitle(); this.SetNotifyIconText(); - if (!this.settings.Common.MinimizeToTray || this.WindowState != FormWindowState.Minimized) + if (this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized) { - this.Visible = true; + this.Visible = false; } // タイマー設定 @@ -776,12 +554,203 @@ ThumbnailGenerator thumbGenerator this.TimerRefreshIcon.Enabled = false; this.ignoreConfigSave = false; - this.TweenMain_Resize(this, EventArgs.Empty); + this.ApplyLayoutFromSettings(); + } - if (this.settings.IsFirstRun) + private void TweenMain_Activated(object sender, EventArgs e) + { + // 画面がアクティブになったら、発言欄の背景色戻す + if (this.StatusText.Focused) { - // 初回起動時だけ右下のメニューを目立たせる - this.HashStripSplitButton.ShowDropDown(); + this.StatusText_Enter(this.StatusText, System.EventArgs.Empty); + } + } + + private bool disposed = false; + + /// + /// 使用中のリソースをすべてクリーンアップします。 + /// + /// マネージ リソースが破棄される場合 true、破棄されない場合は false です。 + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (this.disposed) + return; + + if (disposing) + { + this.components?.Dispose(); + + // 後始末 + this.SearchDialog.Dispose(); + this.urlDialog.Dispose(); + this.themeManager.Dispose(); + this.sfTab.Dispose(); + + this.timelineScheduler.Dispose(); + this.workerCts.Cancel(); + this.thumbnailTokenSource?.Dispose(); + + this.hookGlobalHotkey.Dispose(); + } + + // 終了時にRemoveHandlerしておかないとメモリリークする + // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx + Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged; + Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged; + MyCommon.TwitterApiInfo.AccessLimitUpdated -= this.TwitterApiStatus_AccessLimitUpdated; + + this.disposed = true; + } + + private void InitColumns(ListView list, bool startup) + { + this.InitColumnText(); + + ColumnHeader[]? columns = null; + try + { + if (this.Use2ColumnsMode) + { + columns = new[] + { + new ColumnHeader(), // アイコン + new ColumnHeader(), // 本文 + }; + + columns[0].Text = this.columnText[0]; + columns[1].Text = this.columnText[2]; + + if (startup) + { + var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; + + columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]); + columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]); + columns[0].DisplayIndex = 0; + columns[1].DisplayIndex = 1; + } + else + { + var idx = 0; + foreach (var curListColumn in this.CurrentListView.Columns.Cast()) + { + columns[idx].Width = curListColumn.Width; + columns[idx].DisplayIndex = curListColumn.DisplayIndex; + idx++; + } + } + } + else + { + columns = new[] + { + new ColumnHeader(), // アイコン + new ColumnHeader(), // ニックネーム + new ColumnHeader(), // 本文 + new ColumnHeader(), // 日付 + new ColumnHeader(), // ユーザID + new ColumnHeader(), // 未読 + new ColumnHeader(), // マーク&プロテクト + new ColumnHeader(), // ソース + }; + + foreach (var i in Enumerable.Range(0, columns.Length)) + columns[i].Text = this.columnText[i]; + + if (startup) + { + var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width; + + foreach (var (column, index) in columns.WithIndex()) + { + column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]); + column.DisplayIndex = this.settings.Local.ColumnsOrder[index]; + } + } + else + { + var idx = 0; + foreach (var curListColumn in this.CurrentListView.Columns.Cast()) + { + columns[idx].Width = curListColumn.Width; + columns[idx].DisplayIndex = curListColumn.DisplayIndex; + idx++; + } + } + } + + list.Columns.AddRange(columns); + + columns = null; + } + finally + { + if (columns != null) + { + foreach (var column in columns) + column?.Dispose(); + } + } + } + + private void InitColumnText() + { + this.columnText[0] = ""; + this.columnText[1] = Properties.Resources.AddNewTabText2; + this.columnText[2] = Properties.Resources.AddNewTabText3; + this.columnText[3] = Properties.Resources.AddNewTabText4_2; + this.columnText[4] = Properties.Resources.AddNewTabText5; + this.columnText[5] = ""; + this.columnText[6] = ""; + this.columnText[7] = "Source"; + + this.columnOrgText[0] = ""; + this.columnOrgText[1] = Properties.Resources.AddNewTabText2; + this.columnOrgText[2] = Properties.Resources.AddNewTabText3; + this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2; + this.columnOrgText[4] = Properties.Resources.AddNewTabText5; + this.columnOrgText[5] = ""; + this.columnOrgText[6] = ""; + this.columnOrgText[7] = "Source"; + + var c = this.statuses.SortMode switch + { + ComparerMode.Nickname => 1, // ニックネーム + ComparerMode.Data => 2, // 本文 + ComparerMode.Id => 3, // 時刻=発言Id + ComparerMode.Name => 4, // 名前 + ComparerMode.Source => 7, // Source + _ => 0, + }; + + if (this.Use2ColumnsMode) + { + if (this.statuses.SortOrder == SortOrder.Descending) + { + // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE + this.columnText[2] = this.columnOrgText[2] + "▾"; + } + else + { + // U+25B4 BLACK UP-POINTING SMALL TRIANGLE + this.columnText[2] = this.columnOrgText[2] + "▴"; + } + } + else + { + if (this.statuses.SortOrder == SortOrder.Descending) + { + // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE + this.columnText[c] = this.columnOrgText[c] + "▾"; + } + else + { + // U+25B4 BLACK UP-POINTING SMALL TRIANGLE + this.columnText[c] = this.columnOrgText[c] + "▴"; + } } } @@ -1245,7 +1214,7 @@ private async void PostButton_Click(object sender, EventArgs e) var status = new PostStatusParams(); var statusTextCompat = this.FormatStatusText(this.StatusText.Text); - if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AppToken.AuthType == APIAuthType.OAuth1) + if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AuthType == APIAuthType.OAuth1) { // auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に // 収まる場合はこれらのオプションを使用せずに投稿する @@ -2588,9 +2557,9 @@ private async void SettingStripMenuItem_Click(object sender, EventArgs e) var account = this.settings.Common.SelectedAccount; if (account != null) - this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId); + this.tw.Initialize(account.GetTwitterCredential(), account.Username, account.UserId); else - this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L); + this.tw.Initialize(new TwitterCredentialNone(), "", 0L); this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; @@ -3561,14 +3530,17 @@ private string FormatStatusTextExtended(string statusText, out long[] autoPopula return this.FormatStatusText(statusText); } + internal string FormatStatusText(string statusText) + => this.FormatStatusText(statusText, Control.ModifierKeys); + /// /// ツイート投稿前のフッター付与などの前処理を行います /// - private string FormatStatusText(string statusText) + internal string FormatStatusText(string statusText, Keys modifierKeys) { statusText = statusText.Replace("\r\n", "\n"); - if (this.urlMultibyteSplit) + if (this.SeparateUrlAndFullwidthCharacter) { // URLと全角文字の切り離し statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& "); @@ -3587,14 +3559,14 @@ private string FormatStatusText(string statusText) bool disableFooter; if (this.settings.Common.PostShiftEnter) { - disableFooter = MyCommon.IsKeyDown(Keys.Control); + disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control); } else { - if (this.StatusText.Multiline && !this.settings.Common.PostCtrlEnter) - disableFooter = MyCommon.IsKeyDown(Keys.Control); + if (this.settings.Local.StatusMultiline && !this.settings.Common.PostCtrlEnter) + disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control); else - disableFooter = MyCommon.IsKeyDown(Keys.Shift); + disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Shift); } if (statusText.Contains("RT @")) @@ -5728,10 +5700,6 @@ private void SaveConfigsCommon() this.ModifySettingCommon = false; lock (this.syncObject) { - this.settings.Common.UserName = this.tw.Username; - this.settings.Common.UserId = this.tw.UserId; - this.settings.Common.Token = this.tw.AccessToken; - this.settings.Common.TokenSecret = this.tw.AccessTokenSecret; this.settings.Common.SortOrder = (int)this.statuses.SortOrder; this.settings.Common.SortColumn = this.statuses.SortMode switch { @@ -7084,7 +7052,7 @@ private void SetApiStatusLabel(string? endpointName = null) if (endpointName == null) { - var authByCookie = this.tw.Api.AppToken.AuthType == APIAuthType.TwitterComCookie; + var authByCookie = this.tw.Api.AuthType == APIAuthType.TwitterComCookie; // 表示中のタブに応じて更新 endpointName = tabType switch @@ -7214,56 +7182,56 @@ private void TweenMain_Resize(object sender, EventArgs e) { this.Visible = false; } - if (this.initialLayout && this.settings.Local != null && this.WindowState == FormWindowState.Normal && this.Visible) + if (this.WindowState != FormWindowState.Minimized) { - // 現在の DPI と設定保存時の DPI との比を取得する - var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); + this.formWindowState = this.WindowState; + } + } - this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize); + private void ApplyLayoutFromSettings() + { + // 現在の DPI と設定保存時の DPI との比を取得する + var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions); - // Splitterの位置設定 - var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance); - if (splitterDistance > this.SplitContainer1.Panel1MinSize && - splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth) - { - this.SplitContainer1.SplitterDistance = splitterDistance; - } + this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize); - // 発言欄複数行 - this.StatusText.Multiline = this.settings.Local.StatusMultiline; - if (this.StatusText.Multiline) - { - var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight); - var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; - if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth) - { - this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; - } - this.StatusText.Height = statusTextHeight; - } - else + // Splitterの位置設定 + var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance); + if (splitterDistance > this.SplitContainer1.Panel1MinSize && + splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth) + { + this.SplitContainer1.SplitterDistance = splitterDistance; + } + + // 発言欄複数行 + this.StatusText.Multiline = this.settings.Local.StatusMultiline; + if (this.StatusText.Multiline) + { + var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight); + var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; + if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth) { - if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0) - { - this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth; - } + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth; } - - var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance); - if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth) + this.StatusText.Height = statusTextHeight; + } + else + { + if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0) { - this.SplitContainer3.SplitterDistance = previewDistance; + this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth; } - - // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない - this.SplitContainer3.Panel2Collapsed = true; - - this.initialLayout = false; } - if (this.WindowState != FormWindowState.Minimized) + + var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance); + if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth) { - this.formWindowState = this.WindowState; + this.SplitContainer3.SplitterDistance = previewDistance; } + + // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない + this.SplitContainer3.Panel2Collapsed = true; + this.initialLayout = false; } private void PlaySoundMenuItem_CheckedChanged(object sender, EventArgs e) @@ -7979,6 +7947,13 @@ private void SelectListItem(DetailsListView lView, int index) private async void TweenMain_Shown(object sender, EventArgs e) { this.NotifyIcon1.Visible = true; + this.StartTimers(); + + if (this.settings.IsFirstRun) + { + // 初回起動時だけ右下のメニューを目立たせる + this.HashStripSplitButton.ShowDropDown(); + } if (this.IsNetworkAvailable()) { @@ -8059,8 +8034,16 @@ private async void TweenMain_Shown(object sender, EventArgs e) } this.initial = false; + } + + private void StartTimers() + { + if (!this.StopRefreshAllMenuItem.Checked) + this.timelineScheduler.Enabled = true; - this.timelineScheduler.Enabled = true; + this.selectionDebouncer.Enabled = true; + this.saveConfigDebouncer.Enabled = true; + this.thumbGenerator.ImgAzyobuziNet.AutoUpdate = true; } private async Task DoGetFollowersMenu() @@ -8208,14 +8191,14 @@ private void DumpPostClassToolStripMenuItem_Click(object sender, EventArgs e) private void MenuItemHelp_DropDownOpening(object sender, EventArgs e) { - if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock, Keys.Control, Keys.Shift)) + if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock | Keys.Control | Keys.Shift)) this.DebugModeToolStripMenuItem.Visible = true; else this.DebugModeToolStripMenuItem.Visible = false; } private void UrlMultibyteSplitMenuItem_CheckedChanged(object sender, EventArgs e) - => this.urlMultibyteSplit = ((ToolStripMenuItem)sender).Checked; + => this.SeparateUrlAndFullwidthCharacter = ((ToolStripMenuItem)sender).Checked; private void PreventSmsCommandMenuItem_CheckedChanged(object sender, EventArgs e) => this.preventSmsCommand = ((ToolStripMenuItem)sender).Checked; @@ -8237,7 +8220,7 @@ private void FocusLockMenuItem_CheckedChanged(object sender, EventArgs e) private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e) { - this.UrlMultibyteSplitMenuItem.Checked = this.urlMultibyteSplit; + this.UrlMultibyteSplitMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter; this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand; this.UrlAutoShortenMenuItem.Checked = this.settings.Common.UrlConvertAuto; this.IdeographicSpaceToSpaceMenuItem.Checked = this.settings.Common.WideSpaceConvert; @@ -8247,7 +8230,7 @@ private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e) private void ContextMenuPostMode_Opening(object sender, CancelEventArgs e) { - this.UrlMultibyteSplitPullDownMenuItem.Checked = this.urlMultibyteSplit; + this.UrlMultibyteSplitPullDownMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter; this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand; this.UrlAutoShortenPullDownMenuItem.Checked = this.settings.Common.UrlConvertAuto; this.IdeographicSpaceToSpacePullDownMenuItem.Checked = this.settings.Common.WideSpaceConvert; diff --git a/OpenTween/Twitter.cs b/OpenTween/Twitter.cs index 4e34545ba..ba09b07b8 100644 --- a/OpenTween/Twitter.cs +++ b/OpenTween/Twitter.cs @@ -173,12 +173,14 @@ public class Twitter : IDisposable private long[] noRTId = Array.Empty(); private readonly TwitterPostFactory postFactory; + private readonly PostUrlExpander urlExpander; private string? previousStatusId = null; public Twitter(TwitterApi api) { this.postFactory = new(TabInformations.GetInstance()); + this.urlExpander = new(ShortUrl.Instance); this.Api = api; this.Configuration = TwitterConfiguration.DefaultConfiguration(); @@ -217,26 +219,14 @@ public async Task VerifyCredentialsAsync() this.UpdateUserStats(user); } - public void Initialize(string token, string tokenSecret, string username, long userId) + public void Initialize(ITwitterCredential credential, string username, long userId) { // OAuth認証 - if (MyCommon.IsNullOrEmpty(token) || MyCommon.IsNullOrEmpty(tokenSecret) || MyCommon.IsNullOrEmpty(username)) - { + if (credential is TwitterCredentialNone) Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid; - } - this.ResetApiStatus(); - this.Api.Initialize(token, tokenSecret, userId, username); - } - public void Initialize(TwitterAppToken appToken, string token, string tokenSecret, string username, long userId) - { - // OAuth認証 - if (MyCommon.IsNullOrEmpty(token) || MyCommon.IsNullOrEmpty(tokenSecret) || MyCommon.IsNullOrEmpty(username)) - { - Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid; - } this.ResetApiStatus(); - this.Api.Initialize(appToken, token, tokenSecret, userId, username); + this.Api.Initialize(credential, userId, username); } public async Task PostStatus(PostStatusParams param) @@ -254,7 +244,7 @@ await this.SendDirectMessage(param.Text, mediaId) TwitterStatus status; - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new CreateTweetRequest { @@ -299,7 +289,7 @@ await this.SendDirectMessage(param.Text, mediaId) public async Task DeleteTweet(TwitterStatusId tweetId) { - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new DeleteTweetRequest { @@ -405,7 +395,7 @@ await this.CreateDirectMessagesEventFromJson(messageEventSingle, read: true) var target = post.RetweetedId ?? id; // 再RTの場合は元発言をRT - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new CreateRetweetRequest { @@ -447,7 +437,7 @@ public async Task DeleteRetweet(PostClass post) if (post.RetweetedId == null) throw new ArgumentException("post is not retweeted status", nameof(post)); - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new DeleteRetweetRequest { @@ -464,7 +454,7 @@ await this.Api.StatusesDestroy(post.StatusId.ToTwitterStatusId()) public async Task GetUserInfo(string screenName) { - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new UserByScreenNameRequest { @@ -667,7 +657,7 @@ public async Task GetUserTimelineApi(bool read, UserTimelineTabModel tab, bool m var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false); TwitterStatus[] statuses; - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var userId = tab.UserId; if (MyCommon.IsNullOrEmpty(userId)) @@ -723,7 +713,7 @@ public async Task GetStatusApi(bool read, TwitterStatusId id) this.CheckAccountState(); TwitterStatus status; - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new TweetDetailRequest { @@ -764,7 +754,12 @@ private PostClass CreatePostsFromStatusData(TwitterStatus status) => this.CreatePostsFromStatusData(status, favTweet: false); private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet) - => this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet); + { + var post = this.postFactory.CreateFromStatus(status, this.UserId, this.followerId, favTweet); + _ = this.urlExpander.Expand(post); + + return post; + } private PostId? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel? tab, bool read) { @@ -879,7 +874,7 @@ public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup); TwitterStatus[] statuses; - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new ListLatestTweetsTimelineRequest(tab.ListInfo.Id.ToString()) { @@ -1089,7 +1084,7 @@ public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more) var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false); TwitterStatus[] statuses; - if (this.Api.AppToken.AuthType == APIAuthType.TwitterComCookie) + if (this.Api.AuthType == APIAuthType.TwitterComCookie) { var request = new SearchTimelineRequest(tab.SearchWords) { @@ -1207,6 +1202,7 @@ private void CreateDirectMessagesEventFromJson( foreach (var eventItem in events) { var post = this.postFactory.CreateFromDirectMessageEvent(eventItem, users, apps, this.UserId); + _ = this.urlExpander.Expand(post); post.IsRead = read; if (post.IsMe && !read && this.ReadOwnPost) @@ -1437,12 +1433,6 @@ public async Task RefreshMuteUserIdsAsync() public string[] GetHashList() => this.postFactory.GetReceivedHashtags(); - public string AccessToken - => ((TwitterApiConnection)this.Api.Connection).AccessToken; - - public string AccessTokenSecret - => ((TwitterApiConnection)this.Api.Connection).AccessSecret; - private void CheckAccountState() { if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) diff --git a/appveyor.yml b/appveyor.yml index 6d75fa444..1893d8839 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.8.0.{build} +version: 3.9.0.{build} os: Visual Studio 2022 @@ -57,22 +57,14 @@ init: before_build: - nuget restore + - ps: Set-Content .\msbuild.rsp "/warnaserror /p:DebugType=None" -test_script: - - cmd: | - set altCoverVersion=8.6.61 - set xunitVersion=2.4.2 - set targetFramework=net48 - set nugetPackages=%UserProfile%\.nuget\packages - - %nugetPackages%\altcover\%altCoverVersion%\tools\net472\AltCover.exe --inputDirectory .\OpenTween.Tests\bin\%CONFIGURATION%\%targetFramework%\ --outputDirectory .\__Instrumented\ --assemblyFilter "?^OpenTween(?!\.Tests)" --typeFilter "?^OpenTween\." --fileFilter "\.Designer\.cs" --visibleBranches - - %nugetPackages%\altcover\%altCoverVersion%\tools\net472\AltCover.exe runner --recorderDirectory .\__Instrumented\ --executable %nugetPackages%\xunit.runner.console\%xunitVersion%\tools\net472\xunit.console.exe -- .\__Instrumented\OpenTween.Tests.dll +test: + assemblies: + only: + - OpenTween.Tests.dll after_test: - - ps: | - Invoke-WebRequest -Uri https://uploader.codecov.io/latest/windows/codecov.exe -Outfile codecov.exe - .\codecov.exe -f coverage.xml - ps: | $env:PATH = $env:PATH + ';C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\Roslyn\;C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\' $binDir = '.\OpenTween\bin\' + $env:CONFIGURATION + '\net48\' diff --git a/msbuild.rsp b/msbuild.rsp deleted file mode 100644 index 7f712b3d1..000000000 --- a/msbuild.rsp +++ /dev/null @@ -1,3 +0,0 @@ -# MSBuild response file for AppVeyor build - -/warnaserror /p:DebugType=None diff --git a/tools/attach-svn-logs.sh b/tools/attach-svn-logs.sh new file mode 100755 index 000000000..48bee95f3 --- /dev/null +++ b/tools/attach-svn-logs.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Subversion で管理されていた頃の Tween のコミットログ (SourceForge.JP, CodeRepos) を接続するスクリプト +# +# 実行後は `git blame Tween_svn-last -- Tween/Tween.vb` のように変更履歴を追うことができます + +set -eu + +git remote add tween_coderepos https://github.com/opentween/Tween_CodeRepos.git + +# Tween_CodeRepos を取得 + Tween_v1.1.0.0 などのタグを強制的に上書き +git fetch --force --tags tween_coderepos + +# OpenTween のコミットログのうち以下の 2 つを git replace で置換する + +# r1643: 3ポスト以上の通知はまとめる +git replace 6a654b6edaa338fc890494c9fa6a19594277b6b2 $(git rev-parse Tween_sourceforge-last^0) + +# r1521: 1010リリース +git replace ddbe79b3cfb2baa4e4799a00a2004ba10546aef1 $(git rev-parse Tween_v1.0.1.0^0)