From d758e4005bc9a965a91cab3a77c1609276305a1c Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Fri, 5 Apr 2024 13:10:40 +0200 Subject: [PATCH] Wrapping up interceptors, fixing typos, removing Moq (#2141) * Wrapping up interceptors, fixing typos, removing Moq * Fix the readme issue * Added compatibility interceptor * Added docs * Updated packages * Refactoring tests to WireMock * Added matrix --- .github/ISSUE_TEMPLATE/bug_report.md | 12 +- .github/workflows/pull-request.yml | 68 ++++- .github/workflows/test-results.yml | 37 +++ Directory.Packages.props | 49 ++++ RestSharp.sln.DotSettings | 4 + .../RestSharp.Benchmarks.csproj | 21 +- docs/.vuepress/config.js | 1 + docs/interceptors.md | 84 ++++++ docs/serialization.md | 28 +- gen/SourceGenerator/ImmutableGenerator.cs | 16 +- gen/SourceGenerator/SourceGenerator.csproj | 9 +- global.json | 7 - props/Common.props | 2 +- src/Directory.Build.props | 13 +- .../CsvHelperSerializer.cs | 18 +- src/RestSharp.Serializers.CsvHelper/README.md | 24 ++ .../RestSharp.Serializers.CsvHelper.csproj | 5 +- .../README.md | 19 ++ ...estSharp.Serializers.NewtonsoftJson.csproj | 5 +- src/RestSharp.Serializers.Xml/README.md | 22 ++ .../RestSharp.Serializers.Xml.csproj | 3 + .../Authenticators/AuthenticatorBase.cs | 6 +- .../Authenticators/HttpBasicAuthenticator.cs | 6 +- .../Authenticators/JwtAuthenticator.cs | 4 +- src/RestSharp/Authenticators/OAuth/WebPair.cs | 14 +- src/RestSharp/ContentType.cs | 3 +- .../Extensions/GenerateImmutableAttribute.cs | 5 +- .../Interceptors/CompatibilityInterceptor.cs | 40 +++ src/RestSharp/Interceptors/Interceptor.cs | 53 ++-- .../Options/ReadOnlyRestClientOptions.cs | 32 +++ src/RestSharp/Options/RestClientOptions.cs | 5 + src/RestSharp/Parameters/DefaultParameters.cs | 38 ++- src/RestSharp/Parameters/FileParameter.cs | 5 - src/RestSharp/Parameters/ObjectParser.cs | 29 +- .../Parameters/UrlSegmentParameter.cs | 11 +- .../Request/HttpRequestMessageExtensions.cs | 3 +- src/RestSharp/Request/RequestContent.cs | 63 ++--- src/RestSharp/Request/RestRequest.cs | 23 +- src/RestSharp/Response/RestResponse.cs | 10 +- src/RestSharp/Response/RestResponseBase.cs | 2 +- src/RestSharp/RestClient.Async.cs | 87 +++--- src/RestSharp/RestClient.Extensions.cs | 20 +- src/RestSharp/RestClient.cs | 18 +- src/RestSharp/RestSharp.csproj | 37 ++- .../Serializers/DeseralizationException.cs | 11 +- src/RestSharp/Serializers/RestSerializers.cs | 31 ++- test/Directory.Build.props | 20 +- test/RestSharp.InteractiveTests/Program.cs | 2 + .../RestSharp.InteractiveTests.csproj | 2 +- .../Authentication/AuthenticationTests.cs | 17 +- .../Authentication/OAuth2Tests.cs | 13 +- .../CompressionTests.cs | 66 ++--- .../RestSharp.Tests.Integrated/CookieTests.cs | 91 ++++++- .../DefaultParameterTests.cs | 37 +-- .../DownloadFileTests.cs | 81 +++--- .../Fixtures/CookieExtensions.cs | 15 ++ .../Fixtures/RequestBodyFixture.cs | 11 - .../HttpClientTests.cs | 22 +- .../HttpHeadersTests.cs | 48 +++- .../Interceptor/InterceptorTests.cs | 249 +++++++++++------- .../Interceptor/TestInterceptor.cs | 45 ++++ .../JsonBodyTests.cs | 44 ++-- .../MultipartFormDataTests.cs | 96 ++++--- .../NonProtocolExceptionHandlingTests.cs | 35 ++- .../{RequestHeadTests.cs => NtlmTests.cs} | 19 +- test/RestSharp.Tests.Integrated/PostTests.cs | 10 +- test/RestSharp.Tests.Integrated/ProxyTests.cs | 14 +- test/RestSharp.Tests.Integrated/PutTests.cs | 35 +-- .../RedirectTests.cs | 27 +- .../RequestBodyTests.cs | 83 ++---- .../RequestFailureTests.cs | 32 +-- .../RequestTests.cs | 50 ++-- .../ResourceStringParametersTests.cs | 30 +-- .../RestSharp.Tests.Integrated.csproj | 37 ++- .../RootElementTests.cs | 39 ++- .../Server/Handlers/CookieHandlers.cs | 22 +- .../Server/Handlers/FileHandlers.cs | 78 +++--- .../Server/Handlers/FormRequest.cs | 24 +- .../Server/Handlers/HeaderHandlers.cs | 12 +- .../Server/Models.cs | 8 +- .../Server/TestServer.cs | 154 +++++------ .../Server/TestServerFixture.cs | 14 +- .../Server/WireMockTestServer.cs | 136 ++++++++++ .../StatusCodeTests.cs | 134 ++-------- .../StructuredSyntaxSuffixTests.cs | 45 ++-- .../TestResponse.cs | 1 + .../UploadFileTests.cs | 73 ++++- .../XmlResponseTests.cs | 127 +++++++++ .../CsvHelperTests.cs | 92 +++---- .../RestSharp.Tests.Serializers.Csv.csproj | 9 + .../NewtonsoftJson/IntegratedSimpleTests.cs | 73 ++--- .../NewtonsoftJson/IntegratedTests.cs | 23 +- .../RestSharp.Tests.Serializers.Json.csproj | 20 +- .../SystemTextJson/SystemTextJsonTests.cs | 65 ++--- .../NamespacedXmlTests.cs | 2 +- .../SampleClasses/twitter.cs | 2 + .../XmlAttributeDeserializerTests.cs | 6 +- .../XmlDeserializerTests.cs | 2 +- .../Fixtures/Handlers.cs | 11 - .../Fixtures/HttpServerFixture.cs | 23 -- .../Fixtures/RequestBodyCapturer.cs | 36 +-- .../Fixtures/SimpleServer.cs | 8 +- .../Fixtures/TestHttpServer.cs | 112 -------- .../Fixtures/TestHttpServerExtensions.cs | 19 -- .../Fixtures/TestRequestHandler.cs | 64 ----- .../Fixtures/WireMockExtensions.cs | 21 ++ .../RestSharp.Tests.Shared.csproj | 6 + .../OAuth1Tests.cs | 3 +- test/RestSharp.Tests/RestSharp.Tests.csproj | 57 ++-- 109 files changed, 2154 insertions(+), 1601 deletions(-) create mode 100644 .github/workflows/test-results.yml create mode 100644 Directory.Packages.props create mode 100644 docs/interceptors.md delete mode 100644 global.json create mode 100644 src/RestSharp.Serializers.CsvHelper/README.md create mode 100644 src/RestSharp.Serializers.NewtonsoftJson/README.md create mode 100644 src/RestSharp.Serializers.Xml/README.md create mode 100644 src/RestSharp/Interceptors/CompatibilityInterceptor.cs create mode 100644 src/RestSharp/Options/ReadOnlyRestClientOptions.cs create mode 100644 test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs delete mode 100644 test/RestSharp.Tests.Integrated/Fixtures/RequestBodyFixture.cs create mode 100644 test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs rename test/RestSharp.Tests.Integrated/{RequestHeadTests.cs => NtlmTests.cs} (82%) create mode 100644 test/RestSharp.Tests.Integrated/Server/WireMockTestServer.cs create mode 100644 test/RestSharp.Tests.Integrated/TestResponse.cs create mode 100644 test/RestSharp.Tests.Integrated/XmlResponseTests.cs delete mode 100644 test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs delete mode 100644 test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs delete mode 100644 test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs delete mode 100644 test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs create mode 100644 test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs rename test/{RestSharp.Tests.Integrated => RestSharp.Tests}/OAuth1Tests.cs (98%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c4e29888f..78f38d41b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,13 @@ assignees: '' **DO NOT USE ISSUES FOR QUESTIONS** +Note: New issues raised, where it is clear the submitter has not read the issue template, +are likely to be closed with `invalid` label. Please understand that this is not meant to be rude, +but to keep the issue list clean and useful for everyone. If one opening the issue decides to ignore the issue template, +the maintainers might also decide to ignore the issue. + +**Remove** all the text above the next line when submitting your issue. + **Describe the bug** A clear and concise description of what the bug is. Hint: use a tool like https://requestbin.com to compare working and non-working requests. @@ -22,14 +29,13 @@ A clear and concise description of what you expected to happen. Post the working request here as well if you made it work using Postman, Swagger, or any other client. You can use https://requestbin.com/r to create a public request bin and share the link in the issue. - **Stack trace** Copy the full stack trace here if you get an exception. **Desktop (please complete the following information):** - OS: [e.g. macOS] - - .NET version [e.g. .NET 5] - - Version [e.g. 107.0.4] + - .NET version [e.g. .NET 6] + - Version [e.g. 110.2.0] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index c566a4cc8..1daf73fbb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -4,24 +4,76 @@ on: [pull_request] permissions: contents: read + checks: write jobs: - test: + event_file: + name: "Event File" + runs-on: ubuntu-latest + steps: + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: Event File + path: ${{ github.event_path }} + test-windows: runs-on: windows-latest + strategy: + matrix: + dotnet: ['net48', 'net6.0', 'net7.0', 'net8.0'] + + steps: + - + name: Checkout + uses: actions/checkout@v4 +# - +# name: Setup .NET +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: '8.0' + - + name: Run tests + run: dotnet test -f ${{ matrix.dotnet }} + - + name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Test Results Windows ${{ matrix.dotnet }} + path: | + test-results/**/*.xml + test-results/**/*.trx + test-results/**/*.json + + test-linux: + runs-on: ubuntu-latest + strategy: + matrix: + dotnet: ['net6.0', 'net7.0', 'net8.0'] steps: - name: Checkout uses: actions/checkout@v3 - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '7.0' +# - +# name: Setup .NET +# uses: actions/setup-dotnet@v3 +# with: +# dotnet-version: '8.0' - name: Run tests - run: dotnet test -c Release - + run: dotnet test -f ${{ matrix.dotnet }} + - + name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Test Results Ubuntu ${{ matrix.dotnet }} + path: | + test-results/**/*.xml + test-results/**/*.trx + test-results/**/*.json + docs: runs-on: ubuntu-latest diff --git a/.github/workflows/test-results.yml b/.github/workflows/test-results.yml new file mode 100644 index 000000000..394bf9dc7 --- /dev/null +++ b/.github/workflows/test-results.yml @@ -0,0 +1,37 @@ +name: Test Results + +on: + workflow_run: + workflows: ["Build and test PRs"] + types: + - completed +permissions: {} + +jobs: + test-results: + name: Test Results + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion != 'skipped' + + permissions: + checks: write + pull-requests: write + actions: read + + steps: + - + name: Download and Extract Artifacts + uses: dawidd6/action-download-artifact@e7466d1a7587ed14867642c2ca74b5bcc1e19a2d + with: + run_id: ${{ github.event.workflow_run.id }} + path: artifacts + - + name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + commit: ${{ github.event.workflow_run.head_sha }} + event_file: artifacts/Event File/event.json + event_name: ${{ github.event.workflow_run.event }} + files: | + artifacts/**/*.xml + artifacts/**/*.trx \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 000000000..df91988b6 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,49 @@ + + + true + + + [6.0.28,7) + + + 7.0.17 + + + 8.0.3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/RestSharp.sln.DotSettings b/RestSharp.sln.DotSettings index f22b9a27f..d0bb9d525 100644 --- a/RestSharp.sln.DotSettings +++ b/RestSharp.sln.DotSettings @@ -67,6 +67,9 @@ False FDIC <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + True True True True @@ -77,6 +80,7 @@ True True True + True True True True diff --git a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj index e1da87b9a..f60295a8f 100644 --- a/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj +++ b/benchmarks/RestSharp.Benchmarks/RestSharp.Benchmarks.csproj @@ -1,24 +1,25 @@ Exe - net6.0 + net7.0 false - 10 + preview enable + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'RestSharp.sln')) - - - + + + - - + + - - - + + + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 52aa3bf19..31d27d4b4 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -27,6 +27,7 @@ module.exports = { "usage.md", "serialization.md", "authenticators.md", + "interceptors.md", "error-handling.md" ] } diff --git a/docs/interceptors.md b/docs/interceptors.md new file mode 100644 index 000000000..86387fb3d --- /dev/null +++ b/docs/interceptors.md @@ -0,0 +1,84 @@ +--- +title: Interceptors +--- + +## Intercepting requests and responses + +Interceptors are a powerful feature of RestSharp that allows you to modify requests and responses before they are sent or received. You can use interceptors to add headers, modify the request body, or even cancel the request. You can also use interceptors to modify the response before it is returned to the caller. + +### Implementing an interceptor + +To implement an interceptor, you need to create a class that inherits the `Interceptor` base class. The base class implements all interceptor methods as virtual, so you can override them in your derived class. + +Methods that you can override are: +- `BeforeRequest(RestRequest request, CancellationToken cancellationToken)` +- `AfterRequest(RestResponse response, CancellationToken cancellationToken)` +- `BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken)` +- `AfterHttpResponse(HttpResponseMessage responseMessage, CancellationToken cancellationToken)` +- `BeforeDeserialization(RestResponse response, CancellationToken cancellationToken)` + +All those functions must return a `ValueTask` instance. + +Here's an example of an interceptor that adds a header to a request: + +```csharp +// This interceptor adds a header to the request +// You'd not normally use this interceptor, as RestSharp already has a method +// to add headers to the request +class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return ValueTask.CompletedTask; + } +} +``` + +Because interceptor functions return `ValueTask`, you can use `async` and `await` inside them. + +### Using an interceptor + +It's possible to add as many interceptors as you want, both to the client and to the request. The interceptors are executed in the order they were added. + +Adding interceptors to the client is done via the client options: + +```csharp +var options = new RestClientOptions("https://api.example.com") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +var client = new RestClient(options); +``` + +When you add an interceptor to the client, it will be executed for every request made by that client. + +You can also add an interceptor to a specific request: + +```csharp +var request = new RestRequest("resource") { + Interceptors = [new HeaderInterceptor("Authorization", token)] +}; +``` + +In this case, the interceptor will only be executed for that specific request. + +### Deprecation notice + +Interceptors aim to replace the existing request hooks available in RestSharp prior to version 111.0. Those hooks are marked with `Obsolete` attribute and will be removed in the future. If you are using those hooks, we recommend migrating to interceptors as soon as possible. + +To make the migration easier, RestSharp provides a class called `CompatibilityInterceptor`. It has properties for the hooks available in RestSharp 110.0 and earlier. You can use it to migrate your code to interceptors without changing the existing logic. + +For example, a code that uses `OnBeforeRequest` hook: + +```csharp +var request = new RestRequest("success"); +request.OnBeforeDeserialization += _ => throw new Exception(exceptionMessage); +``` + +Can be migrated to interceptors like this: + +```csharp +var request = new RestRequest("success") { + Interceptors = [new CompatibilityInterceptor { + OnBeforeDeserialization = _ => throw new Exception(exceptionMessage) + }] +}; +``` \ No newline at end of file diff --git a/docs/serialization.md b/docs/serialization.md index a9458523d..cf8d8387b 100644 --- a/docs/serialization.md +++ b/docs/serialization.md @@ -37,7 +37,10 @@ In previous versions of RestSharp, the default XML serializer was a custom RestS You can add it back if necessary by installing the package and adding it to the client: ```csharp -client.UseXmlSerializer(); +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); ``` As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. @@ -77,6 +80,29 @@ JsonSerializerSettings DefaultSettings = new JsonSerializerSettings { If you need to use different settings, you can supply your instance of `JsonSerializerSettings` as a parameter for the extension method. +## CSV + +A separate package `RestSharp.Serializers.CsvHelper` provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper(new CsvConfiguration(CultureInfo.InvariantCulture) {...}) +); +``` + ## Custom You can also implement your custom serializer. To support both serialization and diff --git a/gen/SourceGenerator/ImmutableGenerator.cs b/gen/SourceGenerator/ImmutableGenerator.cs index 367560d68..24adff24d 100644 --- a/gen/SourceGenerator/ImmutableGenerator.cs +++ b/gen/SourceGenerator/ImmutableGenerator.cs @@ -58,17 +58,22 @@ public class ImmutableGenerator : ISourceGenerator { var mutableProperties = props .Select(prop => $" {prop.Identifier.Text} = {argName}.{prop.Identifier.Text};"); - var constructor = $@" public ReadOnly{className}({className} {argName}) {{ -{string.Join("\n", mutableProperties)} - }}"; + var constructor = $$""" + public ReadOnly{{className}}({{className}} {{argName}}) { + {{string.Join("\n", mutableProperties)}} + CopyAdditionalProperties({{argName}}); + } + """; const string template = @"{Usings} namespace {Namespace}; -public class ReadOnly{ClassName} { +public partial class ReadOnly{ClassName} { {Constructor} + partial void CopyAdditionalProperties({ClassName} {ArgName}); + {Properties} }"; @@ -77,6 +82,7 @@ public class ReadOnly{ClassName} { .Replace("{Namespace}", namespaceName) .Replace("{ClassName}", className) .Replace("{Constructor}", constructor) + .Replace("{ArgName}", argName) .Replace("{Properties}", string.Join("\n", properties)); return code; @@ -86,7 +92,7 @@ IEnumerable GetDefinitions(SyntaxKind kind) .OfType() .Where( prop => - prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) + prop.AccessorList!.Accessors.Any(accessor => accessor.Keyword.IsKind(kind)) && prop.AttributeLists.All(list => list.Attributes.All(attr => attr.Name.ToString() != "Exclude")) ); } } diff --git a/gen/SourceGenerator/SourceGenerator.csproj b/gen/SourceGenerator/SourceGenerator.csproj index 4ff3c5025..b4aa74be5 100644 --- a/gen/SourceGenerator/SourceGenerator.csproj +++ b/gen/SourceGenerator/SourceGenerator.csproj @@ -1,20 +1,17 @@  - netstandard2.0 - 11 + preview enable disable false true false - - - + + - diff --git a/global.json b/global.json deleted file mode 100644 index 36e1a9e95..000000000 --- a/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "7.0.0", - "rollForward": "latestMajor", - "allowPrerelease": false - } -} \ No newline at end of file diff --git a/props/Common.props b/props/Common.props index 26bf7321a..f0ee2b686 100644 --- a/props/Common.props +++ b/props/Common.props @@ -3,7 +3,7 @@ $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(RepoRoot)\RestSharp.snk true - 10 + preview enable enable diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cc4eccfef..2fa59dd14 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - netstandard2.0;net471;net6.0;net7.0 + netstandard2.0;net471;net48;net6.0;net7.0;net8.0 restsharp.png Apache-2.0 https://restsharp.dev @@ -15,14 +15,17 @@ snupkg true $(NoWarn);1591 - 11 README.md + + true + true + - - - + + + diff --git a/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs index 96c19536c..ac4c852c0 100644 --- a/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs +++ b/src/RestSharp.Serializers.CsvHelper/CsvHelperSerializer.cs @@ -5,26 +5,20 @@ namespace RestSharp.Serializers.CsvHelper; -public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer { - const string TextCsvContentType = "text/csv"; - - readonly CsvConfiguration _configuration; - +public class CsvHelperSerializer(CsvConfiguration configuration) : IDeserializer, IRestSerializer, ISerializer { public ISerializer Serializer => this; public IDeserializer Deserializer => this; - public string[] AcceptedContentTypes => new[] { TextCsvContentType, "application/x-download" }; + public string[] AcceptedContentTypes => [ContentType.Csv, "application/x-download"]; public SupportsContentType SupportsContentType => x => Array.IndexOf(AcceptedContentTypes, x) != -1 || x.Value.Contains("csv"); public DataFormat DataFormat => DataFormat.None; - public ContentType ContentType { get; set; } = TextCsvContentType; - - public CsvHelperSerializer() => _configuration = new CsvConfiguration(CultureInfo.InvariantCulture); + public ContentType ContentType { get; set; } = ContentType.Csv; - public CsvHelperSerializer(CsvConfiguration configuration) => _configuration = configuration; + public CsvHelperSerializer() : this(new CsvConfiguration(CultureInfo.InvariantCulture)) { } public T? Deserialize(RestResponse response) { try { @@ -33,7 +27,7 @@ public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer { using var stringReader = new StringReader(response.Content); - using var csvReader = new CsvReader(stringReader, _configuration); + using var csvReader = new CsvReader(stringReader, configuration); var @interface = typeof(T).GetInterface("IEnumerable`1"); @@ -81,7 +75,7 @@ public class CsvHelperSerializer : IDeserializer, IRestSerializer, ISerializer { using var stringWriter = new StringWriter(); - using var csvWriter = new CsvWriter(stringWriter, _configuration); + using var csvWriter = new CsvWriter(stringWriter, configuration); if (obj is IEnumerable records) { csvWriter.WriteRecords(records); diff --git a/src/RestSharp.Serializers.CsvHelper/README.md b/src/RestSharp.Serializers.CsvHelper/README.md new file mode 100644 index 000000000..3c7d3815c --- /dev/null +++ b/src/RestSharp.Serializers.CsvHelper/README.md @@ -0,0 +1,24 @@ +# About + +The `RestSharp.Serializers.CsvHelper` library provides a CSV serializer for RestSharp. It is based on the +`CsvHelper` library. + +# How to use + +Use the extension method provided by the package to configure the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper() +); +``` + +You can also supply your instance of `CsvConfiguration` as a parameter for the extension method. + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseCsvHelper(new CsvConfiguration(CultureInfo.InvariantCulture) {...}) +); +``` diff --git a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj index 5fff5b301..9c503dd50 100644 --- a/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj +++ b/src/RestSharp.Serializers.CsvHelper/RestSharp.Serializers.CsvHelper.csproj @@ -1,6 +1,6 @@  - + @@ -8,4 +8,7 @@ + + + diff --git a/src/RestSharp.Serializers.NewtonsoftJson/README.md b/src/RestSharp.Serializers.NewtonsoftJson/README.md new file mode 100644 index 000000000..5cc11a71b --- /dev/null +++ b/src/RestSharp.Serializers.NewtonsoftJson/README.md @@ -0,0 +1,19 @@ +# About + +This library allows using Newtonsoft.Json as a serializer for RestSharp instead of the default JSON serializer based +on `System.Text.Json`. + +# How to use + +The default JSON serializer uses `System.Text.Json`, which is a part of .NET since .NET 6. + +If you want to use Newtonsoft.Json, you can install the `RestSharp.Serializers.NewtonsoftJson` package and configure +the +client to use it: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseNewtonsoftJson() +); +``` \ No newline at end of file diff --git a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj index bdc5a5c18..45bcf8ca9 100644 --- a/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj +++ b/src/RestSharp.Serializers.NewtonsoftJson/RestSharp.Serializers.NewtonsoftJson.csproj @@ -1,6 +1,6 @@ - + @@ -9,4 +9,7 @@ + + + diff --git a/src/RestSharp.Serializers.Xml/README.md b/src/RestSharp.Serializers.Xml/README.md new file mode 100644 index 000000000..63a7f53cc --- /dev/null +++ b/src/RestSharp.Serializers.Xml/README.md @@ -0,0 +1,22 @@ +# About + +This package is a custom XML serializer for RestSharp. It is based on the original XML serializer that was part of RestSharp but was removed in version 107.0.0. + +# How to use + +The default XML serializer in RestSharp is `DotNetXmlSerializer`, which uses `System.Xml.Serialization` library from . +NET. + +In previous versions of RestSharp, the default XML serializer was a custom RestSharp XML serializer. To make the +code library size smaller, the custom serializer was removed from RestSharp. + +You can add it back if necessary by installing the `RestSharp.Serializers.Xml` package and adding it to the client: + +```csharp +var client = new RestClient( + options, + configureSerialization: s => s.UseXmlSerializer() +); +``` + +As before, you can supply three optional arguments for a custom namespace, custom root element, and if you want to use `SerializeAs` and `DeserializeAs` attributed. diff --git a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj index 1d6c8eaab..42ee74306 100644 --- a/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj +++ b/src/RestSharp.Serializers.Xml/RestSharp.Serializers.Xml.csproj @@ -8,4 +8,7 @@ + + + diff --git a/src/RestSharp/Authenticators/AuthenticatorBase.cs b/src/RestSharp/Authenticators/AuthenticatorBase.cs index 1f311dba4..dc765e8c7 100644 --- a/src/RestSharp/Authenticators/AuthenticatorBase.cs +++ b/src/RestSharp/Authenticators/AuthenticatorBase.cs @@ -14,10 +14,8 @@ namespace RestSharp.Authenticators; -public abstract class AuthenticatorBase : IAuthenticator { - protected AuthenticatorBase(string token) => Token = token; - - protected string Token { get; set; } +public abstract class AuthenticatorBase(string token) : IAuthenticator { + protected string Token { get; set; } = token; protected abstract ValueTask GetAuthenticationParameter(string accessToken); diff --git a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs index 006ac813f..07d775664 100644 --- a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs +++ b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs @@ -24,12 +24,10 @@ namespace RestSharp.Authenticators; /// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding. /// [PublicAPI] -public class HttpBasicAuthenticator : AuthenticatorBase { +public class HttpBasicAuthenticator(string username, string password, Encoding encoding) + : AuthenticatorBase(GetHeader(username, password, encoding)) { public HttpBasicAuthenticator(string username, string password) : this(username, password, Encoding.UTF8) { } - public HttpBasicAuthenticator(string username, string password, Encoding encoding) - : base(GetHeader(username, password, encoding)) { } - static string GetHeader(string username, string password, Encoding encoding) => Convert.ToBase64String(encoding.GetBytes($"{username}:{password}")); diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs index 10e8493df..1b90bdd60 100644 --- a/src/RestSharp/Authenticators/JwtAuthenticator.cs +++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs @@ -18,9 +18,7 @@ namespace RestSharp.Authenticators; /// JSON WEB TOKEN (JWT) Authenticator class. /// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token /// -public class JwtAuthenticator : AuthenticatorBase { - public JwtAuthenticator(string accessToken) : base(GetToken(accessToken)) { } - +public class JwtAuthenticator(string accessToken) : AuthenticatorBase(GetToken(accessToken)) { /// /// Set the new bearer token so the request gets the new header value /// diff --git a/src/RestSharp/Authenticators/OAuth/WebPair.cs b/src/RestSharp/Authenticators/OAuth/WebPair.cs index d83572e05..381fbbe24 100644 --- a/src/RestSharp/Authenticators/OAuth/WebPair.cs +++ b/src/RestSharp/Authenticators/OAuth/WebPair.cs @@ -14,16 +14,10 @@ namespace RestSharp.Authenticators.OAuth; -class WebPair { - public WebPair(string name, string? value, bool encode = false) { - Name = name; - Value = value; - WebValue = encode ? OAuthTools.UrlEncodeRelaxed(value) : value; - } - - public string Name { get; } - public string? Value { get; } - string? WebValue { get; } +class WebPair(string name, string? value, bool encode = false) { + public string Name { get; } = name; + public string? Value { get; } = value; + string? WebValue { get; } = encode ? OAuthTools.UrlEncodeRelaxed(value) : value; public string GetQueryParameter(bool web) { var value = web ? $"\"{WebValue}\"" : Value; diff --git a/src/RestSharp/ContentType.cs b/src/RestSharp/ContentType.cs index 54bec16e8..3a277de69 100644 --- a/src/RestSharp/ContentType.cs +++ b/src/RestSharp/ContentType.cs @@ -29,6 +29,7 @@ public class ContentType : IEquatable { public static readonly ContentType Json = "application/json"; public static readonly ContentType Xml = "application/xml"; public static readonly ContentType Plain = "text/plain"; + public static readonly ContentType Csv = "text/csv"; public static readonly ContentType Binary = "application/octet-stream"; public static readonly ContentType GZip = "application/x-gzip"; public static readonly ContentType FormUrlEncoded = "application/x-www-form-urlencoded"; @@ -84,4 +85,4 @@ public class ContentType : IEquatable { } public override int GetHashCode() => _value.GetHashCode(); -} +} \ No newline at end of file diff --git a/src/RestSharp/Extensions/GenerateImmutableAttribute.cs b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs index fcd8c546d..c4fe51817 100644 --- a/src/RestSharp/Extensions/GenerateImmutableAttribute.cs +++ b/src/RestSharp/Extensions/GenerateImmutableAttribute.cs @@ -16,4 +16,7 @@ namespace RestSharp.Extensions; [AttributeUsage(AttributeTargets.Class)] -public class GenerateImmutableAttribute : Attribute { } +class GenerateImmutableAttribute : Attribute { } + +[AttributeUsage(AttributeTargets.Property)] +class Exclude : Attribute { } diff --git a/src/RestSharp/Interceptors/CompatibilityInterceptor.cs b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs new file mode 100644 index 000000000..f39b2f361 --- /dev/null +++ b/src/RestSharp/Interceptors/CompatibilityInterceptor.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace RestSharp.Interceptors; + +/// +/// This class allows easier migration of legacy request hooks to interceptors. +/// +public class CompatibilityInterceptor : Interceptor { + public Action? OnBeforeDeserialization { get; set; } + public Func? OnBeforeRequest { get; set; } + public Func? OnAfterRequest { get; set; } + + /// + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + OnBeforeDeserialization?.Invoke(response); + return default; + } + + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + OnBeforeRequest?.Invoke(requestMessage); + return default; + } + + public override ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + OnAfterRequest?.Invoke(responseMessage); + return default; + } +} \ No newline at end of file diff --git a/src/RestSharp/Interceptors/Interceptor.cs b/src/RestSharp/Interceptors/Interceptor.cs index 2d0c016be..ddd765f52 100644 --- a/src/RestSharp/Interceptors/Interceptor.cs +++ b/src/RestSharp/Interceptors/Interceptor.cs @@ -13,45 +13,50 @@ // limitations under the License. // -namespace RestSharp.Interceptors; +namespace RestSharp.Interceptors; /// /// Base Interceptor /// public abstract class Interceptor { + static readonly ValueTask Completed = +#if NET + ValueTask.CompletedTask; +#else + new (); +#endif /// - /// Intercepts the request before serialization + /// Intercepts the request before composing the request message /// - /// RestRequest before serialization - /// Value Tags - public virtual ValueTask InterceptBeforeSerialization(RestRequest request) { - return new(); - } + /// RestRequest before composing the request message + /// Cancellation token + public virtual ValueTask BeforeRequest(RestRequest request, CancellationToken cancellationToken) => Completed; /// /// Intercepts the request before being sent /// - /// HttpRequestMessage before being sent - /// Value Tags - public virtual ValueTask InterceptBeforeRequest(HttpRequestMessage req) { - return new(); - } + /// HttpRequestMessage before being sent + /// Cancellation token + public virtual ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) => Completed; /// /// Intercepts the request before being sent /// - /// HttpResponseMessage as received from Server - /// Value Tags - public virtual ValueTask InterceptAfterRequest(HttpResponseMessage responseMessage) { - return new(); - } + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) => Completed; /// - /// Intercepts the request before deserialization + /// Intercepts the request after it's created from HttpResponseMessage /// - /// HttpResponseMessage as received from Server - /// Value Tags - public virtual ValueTask InterceptBeforeDeserialize(RestResponse response) { - return new(); - } -} \ No newline at end of file + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask AfterRequest(RestResponse response, CancellationToken cancellationToken) => Completed; + + /// + /// Intercepts the request before deserialization, won't be called if using non-generic ExecuteAsync + /// + /// HttpResponseMessage as received from the remote server + /// Cancellation token + public virtual ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) => Completed; +} diff --git a/src/RestSharp/Options/ReadOnlyRestClientOptions.cs b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs new file mode 100644 index 000000000..abe93af0d --- /dev/null +++ b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using RestSharp.Interceptors; + +namespace RestSharp; + +public partial class ReadOnlyRestClientOptions { + public IReadOnlyCollection? Interceptors { get; private set; } + + // partial void CopyAdditionalProperties(RestClientOptions inner); + partial void CopyAdditionalProperties(RestClientOptions inner) => Interceptors = GetInterceptors(inner); + + static IReadOnlyCollection? GetInterceptors(RestClientOptions? options) { + if (options == null || options.Interceptors.Count == 0) return null; + + var interceptors = new List(options.Interceptors); + return interceptors.AsReadOnly(); + } +} diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs index 95db15020..9b28c66ad 100644 --- a/src/RestSharp/Options/RestClientOptions.cs +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -65,6 +65,10 @@ public class RestClientOptions { /// public IAuthenticator? Authenticator { get; set; } + /// + /// List of interceptors that will be executed before the request is sent + /// + [Exclude] public List Interceptors { get; set; } = new(); /// @@ -114,6 +118,7 @@ public class RestClientOptions { #if NET [UnsupportedOSPlatform("browser")] #endif + [Exclude] public X509CertificateCollection? ClientCertificates { get; set; } /// diff --git a/src/RestSharp/Parameters/DefaultParameters.cs b/src/RestSharp/Parameters/DefaultParameters.cs index 6d3dc89a7..6ceb740fd 100644 --- a/src/RestSharp/Parameters/DefaultParameters.cs +++ b/src/RestSharp/Parameters/DefaultParameters.cs @@ -13,15 +13,11 @@ // limitations under the License. // -namespace RestSharp; - -public sealed class DefaultParameters : ParametersCollection { - readonly ReadOnlyRestClientOptions _options; +using System.Runtime.CompilerServices; - readonly object _lock = new(); - - public DefaultParameters(ReadOnlyRestClientOptions options) => _options = options; +namespace RestSharp; +public sealed class DefaultParameters(ReadOnlyRestClientOptions options) : ParametersCollection { /// /// Safely add a default parameter to the collection. /// @@ -29,22 +25,21 @@ public sealed class DefaultParameters : ParametersCollection { /// /// /// + [MethodImpl(MethodImplOptions.Synchronized)] public DefaultParameters AddParameter(Parameter parameter) { - lock (_lock) { - if (parameter.Type == ParameterType.RequestBody) - throw new NotSupportedException( - "Cannot set request body using default parameters. Use Request.AddBody() instead." - ); + if (parameter.Type == ParameterType.RequestBody) + throw new NotSupportedException( + "Cannot set request body using default parameters. Use Request.AddBody() instead." + ); - if (!_options.AllowMultipleDefaultParametersWithSameName && - !MultiParameterTypes.Contains(parameter.Type) && - this.Any(x => x.Name == parameter.Name)) { - throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter)); - } - - Parameters.Add(parameter); + if (!options.AllowMultipleDefaultParametersWithSameName && + !MultiParameterTypes.Contains(parameter.Type) && + this.Any(x => x.Name == parameter.Name)) { + throw new ArgumentException("A default parameters with the same name has already been added", nameof(parameter)); } + Parameters.Add(parameter); + return this; } @@ -55,10 +50,9 @@ public sealed class DefaultParameters : ParametersCollection { /// Parameter type /// [PublicAPI] + [MethodImpl(MethodImplOptions.Synchronized)] public DefaultParameters RemoveParameter(string name, ParameterType type) { - lock (_lock) { - Parameters.RemoveAll(x => x.Name == name && x.Type == type); - } + Parameters.RemoveAll(x => x.Name == name && x.Type == type); return this; } diff --git a/src/RestSharp/Parameters/FileParameter.cs b/src/RestSharp/Parameters/FileParameter.cs index 5b58bf44d..dfb4d25d1 100644 --- a/src/RestSharp/Parameters/FileParameter.cs +++ b/src/RestSharp/Parameters/FileParameter.cs @@ -113,11 +113,6 @@ public record FileParameter { [PublicAPI] public class FileParameterOptions { - [Obsolete("Use DisableFilenameStar instead")] - public bool DisableFileNameStar { - get => DisableFilenameStar; - set => DisableFilenameStar = value; - } public bool DisableFilenameStar { get; set; } = true; public bool DisableFilenameEncoding { get; set; } } diff --git a/src/RestSharp/Parameters/ObjectParser.cs b/src/RestSharp/Parameters/ObjectParser.cs index fec2a0a85..52ab89865 100644 --- a/src/RestSharp/Parameters/ObjectParser.cs +++ b/src/RestSharp/Parameters/ObjectParser.cs @@ -36,8 +36,6 @@ static class ObjectParser { properties.Add(GetValue(prop, val)); } - string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value); - IEnumerable GetArray(PropertyInfo propertyInfo, object? value) { var elementType = propertyInfo.PropertyType.GetElementType(); var array = (Array)value!; @@ -47,20 +45,19 @@ static class ObjectParser { var queryType = attribute?.ArrayQueryType ?? RequestArrayQueryType.CommaSeparated; var encode = attribute?.Encode ?? true; - if (array.Length > 0 && elementType != null) { - // convert the array to an array of strings - var values = array - .Cast() - .Select(item => ParseValue(attribute?.Format, item)); + if (array.Length <= 0 || elementType == null) return new ParsedParameter[] { new(name, null, encode) }; - return queryType switch { - RequestArrayQueryType.CommaSeparated => new[] { new ParsedParameter(name, string.Join(",", values), encode) }, - RequestArrayQueryType.ArrayParameters => values.Select(x => new ParsedParameter($"{name}[]", x, encode)), - _ => throw new ArgumentOutOfRangeException() - }; - } + // convert the array to an array of strings + var values = array + .Cast() + .Select(item => ParseValue(attribute?.Format, item)); + + return queryType switch { + RequestArrayQueryType.CommaSeparated => new[] { new ParsedParameter(name, string.Join(",", values), encode) }, + RequestArrayQueryType.ArrayParameters => values.Select(x => new ParsedParameter($"{name}[]", x, encode)), + _ => throw new ArgumentOutOfRangeException() + }; - return new ParsedParameter[] { new(name, null, encode) }; } ParsedParameter GetValue(PropertyInfo propertyInfo, object? value) { @@ -70,10 +67,12 @@ static class ObjectParser { return new ParsedParameter(name, val, attribute?.Encode ?? true); } + return properties; + bool IsAllowedProperty(string propertyName) => includedProperties.Length == 0 || includedProperties.Length > 0 && includedProperties.Contains(propertyName); - return properties; + string? ParseValue(string? format, object? value) => format == null ? value?.ToString() : string.Format($"{{0:{format}}}", value); } } diff --git a/src/RestSharp/Parameters/UrlSegmentParameter.cs b/src/RestSharp/Parameters/UrlSegmentParameter.cs index deb0b3bdc..402215a7f 100644 --- a/src/RestSharp/Parameters/UrlSegmentParameter.cs +++ b/src/RestSharp/Parameters/UrlSegmentParameter.cs @@ -12,9 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System.Text.RegularExpressions; + namespace RestSharp; public record UrlSegmentParameter : NamedParameter { + static readonly Regex RegexPattern = new("%2f", RegexOptions.IgnoreCase | RegexOptions.Compiled); + /// /// Instantiates a new query parameter instance that will be added to the request URL by replacing part of the absolute path. /// The request resource should have a placeholder {name} that will be replaced with the parameter value when the request is made. @@ -23,5 +27,10 @@ public record UrlSegmentParameter : NamedParameter { /// Parameter value /// Optional: encode the value, default is true public UrlSegmentParameter(string name, string value, bool encode = true) - : base(name, Ensure.NotEmpty(value, nameof(value)).Replace("%2F", "/").Replace("%2f", "/"), ParameterType.UrlSegment, encode) { } + : base( + name, + RegexPattern.Replace(Ensure.NotEmpty(value, nameof(value)), "/"), + ParameterType.UrlSegment, + encode + ) { } } \ No newline at end of file diff --git a/src/RestSharp/Request/HttpRequestMessageExtensions.cs b/src/RestSharp/Request/HttpRequestMessageExtensions.cs index a8dc5629a..00601d2d5 100644 --- a/src/RestSharp/Request/HttpRequestMessageExtensions.cs +++ b/src/RestSharp/Request/HttpRequestMessageExtensions.cs @@ -23,6 +23,7 @@ static class HttpRequestMessageExtensions { var headerParameters = headers.Parameters.Where(x => !KnownHeaders.IsContentHeader(x.Name!)); headerParameters.ForEach(x => AddHeader(x, message.Headers)); + return; void AddHeader(Parameter parameter, HttpHeaders httpHeaders) { var parameterStringValue = parameter.Value!.ToString(); @@ -31,4 +32,4 @@ static class HttpRequestMessageExtensions { httpHeaders.TryAddWithoutValidation(parameter.Name!, parameterStringValue); } } -} +} \ No newline at end of file diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index cfc7995ca..a38b46036 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -22,30 +22,22 @@ namespace RestSharp; -class RequestContent : IDisposable { - readonly RestClient _client; - readonly RestRequest _request; - readonly List _streams = new(); - readonly ParametersCollection _parameters; +class RequestContent(IRestClient client, RestRequest request) : IDisposable { + readonly List _streams = new(); + readonly ParametersCollection _parameters = new RequestParameters(request.Parameters.Union(client.DefaultParameters)); HttpContent? Content { get; set; } - public RequestContent(RestClient client, RestRequest request) { - _client = client; - _request = request; - _parameters = new RequestParameters(_request.Parameters.Union(_client.DefaultParameters)); - } - public HttpContent BuildContent() { - var postParameters = _parameters.GetContentParameters(_request.Method).ToArray(); + var postParameters = _parameters.GetContentParameters(request.Method).ToArray(); var postParametersExists = postParameters.Length > 0; - var bodyParametersExists = _request.TryGetBodyParameter(out var bodyParameter); - var filesExists = _request.Files.Any(); + var bodyParametersExists = request.TryGetBodyParameter(out var bodyParameter); + var filesExists = request.Files.Any(); - if (_request.HasFiles() || + if (request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter) || - filesExists || - _request.AlwaysMultipartFormData) { + filesExists || + request.AlwaysMultipartFormData) { Content = CreateMultipartFormDataContent(); } @@ -62,15 +54,15 @@ class RequestContent : IDisposable { void AddFiles() { // File uploading without multipart/form-data - if (_request is { AlwaysSingleFileAsContent: true, Files.Count: 1 }) { - var fileParameter = _request.Files.First(); + if (request is { AlwaysSingleFileAsContent: true, Files.Count: 1 }) { + var fileParameter = request.Files.First(); Content?.Dispose(); Content = ToStreamContent(fileParameter); return; } var mpContent = Content as MultipartFormDataContent; - foreach (var fileParameter in _request.Files) mpContent!.Add(ToStreamContent(fileParameter)); + foreach (var fileParameter in request.Files) mpContent!.Add(ToStreamContent(fileParameter)); } StreamContent ToStreamContent(FileParameter fileParameter) { @@ -83,7 +75,7 @@ class RequestContent : IDisposable { var dispositionHeader = fileParameter.Options.DisableFilenameEncoding ? ContentDispositionHeaderValue.Parse($"form-data; name=\"{fileParameter.Name}\"; filename=\"{fileParameter.FileName}\"") : new ContentDispositionHeaderValue("form-data") { Name = $"\"{fileParameter.Name}\"", FileName = $"\"{fileParameter.FileName}\"" }; - if (!fileParameter.Options.DisableFileNameStar) dispositionHeader.FileNameStar = fileParameter.FileName; + if (!fileParameter.Options.DisableFilenameStar) dispositionHeader.FileNameStar = fileParameter.FileName; streamContent.Headers.ContentDisposition = dispositionHeader; return streamContent; @@ -91,7 +83,7 @@ class RequestContent : IDisposable { HttpContent Serialize(BodyParameter body) { return body.DataFormat switch { - DataFormat.None => new StringContent(body.Value!.ToString()!, _client.Options.Encoding, body.ContentType.Value), + DataFormat.None => new StringContent(body.Value!.ToString()!, client.Options.Encoding, body.ContentType.Value), DataFormat.Binary => GetBinary(), _ => GetSerialized() }; @@ -109,14 +101,14 @@ class RequestContent : IDisposable { } HttpContent GetSerialized() { - var serializer = _client.Serializers.GetSerializer(body.DataFormat); + var serializer = client.Serializers.GetSerializer(body.DataFormat); var content = serializer.Serialize(body); if (content == null) throw new SerializationException("Request body serialized to null"); var contentType = body.ContentType.Or(serializer.Serializer.ContentType); - return new StringContent(content, _client.Options.Encoding, contentType.Value); + return new StringContent(content, client.Options.Encoding, contentType.Value); } } @@ -127,13 +119,13 @@ class RequestContent : IDisposable { return bodyParameter.Name.IsNotEmpty() && bodyParameter.Name != bodyContentType; } - string GetOrSetFormBoundary() => _request.FormBoundary ?? (_request.FormBoundary = Guid.NewGuid().ToString()); + string GetOrSetFormBoundary() => request.FormBoundary ?? (request.FormBoundary = Guid.NewGuid().ToString()); MultipartFormDataContent CreateMultipartFormDataContent() { var boundary = GetOrSetFormBoundary(); var mpContent = new MultipartFormDataContent(boundary); var contentType = new MediaTypeHeaderValue("multipart/form-data"); - contentType.Parameters.Add(new NameValueHeaderValue(nameof(boundary), GetBoundary(boundary, _request.MultipartFormQuoteBoundary))); + contentType.Parameters.Add(new NameValueHeaderValue(nameof(boundary), GetBoundary(boundary, request.MultipartFormQuoteBoundary))); mpContent.Headers.ContentType = contentType; return mpContent; } @@ -142,7 +134,7 @@ class RequestContent : IDisposable { var bodyContent = Serialize(bodyParameter); // we need to send the body - if (hasPostParameters || _request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter) || _request.AlwaysMultipartFormData) { + if (hasPostParameters || request.HasFiles() || BodyShouldBeMultipartForm(bodyParameter) || request.AlwaysMultipartFormData) { // here we must use multipart form data var mpContent = Content as MultipartFormDataContent ?? CreateMultipartFormDataContent(); var ct = bodyContent.Headers.ContentType?.MediaType; @@ -159,7 +151,7 @@ class RequestContent : IDisposable { Content = bodyContent; } - if (_client.Options.DisableCharset) { + if (client.Options.DisableCharset) { Content.Headers.ContentType!.CharSet = ""; } } @@ -173,16 +165,16 @@ class RequestContent : IDisposable { var parameterName = postParameter.Name!; mpContent.Add( - new StringContent(postParameter.Value?.ToString() ?? string.Empty, _client.Options.Encoding, postParameter.ContentType.Value), - _request.MultipartFormQuoteParameters ? $"\"{parameterName}\"" : parameterName + new StringContent(postParameter.Value?.ToString() ?? string.Empty, client.Options.Encoding, postParameter.ContentType.Value), + request.MultipartFormQuoteParameters ? $"\"{parameterName}\"" : parameterName ); } } else { - var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString()!.UrlEncode() ?? string.Empty}"); - var encodedContent = new StringContent(encodedItems.JoinToString("&"), _client.Options.Encoding, ContentType.FormUrlEncoded.Value); + var encodedItems = postParameters.Select(x => $"{x.Name!.UrlEncode()}={x.Value?.ToString()?.UrlEncode() ?? string.Empty}"); + var encodedContent = new StringContent(encodedItems.JoinToString("&"), client.Options.Encoding, ContentType.FormUrlEncoded.Value); - if (_client.Options.DisableCharset) { + if (client.Options.DisableCharset) { encodedContent.Headers.ContentType!.CharSet = ""; } @@ -204,6 +196,7 @@ class RequestContent : IDisposable { } contentHeaders.ForEach(AddHeader); + return; void AddHeader(HeaderParameter parameter) { var parameterStringValue = parameter.Value!.ToString(); @@ -218,7 +211,7 @@ class RequestContent : IDisposable { string GetContentTypeHeader(string contentType) => Content is MultipartFormDataContent - ? $"{contentType}; boundary={GetBoundary(GetOrSetFormBoundary(), _request.MultipartFormQuoteBoundary)}" + ? $"{contentType}; boundary={GetBoundary(GetOrSetFormBoundary(), request.MultipartFormQuoteBoundary)}" : contentType; } @@ -231,4 +224,4 @@ string GetContentTypeHeader(string contentType) _streams.ForEach(x => x.Dispose()); Content?.Dispose(); } -} +} \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index 9748fa9cf..16be4837d 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -16,6 +16,7 @@ using System.Net.Http.Headers; using RestSharp.Authenticators; using RestSharp.Extensions; +using RestSharp.Interceptors; // ReSharper disable ReplaceSubstringWithRangeIndexer // ReSharper disable UnusedAutoPropertyAccessor.Global @@ -47,12 +48,14 @@ public class RestRequest { var queryStringStart = Resource.IndexOf('?'); - if (queryStringStart >= 0 && Resource.IndexOf('=') > queryStringStart) { - var queryParams = ParseQuery(Resource.Substring(queryStringStart + 1)); - Resource = Resource.Substring(0, queryStringStart); + if (queryStringStart < 0 || Resource.IndexOf('=') <= queryStringStart) return; - foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value, false); - } + var queryParams = ParseQuery(Resource.Substring(queryStringStart + 1)); + Resource = Resource.Substring(0, queryStringStart); + + foreach (var param in queryParams) this.AddQueryParameter(param.Key, param.Value, false); + + return; static IEnumerable> ParseQuery(string query) => query.Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries) @@ -167,19 +170,22 @@ public RestRequest(Uri resource, Method method = Method.Get) /// /// When supplied, the function will be called before calling the deserializer /// + [Obsolete("Use Interceptors instead")] public Action? OnBeforeDeserialization { get; set; } /// /// When supplied, the function will be called before making a request /// + [Obsolete("Use Interceptors instead")] public Func? OnBeforeRequest { get; set; } /// /// When supplied, the function will be called after the request is complete /// + [Obsolete("Use Interceptors instead")] public Func? OnAfterRequest { get; set; } - internal void IncreaseNumAttempts() => Attempts++; + internal void IncreaseNumberOfAttempts() => Attempts++; /// /// How many attempts were made to send this Request @@ -226,6 +232,11 @@ public RestRequest(Uri resource, Method method = Method.Get) _advancedResponseHandler = value; } } + + /// + /// Request-level interceptors. Will be combined with client-level interceptors if set. + /// + public List? Interceptors { get; set; } /// /// Adds a parameter object to the request parameters diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 0240946e0..68290b28d 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -26,7 +26,7 @@ namespace RestSharp; /// /// Type of data to deserialize to [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "()}")] -public class RestResponse : RestResponse { +public class RestResponse(RestRequest request) : RestResponse(request) { /// /// Deserialized entity data /// @@ -52,15 +52,13 @@ public static RestResponse FromResponse(RestResponse response) StatusDescription = response.StatusDescription, RootElement = response.RootElement }; - - public RestResponse(RestRequest request) : base(request) { } } /// /// Container for data sent back from API /// [DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] -public class RestResponse : RestResponseBase { +public class RestResponse(RestRequest request) : RestResponseBase(request) { internal static async Task FromHttpResponse( HttpResponseMessage httpResponse, RestRequest request, @@ -103,9 +101,7 @@ CancellationToken cancellationToken } } - public RestResponse(RestRequest request) : base(request) { } - - public RestResponse() : base(new RestRequest()) { } + public RestResponse() : this(new RestRequest()) { } } public delegate ResponseStatus CalculateResponseStatus(HttpResponseMessage httpResponse); diff --git a/src/RestSharp/Response/RestResponseBase.cs b/src/RestSharp/Response/RestResponseBase.cs index 6bbe710b0..0bbff2d55 100644 --- a/src/RestSharp/Response/RestResponseBase.cs +++ b/src/RestSharp/Response/RestResponseBase.cs @@ -28,7 +28,7 @@ public abstract class RestResponseBase { protected RestResponseBase(RestRequest request) { ResponseStatus = ResponseStatus.None; Request = request; - Request.IncreaseNumAttempts(); + Request.IncreaseNumberOfAttempts(); } /// diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 9c99b9fd7..0e9c80c9e 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -14,6 +14,7 @@ using System.Net; using RestSharp.Extensions; +using RestSharp.Interceptors; namespace RestSharp; @@ -33,6 +34,7 @@ public partial class RestClient { ) .ConfigureAwait(false) : GetErrorResponse(request, internalResponse.Exception, internalResponse.TimeoutToken); + await OnAfterRequest(response, cancellationToken).ConfigureAwait(false); return Options.ThrowOnAnyError ? response.ThrowIfError() : response; } @@ -69,6 +71,21 @@ public partial class RestClient { bool TimedOut() => timeoutToken.IsCancellationRequested || exception.Message.Contains("HttpClient.Timeout"); } + void CombineInterceptors(RestRequest request) { + if (request.Interceptors == null) { + if (Options.Interceptors == null) { + return; + } + + request.Interceptors = Options.Interceptors.ToList(); + return; + } + + if (Options.Interceptors != null) { + request.Interceptors.AddRange(Options.Interceptors); + } + } + async Task ExecuteRequestAsync(RestRequest request, CancellationToken cancellationToken) { Ensure.NotNull(request, nameof(request)); @@ -77,7 +94,8 @@ public partial class RestClient { throw new ObjectDisposedException(nameof(RestClient)); } - await OnBeforeSerialization(request).ConfigureAwait(false); + CombineInterceptors(request); + await OnBeforeRequest(request, cancellationToken).ConfigureAwait(false); request.ValidateParameters(); var authenticator = request.Authenticator ?? Options.Authenticator; @@ -90,7 +108,8 @@ public partial class RestClient { var httpMethod = AsHttpMethod(request.Method); var url = this.BuildUri(request); - using var message = new HttpRequestMessage(httpMethod, url) { Content = requestContent.BuildContent() }; + using var message = new HttpRequestMessage(httpMethod, url); + message.Content = requestContent.BuildContent(); message.Headers.Host = Options.BaseHost; message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy; @@ -99,11 +118,10 @@ public partial class RestClient { var ct = cts.Token; - HttpResponseMessage? responseMessage; // Make sure we have a cookie container if not provided in the request - CookieContainer cookieContainer = request.CookieContainer ??= new CookieContainer(); - + var cookieContainer = request.CookieContainer ??= new CookieContainer(); + var headers = new RequestHeaders() .AddHeaders(request.Parameters) .AddHeaders(DefaultParameters) @@ -112,11 +130,14 @@ public partial class RestClient { .AddCookieHeaders(url, Options.CookieContainer); message.AddHeaders(headers); +#pragma warning disable CS0618 // Type or member is obsolete if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false); - await OnBeforeRequest(message).ConfigureAwait(false); - +#pragma warning restore CS0618 // Type or member is obsolete + await OnBeforeHttpRequest(request, message, cancellationToken).ConfigureAwait(false); + try { responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false); + // Parse all the cookies from the response and update the cookie jar with cookies if (responseMessage.Headers.TryGetValues(KnownHeaders.SetCookie, out var cookiesHeader)) { // ReSharper disable once PossibleMultipleEnumeration @@ -128,37 +149,43 @@ public partial class RestClient { catch (Exception ex) { return new HttpResponse(null, url, null, ex, timeoutCts.Token); } + +#pragma warning disable CS0618 // Type or member is obsolete if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false); - await OnAfterRequest(responseMessage).ConfigureAwait(false); +#pragma warning restore CS0618 // Type or member is obsolete + await OnAfterHttpRequest(request, responseMessage, cancellationToken).ConfigureAwait(false); return new HttpResponse(responseMessage, url, cookieContainer, null, timeoutCts.Token); - } - /// - /// Will be called before the Request becomes Serialized - /// - /// RestRequest before it will be serialized - async Task OnBeforeSerialization(RestRequest request) { - foreach (var interceptor in Options.Interceptors) { - await interceptor.InterceptBeforeSerialization(request); //.ThrowExceptionIfAvailable(); + static async ValueTask OnBeforeRequest(RestRequest request, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.BeforeRequest(request, cancellationToken).ConfigureAwait(false); + } + } + + static async ValueTask OnBeforeHttpRequest(RestRequest request, HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.BeforeHttpRequest(requestMessage, cancellationToken).ConfigureAwait(false); } } - /// - /// Will be called before the Request will be sent - /// - /// HttpRequestMessage ready to be sent - async Task OnBeforeRequest(HttpRequestMessage requestMessage) { - foreach (var interceptor in Options.Interceptors) { - await interceptor.InterceptBeforeRequest(requestMessage); + + static async ValueTask OnAfterHttpRequest(RestRequest request, HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + if (request.Interceptors == null) return; + + foreach (var interceptor in request.Interceptors) { + await interceptor.AfterHttpRequest(responseMessage, cancellationToken).ConfigureAwait(false); } } - /// - /// Will be called after the Response has been received from Server - /// - /// HttpResponseMessage as received from server - async Task OnAfterRequest(HttpResponseMessage responseMessage) { - foreach (var interceptor in Options.Interceptors) { - await interceptor.InterceptAfterRequest(responseMessage); + + static async ValueTask OnAfterRequest(RestResponse response, CancellationToken cancellationToken) { + if (response.Request.Interceptors == null) return; + + foreach (var interceptor in response.Request.Interceptors) { + await interceptor.AfterRequest(response, cancellationToken).ConfigureAwait(false); } } diff --git a/src/RestSharp/RestClient.Extensions.cs b/src/RestSharp/RestClient.Extensions.cs index 4daede03b..5ac631d87 100644 --- a/src/RestSharp/RestClient.Extensions.cs +++ b/src/RestSharp/RestClient.Extensions.cs @@ -20,8 +20,8 @@ namespace RestSharp; [PublicAPI] public static partial class RestClientExtensions { [PublicAPI] - public static RestResponse Deserialize(this IRestClient client, RestResponse response) - => client.Serializers.Deserialize(response.Request, response, client.Options); + public static ValueTask> Deserialize(this IRestClient client, RestResponse response, CancellationToken cancellationToken) + => client.Serializers.Deserialize(response.Request, response, client.Options, cancellationToken); /// /// Executes the request asynchronously, authenticating if needed @@ -35,22 +35,10 @@ public static RestResponse Deserialize(this IRestClient client, RestRespon RestRequest request, CancellationToken cancellationToken = default ) { - if (request == null) throw new ArgumentNullException(nameof(request)); + Ensure.NotNull(request, nameof(request)); var response = await client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); - await OnBeforeDeserialization(response, client.Options).ConfigureAwait(false); - return client.Serializers.Deserialize(request, response, client.Options); - } - - /// - /// Will be called before the Data will be serialized - /// - /// RestResponse with Data still in Content - /// RestClient options but readonly - static async Task OnBeforeDeserialization(RestResponse raw, ReadOnlyRestClientOptions options) { - foreach (var interceptor in options.Interceptors) { - await interceptor.InterceptBeforeDeserialize(raw); - } + return await client.Serializers.Deserialize(request, response, client.Options, cancellationToken); } /// diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs index ccaae9f21..319b4bd64 100644 --- a/src/RestSharp/RestClient.cs +++ b/src/RestSharp/RestClient.cs @@ -14,7 +14,9 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; +using System.Runtime.CompilerServices; using RestSharp.Authenticators; +using RestSharp.Interceptors; using RestSharp.Serializers; // ReSharper disable VirtualMemberCallInConstructor @@ -36,7 +38,11 @@ public partial class RestClient : IRestClient { /// Content types that will be sent in the Accept header. The list is populated from the known serializers. /// If you need to send something else by default, set this property to a different value. /// - public string[] AcceptedContentTypes { get; set; } + public string[] AcceptedContentTypes { + get; + [MethodImpl(MethodImplOptions.Synchronized)] + set; + } internal HttpClient HttpClient { get; } @@ -84,11 +90,12 @@ public partial class RestClient : IRestClient { HttpClient = GetClient(); } + return; + HttpClient GetClient() { var handler = new HttpClientHandler(); - ConfigureHttpMessageHandler(handler, Options); + ConfigureHttpMessageHandler(handler, options); var finalHandler = options.ConfigureMessageHandler?.Invoke(handler) ?? handler; - var httpClient = new HttpClient(finalHandler); ConfigureHttpClient(httpClient, options); ConfigureDefaultParameters(options); @@ -225,7 +232,7 @@ public partial class RestClient : IRestClient { } // ReSharper disable once CognitiveComplexity - static void ConfigureHttpMessageHandler(HttpClientHandler handler, ReadOnlyRestClientOptions options) { + static void ConfigureHttpMessageHandler(HttpClientHandler handler, RestClientOptions options) { #if NET if (!OperatingSystem.IsBrowser()) { #endif @@ -278,8 +285,7 @@ public partial class RestClient : IRestClient { } readonly bool _disposeHttpClient; - - bool _disposed; + bool _disposed; protected virtual void Dispose(bool disposing) { if (disposing && !_disposed) { diff --git a/src/RestSharp/RestSharp.csproj b/src/RestSharp/RestSharp.csproj index 561e30c82..89f15d0d3 100644 --- a/src/RestSharp/RestSharp.csproj +++ b/src/RestSharp/RestSharp.csproj @@ -3,18 +3,17 @@ true - + - - + + + - - - - + + - - + + @@ -24,35 +23,35 @@ RestClient.cs - PropertyCache.cs + PropertyCache.cs - PropertyCache.cs + PropertyCache.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - RestClient.Extensions.cs + RestClient.Extensions.cs - + diff --git a/src/RestSharp/Serializers/DeseralizationException.cs b/src/RestSharp/Serializers/DeseralizationException.cs index 61c590287..6bb193edd 100644 --- a/src/RestSharp/Serializers/DeseralizationException.cs +++ b/src/RestSharp/Serializers/DeseralizationException.cs @@ -13,12 +13,11 @@ // limitations under the License. // ReSharper disable once CheckNamespace -namespace RestSharp; -public class DeserializationException : Exception { - public DeserializationException(RestResponse response, Exception innerException) - : base("Error occured while deserializing the response", innerException) - => Response = response; +namespace RestSharp; - public RestResponse Response { get; } +public class DeserializationException(RestResponse response, Exception innerException) + : Exception("Error occured while deserializing the response", innerException) { + [PublicAPI] + public RestResponse Response { get; } = response; } \ No newline at end of file diff --git a/src/RestSharp/Serializers/RestSerializers.cs b/src/RestSharp/Serializers/RestSerializers.cs index 93aa464ba..4216c0452 100644 --- a/src/RestSharp/Serializers/RestSerializers.cs +++ b/src/RestSharp/Serializers/RestSerializers.cs @@ -19,11 +19,9 @@ namespace RestSharp.Serializers; -public class RestSerializers { - public IReadOnlyDictionary Serializers { get; } - - public RestSerializers(Dictionary records) - => Serializers = new ReadOnlyDictionary(records); +public class RestSerializers(Dictionary records) { + [PublicAPI] + public IReadOnlyDictionary Serializers { get; } = new ReadOnlyDictionary(records); public RestSerializers(SerializerConfig config) : this(config.Serializers) { } @@ -34,11 +32,14 @@ public IRestSerializer GetSerializer(DataFormat dataFormat) internal string[] GetAcceptedContentTypes() => Serializers.SelectMany(x => x.Value.AcceptedContentTypes).Distinct().ToArray(); - internal RestResponse Deserialize(RestRequest request, RestResponse raw, ReadOnlyRestClientOptions options) { + internal async ValueTask> Deserialize(RestRequest request, RestResponse raw, ReadOnlyRestClientOptions options, CancellationToken cancellationToken) { var response = RestResponse.FromResponse(raw); try { + await OnBeforeDeserialization(raw, cancellationToken).ConfigureAwait(false); +#pragma warning disable CS0618 // Type or member is obsolete request.OnBeforeDeserialization?.Invoke(raw); +#pragma warning restore CS0618 // Type or member is obsolete response.Data = DeserializeContent(raw); } catch (Exception ex) { @@ -54,6 +55,13 @@ public IRestSerializer GetSerializer(DataFormat dataFormat) return response; } + static async ValueTask OnBeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + if (response.Request.Interceptors == null) return; + + foreach (var interceptor in response.Request.Interceptors) { + await interceptor.BeforeDeserialization(response, cancellationToken).ConfigureAwait(false); + } + } /// /// Deserialize the response content into the specified type @@ -74,13 +82,14 @@ public IRestSerializer GetSerializer(DataFormat dataFormat) // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource var deserializer = GetContentDeserializer(response); - if (deserializer is IXmlDeserializer xml && response.Request is RestXmlRequest xmlRequest) { - if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; + if (deserializer is not IXmlDeserializer xml || response.Request is not RestXmlRequest xmlRequest) + return deserializer != null ? deserializer.Deserialize(response) : default; - if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) withDateFormat.DateFormat = xmlRequest.DateFormat!; - } + if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; + + if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) withDateFormat.DateFormat = xmlRequest.DateFormat!; - return deserializer != null ? deserializer.Deserialize(response) : default; + return deserializer.Deserialize(response); } IDeserializer? GetContentDeserializer(RestResponseBase response) { diff --git a/test/Directory.Build.props b/test/Directory.Build.props index ec109e2e1..9ad7f24e7 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,23 +3,25 @@ true false - net472;net6.0;net7.0 + net48;net6.0;net7.0;net8.0 disable xUnit1033 + trx%3bLogFileName=$(MSBuildProjectName).trx + $(RepoRoot)/test-results/$(TargetFramework) - - - + + + - - - + + + - - + + diff --git a/test/RestSharp.InteractiveTests/Program.cs b/test/RestSharp.InteractiveTests/Program.cs index cee0cbbcc..78c36c141 100644 --- a/test/RestSharp.InteractiveTests/Program.cs +++ b/test/RestSharp.InteractiveTests/Program.cs @@ -8,12 +8,14 @@ return; +#pragma warning disable CS0162 // Unreachable code detected var keys = new AuthenticationTests.TwitterKeys { ConsumerKey = Prompt("Consumer key"), ConsumerSecret = Prompt("Consumer secret"), }; await AuthenticationTests.Can_Authenticate_With_OAuth_Async_With_Callback(keys); +#pragma warning restore CS0162 // Unreachable code detected static string Prompt(string message) { Console.Write($"{message}: "); diff --git a/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj b/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj index c25f980cf..36f53edc9 100644 --- a/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj +++ b/test/RestSharp.InteractiveTests/RestSharp.InteractiveTests.csproj @@ -2,7 +2,7 @@ Exe false - net6 + net6.0 diff --git a/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs index 8c9f0081d..0bb8257d3 100644 --- a/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs +++ b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs @@ -5,15 +5,8 @@ namespace RestSharp.Tests.Integrated.Authentication; -[Collection(nameof(TestServerCollection))] -public class AuthenticationTests { - readonly TestServerFixture _fixture; - readonly ITestOutputHelper _output; - - public AuthenticationTests(TestServerFixture fixture, ITestOutputHelper output) { - _fixture = fixture; - _output = output; - } +public class AuthenticationTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); [Fact] public async Task Can_Authenticate_With_Basic_Http_Auth() { @@ -21,7 +14,7 @@ public class AuthenticationTests { const string password = "testpassword"; var client = new RestClient( - _fixture.Server.Url, + _server.Url!, o => o.Authenticator = new HttpBasicAuthenticator(userName, password) ); var request = new RestRequest("headers"); @@ -35,4 +28,6 @@ public class AuthenticationTests { parts[0].Should().Be(userName); parts[1].Should().Be(password); } -} + + public void Dispose() => _server.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs b/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs index 78a1bf985..d9195bfd2 100644 --- a/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs +++ b/test/RestSharp.Tests.Integrated/Authentication/OAuth2Tests.cs @@ -3,16 +3,13 @@ namespace RestSharp.Tests.Integrated.Authentication; -[Collection(nameof(TestServerCollection))] -public class OAuth2Tests { - readonly TestServerFixture _fixture; - - public OAuth2Tests(TestServerFixture fixture) => _fixture = fixture; - +public class OAuth2Tests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + [Fact] public async Task ShouldHaveProperHeader() { var auth = new OAuth2AuthorizationRequestHeaderAuthenticator("token", "Bearer"); - var client = new RestClient(_fixture.Server.Url, o => o.Authenticator = auth); + var client = new RestClient(_server.Url!, o => o.Authenticator = auth); var response = await client.GetJsonAsync("headers"); var authHeader = response!.FirstOrDefault(x => x.Name == KnownHeaders.Authorization); @@ -20,4 +17,6 @@ public class OAuth2Tests { authHeader.Should().NotBeNull(); authHeader!.Value.Should().Be("Bearer token"); } + + public void Dispose() => _server.Dispose(); } diff --git a/test/RestSharp.Tests.Integrated/CompressionTests.cs b/test/RestSharp.Tests.Integrated/CompressionTests.cs index 2d03cb9ab..9aefcd8a0 100644 --- a/test/RestSharp.Tests.Integrated/CompressionTests.cs +++ b/test/RestSharp.Tests.Integrated/CompressionTests.cs @@ -1,63 +1,69 @@ using System.IO.Compression; -using System.Net; +using RestSharp.Extensions; using RestSharp.Tests.Shared.Extensions; -using RestSharp.Tests.Shared.Fixtures; -namespace RestSharp.Tests.Integrated; +namespace RestSharp.Tests.Integrated; public class CompressionTests { - readonly ITestOutputHelper _output; + static async Task GetBody(Func getStream, string value) { + using var memoryStream = new MemoryStream(); - static Action GzipEchoValue(string value) - => context => { - context.Response.Headers.Add("Content-encoding", "gzip"); + using (var stream = getStream(memoryStream)) { + stream.WriteStringUtf8(value); + } - using var gzip = new GZipStream(context.Response.OutputStream, CompressionMode.Compress, true); - - gzip.WriteStringUtf8(value); - }; - - static Action DeflateEchoValue(string value) - => context => { - context.Response.Headers.Add("Content-encoding", "deflate"); - - using var gzip = new DeflateStream(context.Response.OutputStream, CompressionMode.Compress, true); + memoryStream.Seek(0, SeekOrigin.Begin); + var body = await memoryStream.ReadAsBytes(default); + return body; + } - gzip.WriteStringUtf8(value); - }; - - public CompressionTests(ITestOutputHelper output) => _output = output; + static void ConfigureServer(WireMockServer server, byte[] body, string encoding) + => server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(body).WithHeader("Content-Encoding", encoding)); [Fact] public async Task Can_Handle_Deflate_Compressed_Content() { - using var server = SimpleServer.Create(DeflateEchoValue("This is some deflated content")); + const string value = "This is some deflated content"; + using var server = WireMockServer.Start(); + + var body = await GetBody(s => new DeflateStream(s, CompressionMode.Compress, true), value); + ConfigureServer(server, body, "deflate"); - var client = new RestClient(server.Url); + var client = new RestClient(server.Url!); var request = new RestRequest(""); var response = await client.ExecuteAsync(request); - Assert.Equal("This is some deflated content", response.Content); + response.Content.Should().Be(value); } [Fact] public async Task Can_Handle_Gzip_Compressed_Content() { - using var server = SimpleServer.Create(GzipEchoValue("This is some gzipped content")); + const string value = "This is some gzipped content"; + using var server = WireMockServer.Start(); + + var body = await GetBody(s => new GZipStream(s, CompressionMode.Compress, true), value); + ConfigureServer(server, body, "gzip"); - var client = new RestClient(server.Url); + var client = new RestClient(server.Url!); var request = new RestRequest(""); var response = await client.ExecuteAsync(request); - Assert.Equal("This is some gzipped content", response.Content); + response.Content.Should().Be(value); } [Fact] public async Task Can_Handle_Uncompressed_Content() { - using var server = SimpleServer.Create(Handlers.EchoValue("This is some sample content")); + const string value = "This is some sample content"; + using var server = WireMockServer.Start(); + server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(value)); - var client = new RestClient(server.Url); + var client = new RestClient(server.Url!); var request = new RestRequest(""); var response = await client.ExecuteAsync(request); - Assert.Equal("This is some sample content", response.Content); + response.Content.Should().Be(value); } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/CookieTests.cs b/test/RestSharp.Tests.Integrated/CookieTests.cs index 30c4d2743..9c294b237 100644 --- a/test/RestSharp.Tests.Integrated/CookieTests.cs +++ b/test/RestSharp.Tests.Integrated/CookieTests.cs @@ -1,19 +1,32 @@ -using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using RestSharp.Tests.Integrated.Fixtures; using RestSharp.Tests.Integrated.Server; +using WireMock.Types; +using WireMock.Util; namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class CookieTests { - readonly RestClient _client; - readonly string _host; +public sealed class CookieTests : IDisposable { + readonly RestClient _client; + readonly string _host; + readonly WireMockServer _server = WireMockServer.Start(); - public CookieTests(TestServerFixture fixture) { - var options = new RestClientOptions(fixture.Server.Url) { + public CookieTests() { + var options = new RestClientOptions(_server.Url!) { CookieContainer = new CookieContainer() }; _client = new RestClient(options); _host = _client.Options.BaseUrl!.Host; + + _server + .Given(Request.Create().WithPath("/get-cookies")) + .RespondWith(Response.Create().WithCallback(HandleGetCookies)); + + _server + .Given(Request.Create().WithPath("/set-cookies")) + .RespondWith(Response.Create().WithCallback(HandleSetCookies)); } [Fact] @@ -49,16 +62,15 @@ public class CookieTests { response.Content.Should().Be("success"); AssertCookie("cookie1", "value1", x => x == DateTime.MinValue); - FindCookie("cookie2").Should().BeNull("Cookie 2 should vanish as the path will not match"); + response.Cookies.Find("cookie2").Should().BeNull("Cookie 2 should vanish as the path will not match"); AssertCookie("cookie3", "value3", x => x > DateTime.Now); AssertCookie("cookie4", "value4", x => x > DateTime.Now); - FindCookie("cookie5").Should().BeNull("Cookie 5 should vanish as the request is not SSL"); + response.Cookies.Find("cookie5").Should().BeNull("Cookie 5 should vanish as the request is not SSL"); AssertCookie("cookie6", "value6", x => x == DateTime.MinValue, true); - - Cookie? FindCookie(string name) => response.Cookies!.FirstOrDefault(p => p.Name == name); + return; void AssertCookie(string name, string value, Func checkExpiration, bool httpOnly = false) { - var c = FindCookie(name)!; + var c = response.Cookies.Find(name)!; c.Value.Should().Be(value); c.Path.Should().Be("/"); c.Domain.Should().Be(_host); @@ -73,14 +85,65 @@ public class CookieTests { var response = await _client.ExecuteAsync(request); response.Content.Should().Be("success"); - var notFoundCookie = FindCookie("cookie_empty_domain"); + var notFoundCookie = response.Cookies.Find("cookie_empty_domain"); notFoundCookie.Should().BeNull(); var emptyDomainCookieHeader = response.Headers! .SingleOrDefault(h => h.Name == KnownHeaders.SetCookie && ((string)h.Value!).StartsWith("cookie_empty_domain")); emptyDomainCookieHeader.Should().NotBeNull(); ((string)emptyDomainCookieHeader!.Value!).Should().Contain("domain=;"); + } - Cookie? FindCookie(string name) => response.Cookies!.FirstOrDefault(p => p.Name == name); + static ResponseMessage HandleGetCookies(IRequestMessage request) { + var response = request.Cookies!.Select(x => $"{x.Key}={x.Value}").ToArray(); + return WireMockTestServer.CreateJson(response); + } + + static ResponseMessage HandleSetCookies(IRequestMessage request) { + var cookies = new List { + new("cookie1", "value1", new CookieOptions()), + new("cookie2", "value2", new CookieOptions { Path = "/path_extra" }), + new("cookie3", "value3", new CookieOptions { Expires = DateTimeOffset.Now.AddDays(2) }), + new("cookie4", "value4", new CookieOptions { MaxAge = TimeSpan.FromSeconds(100) }), + new("cookie5", "value5", new CookieOptions { Secure = true }), + new("cookie6", "value6", new CookieOptions { HttpOnly = true }), + new("cookie_empty_domain", "value_empty_domain", new CookieOptions { HttpOnly = true, Domain = string.Empty }) + }; + + var response = new ResponseMessage { + Headers = new Dictionary>(), + BodyData = new BodyData { + DetectedBodyType = BodyType.String, + BodyAsString = "success" + } + }; + + var valuesList = new WireMockList(); + valuesList.AddRange(cookies.Select(cookie => cookie.Options.GetHeader(cookie.Name, cookie.Value))); + response.Headers.Add(KnownHeaders.SetCookie, valuesList); + + return response; + } + + record CookieInternal(string Name, string Value, CookieOptions Options); + + public void Dispose() { + _client.Dispose(); + _server.Dispose(); } } + +static class CookieExtensions { + public static string GetHeader(this CookieOptions self, string name, string value) { + var cookieHeader = new SetCookieHeaderValue((StringSegment)name, (StringSegment)value) { + Domain = (StringSegment)self.Domain, + Path = (StringSegment)self.Path, + Expires = self.Expires, + Secure = self.Secure, + HttpOnly = self.HttpOnly, + MaxAge = self.MaxAge, + SameSite = (Microsoft.Net.Http.Headers.SameSiteMode)self.SameSite + }; + return cookieHeader.ToString(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs index 742717d88..ec9aaefd2 100644 --- a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs +++ b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs @@ -1,59 +1,44 @@ -using System.Net; +using RestSharp.Tests.Integrated.Fixtures; using RestSharp.Tests.Integrated.Server; using RestSharp.Tests.Shared.Fixtures; namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] public sealed class DefaultParameterTests : IDisposable { - readonly TestServerFixture _fixture; - readonly ITestOutputHelper _testOutputHelper; - readonly SimpleServer _server; - - public DefaultParameterTests(TestServerFixture fixture, ITestOutputHelper testOutputHelper) { - _fixture = fixture; - _testOutputHelper = testOutputHelper; - _server = SimpleServer.Create(RequestHandler.Handle); - } + readonly WireMockServer _server = WireMockServer.Start(); + readonly RequestBodyCapturer _capturer; + + public DefaultParameterTests() => _capturer = _server.ConfigureBodyCapturer(Method.Get, false); public void Dispose() => _server.Dispose(); [Fact] public async Task Should_add_default_and_request_query_get_parameters() { - var client = new RestClient(_server.Url).AddDefaultParameter("foo", "bar", ParameterType.QueryString); - var request = new RestRequest().AddParameter("foo1", "bar1", ParameterType.QueryString); + var client = new RestClient(_server.Url!).AddDefaultParameter("foo", "bar", ParameterType.QueryString); + var request = new RestRequest().AddParameter("foo1", "bar1", ParameterType.QueryString); await client.GetAsync(request); - var query = RequestHandler.Url!.Query; + var query = _capturer.Url!.Query; query.Should().Contain("foo=bar"); query.Should().Contain("foo1=bar1"); } [Fact] public async Task Should_add_default_and_request_url_get_parameters() { - var client = new RestClient($"{_server.Url}{{foo}}/").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); + var client = new RestClient($"{_server.Url}/{{foo}}/").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); var request = new RestRequest("{foo1}").AddParameter("foo1", "bar1", ParameterType.UrlSegment); await client.GetAsync(request); - RequestHandler.Url!.Segments.Should().BeEquivalentTo("/", "bar/", "bar1"); + _capturer.Url!.Segments.Should().BeEquivalentTo("/", "bar/", "bar1"); } [Fact] public async Task Should_not_throw_exception_when_name_is_null() { - var client = new RestClient($"{_fixture.Server.Url}/request-echo").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); + var client = new RestClient($"{_server.Url}/request-echo").AddDefaultParameter("foo", "bar", ParameterType.UrlSegment); var request = new RestRequest("{foo1}").AddParameter(null, "value", ParameterType.RequestBody); await client.ExecuteAsync(request); } - - static class RequestHandler { - public static Uri? Url { get; private set; } - - public static void Handle(HttpListenerContext context) { - Url = context.Request.Url; - Handlers.Echo(context); - } - } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs index f2cd59482..686cc0a0e 100644 --- a/test/RestSharp.Tests.Integrated/DownloadFileTests.cs +++ b/test/RestSharp.Tests.Integrated/DownloadFileTests.cs @@ -1,66 +1,58 @@ -using System.Net; -using System.Text; -using RestSharp.Tests.Shared.Fixtures; +using System.Text; namespace RestSharp.Tests.Integrated; public sealed class DownloadFileTests : IDisposable { + const string LocalPath = "Assets/Koala.jpg"; + public DownloadFileTests() { - _server = HttpServerFixture.StartServer("Assets/Koala.jpg", FileHandler); - var options = new RestClientOptions(_server.Url) { ThrowOnAnyError = true }; + // _server = HttpServerFixture.StartServer("Assets/Koala.jpg", FileHandler); + + var pathToFile = Path.Combine(_path, Path.Combine(LocalPath.Split('/'))); + + _server + .Given(Request.Create().WithPath($"/{LocalPath}")) + .RespondWith(Response.Create().WithBodyFromFile(pathToFile)); + var options = new RestClientOptions($"{_server.Url}/{LocalPath}") { ThrowOnAnyError = true }; _client = new RestClient(options); } public void Dispose() => _server.Dispose(); - void FileHandler(HttpListenerRequest request, HttpListenerResponse response) { - var pathToFile = Path.Combine( - _path, - Path.Combine( - request.Url!.Segments.Select(s => s.Replace("/", "")).ToArray() - ) - ); - - using var reader = new StreamReader(pathToFile); - - reader.BaseStream.CopyTo(response.OutputStream); - } - - readonly HttpServerFixture _server; - readonly RestClient _client; - readonly string _path = AppDomain.CurrentDomain.BaseDirectory; + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + readonly string _path = AppDomain.CurrentDomain.BaseDirectory; [Fact] public async Task AdvancedResponseWriter_without_ResponseWriter_reads_stream() { var tag = string.Empty; - // ReSharper disable once UseObjectOrCollectionInitializer - var rr = new RestRequest("Assets/Koala.jpg"); - - rr.AdvancedResponseWriter = (response, request) => { - var buf = new byte[16]; - // ReSharper disable once MustUseReturnValue - response.Content.ReadAsStream().Read(buf, 0, buf.Length); - tag = Encoding.ASCII.GetString(buf, 6, 4); - return new RestResponse(request); + var rr = new RestRequest("") { + AdvancedResponseWriter = (response, request) => { + var buf = new byte[16]; + // ReSharper disable once MustUseReturnValue + response.Content.ReadAsStreamAsync().GetAwaiter().GetResult().Read(buf, 0, buf.Length); + tag = Encoding.ASCII.GetString(buf, 6, 4); + return new RestResponse(request); + } }; await _client.ExecuteAsync(rr); - Assert.True(string.Compare("JFIF", tag, StringComparison.Ordinal) == 0); + Assert.Equal(0, string.Compare("JFIF", tag, StringComparison.Ordinal)); } [Fact] public async Task Handles_File_Download_Failure() { - var request = new RestRequest("Assets/Koala1.jpg"); + var request = new RestRequest("some/other/path"); var task = () => _client.DownloadDataAsync(request); await task.Should().ThrowAsync().WithMessage("Request failed with status code NotFound"); } [Fact] public async Task Handles_Binary_File_Download() { - var request = new RestRequest("Assets/Koala.jpg"); + var request = new RestRequest(""); var response = await _client.DownloadDataAsync(request); - var expected = await File.ReadAllBytesAsync(Path.Combine(_path, "Assets", "Koala.jpg")); + var expected = File.ReadAllBytes(Path.Combine(_path, Path.Combine(LocalPath.Split('/')))); Assert.Equal(expected, response); } @@ -69,22 +61,21 @@ public sealed class DownloadFileTests : IDisposable { public async Task Writes_Response_To_Stream() { var tempFile = Path.GetTempFileName(); - // ReSharper disable once UseObjectOrCollectionInitializer - var request = new RestRequest("Assets/Koala.jpg"); - - request.ResponseWriter = responseStream => { - using var writer = File.OpenWrite(tempFile); - - responseStream.CopyTo(writer); - return null; + var request = new RestRequest("") { + ResponseWriter = responseStream => { + using var writer = File.OpenWrite(tempFile); + responseStream.CopyTo(writer); + return null; + } }; + var response = await _client.DownloadDataAsync(request); Assert.Null(response); - var fromTemp = await File.ReadAllBytesAsync(tempFile); - var expected = await File.ReadAllBytesAsync(Path.Combine(_path, "Assets", "Koala.jpg")); + var fromTemp = File.ReadAllBytes(tempFile); + var expected = File.ReadAllBytes(Path.Combine(_path, Path.Combine(LocalPath.Split('/')))); Assert.Equal(expected, fromTemp); } -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs b/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs new file mode 100644 index 000000000..b6245d7c5 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Fixtures/CookieExtensions.cs @@ -0,0 +1,15 @@ +using System.Net; + +namespace RestSharp.Tests.Integrated.Fixtures; + +static class CookieExtensions { + public static Cookie? Find(this CookieCollection? cookieCollection, string name) { + if (cookieCollection == null) return null; + for (var i = 0; i < cookieCollection.Count; i++) { + var cookie = cookieCollection[i]; + if (cookie.Name == name) return cookie; + } + + return null; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Fixtures/RequestBodyFixture.cs b/test/RestSharp.Tests.Integrated/Fixtures/RequestBodyFixture.cs deleted file mode 100644 index 367563729..000000000 --- a/test/RestSharp.Tests.Integrated/Fixtures/RequestBodyFixture.cs +++ /dev/null @@ -1,11 +0,0 @@ -using RestSharp.Tests.Shared.Fixtures; - -namespace RestSharp.Tests.Integrated.Fixtures; - -public sealed class RequestBodyFixture : IDisposable { - public SimpleServer Server { get; } - - public RequestBodyFixture() => Server = SimpleServer.Create(Handlers.Generic()); - - public void Dispose() => Server.Dispose(); -} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpClientTests.cs b/test/RestSharp.Tests.Integrated/HttpClientTests.cs index fdd63e692..8f1ca4f2b 100644 --- a/test/RestSharp.Tests.Integrated/HttpClientTests.cs +++ b/test/RestSharp.Tests.Integrated/HttpClientTests.cs @@ -1,25 +1,23 @@ using System.Net; using RestSharp.Tests.Integrated.Server; -namespace RestSharp.Tests.Integrated; +namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class HttpClientTests { - readonly TestServerFixture _fixture; - - public HttpClientTests(TestServerFixture fixture) => _fixture = fixture; +public class HttpClientTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); [Fact] public async Task ShouldUseBaseAddress() { - using var httpClient = new HttpClient { BaseAddress = _fixture.Server.Url }; - using var client = new RestClient(httpClient); - + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri(_server.Url!); + using var client = new RestClient(httpClient); + var request = new RestRequest("success"); - var response = await client.ExecuteAsync(request); + var response = await client.ExecuteAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); response.Data!.Message.Should().Be("Works!"); } - record Response(string Message); -} \ No newline at end of file + public void Dispose() => _server.Dispose(); +} diff --git a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs index 434a23828..c943b4159 100644 --- a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs +++ b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs @@ -1,19 +1,35 @@ using System.Net; using RestSharp.Tests.Integrated.Server; +using WireMock.Util; -namespace RestSharp.Tests.Integrated; +namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class HttpHeadersTests { - readonly ITestOutputHelper _output; - readonly RestClient _client; +public class HttpHeadersTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; - public HttpHeadersTests(TestServerFixture fixture, ITestOutputHelper output) { - _output = output; - _client = new RestClient(new RestClientOptions(fixture.Server.Url) { ThrowOnAnyError = true }); - } + public HttpHeadersTests() => _client = new RestClient(new RestClientOptions(_server.Url!) { ThrowOnAnyError = true }); [Fact] + public async Task Ensure_headers_correctly_set_in_the_interceptor() { + const string headerName = "HeaderName"; + const string headerValue = "HeaderValue"; + + var request = new RestRequest("/headers") { + Interceptors = [new HeaderInterceptor(headerName, headerValue)] + }; + + // Run + var response = await _client.ExecuteAsync(request); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + var header = response.Data!.First(x => x.Name == headerName); + header.Should().NotBeNull(); + header.Value.Should().Be(headerValue); + } + + [Fact, Obsolete("Obsolete")] public async Task Ensure_headers_correctly_set_in_the_hook() { const string headerName = "HeaderName"; const string headerValue = "HeaderValue"; @@ -30,6 +46,7 @@ public class HttpHeadersTests { // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data.Should().NotBeNull(); var header = response.Data!.First(x => x.Name == headerName); header.Should().NotBeNull(); header.Value.Should().Be(headerValue); @@ -48,6 +65,7 @@ public class HttpHeadersTests { var response = await _client.ExecuteAsync(request); CheckHeader(defaultHeader); CheckHeader(requestHeader); + return; void CheckHeader(Header header) { var h = response.Data!.First(x => x.Name == header.Name); @@ -57,4 +75,16 @@ public class HttpHeadersTests { } record Header(string Name, string Value); + + class HeaderInterceptor(string headerName, string headerValue) : Interceptors.Interceptor { + public override ValueTask BeforeHttpRequest(HttpRequestMessage requestMessage, CancellationToken cancellationToken) { + requestMessage.Headers.Add(headerName, headerValue); + return default; + } + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs b/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs index 3357f5204..5183f4e09 100644 --- a/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs +++ b/test/RestSharp.Tests.Integrated/Interceptor/InterceptorTests.cs @@ -1,127 +1,174 @@ -// Copyright (c) .NET Foundation and Contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using Moq; -using RestSharp.Tests.Integrated.Server; - -namespace RestSharp.Tests.Integrated.Interceptor; - -[Collection(nameof(TestServerCollection))] -public class InterceptorTests { - readonly RestClient _client; - - public InterceptorTests(TestServerFixture fixture) => _client = new RestClient(fixture.Server.Url); +using RestSharp.Tests.Integrated.Server; + +namespace RestSharp.Tests.Integrated.Interceptor; + +// [Collection(nameof(TestServerCollection))] +public class InterceptorTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); [Fact] - public async Task AddInterceptor_ShouldBeUsed() { - //Arrange - var body = new TestRequest("foo", 100); - var request = new RestRequest("post/json").AddJsonBody(body); - - var mockInterceptor = new Mock(); - var interceptor = mockInterceptor.Object; - var options = _client.Options; - options.Interceptors.Add(interceptor); + public async Task Should_call_client_interceptor() { + // Arrange + var request = CreateRequest(); + + var (client, interceptor) = SetupClient( + test => test.BeforeRequestAction = req => req.AddHeader("foo", "bar") + ); + //Act - var response = await _client.ExecutePostAsync(request); + var response = await client.ExecutePostAsync(request); + //Assert - mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny())); + response.Request.Parameters.Should().Contain(x => x.Name == "foo" && (string)x.Value! == "bar"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeTrue(); + + client.Dispose(); } + [Fact] - public async Task ThrowExceptionIn_InterceptBeforeSerialization_ShouldBeCatchedInTest() { - //Arrange - var body = new TestRequest("foo", 100); - var request = new RestRequest("post/json").AddJsonBody(body); - - var mockInterceptor = new Mock(); - mockInterceptor.Setup(m => m.InterceptBeforeSerialization(It.IsAny())).Throws(() => throw new Exception("DummyException")); - var interceptor = mockInterceptor.Object; - var options = _client.Options; - options.Interceptors.Add(interceptor); + public async Task Should_call_request_interceptor() { + // Arrange + var request = CreateRequest(); + + var client = new RestClient(_server.Url!); + var interceptor = new TestInterceptor(); + request.Interceptors = new List { interceptor }; + //Act - var action = () => _client.ExecutePostAsync(request); + await client.ExecutePostAsync(request); + //Assert - await action.Should().ThrowAsync().WithMessage("DummyException"); - mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny()),Times.Never); - mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny()),Times.Never); - mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),Times.Never); + interceptor.ShouldHaveCalledAll(); + + client.Dispose(); } + [Fact] - public async Task ThrowExceptionIn_InterceptBeforeRequest_ShouldBeCatchableInTest() { - //Arrange - var body = new TestRequest("foo", 100); - var request = new RestRequest("post/json").AddJsonBody(body); - - var mockInterceptor = new Mock(); - mockInterceptor.Setup(m => m.InterceptBeforeRequest(It.IsAny())).Throws(() => throw new Exception("DummyException")); - var interceptor = mockInterceptor.Object; - var options = _client.Options; - options.Interceptors.Add(interceptor); + public async Task Should_call_both_client_and_request_interceptors() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(); + var requestInterceptor = new TestInterceptor(); + request.Interceptors = new List { requestInterceptor }; + //Act - var action = () => _client.ExecutePostAsync(request); + await client.ExecutePostAsync(request); + //Assert - await action.Should().ThrowAsync().WithMessage("DummyException"); - mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny()),Times.Never); - mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),Times.Never); + interceptor.ShouldHaveCalledAll(); + requestInterceptor.ShouldHaveCalledAll(); + + client.Dispose(); } + [Fact] - public async Task ThrowExceptionIn_InterceptAfterRequest_ShouldBeCatchableInTest() { + public async Task ThrowExceptionIn_InterceptBeforeRequest() { //Arrange - var body = new TestRequest("foo", 100); - var request = new RestRequest("post/json").AddJsonBody(body); + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.BeforeRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeFalse(); + interceptor.AfterHttpRequestCalled.Should().BeFalse(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); - var mockInterceptor = new Mock(); - mockInterceptor.Setup(m => m.InterceptAfterRequest(It.IsAny())).Throws(() => throw new Exception("DummyException")); - var interceptor = mockInterceptor.Object; - var options = _client.Options; - options.Interceptors.Add(interceptor); + client.Dispose(); + } + + [Fact] + public async Task ThrowExceptionIn_InterceptBeforeHttpRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.BeforeHttpRequestAction = req => throw new Exception("DummyException")); + //Act - var action = () => _client.ExecutePostAsync(request); + var action = () => client.ExecutePostAsync(request); + //Assert await action.Should().ThrowAsync().WithMessage("DummyException"); - mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny()),Times.Never); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeFalse(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); } + [Fact] - public async Task ThrowException_InInterceptBeforeDeserialize_ShouldBeCatchableInTest() { - //Arrange - var body = new TestRequest("foo", 100); - var request = new RestRequest("post/json").AddJsonBody(body); + public async Task ThrowException_InInterceptAfterHttpRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.AfterHttpRequestAction = req => throw new Exception("DummyException")); + + //Act + var action = () => client.ExecutePostAsync(request); + + //Assert + await action.Should().ThrowAsync().WithMessage("DummyException"); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeFalse(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); - var mockInterceptor = new Mock(); - mockInterceptor.Setup(m => m.InterceptBeforeDeserialize(It.IsAny())).Throws(() => throw new Exception("DummyException")); - var interceptor = mockInterceptor.Object; - var options = _client.Options; - options.Interceptors.Add(interceptor); + client.Dispose(); + } + + [Fact] + public async Task ThrowExceptionIn_InterceptAfterRequest() { + // Arrange + var request = CreateRequest(); + var (client, interceptor) = SetupClient(test => test.AfterRequestAction = req => throw new Exception("DummyException")); + //Act - var action = () => _client.PostAsync(request); + var action = () => client.ExecutePostAsync(request); + //Assert await action.Should().ThrowAsync().WithMessage("DummyException"); - mockInterceptor.Verify(m => m.InterceptBeforeSerialization(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptAfterRequest(It.IsAny())); - mockInterceptor.Verify(m => m.InterceptBeforeDeserialize(It.IsAny())); + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeFalse(); + + client.Dispose(); + } + + (RestClient client, TestInterceptor interceptor) SetupClient(Action? configureInterceptor = null) { + var interceptor = new TestInterceptor(); + configureInterceptor?.Invoke(interceptor); + + var options = new RestClientOptions(_server.Url!) { + Interceptors = new List { interceptor } + }; + return (new RestClient(options), interceptor); + } + + static RestRequest CreateRequest() { + var body = new TestRequest("foo", 100); + return new RestRequest("post/json").AddJsonBody(body); + } + + public void Dispose() => _server.Dispose(); +} + +static class InterceptorChecks { + public static void ShouldHaveCalledAll(this TestInterceptor interceptor) { + interceptor.BeforeRequestCalled.Should().BeTrue(); + interceptor.BeforeHttpRequestCalled.Should().BeTrue(); + interceptor.AfterHttpRequestCalled.Should().BeTrue(); + interceptor.AfterRequestCalled.Should().BeTrue(); + interceptor.BeforeDeserializationCalled.Should().BeTrue(); } - - -} \ No newline at end of file +} diff --git a/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs b/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs new file mode 100644 index 000000000..c5c88f8f9 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Interceptor/TestInterceptor.cs @@ -0,0 +1,45 @@ +namespace RestSharp.Tests.Integrated.Interceptor; + +class TestInterceptor : Interceptors.Interceptor { + internal bool BeforeRequestCalled { get; private set; } + internal bool BeforeHttpRequestCalled { get; private set; } + internal bool AfterHttpRequestCalled { get; private set; } + internal bool AfterRequestCalled { get; private set; } + internal bool BeforeDeserializationCalled { get; private set; } + + internal Action? BeforeRequestAction { get; set; } + internal Action? BeforeHttpRequestAction { get; set; } + internal Action? AfterHttpRequestAction { get; set; } + internal Action? AfterRequestAction { get; set; } + internal Action? BeforeDeserializationAction { get; set; } + + public override ValueTask BeforeHttpRequest(HttpRequestMessage req, CancellationToken cancellationToken) { + BeforeHttpRequestCalled = true; + BeforeHttpRequestAction?.Invoke(req); + return base.BeforeHttpRequest(req, cancellationToken); + } + + public override ValueTask AfterHttpRequest(HttpResponseMessage responseMessage, CancellationToken cancellationToken) { + AfterHttpRequestCalled = true; + AfterHttpRequestAction?.Invoke(responseMessage); + return base.AfterHttpRequest(responseMessage, cancellationToken); + } + + public override ValueTask AfterRequest(RestResponse response, CancellationToken cancellationToken) { + AfterRequestCalled = true; + AfterRequestAction?.Invoke(response); + return base.AfterRequest(response, cancellationToken); + } + + public override ValueTask BeforeRequest(RestRequest request, CancellationToken cancellationToken) { + BeforeRequestCalled = true; + BeforeRequestAction?.Invoke(request); + return base.BeforeRequest(request, cancellationToken); + } + + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) { + BeforeDeserializationCalled = true; + BeforeDeserializationAction?.Invoke(response); + return base.BeforeDeserialization(response, cancellationToken); + } +} diff --git a/test/RestSharp.Tests.Integrated/JsonBodyTests.cs b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs index 2687a7a38..9e05ba6b8 100644 --- a/test/RestSharp.Tests.Integrated/JsonBodyTests.cs +++ b/test/RestSharp.Tests.Integrated/JsonBodyTests.cs @@ -5,40 +5,38 @@ namespace RestSharp.Tests.Integrated; #pragma warning disable xUnit1033 -public sealed class JsonBodyTests : IClassFixture { - readonly SimpleServer _server; - readonly ITestOutputHelper _output; - readonly RestClient _client; - - public JsonBodyTests(RequestBodyFixture fixture, ITestOutputHelper output) { - _output = output; - _server = fixture.Server; - _client = new RestClient(_server.Url); - } +public sealed class JsonBodyTests : IDisposable { + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; + + public JsonBodyTests() => _client = new RestClient(_server.Url!); [Fact] public async Task Query_Parameters_With_Json_Body() { + var capturer = _server.ConfigureBodyCapturer(Method.Put); + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Put) .AddJsonBody(new { displayName = "Display Name" }) .AddQueryParameter("key", "value"); await _client.ExecuteAsync(request); - RequestBodyCapturer.CapturedUrl.ToString().Should().Be($"{_server.Url}Capture?key=value"); - RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8"); - RequestBodyCapturer.CapturedEntityBody.Should().Be("{\"displayName\":\"Display Name\"}"); + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be("{\"displayName\":\"Display Name\"}"); + capturer.Url.Should().Be($"{_server.Url}{RequestBodyCapturer.Resource}?key=value"); } [Fact] public async Task Add_JSON_body_JSON_string() { const string payload = "{\"displayName\":\"Display Name\"}"; - var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload); + var capturer = _server.ConfigureBodyCapturer(Method.Post); + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload); await _client.ExecuteAsync(request); - RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8"); - RequestBodyCapturer.CapturedEntityBody.Should().Be(payload); + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be(payload); } [Fact] @@ -55,11 +53,17 @@ public sealed class JsonBodyTests : IClassFixture { },"; var expected = JsonSerializer.Serialize(payload); - var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload, true); + var capturer = _server.ConfigureBodyCapturer(Method.Post); + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post).AddJsonBody(payload, true); await _client.ExecuteAsync(request); - RequestBodyCapturer.CapturedContentType.Should().Be("application/json; charset=utf-8"); - RequestBodyCapturer.CapturedEntityBody.Should().Be(expected); + capturer.ContentType.Should().Be("application/json; charset=utf-8"); + capturer.Body.Should().Be(expected); + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); } -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs index 0dc147825..993c03637 100644 --- a/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs +++ b/test/RestSharp.Tests.Integrated/MultipartFormDataTests.cs @@ -1,9 +1,6 @@ -using System.Net; -using HttpTracer; +using HttpTracer; using RestSharp.Tests.Integrated.Fixtures; using RestSharp.Tests.Shared.Fixtures; -#pragma warning disable CS8618 -#pragma warning disable CS8601 namespace RestSharp.Tests.Integrated; @@ -12,15 +9,19 @@ public sealed class MultipartFormDataTests : IDisposable { public MultipartFormDataTests(ITestOutputHelper output) { _output = output; - _server = SimpleServer.Create(RequestHandler.Handle); + _server = WireMockServer.Start(); - var options = new RestClientOptions(_server.Url) { + _capturer = _server.ConfigureBodyCapturer(Method.Post); + var options = new RestClientOptions($"{_server.Url!}{RequestBodyCapturer.Resource}") { ConfigureMessageHandler = handler => new HttpTracerHandler(handler, new OutputLogger(output), HttpMessageParts.All) }; _client = new RestClient(options); } - public void Dispose() => _server.Dispose(); + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } const string LineBreak = "\r\n"; @@ -34,30 +35,22 @@ public sealed class MultipartFormDataTests : IDisposable { $"--{{0}}--{LineBreak}"; const string ExpectedFileAndBodyRequestContent = - "--{0}" + - $"{LineBreak}{KnownHeaders.ContentType}: application/octet-stream" + + "--{0}" + + $"{LineBreak}{KnownHeaders.ContentType}: application/octet-stream" + $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=\"fileName\"; filename=\"TestFile.txt\"" + - $"{LineBreak}{LineBreak}This is a test file for RestSharp.{LineBreak}" + - $"--{{0}}{LineBreak}{KnownHeaders.ContentType}: application/json; {CharsetString}" + - $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=controlName" + - $"{LineBreak}{LineBreak}test{LineBreak}" + + $"{LineBreak}{LineBreak}This is a test file for RestSharp.{LineBreak}" + + $"--{{0}}{LineBreak}{KnownHeaders.ContentType}: application/json; {CharsetString}" + + $"{LineBreak}{KnownHeaders.ContentDisposition}: form-data; name=controlName" + + $"{LineBreak}{LineBreak}test{LineBreak}" + $"--{{0}}--{LineBreak}"; const string ExpectedDefaultMultipartContentType = "multipart/form-data; boundary=\"{0}\""; const string ExpectedCustomMultipartContentType = "multipart/vnd.resteasy+form-data; boundary=\"{0}\""; - readonly SimpleServer _server; - readonly RestClient _client; - - static class RequestHandler { - public static string CapturedContentType { get; set; } - - public static void Handle(HttpListenerContext context) { - CapturedContentType = context.Request.ContentType; - Handlers.Echo(context); - } - } + readonly WireMockServer _server; + readonly RestClient _client; + readonly RequestBodyCapturer _capturer; static void AddParameters(RestRequest request) { request.AddParameter("foo", "bar"); @@ -85,12 +78,12 @@ static class RequestHandler { AddParameters(request); request.MultipartFormQuoteBoundary = false; - var response = await _client.ExecuteAsync(request); + await _client.ExecuteAsync(request); var expected = string.Format(Expected, request.FormBoundary); - response.Content.Should().Be(expected); - RequestHandler.CapturedContentType.Should().Be($"multipart/form-data; boundary={request.FormBoundary}"); + _capturer.Body.Should().Be(expected); + _capturer.ContentType.Should().Be($"multipart/form-data; boundary={request.FormBoundary}"); } [Fact] @@ -106,8 +99,8 @@ static class RequestHandler { _output.WriteLine($"Expected: {expected}"); _output.WriteLine($"Actual: {response.Content}"); - response.Content.Should().Be(expected); - RequestHandler.CapturedContentType.Should().Be($"multipart/form-data; boundary=\"{request.FormBoundary}\""); + _capturer.Body.Should().Be(expected); + _capturer.ContentType.Should().Be($"multipart/form-data; boundary=\"{request.FormBoundary}\""); } [Fact] @@ -129,8 +122,8 @@ static class RequestHandler { _output.WriteLine($"Expected: {expectedFileAndBodyRequestContent}"); _output.WriteLine($"Actual: {response.Content}"); - response.Content.Should().Be(expectedFileAndBodyRequestContent); - expectedDefaultMultipartContentType.Should().Be(RequestHandler.CapturedContentType); + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); + _capturer.ContentType.Should().Be(expectedDefaultMultipartContentType); } [Fact] @@ -144,14 +137,14 @@ static class RequestHandler { request.AddFile("fileName", path); request.AddParameter(new BodyParameter("controlName", "test", "application/json")); - var response = await _client.ExecuteAsync(request); + await _client.ExecuteAsync(request); var boundary = request.FormBoundary; var expectedFileAndBodyRequestContent = string.Format(ExpectedFileAndBodyRequestContent, boundary); var expectedCustomMultipartContentType = string.Format(ExpectedCustomMultipartContentType, boundary); - response.Content.Should().Be(expectedFileAndBodyRequestContent); - RequestHandler.CapturedContentType.Should().Be(expectedCustomMultipartContentType); + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); + _capturer.ContentType.Should().Be(expectedCustomMultipartContentType); } [Fact] @@ -165,12 +158,12 @@ static class RequestHandler { request.AddParameter(new BodyParameter("controlName", "test", "application/json")); - var response = await _client.ExecuteAsync(request); + await _client.ExecuteAsync(request); var boundary = request.FormBoundary; var expectedFileAndBodyRequestContent = string.Format(ExpectedFileAndBodyRequestContent, boundary); - response.Content.Should().Be(expectedFileAndBodyRequestContent); + _capturer.Body.Should().Be(expectedFileAndBodyRequestContent); } [Fact] @@ -179,12 +172,37 @@ static class RequestHandler { AddParameters(request); - var response = await _client.ExecuteAsync(request); + await _client.ExecuteAsync(request); var boundary = request.FormBoundary; var expected = string.Format(Expected, boundary); - response.Content.Should().Be(expected); + _capturer.Body.Should().Be(expected); } + + [Fact] + public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() { + using var client = new RestClient(_server.Url!); -} + var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post) { + AlwaysMultipartFormData = true, + }; + var capturer = _server.ConfigureBodyCapturer(Method.Post); + + const string bodyData = "abc123 foo bar baz BING!"; + const string multipartName = "mybody"; + + request.AddParameter(new BodyParameter(multipartName, bodyData, ContentType.Plain)); + + await client.ExecuteAsync(request); + + var expectedBody = new[] { + ContentTypeString, + $"{ContentDispositionString} name={multipartName}", + bodyData + }; + + var actual = capturer.Body!.Replace("\n", string.Empty).Split('\r'); + actual.Should().Contain(expectedBody); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs index 6f6d8ebda..a05fe1dd7 100644 --- a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs +++ b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs @@ -1,26 +1,22 @@ -using System.Net; -using RestSharp.Tests.Shared.Fixtures; +using RestSharp.Tests.Integrated.Server; namespace RestSharp.Tests.Integrated; public sealed class NonProtocolExceptionHandlingTests : IDisposable { + public NonProtocolExceptionHandlingTests() + => _server + .Given(Request.Create().WithPath("/timeout")) + .RespondWith(Response.Create().WithDelay(TimeSpan.FromSeconds(1))); + // ReSharper disable once ClassNeverInstantiated.Local class StupidClass { // ReSharper disable once UnusedMember.Local public string Property { get; set; } = null!; } - /// - /// Simulates a long server process that should result in a client timeout - /// - /// - static void TimeoutHandler(HttpListenerContext context) => Thread.Sleep(101000); - - public NonProtocolExceptionHandlingTests() => _server = SimpleServer.Create(TimeoutHandler); - public void Dispose() => _server.Dispose(); - readonly SimpleServer _server; + readonly WireMockServer _server = WireMockServer.Start(); /// /// Success of this test is based largely on the behavior of your current DNS. @@ -32,14 +28,14 @@ class StupidClass { var request = new RestRequest("foo"); var response = await client.ExecuteAsync(request); - Assert.Equal(ResponseStatus.Error, response.ResponseStatus); + response.ResponseStatus.Should().Be(ResponseStatus.Error); } [Fact] public async Task Handles_HttpClient_Timeout_Error() { - var client = new RestClient(new HttpClient {Timeout = TimeSpan.FromMilliseconds(500)}); + var client = new RestClient(new HttpClient { Timeout = TimeSpan.FromMilliseconds(500) }); - var request = new RestRequest($"{_server.Url}/404"); + var request = new RestRequest($"{_server.Url}/timeout"); var response = await client.ExecuteAsync(request); response.ErrorException.Should().BeOfType(); @@ -48,9 +44,8 @@ class StupidClass { [Fact] public async Task Handles_Server_Timeout_Error() { - var client = new RestClient(_server.Url); - - var request = new RestRequest("404") { Timeout = 500 }; + var client = new RestClient(_server.Url!); + var request = new RestRequest("timeout") { Timeout = 500 }; var response = await client.ExecuteAsync(request); response.ErrorException.Should().BeOfType(); @@ -59,9 +54,9 @@ class StupidClass { [Fact] public async Task Handles_Server_Timeout_Error_With_Deserializer() { - var client = new RestClient(_server.Url); - var request = new RestRequest("404") { Timeout = 500 }; - var response = await client.ExecuteAsync(request); + var client = new RestClient(_server.Url!); + var request = new RestRequest("timeout") { Timeout = 500 }; + var response = await client.ExecuteAsync(request); response.Data.Should().BeNull(); response.ErrorException.Should().BeOfType(); diff --git a/test/RestSharp.Tests.Integrated/RequestHeadTests.cs b/test/RestSharp.Tests.Integrated/NtlmTests.cs similarity index 82% rename from test/RestSharp.Tests.Integrated/RequestHeadTests.cs rename to test/RestSharp.Tests.Integrated/NtlmTests.cs index 77c2d1414..f1a4a1b87 100644 --- a/test/RestSharp.Tests.Integrated/RequestHeadTests.cs +++ b/test/RestSharp.Tests.Integrated/NtlmTests.cs @@ -1,22 +1,25 @@ using System.Net; +using System.Runtime.InteropServices; using RestSharp.Tests.Integrated.Fixtures; using RestSharp.Tests.Shared.Fixtures; namespace RestSharp.Tests.Integrated; -public class RequestHeadTests : CaptureFixture { +/// +/// These tests use NTML auth and don't work on Linux, at least not in GH Actions +/// +public class NtlmTests : CaptureFixture { [Fact] public async Task Does_Not_Pass_Default_Credentials_When_Server_Does_Not_Negotiate() { - using var server = SimpleServer.Create(Handlers.Generic()); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return; - var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = true }); + using var server = SimpleServer.Create(Handlers.Generic()); + var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = true }); var request = new RestRequest(RequestHeadCapturer.Resource); - await client.ExecuteAsync(request); Assert.NotNull(RequestHeadCapturer.CapturedHeaders); - var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray(); Assert.False( @@ -27,6 +30,8 @@ public class RequestHeadTests : CaptureFixture { [Fact] public async Task Does_Not_Pass_Default_Credentials_When_UseDefaultCredentials_Is_False() { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return; + using var server = SimpleServer.Create(Handlers.Generic(), AuthenticationSchemes.Negotiate); var client = new RestClient(new RestClientOptions(server.Url) { UseDefaultCredentials = false }); @@ -39,7 +44,7 @@ public class RequestHeadTests : CaptureFixture { [Fact] public async Task Passes_Default_Credentials_When_UseDefaultCredentials_Is_True() { - if (!OperatingSystem.IsWindows()) return; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; using var server = SimpleServer.Create(Handlers.Generic(), AuthenticationSchemes.Negotiate); @@ -58,4 +63,4 @@ public class RequestHeadTests : CaptureFixture { "Authorization header not present in HTTP request from client, even though UseDefaultCredentials = true" ); } -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/PostTests.cs b/test/RestSharp.Tests.Integrated/PostTests.cs index d31e7c801..b695c80a5 100644 --- a/test/RestSharp.Tests.Integrated/PostTests.cs +++ b/test/RestSharp.Tests.Integrated/PostTests.cs @@ -3,11 +3,11 @@ namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] public class PostTests { - readonly RestClient _client; - - public PostTests(TestServerFixture fixture) => _client = new RestClient(fixture.Server.Url); + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; + + public PostTests() => _client = new RestClient(_server.Url!); [Fact] public async Task Should_post_json() { @@ -75,4 +75,4 @@ class Response { } record PostParameter(string Name, string Value); -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/ProxyTests.cs b/test/RestSharp.Tests.Integrated/ProxyTests.cs index cec1571bd..5562fe3fd 100644 --- a/test/RestSharp.Tests.Integrated/ProxyTests.cs +++ b/test/RestSharp.Tests.Integrated/ProxyTests.cs @@ -1,19 +1,15 @@ -using System.Net; -using RestSharp.Tests.Shared.Fixtures; - -namespace RestSharp.Tests.Integrated; +namespace RestSharp.Tests.Integrated; public class ProxyTests { [Fact] public async Task Set_Invalid_Proxy_Fails() { - using var server = HttpServerFixture.StartServer((_, _) => { }); - - var client = new RestClient(new RestClientOptions(server.Url) { Proxy = new WebProxy("non_existent_proxy", false) }); - var request = new RestRequest(); + using var server = WireMockServer.Start(); + using var client = new RestClient(new RestClientOptions(server.Url!) { Proxy = new WebProxy("non_existent_proxy", false) }); + var request = new RestRequest(); var response = await client.ExecuteAsync(request); - Assert.False(response.IsSuccessful); + response.IsSuccessful.Should().BeFalse(); response.ErrorException.Should().BeOfType(); } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/PutTests.cs b/test/RestSharp.Tests.Integrated/PutTests.cs index 1a0e09f89..2bd2c9bd0 100644 --- a/test/RestSharp.Tests.Integrated/PutTests.cs +++ b/test/RestSharp.Tests.Integrated/PutTests.cs @@ -1,25 +1,22 @@ using System.Text.Json; using RestSharp.Tests.Integrated.Server; -using static RestSharp.Tests.Integrated.Server.HttpServer; -namespace RestSharp.Tests.Integrated; +// using static RestSharp.Tests.Integrated.Server.HttpServer; -[Collection(nameof(TestServerCollection))] -public class PutTests { - readonly ITestOutputHelper _output; - readonly RestClient _client; +namespace RestSharp.Tests.Integrated; - static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); +public class PutTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; + + public PutTests() => _client = new(_server.Url!); - public PutTests(TestServerFixture fixture, ITestOutputHelper output) { - _output = output; - _client = new RestClient(fixture.Server.Url); - } + static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); [Fact] public async Task Should_put_json_body() { - var body = new TestRequest("foo", 100); - var request = new RestRequest(ContentResource).AddJsonBody(body); + var body = new TestRequest("foo", 100); + var request = new RestRequest("/content").AddJsonBody(body); var response = await _client.PutAsync(request); @@ -30,14 +27,14 @@ public class PutTests { [Fact] public async Task Should_put_json_body_using_extension() { var body = new TestRequest("foo", 100); - var response = await _client.PutJsonAsync(ContentResource, body); - + var response = await _client.PutJsonAsync("/content", body); + response.Should().BeEquivalentTo(body); } [Fact] public async Task Can_Timeout_PUT_Async() { - var request = new RestRequest(TimeoutResource, Method.Put).AddBody("Body_Content"); + var request = new RestRequest("/timeout", Method.Put).AddBody("Body_Content"); // Half the value of ResponseHandler.Timeout request.Timeout = 200; @@ -46,7 +43,11 @@ public class PutTests { Assert.Equal(ResponseStatus.TimedOut, response.ResponseStatus); } - + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } } public record TestRequest(string Data, int Number); \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RedirectTests.cs b/test/RestSharp.Tests.Integrated/RedirectTests.cs index 47b4954a2..7b00bb4a7 100644 --- a/test/RestSharp.Tests.Integrated/RedirectTests.cs +++ b/test/RestSharp.Tests.Integrated/RedirectTests.cs @@ -13,21 +13,15 @@ // limitations under the License. // -using System.Net; -using RestSharp.Tests.Integrated.Server; - namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class RedirectTests { - readonly RestClient _client; +using Server; - public RedirectTests(TestServerFixture fixture) { - var options = new RestClientOptions(fixture.Server.Url) { - FollowRedirects = true - }; - _client = new RestClient(options); - } +public class RedirectTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; + + public RedirectTests() => _client = new RestClient(new RestClientOptions(_server.Url!) { FollowRedirects = true }); [Fact] public async Task Can_Perform_GET_Async_With_Redirect() { @@ -35,12 +29,13 @@ public class RedirectTests { var request = new RestRequest("redirect"); - var response = await _client.ExecuteAsync(request); + var response = await _client.ExecuteAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); response.Data!.Message.Should().Be(val); } - class Response { - public string? Message { get; set; } + public void Dispose() { + _server.Dispose(); + _client.Dispose(); } -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestBodyTests.cs b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs index a7c5df2c0..3897d9321 100644 --- a/test/RestSharp.Tests.Integrated/RequestBodyTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestBodyTests.cs @@ -3,40 +3,28 @@ namespace RestSharp.Tests.Integrated; -public class RequestBodyTests : IClassFixture { - readonly ITestOutputHelper _output; - readonly SimpleServer _server; +public class RequestBodyTests : IDisposable { + // const string NewLine = "\r\n"; - const string NewLine = "\r\n"; + static readonly string ExpectedTextContentType = $"{ContentType.Plain}; charset=utf-8"; + static readonly string ExpectedTextContentTypeNoCharset = ContentType.Plain; - const string TextPlainContentType = "text/plain"; - const string ExpectedTextContentType = $"{TextPlainContentType}; charset=utf-8"; - const string ExpectedTextContentTypeNoCharset = TextPlainContentType; - - public RequestBodyTests(RequestBodyFixture fixture, ITestOutputHelper output) { - _output = output; - _server = fixture.Server; - } + readonly WireMockServer _server = WireMockServer.Start(s => s.AllowBodyForAllHttpMethods = true); async Task AssertBody(Method method, bool disableCharset = false) { - var options = new RestClientOptions(_server.Url) { DisableCharset = disableCharset }; - var client = new RestClient(options); - - var request = new RestRequest(RequestBodyCapturer.Resource, method) { - OnBeforeRequest = async m => { - _output.WriteLine(m.ToString()); - _output.WriteLine(await m.Content!.ReadAsStringAsync()); - } - }; + var options = new RestClientOptions(_server.Url!) { DisableCharset = disableCharset }; + using var client = new RestClient(options); + var request = new RestRequest(RequestBodyCapturer.Resource, method); + var capturer = _server.ConfigureBodyCapturer(method); const string bodyData = "abc123 foo bar baz BING!"; - request.AddParameter(TextPlainContentType, bodyData, ParameterType.RequestBody); + request.AddBody(bodyData, ContentType.Plain); await client.ExecuteAsync(request); var expected = disableCharset ? ExpectedTextContentTypeNoCharset : ExpectedTextContentType; - AssertHasRequestBody(expected, bodyData); + AssertHasRequestBody(capturer, expected, bodyData); } [Fact] @@ -53,7 +41,7 @@ public class RequestBodyTests : IClassFixture { [Fact] public Task Can_Be_Added_To_POST_Request_NoCharset() => AssertBody(Method.Post, true); - + [Fact] public Task Can_Be_Added_To_POST_Request() => AssertBody(Method.Post); @@ -67,12 +55,13 @@ public class RequestBodyTests : IClassFixture { public async Task Can_Have_No_Body_Added_To_POST_Request() { const Method httpMethod = Method.Post; - var client = new RestClient(_server.Url); - var request = new RestRequest(RequestBodyCapturer.Resource, httpMethod); + using var client = new RestClient(_server.Url!); + var request = new RestRequest(RequestBodyCapturer.Resource, httpMethod); + var capturer = _server.ConfigureBodyCapturer(httpMethod); await client.ExecuteAsync(request); - AssertHasNoRequestBody(); + AssertHasNoRequestBody(capturer); } [Fact] @@ -81,40 +70,18 @@ public class RequestBodyTests : IClassFixture { [Fact] public Task Can_Be_Added_To_HEAD_Request() => AssertBody(Method.Head); - [Fact] - public async Task MultipartFormData_Without_File_Creates_A_Valid_RequestBody() { - var client = new RestClient(_server.Url); - - var request = new RestRequest(RequestBodyCapturer.Resource, Method.Post) { - AlwaysMultipartFormData = true - }; - const string bodyData = "abc123 foo bar baz BING!"; - const string multipartName = "mybody"; - - request.AddParameter(new BodyParameter(multipartName, bodyData, TextPlainContentType)); - - await client.ExecuteAsync(request); - - var expectedBody = new[] { - $"{KnownHeaders.ContentType}: {ExpectedTextContentType}", - $"{KnownHeaders.ContentDisposition}: form-data; name={multipartName}", - bodyData - }; - - var actual = RequestBodyCapturer.CapturedEntityBody.Split(NewLine); - actual.Should().Contain(expectedBody); + static void AssertHasNoRequestBody(RequestBodyCapturer capturer) { + capturer.ContentType.Should().BeNull(); + capturer.HasBody.Should().BeFalse(); + capturer.Body.Should().BeNullOrEmpty(); } - static void AssertHasNoRequestBody() { - RequestBodyCapturer.CapturedContentType.Should().BeNull(); - RequestBodyCapturer.CapturedHasEntityBody.Should().BeFalse(); - RequestBodyCapturer.CapturedEntityBody.Should().BeNullOrEmpty(); + static void AssertHasRequestBody(RequestBodyCapturer capturer, string contentType, string bodyData) { + capturer.ContentType.Should().Be(contentType); + capturer.HasBody.Should().BeTrue(); + capturer.Body.Should().Be(bodyData); } - static void AssertHasRequestBody(string contentType, string bodyData) { - RequestBodyCapturer.CapturedContentType.Should().Be(contentType); - RequestBodyCapturer.CapturedHasEntityBody.Should().BeTrue(); - RequestBodyCapturer.CapturedEntityBody.Should().Be(bodyData); - } + public void Dispose() => _server.Dispose(); } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs index 7c62b5ea5..1ff517fdc 100644 --- a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs @@ -1,18 +1,14 @@ -using System.Net; -using RestSharp.Tests.Integrated.Server; // ReSharper disable ClassNeverInstantiated.Local namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class RequestFailureTests { - readonly RestClient _client; - readonly TestServerFixture _fixture; +using Server; - public RequestFailureTests(TestServerFixture fixture) { - _client = new RestClient(fixture.Server.Url); - _fixture = fixture; - } +public class RequestFailureTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; + + public RequestFailureTests() => _client = new RestClient(_server.Url!); [Fact] public async Task Handles_GET_Request_Errors() { @@ -25,7 +21,7 @@ public class RequestFailureTests { [Fact] public async Task Handles_GET_Request_Errors_With_Response_Type() { var request = new RestRequest("status?code=404"); - var response = await _client.ExecuteAsync(request); + var response = await _client.ExecuteAsync(request); response.StatusCode.Should().Be(HttpStatusCode.NotFound); response.Data.Should().Be(null); @@ -33,10 +29,10 @@ public class RequestFailureTests { [Fact] public async Task Throws_on_unsuccessful_call() { - var client = new RestClient(new RestClientOptions(_fixture.Server.Url) { ThrowOnAnyError = true }); + using var client = new RestClient(new RestClientOptions(_server.Url!) { ThrowOnAnyError = true }); var request = new RestRequest("status?code=500"); - var task = () => client.ExecuteAsync(request); + var task = () => client.ExecuteAsync(request); await task.Should().ThrowExactlyAsync(); } @@ -61,7 +57,7 @@ public class RequestFailureTests { public async Task GetAsync_generic_throws_on_unsuccessful_call() { var request = new RestRequest("status?code=500"); - var task = () => _client.GetAsync(request); + var task = () => _client.GetAsync(request); await task.Should().ThrowExactlyAsync(); } @@ -69,12 +65,12 @@ public class RequestFailureTests { public async Task GetAsync_returns_null_on_404() { var request = new RestRequest("status?code=404"); - var response = await _client.GetAsync(request); + var response = await _client.GetAsync(request); response.Should().BeNull(); } - class Response { - // ReSharper disable once UnusedMember.Local - public string Message { get; set; } = null!; + public void Dispose() { + _server.Dispose(); + _client.Dispose(); } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RequestTests.cs b/test/RestSharp.Tests.Integrated/RequestTests.cs index 98c04e65e..d80a52684 100644 --- a/test/RestSharp.Tests.Integrated/RequestTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestTests.cs @@ -3,23 +3,27 @@ namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] -public class AsyncTests { - readonly ITestOutputHelper _output; - readonly RestClient _client; - readonly string _host; - - public AsyncTests(TestServerFixture fixture, ITestOutputHelper output) { - _output = output; - _client = new RestClient(fixture.Server.Url); - _host = _client.Options.BaseUrl!.Host; - } +public class AsyncTests : IDisposable { + readonly WireMockServer _server = WireMockTestServer.StartTestServer(); + readonly RestClient _client; + + public AsyncTests() => _client = new RestClient(_server.Url!); + + [Fact] + public async Task Can_Handle_Exception_Thrown_By_Interceptor_BeforeDeserialization() { + const string exceptionMessage = "Thrown from OnBeforeDeserialization"; - class Response { - public string Message { get; set; } = null!; + var request = new RestRequest("success") { + Interceptors = [new ThrowingInterceptor(exceptionMessage)] + }; + + var response = await _client.ExecuteAsync(request); + + Assert.Equal(exceptionMessage, response.ErrorMessage); + Assert.Equal(ResponseStatus.Error, response.ResponseStatus); } - [Fact] + [Fact, Obsolete("Obsolete")] public async Task Can_Handle_Exception_Thrown_By_OnBeforeDeserialization_Handler() { const string exceptionMessage = "Thrown from OnBeforeDeserialization"; @@ -36,7 +40,7 @@ class Response { [Fact] public async Task Can_Perform_ExecuteGetAsync_With_Response_Type() { var request = new RestRequest("success"); - var response = await _client.ExecuteAsync(request); + var response = await _client.ExecuteAsync(request); response.StatusCode.Should().Be(HttpStatusCode.OK); response.Data!.Message.Should().Be("Works!"); @@ -67,7 +71,7 @@ class Response { [Fact] public async Task Can_Perform_Delete_With_Response_Type() { var request = new RestRequest("delete"); - var response = await _client.ExecuteAsync(request, Method.Delete); + var response = await _client.ExecuteAsync(request, Method.Delete); response.StatusCode.Should().Be(HttpStatusCode.OK); response.Data!.Message.Should().Be("Works!"); @@ -76,8 +80,18 @@ class Response { [Fact] public async Task Can_Delete_With_Response_Type_using_extension() { var request = new RestRequest("delete"); - var response = await _client.DeleteAsync(request); + var response = await _client.DeleteAsync(request); response!.Message.Should().Be("Works!"); } -} + + class ThrowingInterceptor(string errorMessage) : Interceptors.Interceptor { + public override ValueTask BeforeDeserialization(RestResponse response, CancellationToken cancellationToken) + => throw new Exception(errorMessage); + } + + public void Dispose() { + _server.Dispose(); + _client.Dispose(); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs index f214a38db..e046cf926 100644 --- a/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs +++ b/test/RestSharp.Tests.Integrated/ResourceStringParametersTests.cs @@ -1,12 +1,7 @@ -using System.Net; -using RestSharp.Tests.Shared.Fixtures; - namespace RestSharp.Tests.Integrated; public sealed class ResourceStringParametersTests : IDisposable { - readonly SimpleServer _server; - - public ResourceStringParametersTests() => _server = SimpleServer.Create(RequestHandler.Handle); + readonly WireMockServer _server = WireMockServer.Start(); public void Dispose() => _server.Dispose(); @@ -14,23 +9,20 @@ public sealed class ResourceStringParametersTests : IDisposable { public async Task Should_keep_to_parameters_with_the_same_name() { const string parameters = "?priority=Low&priority=Medium"; - var client = new RestClient(_server.Url); + var url = ""; + _server + .Given(Request.Create()) + .RespondWith(Response.Create().WithCallback(req => { + url = req.Url; + return new ResponseMessage(); + })); + + using var client = new RestClient(_server.Url!); var request = new RestRequest(parameters); await client.GetAsync(request); - var query = RequestHandler.Url?.Query; + var query = new Uri(url).Query; query.Should().Be(parameters); } - - #nullable disable - static class RequestHandler { - public static Uri Url { get; private set; } - - public static void Handle(HttpListenerContext context) { - Url = context.Request.Url; - Handlers.Echo(context); - } - } - #nullable enable } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj index cd511eb9b..c1e098fe1 100644 --- a/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj +++ b/test/RestSharp.Tests.Integrated/RestSharp.Tests.Integrated.csproj @@ -1,27 +1,36 @@ enable - net6.0;net7.0 + - - - + + + - - - - + + + + - - - - - + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/RootElementTests.cs b/test/RestSharp.Tests.Integrated/RootElementTests.cs index 608bf5238..0285558f5 100644 --- a/test/RestSharp.Tests.Integrated/RootElementTests.cs +++ b/test/RestSharp.Tests.Integrated/RootElementTests.cs @@ -1,16 +1,27 @@ -using System.Net; -using RestSharp.Serializers.Xml; -using RestSharp.Tests.Shared.Extensions; -using RestSharp.Tests.Shared.Fixtures; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Integrated.Server; namespace RestSharp.Tests.Integrated; public class RootElementTests { [Fact] public async Task Copy_RootElement_From_Request_To_IWithRootElement_Deserializer() { - using var server = HttpServerFixture.StartServer("success", Handle); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseXmlSerializer()); + using var server = WireMockServer.Start(); + + const string xmlBody = + """ + + + + Works! + + + """; + server + .Given(Request.Create().WithPath("/success")) + .RespondWith(Response.Create().WithBody(xmlBody).WithHeader(KnownHeaders.ContentType, ContentType.Xml)); + + using var client = new RestClient(server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); var request = new RestRequest("success") { RootElement = "Success" }; @@ -18,19 +29,5 @@ public class RootElementTests { response.Data.Should().NotBeNull(); response.Data!.Message.Should().Be("Works!"); - - static void Handle(HttpListenerRequest req, HttpListenerResponse response) { - response.StatusCode = 200; - response.Headers.Add(KnownHeaders.ContentType, ContentType.Xml); - - response.OutputStream.WriteStringUtf8( - @" - - - Works! - -" - ); - } } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/CookieHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/CookieHandlers.cs index 8dd64962a..40cfc3f25 100644 --- a/test/RestSharp.Tests.Integrated/Server/Handlers/CookieHandlers.cs +++ b/test/RestSharp.Tests.Integrated/Server/Handlers/CookieHandlers.cs @@ -3,17 +3,17 @@ namespace RestSharp.Tests.Integrated.Server.Handlers; public static class CookieHandlers { - public static IResult HandleCookies(HttpContext ctx) { - var results = new List(); + // public static IResult HandleCookies(HttpContext ctx) { + // var results = new List(); + // + // foreach (var (key, value) in ctx.Request.Cookies) { + // results.Add($"{key}={value}"); + // } + // + // return Results.Ok(results); + // } - foreach (var (key, value) in ctx.Request.Cookies) { - results.Add($"{key}={value}"); - } - - return Results.Ok(results); - } - - public static IResult HandleSetCookies(HttpContext ctx) { + public static void HandleSetCookies(HttpContext ctx) { ctx.Response.Cookies.Append("cookie1", "value1"); ctx.Response.Cookies.Append( @@ -65,6 +65,6 @@ public static class CookieHandlers { } ); - return Results.Content("success"); + // return Results.Content("success"); } } diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs index 56740fc7f..88ec8fb02 100644 --- a/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs +++ b/test/RestSharp.Tests.Integrated/Server/Handlers/FileHandlers.cs @@ -1,39 +1,39 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using RestSharp.Extensions; - -namespace RestSharp.Tests.Integrated.Server.Handlers; - -[ApiController] -public class UploadController : ControllerBase { - [HttpPost] - [Route("upload")] - [SuppressMessage("Performance", "CA1822:Mark members as static")] - public async Task Upload([FromForm] FormFile formFile, [FromQuery] bool checkFile = true) { - var file = formFile.File; - - if (!checkFile) { - return Ok(new UploadResponse(file.FileName, file.Length, true)); - } - - var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); - - await using var stream = file.OpenReadStream(); - - var received = await stream.ReadAsBytes(default); - - try { - var expected = await System.IO.File.ReadAllBytesAsync(Path.Combine(assetPath, file.FileName)); - var response = new UploadResponse(file.FileName, file.Length, received.SequenceEqual(expected)); - return Ok(response); - } - catch (Exception e) { - return BadRequest(new { Message = e.Message, Filename = file.FileName }); - } - } -} - -public class FormFile { - public IFormFile File { get; set; } = null!; -} +// using System.Diagnostics.CodeAnalysis; +// using Microsoft.AspNetCore.Http; +// using Microsoft.AspNetCore.Mvc; +// using RestSharp.Extensions; +// +// namespace RestSharp.Tests.Integrated.Server.Handlers; +// +// [ApiController] +// public class UploadController : ControllerBase { +// [HttpPost] +// [Route("upload")] +// [SuppressMessage("Performance", "CA1822:Mark members as static")] +// public async Task Upload([FromForm] FormFile formFile, [FromQuery] bool checkFile = true) { +// var file = formFile.File; +// +// if (!checkFile) { +// return Ok(new UploadResponse(file.FileName, file.Length, true)); +// } +// +// var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); +// +// await using var stream = file.OpenReadStream(); +// +// var received = await stream.ReadAsBytes(default); +// +// try { +// var expected = await System.IO.File.ReadAllBytesAsync(Path.Combine(assetPath, file.FileName)); +// var response = new UploadResponse(file.FileName, file.Length, received.SequenceEqual(expected)); +// return Ok(response); +// } +// catch (Exception e) { +// return BadRequest(new { Message = e.Message, Filename = file.FileName }); +// } +// } +// } +// +// public class FormFile { +// public IFormFile File { get; set; } = null!; +// } diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs index a70e88117..889151cc9 100644 --- a/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs +++ b/test/RestSharp.Tests.Integrated/Server/Handlers/FormRequest.cs @@ -1,12 +1,12 @@ -using Microsoft.AspNetCore.Http; - -namespace RestSharp.Tests.Integrated.Server.Handlers; - -public static class FormRequestHandler { - public static IResult HandleForm(HttpContext ctx) { - var response = ctx.Request.Form.Select( - x => new TestServerResponse(x.Key, x.Value!) - ); - return Results.Ok(response); - } -} +// using Microsoft.AspNetCore.Http; +// +// namespace RestSharp.Tests.Integrated.Server.Handlers; +// +// public static class FormRequestHandler { +// public static IResult HandleForm(HttpContext ctx) { +// var response = ctx.Request.Form.Select( +// x => new TestServerResponse(x.Key, x.Value!) +// ); +// return Results.Ok(response); +// } +// } diff --git a/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs b/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs index d687be698..4df2a1f24 100644 --- a/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs +++ b/test/RestSharp.Tests.Integrated/Server/Handlers/HeaderHandlers.cs @@ -2,9 +2,9 @@ namespace RestSharp.Tests.Integrated.Server.Handlers; -public static class HeaderHandlers { - public static IResult HandleHeaders(HttpContext ctx) { - var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value!)); - return Results.Ok(response); - } -} +// public static class HeaderHandlers { +// public static IResult HandleHeaders(HttpContext ctx) { +// var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value!)); +// return Results.Ok(response); +// } +// } diff --git a/test/RestSharp.Tests.Integrated/Server/Models.cs b/test/RestSharp.Tests.Integrated/Server/Models.cs index 9fd14e186..7fa4a9226 100644 --- a/test/RestSharp.Tests.Integrated/Server/Models.cs +++ b/test/RestSharp.Tests.Integrated/Server/Models.cs @@ -2,4 +2,10 @@ namespace RestSharp.Tests.Integrated.Server; record TestServerResponse(string Name, string Value); -public record UploadResponse(string FileName, long Length, bool Equal); \ No newline at end of file +public record UploadResponse(string FileName, long Length, bool Equal); + +public record SuccessResponse(string Message); + +public class TestResponse { + public string Message { get; set; } = null!; +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/Server/TestServer.cs b/test/RestSharp.Tests.Integrated/Server/TestServer.cs index dd075532a..464bbbcd1 100644 --- a/test/RestSharp.Tests.Integrated/Server/TestServer.cs +++ b/test/RestSharp.Tests.Integrated/Server/TestServer.cs @@ -1,77 +1,77 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using RestSharp.Tests.Integrated.Server.Handlers; -using RestSharp.Tests.Shared.Extensions; -// ReSharper disable ConvertClosureToMethodGroup - -namespace RestSharp.Tests.Integrated.Server; - -public sealed class HttpServer { - readonly WebApplication _app; - - const string Address = "http://localhost:5151"; - - public const string ContentResource = "content"; - public const string TimeoutResource = "timeout"; - - public HttpServer(ITestOutputHelper? output = null) { - var builder = WebApplication.CreateBuilder(); - - if (output != null) builder.Logging.AddXunit(output, LogLevel.Debug); - - builder.Services.AddControllers().AddApplicationPart(typeof(UploadController).Assembly); - builder.WebHost.UseUrls(Address); - _app = builder.Build(); - - _app.MapControllers(); - - _app.MapGet("success", () => new TestResponse { Message = "Works!" }); - _app.MapGet("echo", (string msg) => msg); - _app.MapGet(TimeoutResource, async () => await Task.Delay(2000)); - _app.MapPut(TimeoutResource, async () => await Task.Delay(2000)); - _app.MapGet("status", (int code) => Results.StatusCode(code)); - _app.MapGet("headers", HeaderHandlers.HandleHeaders); - _app.MapGet("request-echo", async context => await context.Request.BodyReader.AsStream().CopyToAsync(context.Response.BodyWriter.AsStream())); - _app.MapDelete("delete", () => new TestResponse { Message = "Works!" }); - - // Cookies - _app.MapGet("get-cookies", CookieHandlers.HandleCookies); - _app.MapGet("set-cookies", CookieHandlers.HandleSetCookies); - _app.MapGet("redirect", () => Results.Redirect("/success", false, true)); - - // PUT - _app.MapPut( - ContentResource, - async context => { - var content = await context.Request.Body.StreamToStringAsync(); - await context.Response.WriteAsync(content); - } - ); - - // Upload file - // var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); - // _app.MapPost("/upload", ctx => FileHandlers.HandleUpload(assetPath, ctx.Request)); - - // POST - _app.MapPost("/post/json", (TestRequest request) => new TestResponse { Message = request.Data }); - - _app.MapPost( - "/post/form", - (HttpContext context) => new TestResponse { Message = $"Works! Length: {context.Request.Form["big_string"].ToString().Length}" } - ); - - _app.MapPost("/post/data", FormRequestHandler.HandleForm); - } - - public Uri Url => new(Address); - - public Task Start() => _app.StartAsync(); - - public async Task Stop() { - await _app.StopAsync(); - await _app.DisposeAsync(); - } -} +// using Microsoft.AspNetCore.Builder; +// using Microsoft.AspNetCore.Hosting; +// using Microsoft.AspNetCore.Http; +// using Microsoft.Extensions.DependencyInjection; +// using Microsoft.Extensions.Logging; +// using RestSharp.Tests.Integrated.Server.Handlers; +// using RestSharp.Tests.Shared.Extensions; +// // ReSharper disable ConvertClosureToMethodGroup +// +// namespace RestSharp.Tests.Integrated.Server; +// +// public sealed class HttpServer1 { +// readonly WebApplication _app; +// +// const string Address = "http://localhost:5151"; +// +// public const string ContentResource = "content"; +// public const string TimeoutResource = "timeout"; +// +// public HttpServer(ITestOutputHelper? output = null) { +// var builder = WebApplication.CreateBuilder(); +// +// if (output != null) builder.Logging.AddXunit(output, LogLevel.Debug); +// +// builder.Services.AddControllers().AddApplicationPart(typeof(UploadController).Assembly); +// builder.WebHost.UseUrls(Address); +// _app = builder.Build(); +// +// _app.MapControllers(); +// +// _app.MapGet("success", () => new TestResponse { Message = "Works!" }); +// _app.MapGet("echo", (string msg) => msg); +// _app.MapGet(TimeoutResource, async () => await Task.Delay(2000)); +// _app.MapPut(TimeoutResource, async () => await Task.Delay(2000)); +// _app.MapGet("status", (int code) => Results.StatusCode(code)); +// _app.MapGet("headers", HeaderHandlers.HandleHeaders); +// _app.MapGet("request-echo", async context => await context.Request.BodyReader.AsStream().CopyToAsync(context.Response.BodyWriter.AsStream())); +// _app.MapDelete("delete", () => new TestResponse { Message = "Works!" }); +// +// // Cookies +// _app.MapGet("get-cookies", CookieHandlers.HandleCookies); +// _app.MapGet("set-cookies", CookieHandlers.HandleSetCookies); +// _app.MapGet("redirect", () => Results.Redirect("/success", false, true)); +// +// // PUT +// _app.MapPut( +// ContentResource, +// async context => { +// var content = await context.Request.Body.StreamToStringAsync(); +// await context.Response.WriteAsync(content); +// } +// ); +// +// // Upload file +// // var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); +// // _app.MapPost("/upload", ctx => FileHandlers.HandleUpload(assetPath, ctx.Request)); +// +// // POST +// _app.MapPost("/post/json", (TestRequest request) => new TestResponse { Message = request.Data }); +// +// _app.MapPost( +// "/post/form", +// (HttpContext context) => new TestResponse { Message = $"Works! Length: {context.Request.Form["big_string"].ToString().Length}" } +// ); +// +// _app.MapPost("/post/data", FormRequestHandler.HandleForm); +// } +// +// public Uri Url => new(Address); +// +// public Task Start() => _app.StartAsync(); +// +// public async Task Stop() { +// await _app.StopAsync(); +// await _app.DisposeAsync(); +// } +// } diff --git a/test/RestSharp.Tests.Integrated/Server/TestServerFixture.cs b/test/RestSharp.Tests.Integrated/Server/TestServerFixture.cs index 44e6312af..84eb6d46c 100644 --- a/test/RestSharp.Tests.Integrated/Server/TestServerFixture.cs +++ b/test/RestSharp.Tests.Integrated/Server/TestServerFixture.cs @@ -1,13 +1,13 @@ namespace RestSharp.Tests.Integrated.Server; -public class TestServerFixture : IAsyncLifetime { - public HttpServer Server { get; } = new(); +// public class TestServerFixture1 : IAsyncLifetime { + // public HttpServer Server { get; } = new(); - public Task InitializeAsync() => Server.Start(); + // public Task InitializeAsync() => Server.Start(); - public Task DisposeAsync() => Server.Stop(); -} + // public Task DisposeAsync() => Server.Stop(); +// } -[CollectionDefinition(nameof(TestServerCollection))] -public class TestServerCollection : ICollectionFixture { } +// [CollectionDefinition(nameof(TestServerCollection))] +// public class TestServerCollection : ICollectionFixture { } diff --git a/test/RestSharp.Tests.Integrated/Server/WireMockTestServer.cs b/test/RestSharp.Tests.Integrated/Server/WireMockTestServer.cs new file mode 100644 index 000000000..59218df32 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/Server/WireMockTestServer.cs @@ -0,0 +1,136 @@ +using System.Text.Json; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using WireMock.Types; +using WireMock.Util; + +namespace RestSharp.Tests.Integrated.Server; + +static class WireMockTestServer { + public static WireMockServer StartTestServer() { + var server = WireMockServer.Start(); + + server + .Given(Request.Create().WithPath("/echo")) + .RespondWith(Response.Create().WithCallback(EchoQuery)); + + server + .Given(Request.Create().WithPath("/success").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(new SuccessResponse("Works!"))); + + server + .Given(Request.Create().WithPath("/delete").UsingDelete()) + .RespondWith(Response.Create().WithBodyAsJson(new SuccessResponse("Works!"))); + + server + .Given(Request.Create().WithPath("/content")) + .RespondWith(Response.Create().WithCallback(EchoJsonBody)); + + server + .Given(Request.Create().WithPath("/post/json").UsingPost()) + .RespondWith(Response.Create().WithCallback(WrapBody)); + + server + .Given(Request.Create().WithPath("/post/data").UsingPost()) + .RespondWith(Response.Create().WithCallback(HandleForm)); + + server + .Given(Request.Create().WithPath("/post/form").UsingPost()) + .RespondWith(Response.Create().WithCallback(WrapForm)); + + server + .Given(Request.Create().WithPath("/timeout")) + .RespondWith(Response.Create().WithDelay(1000)); + + server + .Given(Request.Create().WithPath("/redirect")) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.Redirect).WithHeader("Location", "/success")); + + server + .Given(Request.Create().WithPath("/status").UsingGet()) + .RespondWith(Response.Create().WithCallback(StatusCode)); + + server + .Given(Request.Create().WithPath("/headers")) + .RespondWith(Response.Create().WithCallback(EchoHeaders)); + + return server; + } + + static ResponseMessage WrapForm(IRequestMessage request) { + var response = request.BodyData!.BodyAsFormUrlEncoded!["big_string"].Length; + return CreateJson(new SuccessResponse($"Works! Length: {response}")); + } + + static readonly JsonSerializerOptions JsonOptions = new() { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + static ResponseMessage HandleForm(IRequestMessage request) { + var result = request.BodyData!.BodyAsFormUrlEncoded!.Select(x => new TestServerResponse(x.Key, x.Value)); + return CreateJson(result); + } + + static ResponseMessage EchoQuery(IRequestMessage request) { + var query = request.Query!["msg"]; + var msg = query[0]; + + return new ResponseMessage { + BodyData = new BodyData { + DetectedBodyType = BodyType.String, + BodyAsString = msg + } + }; + } + + static ResponseMessage EchoHeaders(IRequestMessage request) { + var headers = request.Headers!.Select(x => new TestServerResponse(x.Key, x.Value.First())); + return CreateJson(headers); + } + + static ResponseMessage EchoJsonBody(IRequestMessage request) => CreateJson(request.BodyAsJson!); + + static ResponseMessage WrapBody(IRequestMessage request) { + var data = JsonSerializer.Deserialize(request.Body!, JsonOptions); + return CreateJson(new TestResponse { Message = data?.Data ?? "" }); + } + + static ResponseMessage StatusCode(IRequestMessage request) { + var query = request.Query!["code"]; + var statusCode = int.Parse(query[0]); + + return new ResponseMessage { + StatusCode = statusCode + }; + } + + public static ResponseMessage CreateJson(object response) + => new() { + BodyData = new BodyData { + BodyAsJson = response, + DetectedBodyType = BodyType.Json + } + }; + + public static async Task GetFileSection(this IRequestMessage request, string name) { + var headerValue = request.Headers![KnownHeaders.ContentType][0]; + var mediaType = MediaTypeHeaderValue.Parse(headerValue); + var boundary = mediaType.Boundary; + + using var stream = new MemoryStream(request.BodyAsBytes!); + var reader = new MultipartReader(boundary.Value!, stream); + reader.HeadersLengthLimit = int.MaxValue; + + FileMultipartSection? fileSection = null; + while (true) { + var section = await reader.ReadNextSectionAsync(); + if (section == null) break; + fileSection = section.AsFileSection(); + if (fileSection == null) continue; + if (fileSection.Name == name) break; + } + + return fileSection; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/StatusCodeTests.cs b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs index e78a386db..cd9fccadd 100644 --- a/test/RestSharp.Tests.Integrated/StatusCodeTests.cs +++ b/test/RestSharp.Tests.Integrated/StatusCodeTests.cs @@ -1,7 +1,6 @@ using System.Net; using RestSharp.Serializers.Xml; -using RestSharp.Tests.Shared.Extensions; -using RestSharp.Tests.Shared.Fixtures; +using WireMock; // ReSharper disable UnusedMember.Local // ReSharper disable InconsistentNaming @@ -10,71 +9,28 @@ namespace RestSharp.Tests.Integrated; public sealed class StatusCodeTests : IDisposable { public StatusCodeTests() { - _server = SimpleServer.Create(UrlToStatusCodeHandler); - _client = new RestClient(_server.Url, configureSerialization: cfg => cfg.UseXmlSerializer()); + _client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + _server + .Given(Request.Create()) + .RespondWith(Response.Create().WithCallback(CreateResponse)); + return; + + ResponseMessage CreateResponse(IRequestMessage request) { + var url = new Uri(request.Url); + + return new ResponseMessage() { + StatusCode = int.Parse(url.Segments.Last()) + }; + } } - public void Dispose() => _server.Dispose(); - - readonly SimpleServer _server; - readonly RestClient _client; - - static void UrlToStatusCodeHandler(HttpListenerContext obj) => obj.Response.StatusCode = int.Parse(obj.Request.Url!.Segments.Last()); - - [Fact] - public async Task ContentType_Additional_Information() { - _server.SetHandler(Handlers.Generic()); - - var request = new RestRequest("", Method.Post) { - RequestFormat = DataFormat.Json, - Resource = "contenttype_odata" - }; - request.AddBody("bodyadsodajjd"); - request.AddHeader("X-RequestDigest", "xrequestdigestasdasd"); - request.AddHeader(KnownHeaders.Accept, $"{ContentType.Json}; odata=verbose"); - request.AddHeader(KnownHeaders.ContentType, $"{ContentType.Json}; odata=verbose"); - - var response = await _client.ExecuteAsync(request); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - response.IsSuccessful.Should().BeTrue(); - response.IsSuccessStatusCode.Should().BeTrue(); - } - - [Fact] - public async Task Handles_Default_Root_Element_On_No_Error() { - _server.SetHandler(Handlers.Generic()); - - var request = new RestRequest("success") { - RootElement = "Success" - }; - - request.OnBeforeDeserialization = resp => { - if (resp.StatusCode == HttpStatusCode.NotFound) request.RootElement = "Error"; - }; - - var response = await _client.ExecuteAsync(request); - - response.StatusCode.Should().Be(HttpStatusCode.OK); - response.Data!.Message.Should().Be("Works!"); + public void Dispose() { + _server.Dispose(); + _client.Dispose(); } - [Fact] - public async Task Handles_Different_Root_Element_On_Http_Error() { - _server.SetHandler(Handlers.Generic()); - - var request = new RestRequest("error") { - RootElement = "Success", - OnBeforeDeserialization = resp => { - if (resp.StatusCode == HttpStatusCode.BadRequest) resp.RootElement = "Error"; - } - }; - - var response = await _client.ExecuteAsync(request); - - response.StatusCode.Should().Be(HttpStatusCode.BadRequest); - response.Data!.Message.Should().Be("Not found!"); - } + readonly WireMockServer _server = WireMockServer.Start(); + readonly RestClient _client; [Fact] public async Task Handles_GET_Request_404_Error() { @@ -128,54 +84,4 @@ public sealed class StatusCodeTests : IDisposable { response.IsSuccessful.Should().BeFalse(); response.IsSuccessStatusCode.Should().BeFalse(); } -} - -public class ResponseHandler { - void contenttype_odata(HttpListenerContext context) { - var contentType = context.Request.Headers[KnownHeaders.ContentType]; - var hasCorrectHeader = contentType!.Contains($"{ContentType.Json}; odata=verbose"); - context.Response.StatusCode = hasCorrectHeader ? 200 : 400; - } - - void error(HttpListenerContext context) { - context.Response.StatusCode = 400; - context.Response.Headers.Add(KnownHeaders.ContentType, ContentType.Xml); - - context.Response.OutputStream.WriteStringUtf8( - @" - - - Not found! - -" - ); - } - - void errorwithbody(HttpListenerContext context) { - context.Response.StatusCode = 400; - context.Response.Headers.Add(KnownHeaders.ContentType, "application/xml"); - - context.Response.OutputStream.WriteStringUtf8( - @" - - - Not found! - -" - ); - } - - void success(HttpListenerContext context) - => context.Response.OutputStream.WriteStringUtf8( - @" - - - Works! - -" - ); -} - -public class TestResponse { - public string Message { get; set; } = null!; -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs b/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs index 1011f88b1..50c996598 100644 --- a/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs +++ b/test/RestSharp.Tests.Integrated/StructuredSyntaxSuffixTests.cs @@ -1,15 +1,14 @@ -using System.Net; +using System.Text; using RestSharp.Serializers.Xml; -using RestSharp.Tests.Shared.Extensions; -using RestSharp.Tests.Shared.Fixtures; +using WireMock.Types; +using WireMock.Util; // ReSharper disable UnusedAutoPropertyAccessor.Local namespace RestSharp.Tests.Integrated; public sealed class StructuredSyntaxSuffixTests : IDisposable { - readonly TestHttpServer _server; - readonly string _url; + readonly WireMockServer _server; class Person { public string Name { get; set; } = null!; @@ -18,16 +17,26 @@ class Person { } const string XmlContent = "Bob50"; - const string JsonContent = @"{ ""name"":""Bob"", ""age"":50 }"; + const string JsonContent = """{ "name":"Bob", "age":50 }"""; public StructuredSyntaxSuffixTests() { - _server = new TestHttpServer(0, "", HandleRequest); - _url = $"http://localhost:{_server.Port}"; - - static void HandleRequest(HttpListenerRequest request, HttpListenerResponse response, Dictionary p) { - response.ContentType = request.QueryString["ct"]; - response.OutputStream.WriteStringUtf8(request.QueryString["c"]); - response.StatusCode = 200; + _server = WireMockServer.Start(); + _server.Given(Request.Create().WithPath("/").UsingGet()).RespondWith(Response.Create().WithCallback(Handle)); + return; + + static ResponseMessage Handle(IRequestMessage request) { + var response = new ResponseMessage { + Headers = new Dictionary> { + [KnownHeaders.ContentType] = new(request.Query!["ct"]) + }, + StatusCode = 200, + BodyData = new BodyData { + BodyAsString = request.Query["c"].First(), + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + } + }; + return response; } } @@ -35,7 +44,7 @@ class Person { [Fact] public async Task By_default_application_json_content_type_should_deserialize_as_JSON() { - var client = new RestClient(_url); + using var client = new RestClient(_server.Url!); var request = new RestRequest() .AddParameter("ct", "application/json") @@ -49,7 +58,7 @@ class Person { [Fact] public async Task By_default_content_types_with_JSON_structured_syntax_suffix_should_deserialize_as_JSON() { - var client = new RestClient(_url); + using var client = new RestClient(_server.Url!); var request = new RestRequest() .AddParameter("ct", "application/vnd.somebody.something+json") @@ -63,7 +72,7 @@ class Person { [Fact] public async Task By_default_content_types_with_XML_structured_syntax_suffix_should_deserialize_as_XML() { - var client = new RestClient(_url, configureSerialization: cfg => cfg.UseXmlSerializer()); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); var request = new RestRequest() .AddParameter("ct", "application/vnd.somebody.something+xml") @@ -77,7 +86,7 @@ class Person { [Fact] public async Task By_default_text_xml_content_type_should_deserialize_as_XML() { - var client = new RestClient(_url, configureSerialization: cfg => cfg.UseXmlSerializer()); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); var request = new RestRequest() .AddParameter("ct", "text/xml") @@ -88,4 +97,4 @@ class Person { Assert.Equal("Bob", response.Data!.Name); Assert.Equal(50, response.Data.Age); } -} +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/TestResponse.cs b/test/RestSharp.Tests.Integrated/TestResponse.cs new file mode 100644 index 000000000..b3b422c94 --- /dev/null +++ b/test/RestSharp.Tests.Integrated/TestResponse.cs @@ -0,0 +1 @@ +namespace RestSharp.Tests.Integrated; diff --git a/test/RestSharp.Tests.Integrated/UploadFileTests.cs b/test/RestSharp.Tests.Integrated/UploadFileTests.cs index fd9f2c498..ad84e1ad2 100644 --- a/test/RestSharp.Tests.Integrated/UploadFileTests.cs +++ b/test/RestSharp.Tests.Integrated/UploadFileTests.cs @@ -1,24 +1,33 @@ -using System.Net; -using RestSharp.Tests.Integrated.Server; +// ReSharper disable MethodHasAsyncOverload + +using HttpMultipartParser; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using RestSharp.Extensions; namespace RestSharp.Tests.Integrated; -[Collection(nameof(TestServerCollection))] +using Server; + public class UploadFileTests { readonly ITestOutputHelper _output; readonly RestClient _client; readonly string _basePath = AppDomain.CurrentDomain.BaseDirectory; readonly string _path; readonly UploadResponse _expected; + readonly WireMockServer _server = WireMockServer.Start(); const string Filename = "Koala.jpg"; - public UploadFileTests(TestServerFixture fixture, ITestOutputHelper output) { - _output = output; - // _client = new RestClient(new RestClientOptions(fixture.Server.Url) { ThrowOnAnyError = true }); - _client = new RestClient(new RestClientOptions(fixture.Server.Url) { ThrowOnAnyError = false }); + public UploadFileTests(ITestOutputHelper output) { + _output = output; + _client = new RestClient(new RestClientOptions(_server.Url!)); _path = Path.Combine(_basePath, "Assets", Filename); _expected = new UploadResponse(Filename, new FileInfo(_path).Length, true); + + _server + .Given(Request.Create().WithPath("/upload")) + .RespondWith(Response.Create().WithCallback(HandleUpload)); } [Fact] @@ -34,7 +43,7 @@ public class UploadFileTests { [Fact] public async Task Should_upload_from_bytes() { - var bytes = await File.ReadAllBytesAsync(_path); + var bytes = File.ReadAllBytes(_path); var request = new RestRequest("upload").AddFile("file", bytes, Filename); var response = await _client.ExecutePostAsync(request); @@ -52,11 +61,13 @@ public class UploadFileTests { response.Data.Should().BeEquivalentTo(_expected); } +#if !NET6_0 + // This test fails because MultipartFormDataParser doesn't understand filename* [Fact] public async Task Should_upload_from_stream_non_ascii() { const string nonAsciiFilename = "Präsentation_Export.zip"; - var options = new FileParameterOptions { DisableFilenameEncoding = true, DisableFilenameStar = false}; + var options = new FileParameterOptions { DisableFilenameEncoding = true, DisableFilenameStar = false }; var request = new RestRequest("upload") .AddFile("file", () => File.OpenRead(_path), nonAsciiFilename, options: options) @@ -66,4 +77,46 @@ public class UploadFileTests { _output.WriteLine(response.Content); response.Data.Should().BeEquivalentTo(new UploadResponse(nonAsciiFilename, new FileInfo(_path).Length, true)); } -} +#endif + + static async Task HandleUpload(IRequestMessage request) { + var response = new ResponseMessage(); + + var checkFile = request.Query == null || + request.Query.Count == 0 || + request.Query.ContainsKey("checkFile") && bool.Parse(request.Query["checkFile"][0]); + + using var stream = new MemoryStream(request.BodyAsBytes!); + var form = await MultipartFormDataParser.ParseAsync(stream); + if (form.Files.Count == 0) return response; + + var fileSection = form.Files[0]; + var fileLength = fileSection.Data.Length; + +#if !NET6_0 + // Doing this because MultipartFormDataParser doesn't understand filename* + var section = await request.GetFileSection("file"); + var fileName = section!.FileName; +#else + var fileName = fileSection.FileName; +#endif + + // ReSharper disable once InvertIf + if (checkFile) { + var assetPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets"); + + try { + var expected = File.ReadAllBytes(Path.Combine(assetPath, fileName)); + fileSection.Data.Seek(0, SeekOrigin.Begin); + var received = await fileSection.Data.ReadAsBytes(default); + var equal = received.SequenceEqual(expected); + return WireMockTestServer.CreateJson(new UploadResponse(fileName, fileLength, equal)); + } + catch (Exception) { + return response; + } + } + + return WireMockTestServer.CreateJson(new UploadResponse(fileName, fileLength, true)); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/XmlResponseTests.cs b/test/RestSharp.Tests.Integrated/XmlResponseTests.cs new file mode 100644 index 000000000..45cb687fc --- /dev/null +++ b/test/RestSharp.Tests.Integrated/XmlResponseTests.cs @@ -0,0 +1,127 @@ +using System.Net; +using RestSharp.Interceptors; +using RestSharp.Serializers.Xml; +using RestSharp.Tests.Integrated.Server; +using WireMock; + +namespace RestSharp.Tests.Integrated; + +public sealed class XmlResponseTests : IDisposable { + public XmlResponseTests() { + _server = WireMockServer.Start(); + + _server + .Given(Request.Create().WithPath("/contenttype_odata")) + .RespondWith(Response.Create().WithCallback(ContentTypeOData)); + + _server + .Given(Request.Create().WithPath("/success")) + .RespondWith( + Response + .Create() + .WithHeader(KnownHeaders.ContentType, ContentType.Xml) + .WithBody( + """ + + + + Works! + + + """ + ) + ); + + _server + .Given(Request.Create().WithPath("/error")) + .RespondWith( + Response + .Create() + .WithStatusCode(400) + .WithHeader(KnownHeaders.ContentType, ContentType.Xml) + .WithBody( + """ + + + + Not found! + + + """ + ) + ); + + _client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseXmlSerializer()); + } + + public void Dispose() => _server.Dispose(); + + readonly WireMockServer _server; + readonly RestClient _client; + + [Fact] + public async Task Handles_Default_Root_Element_On_No_Error() { + var request = new RestRequest("success") { + RootElement = "Success" + }; + + var interceptor = new CompatibilityInterceptor { + OnBeforeDeserialization = resp => { + if (resp.StatusCode == HttpStatusCode.NotFound) request.RootElement = "Error"; + } + }; + request.Interceptors = [interceptor]; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.Data!.Message.Should().Be("Works!"); + } + + [Fact] + public async Task Handles_Different_Root_Element_On_Http_Error() { + var request = new RestRequest("error") { + RootElement = "Success", + Interceptors = [ + new CompatibilityInterceptor { + OnBeforeDeserialization = resp => { + if (resp.StatusCode == HttpStatusCode.BadRequest) resp.RootElement = "Error"; + } + } + ] + }; + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); + response.Data!.Message.Should().Be("Not found!"); + } + + [Fact] + public async Task ContentType_Additional_Information() { + var request = new RestRequest("", Method.Post) { + RequestFormat = DataFormat.Json, + Resource = "contenttype_odata" + }; + request.AddBody("bodyadsodajjd"); + request.AddHeader("X-RequestDigest", "xrequestdigestasdasd"); + request.AddHeader(KnownHeaders.Accept, $"{ContentType.Json}; odata=verbose"); + request.AddHeader(KnownHeaders.ContentType, $"{ContentType.Json}; odata=verbose"); + + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + response.IsSuccessful.Should().BeTrue(); + response.IsSuccessStatusCode.Should().BeTrue(); + } + + static ResponseMessage ContentTypeOData(IRequestMessage request) { + var contentType = request.Headers![KnownHeaders.ContentType]; + var hasCorrectHeader = contentType!.Contains($"{ContentType.Json}; odata=verbose"); + + var response = new ResponseMessage { + StatusCode = hasCorrectHeader ? 200 : 400 + }; + return response; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs b/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs index e4aee68e8..4f3202b97 100644 --- a/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs +++ b/test/RestSharp.Tests.Serializers.Csv/CsvHelperTests.cs @@ -9,11 +9,20 @@ namespace RestSharp.Tests.Serializers.Csv; -public class CsvHelperTests { +public sealed class CsvHelperTests : IDisposable { static readonly Fixture Fixture = new(); - [Fact] - public async Task Use_CsvHelper_For_Response() { + readonly WireMockServer _server = WireMockServer.Start(); + + void ConfigureResponse(object expected) { + var serializer = new CsvHelperSerializer(); + + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody(serializer.Serialize(expected)!).WithHeader(KnownHeaders.ContentType, ContentType.Csv)); + } + + TestObject CreateTestObject() { var expected = Fixture.Create(); expected.DateTimeValue = new DateTime( @@ -25,18 +34,16 @@ public class CsvHelperTests { expected.DateTimeValue.Second ); - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new CsvHelperSerializer(); + return expected; + } - response.ContentType = "text/csv"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)!); - } - ); + [Fact] + public async Task Use_CsvHelper_For_Response() { + var expected = CreateTestObject(); - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseCsvHelper()); + ConfigureResponse(expected); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); var actual = await client.GetAsync(new RestRequest()); actual.Should().BeEquivalentTo(expected); @@ -48,49 +55,25 @@ public class CsvHelperTests { var expected = new List(count); for (var i = 0; i < count; i++) { - var item = Fixture.Create(); - - item.DateTimeValue = new DateTime( - item.DateTimeValue.Year, - item.DateTimeValue.Month, - item.DateTimeValue.Day, - item.DateTimeValue.Hour, - item.DateTimeValue.Minute, - item.DateTimeValue.Second - ); - + var item = CreateTestObject(); expected.Add(item); } - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new CsvHelperSerializer(); - - response.ContentType = "text/csv"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseCsvHelper()); + ConfigureResponse(expected); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); var actual = await client.GetAsync>(new RestRequest()); actual.Should().BeEquivalentTo(expected); } [Fact] - public async Task DeserilizationFails_IsSuccessfull_Should_BeFalse() { - using var server = HttpServerFixture.StartServer( - (_, response) => { - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "text/csv"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8("invalid csv"); - } - ); + public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() { + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid csv").WithHeader(KnownHeaders.ContentType, ContentType.Csv)); - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseCsvHelper()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseCsvHelper()); var response = await client.ExecuteAsync(new RestRequest()); @@ -100,21 +83,10 @@ public class CsvHelperTests { [Fact] public async Task DeserilizationSucceeds_IsSuccessful_Should_BeTrue() { - var item = Fixture.Create(); - - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new SystemTextJsonSerializer(); - - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "text/csv"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(item)!); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson()); + var item = CreateTestObject(); + ConfigureResponse(item); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson()); var response = await client.ExecuteAsync(new RestRequest()); response.IsSuccessStatusCode.Should().BeTrue(); @@ -180,4 +152,6 @@ public class CsvHelperTests { "StringValue,Int32Value,DecimalValue,DoubleValue,SingleValue,DateTimeValue,TimeSpanValue;hello,32,0,0,16.5,01/20/2024 00:00:00,00:10:00;,65,89.555,0,0,08/19/2022 05:15:21,00:01:01;\"String, with comma\",0,0,20.00001,80000,01/01/0001 00:00:00,00:00:00;" ); } -} + + public void Dispose() => _server?.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj b/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj index 4477eea34..c5305e02b 100644 --- a/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj +++ b/test/RestSharp.Tests.Serializers.Csv/RestSharp.Tests.Serializers.Csv.csproj @@ -3,4 +3,13 @@ + + + + + + + + + diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs index 214d94795..573a04f52 100644 --- a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs +++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedSimpleTests.cs @@ -1,32 +1,24 @@ -using System.Net; -using System.Text; using RestSharp.Serializers.NewtonsoftJson; -using RestSharp.Tests.Shared.Extensions; using RestSharp.Tests.Shared.Fixtures; namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson; -public class IntegratedSimpleTests { - string _body; - - void CaptureBody(HttpListenerRequest request, HttpListenerResponse response) => _body = request.InputStream.StreamToString(); - +public sealed class IntegratedSimpleTests : IDisposable { static readonly Fixture Fixture = new(); + readonly WireMockServer _server = WireMockServer.Start(); + [Fact] public async Task Use_JsonNet_For_Requests() { - using var server = HttpServerFixture.StartServer(CaptureBody); - _body = null; + var capturer = _server.ConfigureBodyCapturer(Method.Post, false); var serializer = new JsonNetSerializer(); - - var testData = Fixture.Create(); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson()); - var request = new RestRequest().AddJsonBody(testData); + var testData = Fixture.Create(); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); + var request = new RestRequest().AddJsonBody(testData); await client.PostAsync(request); - var actual = serializer.Deserialize(new RestResponse(request) { Content = _body! }); + var actual = serializer.Deserialize(new RestResponse(request) { Content = capturer.Body! }); actual.Should().BeEquivalentTo(testData); } @@ -34,19 +26,11 @@ public class IntegratedSimpleTests { [Fact] public async Task Use_JsonNet_For_Response() { var expected = Fixture.Create(); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(expected)); - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new JsonNetSerializer(); - - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)!); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson()); - + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); var actual = await client.GetAsync(new RestRequest()); actual.Should().BeEquivalentTo(expected); @@ -54,16 +38,11 @@ public class IntegratedSimpleTests { [Fact] public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() { - using var server = HttpServerFixture.StartServer( - (_, response) => { - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8("invalid json"); - } - ); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json)); - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); var response = await client.ExecuteAsync(new RestRequest()); @@ -74,23 +53,17 @@ public class IntegratedSimpleTests { [Fact] public async Task DeserilizationSucceeds_IsSuccessful_Should_BeTrue() { var item = Fixture.Create(); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(item)); - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new JsonNetSerializer(); - - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(item)!); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseNewtonsoftJson()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); var response = await client.ExecuteAsync(new RestRequest()); response.IsSuccessStatusCode.Should().BeTrue(); response.IsSuccessful.Should().BeTrue(); } -} + + public void Dispose() => _server?.Dispose(); +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs index ce74b3a86..9fcd45865 100644 --- a/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs +++ b/test/RestSharp.Tests.Serializers.Json/NewtonsoftJson/IntegratedTests.cs @@ -1,25 +1,24 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using RestMockCore; using RestSharp.Serializers.NewtonsoftJson; namespace RestSharp.Tests.Serializers.Json.NewtonsoftJson; -public class IntegratedTests { +public sealed class IntegratedTests : IDisposable { static readonly Fixture Fixture = new(); - const int Port = 5001; + readonly WireMockServer _server = WireMockServer.Start(); [Fact] public async Task Use_with_GetJsonAsync() { var data = Fixture.Create(); var serialized = JsonConvert.SerializeObject(data, JsonNetSerializer.DefaultSettings); - using var server = new HttpServer(Port); - server.Config.Get("/test").Send(serialized); - server.Run(); + _server + .Given(Request.Create().WithPath("/test").UsingGet()) + .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json)); - using var client = new RestClient($"http://localhost:{Port}", configureSerialization: cfg => cfg.UseNewtonsoftJson()); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson()); var response = await client.GetJsonAsync("/test"); @@ -34,14 +33,16 @@ public class IntegratedTests { var data = Fixture.Create(); var serialized = JsonConvert.SerializeObject(data, settings); - using var server = new HttpServer(Port); - server.Config.Get("/test").Send(serialized); - server.Run(); + _server + .Given(Request.Create().WithPath("/test").UsingGet()) + .RespondWith(Response.Create().WithBody(serialized).WithHeader(KnownHeaders.ContentType, ContentType.Json)); - using var client = new RestClient($"http://localhost:{Port}", configureSerialization: cfg => cfg.UseNewtonsoftJson(settings)); + using var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseNewtonsoftJson(settings)); var response = await client.GetJsonAsync("/test"); response.Should().BeEquivalentTo(data); } + + public void Dispose() => _server?.Dispose(); } diff --git a/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj index a7703166c..ef87290b5 100644 --- a/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj +++ b/test/RestSharp.Tests.Serializers.Json/RestSharp.Tests.Serializers.Json.csproj @@ -1,18 +1,16 @@ - - - + + + - + - - - - - - - + + + + + diff --git a/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs index 5d5976d48..5cfd6ac5d 100644 --- a/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs +++ b/test/RestSharp.Tests.Serializers.Json/SystemTextJson/SystemTextJsonTests.cs @@ -1,51 +1,37 @@ -using System.Net; -using System.Text; using RestSharp.Serializers.Json; -using RestSharp.Tests.Shared.Extensions; using RestSharp.Tests.Shared.Fixtures; namespace RestSharp.Tests.Serializers.Json.SystemTextJson; -public class SystemTextJsonTests { +public sealed class SystemTextJsonTests : IDisposable { static readonly Fixture Fixture = new(); - string _body; - + readonly WireMockServer _server = WireMockServer.Start(); + [Fact] public async Task Use_JsonNet_For_Requests() { - using var server = HttpServerFixture.StartServer(CaptureBody); - _body = null; var serializer = new SystemTextJsonSerializer(); + var capturer = _server.ConfigureBodyCapturer(Method.Post, false); var testData = Fixture.Create(); - - var client = new RestClient(server.Url); + var client = new RestClient(_server.Url!); var request = new RestRequest().AddJsonBody(testData); await client.PostAsync(request); - var actual = serializer.Deserialize(new RestResponse(request) { Content = _body }); + var actual = serializer.Deserialize(new RestResponse(request) { Content = capturer.Body }); actual.Should().BeEquivalentTo(testData); - - void CaptureBody(HttpListenerRequest req, HttpListenerResponse response) => _body = req.InputStream.StreamToString(); } [Fact] public async Task Use_JsonNet_For_Response() { var expected = Fixture.Create(); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(expected)); - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new SystemTextJsonSerializer(); - - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(expected)!); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson()); var actual = await client.GetAsync(new RestRequest()); @@ -54,16 +40,11 @@ public class SystemTextJsonTests { [Fact] public async Task DeserilizationFails_IsSuccessful_Should_BeFalse() { - using var server = HttpServerFixture.StartServer( - (_, response) => { - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8("invalid json"); - } - ); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBody("invalid json").WithHeader(KnownHeaders.ContentType, ContentType.Json)); - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson()); var response = await client.ExecuteAsync(new RestRequest()); @@ -74,23 +55,17 @@ public class SystemTextJsonTests { [Fact] public async Task DeserilizationSucceeds_IsSuccessful_Should_BeTrue() { var item = Fixture.Create(); + _server + .Given(Request.Create().WithPath("/").UsingGet()) + .RespondWith(Response.Create().WithBodyAsJson(item)); - using var server = HttpServerFixture.StartServer( - (_, response) => { - var serializer = new SystemTextJsonSerializer(); - - response.StatusCode = (int)HttpStatusCode.OK; - response.ContentType = "application/json"; - response.ContentEncoding = Encoding.UTF8; - response.OutputStream.WriteStringUtf8(serializer.Serialize(item)!); - } - ); - - var client = new RestClient(server.Url, configureSerialization: cfg => cfg.UseSystemTextJson()); + var client = new RestClient(_server.Url!, configureSerialization: cfg => cfg.UseSystemTextJson()); var response = await client.ExecuteAsync(new RestRequest()); response.IsSuccessStatusCode.Should().BeTrue(); response.IsSuccessful.Should().BeTrue(); } + + public void Dispose() => _server?.Dispose(); } diff --git a/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs index 8b4ad7809..4d3a32b64 100644 --- a/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs +++ b/test/RestSharp.Tests.Serializers.Xml/NamespacedXmlTests.cs @@ -63,7 +63,7 @@ public class NamespacedXmlTests { foes.Add(new XAttribute(ns + "Team", "Yankees")); - for (var i = 0; i < 5; i++) foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", "Foe" + i))); + for (var i = 0; i < 5; i++) foes.Add(new XElement(ns + "Foe", new XElement(ns + "Nickname", $"Foe{i}"))); root.Add(foes); doc.Add(root); diff --git a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs index df64ad212..cd02cc6ad 100644 --- a/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs +++ b/test/RestSharp.Tests.Serializers.Xml/SampleClasses/twitter.cs @@ -1,6 +1,8 @@ using RestSharp.Serializers; // ReSharper disable InconsistentNaming // ReSharper disable UnusedMember.Global +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. +#pragma warning disable CS8981 // The type name only contains lower-cased ascii characters. Such names may become reserved for the language. #pragma warning disable CS8981 namespace RestSharp.Tests.Serializers.Xml.SampleClasses; diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs index 9f6277c6f..ca07e7b53 100644 --- a/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs +++ b/test/RestSharp.Tests.Serializers.Xml/XmlAttributeDeserializerTests.cs @@ -6,11 +6,9 @@ namespace RestSharp.Tests.Serializers.Xml; public class XmlAttributeDeserializerTests { - readonly ITestOutputHelper _output; - const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; - #if NETCORE + #if NET readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData"); #else readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData"); @@ -18,8 +16,6 @@ public class XmlAttributeDeserializerTests { string PathFor(string sampleFile) => Path.Combine(_sampleDataPath, sampleFile); - public XmlAttributeDeserializerTests(ITestOutputHelper output) => _output = output; - [Fact] public void Can_Deserialize_Lists_of_Simple_Types() { var xmlPath = PathFor("xmllists.xml"); diff --git a/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs index c1e39af0f..f9c680b3a 100644 --- a/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs +++ b/test/RestSharp.Tests.Serializers.Xml/XmlDeserializerTests.cs @@ -9,7 +9,7 @@ namespace RestSharp.Tests.Serializers.Xml; public class XmlDeserializerTests { const string GuidString = "AC1FC4BC-087A-4242-B8EE-C53EBE9887A5"; -#if NETCORE +#if NET readonly string _sampleDataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SampleData"); #else readonly string _sampleDataPath = Path.Combine(Directory.GetCurrentDirectory(), "SampleData"); diff --git a/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs index 8183d4607..6aa76d27b 100644 --- a/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs +++ b/test/RestSharp.Tests.Shared/Fixtures/Handlers.cs @@ -1,20 +1,9 @@ using System.Net; using System.Reflection; -using RestSharp.Tests.Shared.Extensions; namespace RestSharp.Tests.Shared.Fixtures; public static class Handlers { - /// - /// Echoes the request input back to the output. - /// - public static void Echo(HttpListenerContext context) => context.Request.InputStream.CopyTo(context.Response.OutputStream); - - /// - /// Echoes the given value back to the output. - /// - public static Action EchoValue(string value) => ctx => ctx.Response.OutputStream.WriteStringUtf8(value); - /// /// T should be a class that implements methods whose names match the urls being called, and take one parameter, an /// HttpListenerContext. diff --git a/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs b/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs deleted file mode 100644 index d94595845..000000000 --- a/test/RestSharp.Tests.Shared/Fixtures/HttpServerFixture.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Net; - -namespace RestSharp.Tests.Shared.Fixtures; - -public sealed class HttpServerFixture : IDisposable { - public static HttpServerFixture StartServer(string url, Action handle) { - var server = new TestHttpServer(0, url, (request, response, _) => handle(request, response)); - return new HttpServerFixture(server); - } - - public static HttpServerFixture StartServer(Action handle) => StartServer("", handle); - - HttpServerFixture(TestHttpServer server) { - Url = $"http://localhost:{server.Port}"; - _server = server; - } - - public string Url { get; } - - readonly TestHttpServer _server; - - public void Dispose() => _server.Dispose(); -} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs index bb21fe4ae..bcb57764a 100644 --- a/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs +++ b/test/RestSharp.Tests.Shared/Fixtures/RequestBodyCapturer.cs @@ -1,23 +1,29 @@ -using System.Net; -using RestSharp.Tests.Shared.Extensions; - namespace RestSharp.Tests.Shared.Fixtures; public class RequestBodyCapturer { - public const string Resource = "Capture"; + public const string Resource = "/capture"; + + public string ContentType { get; private set; } + public bool HasBody { get; private set; } + public string Body { get; private set; } + public Uri Url { get; private set; } - public static string CapturedContentType { get; set; } - public static bool CapturedHasEntityBody { get; set; } - public static string CapturedEntityBody { get; set; } - public static Uri CapturedUrl { get; set; } + public bool CaptureBody(string content) { + Body = content; + HasBody = !string.IsNullOrWhiteSpace(content); + return true; + } - // ReSharper disable once UnusedMember.Global - public static void Capture(HttpListenerContext context) { - var request = context.Request; + public bool CaptureHeaders(IDictionary headers) { + if (headers.TryGetValue("Content-Type", out var contentType)) { + ContentType = contentType[0]; + } + + return true; + } - CapturedContentType = request.ContentType; - CapturedHasEntityBody = request.HasEntityBody; - CapturedEntityBody = request.InputStream.StreamToString(); - CapturedUrl = request.Url; + public bool CaptureUrl(string url) { + Url = new Uri(url); + return true; } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs index d53863c2f..6ebb81d13 100644 --- a/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs +++ b/test/RestSharp.Tests.Shared/Fixtures/SimpleServer.cs @@ -8,17 +8,15 @@ public sealed class SimpleServer : IDisposable { readonly WebServer _server; readonly CancellationTokenSource _cts = new(); - public string Url { get; } - public string ServerUrl { get; } + public string Url { get; } SimpleServer( int port, Action handler = null, AuthenticationSchemes authenticationSchemes = AuthenticationSchemes.Anonymous ) { - Url = $"http://localhost:{port}/"; - ServerUrl = $"http://{Environment.MachineName}:{port}/"; - _server = new WebServer(Url, handler, authenticationSchemes); + Url = $"http://localhost:{port}/"; + _server = new WebServer(Url, handler, authenticationSchemes); Task.Run(() => _server.Run(_cts.Token)); } diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs b/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs deleted file mode 100644 index b3c49b16e..000000000 --- a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServer.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using System.Text; - -namespace RestSharp.Tests.Shared.Fixtures; - -public class TestHttpServer : IDisposable { - readonly HttpListener _listener; - readonly List _requestHandlers; - readonly object _requestHandlersLock = new(); - readonly CancellationTokenSource _cts = new(); - - public int Port { get; } - - public TestHttpServer( - int port, - string url, - Action> handlerAction, - string hostName = "localhost" - ) - : this(port, new List { new(url, handlerAction) }, hostName) { } - - public TestHttpServer(int port, List handlers, string hostName = "localhost") { - _requestHandlers = handlers; - - Port = port > 0 ? port : GetRandomUnusedPort(); - - //create and start listener - _listener = new HttpListener(); - _listener.Prefixes.Add($"http://{hostName}:{Port}/"); - _listener.Start(); - - Task.Run(() => HandleRequests(_cts.Token)); - } - - static int GetRandomUnusedPort() { - var listener = new TcpListener(IPAddress.Any, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - return port; - } - - async Task HandleRequests(CancellationToken cancellationToken) { - try { - //listen for all requests - while (_listener.IsListening && !cancellationToken.IsCancellationRequested) { - //get the request - var context = await _listener.GetContextAsync(); - - try { - Dictionary parameters = null; - TestRequestHandler handler; - - lock (_requestHandlersLock) { - handler = _requestHandlers.FirstOrDefault( - h => h.TryMatchUrl(context.Request.RawUrl, context.Request.HttpMethod, out parameters) - ); - } - - string responseString = null; - - if (handler != null) { - //add the query string parameters to the pre-defined url parameters that were set from MatchesUrl() - foreach (var qsParamName in context.Request.QueryString.AllKeys) - parameters[qsParamName] = context.Request.QueryString[qsParamName]; - - try { - handler.HandlerAction(context.Request, context.Response, parameters); - } - catch (Exception ex) { - responseString = $"Exception in handler: {ex.Message}"; - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - } - } - else { - context.Response.ContentType("text/plain").StatusCode(404); - responseString = $"No handler provided for URL: {context.Request.RawUrl}"; - } - - context.Request.ClearContent(); - - //send the response, if there is not (if responseString is null, then the handler method should have manually set the output stream) - if (responseString != null) { - var buffer = Encoding.UTF8.GetBytes(responseString); - context.Response.ContentLength64 += buffer.Length; - await context.Response.OutputStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); - } - } - finally { - context.Response.OutputStream.Close(); - context.Response.Close(); - } - } - } - catch (HttpListenerException ex) { - //when the listener is stopped, it will throw an exception for being cancelled, so just ignore it - if (ex.Message != "The I/O operation has been aborted because of either a thread exit or an application request") throw; - } - } - - public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) { - if (disposing && _listener.IsListening) { - _listener.Stop(); - } - } -} diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs b/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs deleted file mode 100644 index b1202c772..000000000 --- a/test/RestSharp.Tests.Shared/Fixtures/TestHttpServerExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Net; - -namespace RestSharp.Tests.Shared.Fixtures; - -public static class TestHttpServerExtensions { - static readonly Dictionary RequestContent = new(); - - internal static void ClearContent(this HttpListenerRequest request) => RequestContent.Remove(request); - - public static HttpListenerResponse ContentType(this HttpListenerResponse response, string contentType) { - response.ContentType = contentType; - return response; - } - - public static HttpListenerResponse StatusCode(this HttpListenerResponse response, int statusCode) { - response.StatusCode = statusCode; - return response; - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs b/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs deleted file mode 100644 index e83a8ba01..000000000 --- a/test/RestSharp.Tests.Shared/Fixtures/TestRequestHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Net; -using System.Text.RegularExpressions; - -namespace RestSharp.Tests.Shared.Fixtures; - -public class TestRequestHandler { - readonly Regex _comparisonRegex; - - readonly List _urlParameterNames = new(); - - public TestRequestHandler( - string url, - string httpMethod, - Action> handlerAction - ) { - Url = url; - HttpMethod = httpMethod; - HandlerAction = handlerAction; - - _comparisonRegex = CreateComparisonRegex(url); - } - - public TestRequestHandler(string url, Action> handlerAction) - : this(url, null, handlerAction) { } - - string Url { get; } - string HttpMethod { get; } - internal Action> HandlerAction { get; } - - Regex CreateComparisonRegex(string url) { - var regexString = Regex.Escape(url).Replace(@"\{", "{"); - - regexString += regexString.EndsWith("/") ? "?" : "/?"; - regexString = (regexString.StartsWith("/") ? "^" : "^/") + regexString; - - var regex = new Regex(@"{(.*?)}"); - - foreach (Match match in regex.Matches(regexString)) { - regexString = regexString.Replace(match.Value, @"(.*?)"); - _urlParameterNames.Add(match.Groups[1].Value); - } - - regexString += !regexString.Contains(@"\?") ? @"(\?.*)?$" : "$"; - - return new Regex(regexString); - } - - public bool TryMatchUrl(string rawUrl, string httpMethod, out Dictionary parameters) { - var match = _comparisonRegex.Match(rawUrl); - - var isMethodMatched = HttpMethod == null || HttpMethod.Split(',').Contains(httpMethod); - - if (!match.Success || !isMethodMatched) { - parameters = null; - return false; - } - - parameters = new Dictionary(); - - for (var i = 0; i < _urlParameterNames.Count; i++) - parameters[_urlParameterNames[i]] = match.Groups[i + 1].Value; - return true; - } -} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs b/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs new file mode 100644 index 000000000..2cabf015c --- /dev/null +++ b/test/RestSharp.Tests.Shared/Fixtures/WireMockExtensions.cs @@ -0,0 +1,21 @@ +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; + +namespace RestSharp.Tests.Shared.Fixtures; + +public static class WireMockExtensions { + public static RequestBodyCapturer ConfigureBodyCapturer(this WireMockServer server, Method method, bool usePath = true) { + var capturer = new RequestBodyCapturer(); + + var requestBuilder = Request + .Create() + .WithPath(usePath ? RequestBodyCapturer.Resource : "/") + .WithUrl(capturer.CaptureUrl) + .WithBody(capturer.CaptureBody) + .WithHeader(capturer.CaptureHeaders) + .UsingMethod(method.ToString().ToUpper()); + server.Given(requestBuilder).RespondWith(Response.Create().WithStatusCode(200)); + return capturer; + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj index 274f12fb9..06f32e8e6 100644 --- a/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj +++ b/test/RestSharp.Tests.Shared/RestSharp.Tests.Shared.csproj @@ -2,4 +2,10 @@ false + + + + + + diff --git a/test/RestSharp.Tests.Integrated/OAuth1Tests.cs b/test/RestSharp.Tests/OAuth1Tests.cs similarity index 98% rename from test/RestSharp.Tests.Integrated/OAuth1Tests.cs rename to test/RestSharp.Tests/OAuth1Tests.cs index 04827b1f8..d8fad50d6 100644 --- a/test/RestSharp.Tests.Integrated/OAuth1Tests.cs +++ b/test/RestSharp.Tests/OAuth1Tests.cs @@ -2,9 +2,10 @@ using RestSharp.Authenticators; using RestSharp.Authenticators.OAuth; using RestSharp.Tests.Shared.Extensions; + #pragma warning disable CS8618 -namespace RestSharp.Tests.Integrated; +namespace RestSharp.Tests; public class OAuth1Tests { [XmlRoot("queue")] diff --git a/test/RestSharp.Tests/RestSharp.Tests.csproj b/test/RestSharp.Tests/RestSharp.Tests.csproj index eda460e5b..6c8d9fd1c 100644 --- a/test/RestSharp.Tests/RestSharp.Tests.csproj +++ b/test/RestSharp.Tests/RestSharp.Tests.csproj @@ -1,30 +1,31 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file