From 8b388fbe832633d5c89e2a42e5cf078aa36e6a28 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Sat, 8 Jan 2022 18:22:29 +0100 Subject: [PATCH] Add more tests for PUT and POST (#1688) * Made `AddJsonBody` and `AddXmlBody` generic with class constraint so that people don't use them to add strings * Added an option to disable the charset (`RestClientOptions.DisableCharset`) --- src/RestSharp/Parameters/FileParameter.cs | 1 - src/RestSharp/Request/RequestContent.cs | 4 + .../Request/RestRequestExtensions.cs | 140 +++++++++++++++++- src/RestSharp/RestClientOptions.cs | 5 + test/RestSharp.IntegrationTests/AsyncTests.cs | 13 -- .../Fixtures/TestServer.cs | 20 ++- test/RestSharp.IntegrationTests/PutTests.cs | 49 ++++++ .../RequestBodyTests.cs | 25 +++- .../Extensions/StreamExtensions.cs | 6 + 9 files changed, 231 insertions(+), 32 deletions(-) create mode 100644 test/RestSharp.IntegrationTests/PutTests.cs diff --git a/src/RestSharp/Parameters/FileParameter.cs b/src/RestSharp/Parameters/FileParameter.cs index 61a74e9b9..dea2dd8bb 100644 --- a/src/RestSharp/Parameters/FileParameter.cs +++ b/src/RestSharp/Parameters/FileParameter.cs @@ -76,7 +76,6 @@ Stream GetFile() { public static FileParameter Create( string name, Func getFile, - long contentLength, string fileName, string? contentType = null ) diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index ee79e55e9..e317e4c98 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -117,6 +117,10 @@ void AddBody(bool hasPostParameters) { // we don't have parameters, only the body Content = bodyContent; } + + if (_client.Options.DisableCharset) { + Content.Headers.ContentType.CharSet = ""; + } } void AddPostParameters(ParametersCollection? postParameters) { diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index 43ed5ef95..56383ff2c 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -66,34 +66,106 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, string public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, T value, bool encode = true) where T : struct => request.AddOrUpdateParameter(name, value.ToString(), encode); + /// + /// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work. + /// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path. + /// + /// Request instance + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Encode the value or not, default true + /// public static RestRequest AddUrlSegment(this RestRequest request, string name, string value, bool encode = true) => request.AddParameter(new UrlSegmentParameter(name, value, encode)); + /// + /// Adds a URL segment parameter to the request. The resource URL must have a placeholder for the parameter for it to work. + /// For example, if you add a URL segment parameter with the name "id", the resource URL should contain {id} in its path. + /// + /// Request instance + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Encode the value or not, default true + /// public static RestRequest AddUrlSegment(this RestRequest request, string name, T value, bool encode = true) where T : struct => request.AddUrlSegment(name, Ensure.NotNull(value.ToString(), nameof(value)), encode); + /// + /// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter. + /// The parameter will be added to the request URL as a query string using name=value format. + /// + /// Request instance + /// Parameter name + /// Parameter value + /// Encode the value or not, default true + /// public static RestRequest AddQueryParameter(this RestRequest request, string name, string? value, bool encode = true) => request.AddParameter(new QueryParameter(name, value, encode)); + /// + /// Adds a query string parameter to the request. The request resource should not contain any placeholders for this parameter. + /// The parameter will be added to the request URL as a query string using name=value format. + /// + /// Request instance + /// Parameter name + /// Parameter value + /// Encode the value or not, default true + /// public static RestRequest AddQueryParameter(this RestRequest request, string name, T value, bool encode = true) where T : struct => request.AddQueryParameter(name, value.ToString(), encode); + /// + /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// + /// Request instance + /// Header name + /// Header value + /// public static RestRequest AddHeader(this RestRequest request, string name, string value) { CheckAndThrowsForInvalidHost(name, value); return request.AddParameter(new HeaderParameter(name, value)); } + /// + /// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource. + /// + /// Request instance + /// Header name + /// Header value + /// public static RestRequest AddHeader(this RestRequest request, string name, T value) where T : struct => request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + /// + /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. + /// Existing header with the same name will be replaced. + /// + /// Request instance + /// Header name + /// Header value + /// public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) { CheckAndThrowsForInvalidHost(name, value); return request.AddOrUpdateParameter(new HeaderParameter(name, value)); } + /// + /// Adds or updates the request header. RestSharp will try to separate request and content headers when calling the resource. + /// Existing header with the same name will be replaced. + /// + /// Request instance + /// Header name + /// Header value + /// public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, T value) where T : struct => request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value))); + /// + /// Adds multiple headers to the request, using the key-value pairs provided. + /// + /// Request instance + /// Collection of key-value pairs, where key will be used as header name, and value as header value + /// public static RestRequest AddHeaders(this RestRequest request, ICollection> headers) { CheckAndThrowsDuplicateKeys(headers); @@ -104,6 +176,12 @@ public static RestRequest AddHeaders(this RestRequest request, ICollection + /// Adds or updates multiple headers to the request, using the key-value pairs provided. Existing headers with the same name will be replaced. + /// + /// Request instance + /// Collection of key-value pairs, where key will be used as header name, and value as header value + /// public static RestRequest AddOrUpdateHeaders(this RestRequest request, ICollection> headers) { CheckAndThrowsDuplicateKeys(headers); @@ -114,9 +192,42 @@ public static RestRequest AddOrUpdateHeaders(this RestRequest request, ICollecti return request; } + /// + /// Adds a parameter of a given type to the request. It will create a typed parameter instance based on the type argument. + /// It is not recommended to use this overload unless you must, as it doesn't provide any restrictions, and if the name-value-type + /// combination doesn't match, it will throw. + /// + /// Request instance + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Enum value specifying what kind of parameter is being added + /// Encode the value or not, default true + /// public static RestRequest AddParameter(this RestRequest request, string? name, object value, ParameterType type, bool encode = true) => request.AddParameter(Parameter.CreateParameter(name, value, type, encode)); + /// + /// Adds or updates request parameter of a given type. It will create a typed parameter instance based on the type argument. + /// Parameter will be added or updated based on its name. If the request has a parameter with the same name, it will be updated. + /// It is not recommended to use this overload unless you must, as it doesn't provide any restrictions, and if the name-value-type + /// combination doesn't match, it will throw. + /// + /// Request instance + /// Name of the parameter, must be matching a placeholder in the resource URL as {name} + /// Value of the parameter + /// Enum value specifying what kind of parameter is being added + /// Encode the value or not, default true + /// + public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type, bool encode = true) + => request.AddOrUpdateParameter(Parameter.CreateParameter(name, value, type, encode)); + + /// + /// Adds or updates request parameter, given the parameter instance, for example or . + /// It will replace an existing parameter with the same name. + /// + /// Request instance + /// Parameter instance + /// public static RestRequest AddOrUpdateParameter(this RestRequest request, Parameter parameter) { var p = request.Parameters.FirstOrDefault(x => x.Name == parameter.Name && x.Type == parameter.Type); @@ -126,6 +237,13 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, Paramet return request; } + /// + /// Adds or updates multiple request parameters, given the parameter instance, for example + /// or . Parameters with the same name will be replaced. + /// + /// Request instance + /// Collection of parameter instances + /// public static RestRequest AddOrUpdateParameters(this RestRequest request, IEnumerable parameters) { foreach (var parameter in parameters) request.AddOrUpdateParameter(parameter); @@ -133,9 +251,6 @@ public static RestRequest AddOrUpdateParameters(this RestRequest request, IEnume return request; } - public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type, bool encode = true) - => request.AddOrUpdateParameter(Parameter.CreateParameter(name, value, type, encode)); - /// /// Adds a file parameter to the request body. The file will be read from disk as a stream. /// @@ -159,15 +274,23 @@ public static RestRequest AddFile(this RestRequest request, string name, string public static RestRequest AddFile(this RestRequest request, string name, byte[] bytes, string filename, string? contentType = null) => request.AddFile(FileParameter.Create(name, bytes, filename, contentType)); + /// + /// Adds a file attachment to the request, where the file content will be retrieved from a given stream + /// + /// Request instance + /// Parameter name + /// Function that returns a stream with the file content + /// File name + /// Optional: content type. Default is "application/octet-stream" + /// public static RestRequest AddFile( this RestRequest request, string name, Func getFile, string fileName, - long contentLength, string? contentType = null ) - => request.AddFile(FileParameter.Create(name, getFile, contentLength, fileName, contentType)); + => request.AddFile(FileParameter.Create(name, getFile, fileName, contentType)); /// /// Adds a body parameter to the request @@ -201,7 +324,7 @@ public static RestRequest AddBody(this RestRequest request, object obj, string? /// Object that will be serialized to JSON /// Optional: content type. Default is "application/json" /// - public static RestRequest AddJsonBody(this RestRequest request, object obj, string contentType = ContentType.Json) { + public static RestRequest AddJsonBody(this RestRequest request, T obj, string contentType = ContentType.Json) where T : class { request.RequestFormat = DataFormat.Json; return request.AddParameter(new JsonParameter("", obj, contentType)); } @@ -214,7 +337,8 @@ public static RestRequest AddJsonBody(this RestRequest request, object obj, stri /// Optional: content type. Default is "application/xml" /// Optional: XML namespace /// - public static RestRequest AddXmlBody(this RestRequest request, object obj, string contentType = ContentType.Xml, string xmlNamespace = "") { + public static RestRequest AddXmlBody(this RestRequest request, T obj, string contentType = ContentType.Xml, string xmlNamespace = "") + where T : class { request.RequestFormat = DataFormat.Xml; request.AddParameter(new XmlParameter("", obj, xmlNamespace, contentType)); return request; @@ -227,7 +351,7 @@ public static RestRequest AddXmlBody(this RestRequest request, object obj, strin /// Object to add as form data /// Properties to include, or nothing to include everything /// - public static RestRequest AddObject(this RestRequest request, object obj, params string[] includedProperties) { + public static RestRequest AddObject(this RestRequest request, T obj, params string[] includedProperties) where T : class { var props = obj.GetProperties(includedProperties); foreach (var (name, value) in props) { diff --git a/src/RestSharp/RestClientOptions.cs b/src/RestSharp/RestClientOptions.cs index 69378ed9f..5df868323 100644 --- a/src/RestSharp/RestClientOptions.cs +++ b/src/RestSharp/RestClientOptions.cs @@ -51,6 +51,11 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba /// running) will be sent along to the server. The default is false. /// public bool UseDefaultCredentials { get; set; } + + /// + /// Set to true if you need the Content-Type not to have the charset + /// + public bool DisableCharset { get; set; } #if NETSTANDARD public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip; diff --git a/test/RestSharp.IntegrationTests/AsyncTests.cs b/test/RestSharp.IntegrationTests/AsyncTests.cs index 51fa1133b..552bfeb4e 100644 --- a/test/RestSharp.IntegrationTests/AsyncTests.cs +++ b/test/RestSharp.IntegrationTests/AsyncTests.cs @@ -1,6 +1,5 @@ using System.Net; using RestSharp.IntegrationTests.Fixtures; -using RestSharp.Tests.Shared.Fixtures; namespace RestSharp.IntegrationTests; @@ -63,18 +62,6 @@ public async Task Can_Timeout_GET_Async() { Assert.Equal(ResponseStatus.TimedOut, response.ResponseStatus); } - [Fact] - public async Task Can_Timeout_PUT_Async() { - var request = new RestRequest("timeout", Method.Put).AddBody("Body_Content"); - - // Half the value of ResponseHandler.Timeout - request.Timeout = 200; - - var response = await _client.ExecuteAsync(request); - - Assert.Equal(ResponseStatus.TimedOut, response.ResponseStatus); - } - [Fact] public async Task Handles_GET_Request_Errors_Async() { var request = new RestRequest("status?code=404"); diff --git a/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs b/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs index d9d4f28c7..2903fffa0 100644 --- a/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs +++ b/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs @@ -1,7 +1,9 @@ +using System.Text.Json; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using RestSharp.Tests.Shared.Extensions; namespace RestSharp.IntegrationTests.Fixtures; @@ -29,15 +31,27 @@ public HttpServer(ITestOutputHelper output = null) { builder.WebHost.UseUrls(Address); _app = builder.Build(); + + var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + // GET _app.MapGet("success", () => new TestResponse { Message = "Works!" }); _app.MapGet("echo", (string msg) => msg); _app.MapGet("timeout", async () => await Task.Delay(2000)); _app.MapPut("timeout", async () => await Task.Delay(2000)); // ReSharper disable once ConvertClosureToMethodGroup _app.MapGet("status", (int code) => Results.StatusCode(code)); - _app.MapGet("headers", HandleHeaders); + // PUT + _app.MapPut( + "content", + async context => { + var content = await context.Request.Body.StreamToStringAsync(); + await context.Response.WriteAsync(content); + } + ); + IResult HandleHeaders(HttpContext ctx) { var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value)); return Results.Ok(response); @@ -54,4 +68,6 @@ public async Task Stop() { } } -public record TestServerResponse(string Name, string Value); \ No newline at end of file +record TestServerResponse(string Name, string Value); + +record ContentResponse(string Content); \ No newline at end of file diff --git a/test/RestSharp.IntegrationTests/PutTests.cs b/test/RestSharp.IntegrationTests/PutTests.cs new file mode 100644 index 000000000..44148fd3b --- /dev/null +++ b/test/RestSharp.IntegrationTests/PutTests.cs @@ -0,0 +1,49 @@ +using System.Text.Json; +using RestSharp.IntegrationTests.Fixtures; + +namespace RestSharp.IntegrationTests; + +[Collection(nameof(TestServerCollection))] +public class PutTests { + readonly ITestOutputHelper _output; + readonly RestClient _client; + + static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web); + + public PutTests(TestServerFixture fixture, ITestOutputHelper output) { + _output = output; + _client = new RestClient(fixture.Server.Url); + } + + [Fact] + public async Task Should_put_json_body() { + var body = new TestRequest("foo", 100); + var request = new RestRequest("content").AddJsonBody(body); + var response = await _client.PutAsync(request); + + var expected = JsonSerializer.Serialize(body, Options); + response!.Content.Should().Be(expected); + } + + [Fact] + public async Task Should_put_json_body_using_extension() { + var body = new TestRequest("foo", 100); + var response = await _client.PutJsonAsync("content", body); + response.Should().BeEquivalentTo(response); + } + + [Fact] + public async Task Can_Timeout_PUT_Async() { + var request = new RestRequest("timeout", Method.Put).AddBody("Body_Content"); + + // Half the value of ResponseHandler.Timeout + request.Timeout = 200; + + var response = await _client.ExecuteAsync(request); + + Assert.Equal(ResponseStatus.TimedOut, response.ResponseStatus); + } + +} + +record TestRequest(string Data, int Number); \ No newline at end of file diff --git a/test/RestSharp.IntegrationTests/RequestBodyTests.cs b/test/RestSharp.IntegrationTests/RequestBodyTests.cs index 76baa002d..5f69bcf50 100644 --- a/test/RestSharp.IntegrationTests/RequestBodyTests.cs +++ b/test/RestSharp.IntegrationTests/RequestBodyTests.cs @@ -9,16 +9,18 @@ public class RequestBodyTests : IClassFixture { const string NewLine = "\r\n"; - const string TextPlainContentType = "text/plain"; - const string ExpectedTextContentType = $"{TextPlainContentType}; charset=utf-8"; + 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; } - async Task AssertBody(Method method) { - var client = new RestClient(_server.Url); + 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 => { @@ -33,7 +35,8 @@ async Task AssertBody(Method method) { await client.ExecuteAsync(request); - AssertHasRequestBody(ExpectedTextContentType, bodyData); + var expected = disableCharset ? ExpectedTextContentTypeNoCharset : ExpectedTextContentType; + AssertHasRequestBody(expected, bodyData); } [Fact] @@ -48,9 +51,15 @@ async Task AssertBody(Method method) { [Fact] public Task Can_Be_Added_To_PATCH_Request() => AssertBody(Method.Patch); + [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); + [Fact] + public Task Can_Be_Added_To_PUT_Request_NoCharset() => AssertBody(Method.Put, true); + [Fact] public Task Can_Be_Added_To_PUT_Request() => AssertBody(Method.Put); @@ -109,9 +118,9 @@ public async Task Query_Parameters_With_Json_Body() { await client.ExecuteAsync(request); - Assert.Equal($"{_server.Url}Capture?key=value", RequestBodyCapturer.CapturedUrl.ToString()); - Assert.Equal("application/json; charset=utf-8", RequestBodyCapturer.CapturedContentType); - Assert.Equal("{\"displayName\":\"Display Name\"}", RequestBodyCapturer.CapturedEntityBody); + 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\"}"); } static void AssertHasNoRequestBody() { diff --git a/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs b/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs index 8adce5aee..b76cb906e 100644 --- a/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs +++ b/test/RestSharp.Tests.Shared/Extensions/StreamExtensions.cs @@ -14,4 +14,10 @@ public static string StreamToString(this Stream stream) { return streamReader.ReadToEnd(); } + + public static async Task StreamToStringAsync(this Stream stream) { + using var streamReader = new StreamReader(stream); + + return await streamReader.ReadToEndAsync(); + } } \ No newline at end of file