Skip to content

Commit

Permalink
Add more tests for PUT and POST (#1688)
Browse files Browse the repository at this point in the history
* 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`)
  • Loading branch information
alexeyzimarev authored Jan 8, 2022
1 parent 3e36e84 commit 8b388fb
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 32 deletions.
1 change: 0 additions & 1 deletion src/RestSharp/Parameters/FileParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ Stream GetFile() {
public static FileParameter Create(
string name,
Func<Stream> getFile,
long contentLength,
string fileName,
string? contentType = null
)
Expand Down
4 changes: 4 additions & 0 deletions src/RestSharp/Request/RequestContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
140 changes: 132 additions & 8 deletions src/RestSharp/Request/RestRequestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,34 +66,106 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, string
public static RestRequest AddOrUpdateParameter<T>(this RestRequest request, string name, T value, bool encode = true) where T : struct
=> request.AddOrUpdateParameter(name, value.ToString(), encode);

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Name of the parameter, must be matching a placeholder in the resource URL as {name}</param>
/// <param name="value">Value of the parameter</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddUrlSegment(this RestRequest request, string name, string value, bool encode = true)
=> request.AddParameter(new UrlSegmentParameter(name, value, encode));

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Name of the parameter, must be matching a placeholder in the resource URL as {name}</param>
/// <param name="value">Value of the parameter</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddUrlSegment<T>(this RestRequest request, string name, T value, bool encode = true) where T : struct
=> request.AddUrlSegment(name, Ensure.NotNull(value.ToString(), nameof(value)), encode);

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddQueryParameter(this RestRequest request, string name, string? value, bool encode = true)
=> request.AddParameter(new QueryParameter(name, value, encode));

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddQueryParameter<T>(this RestRequest request, string name, T value, bool encode = true) where T : struct
=> request.AddQueryParameter(name, value.ToString(), encode);

/// <summary>
/// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <returns></returns>
public static RestRequest AddHeader(this RestRequest request, string name, string value) {
CheckAndThrowsForInvalidHost(name, value);
return request.AddParameter(new HeaderParameter(name, value));
}

/// <summary>
/// Adds a header to the request. RestSharp will try to separate request and content headers when calling the resource.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <returns></returns>
public static RestRequest AddHeader<T>(this RestRequest request, string name, T value) where T : struct
=> request.AddHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <returns></returns>
public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) {
CheckAndThrowsForInvalidHost(name, value);
return request.AddOrUpdateParameter(new HeaderParameter(name, value));
}

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Header name</param>
/// <param name="value">Header value</param>
/// <returns></returns>
public static RestRequest AddOrUpdateHeader<T>(this RestRequest request, string name, T value) where T : struct
=> request.AddOrUpdateHeader(name, Ensure.NotNull(value.ToString(), nameof(value)));

/// <summary>
/// Adds multiple headers to the request, using the key-value pairs provided.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="headers">Collection of key-value pairs, where key will be used as header name, and value as header value</param>
/// <returns></returns>
public static RestRequest AddHeaders(this RestRequest request, ICollection<KeyValuePair<string, string>> headers) {
CheckAndThrowsDuplicateKeys(headers);

Expand All @@ -104,6 +176,12 @@ public static RestRequest AddHeaders(this RestRequest request, ICollection<KeyVa
return request;
}

/// <summary>
/// Adds or updates multiple headers to the request, using the key-value pairs provided. Existing headers with the same name will be replaced.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="headers">Collection of key-value pairs, where key will be used as header name, and value as header value</param>
/// <returns></returns>
public static RestRequest AddOrUpdateHeaders(this RestRequest request, ICollection<KeyValuePair<string, string>> headers) {
CheckAndThrowsDuplicateKeys(headers);

Expand All @@ -114,9 +192,42 @@ public static RestRequest AddOrUpdateHeaders(this RestRequest request, ICollecti
return request;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Name of the parameter, must be matching a placeholder in the resource URL as {name}</param>
/// <param name="value">Value of the parameter</param>
/// <param name="type">Enum value specifying what kind of parameter is being added</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddParameter(this RestRequest request, string? name, object value, ParameterType type, bool encode = true)
=> request.AddParameter(Parameter.CreateParameter(name, value, type, encode));

/// <summary>
/// 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.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Name of the parameter, must be matching a placeholder in the resource URL as {name}</param>
/// <param name="value">Value of the parameter</param>
/// <param name="type">Enum value specifying what kind of parameter is being added</param>
/// <param name="encode">Encode the value or not, default true</param>
/// <returns></returns>
public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type, bool encode = true)
=> request.AddOrUpdateParameter(Parameter.CreateParameter(name, value, type, encode));

/// <summary>
/// Adds or updates request parameter, given the parameter instance, for example <see cref="QueryParameter"/> or <see cref="UrlSegmentParameter"/>.
/// It will replace an existing parameter with the same name.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="parameter">Parameter instance</param>
/// <returns></returns>
public static RestRequest AddOrUpdateParameter(this RestRequest request, Parameter parameter) {
var p = request.Parameters.FirstOrDefault(x => x.Name == parameter.Name && x.Type == parameter.Type);

Expand All @@ -126,16 +237,20 @@ public static RestRequest AddOrUpdateParameter(this RestRequest request, Paramet
return request;
}

/// <summary>
/// Adds or updates multiple request parameters, given the parameter instance, for example
/// <see cref="QueryParameter"/> or <see cref="UrlSegmentParameter"/>. Parameters with the same name will be replaced.
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="parameters">Collection of parameter instances</param>
/// <returns></returns>
public static RestRequest AddOrUpdateParameters(this RestRequest request, IEnumerable<Parameter> parameters) {
foreach (var parameter in parameters)
request.AddOrUpdateParameter(parameter);

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));

/// <summary>
/// Adds a file parameter to the request body. The file will be read from disk as a stream.
/// </summary>
Expand All @@ -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));

/// <summary>
/// Adds a file attachment to the request, where the file content will be retrieved from a given stream
/// </summary>
/// <param name="request">Request instance</param>
/// <param name="name">Parameter name</param>
/// <param name="getFile">Function that returns a stream with the file content</param>
/// <param name="fileName">File name</param>
/// <param name="contentType">Optional: content type. Default is "application/octet-stream"</param>
/// <returns></returns>
public static RestRequest AddFile(
this RestRequest request,
string name,
Func<Stream> 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));

/// <summary>
/// Adds a body parameter to the request
Expand Down Expand Up @@ -201,7 +324,7 @@ public static RestRequest AddBody(this RestRequest request, object obj, string?
/// <param name="obj">Object that will be serialized to JSON</param>
/// <param name="contentType">Optional: content type. Default is "application/json"</param>
/// <returns></returns>
public static RestRequest AddJsonBody(this RestRequest request, object obj, string contentType = ContentType.Json) {
public static RestRequest AddJsonBody<T>(this RestRequest request, T obj, string contentType = ContentType.Json) where T : class {
request.RequestFormat = DataFormat.Json;
return request.AddParameter(new JsonParameter("", obj, contentType));
}
Expand All @@ -214,7 +337,8 @@ public static RestRequest AddJsonBody(this RestRequest request, object obj, stri
/// <param name="contentType">Optional: content type. Default is "application/xml"</param>
/// <param name="xmlNamespace">Optional: XML namespace</param>
/// <returns></returns>
public static RestRequest AddXmlBody(this RestRequest request, object obj, string contentType = ContentType.Xml, string xmlNamespace = "") {
public static RestRequest AddXmlBody<T>(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;
Expand All @@ -227,7 +351,7 @@ public static RestRequest AddXmlBody(this RestRequest request, object obj, strin
/// <param name="obj">Object to add as form data</param>
/// <param name="includedProperties">Properties to include, or nothing to include everything</param>
/// <returns></returns>
public static RestRequest AddObject(this RestRequest request, object obj, params string[] includedProperties) {
public static RestRequest AddObject<T>(this RestRequest request, T obj, params string[] includedProperties) where T : class {
var props = obj.GetProperties(includedProperties);

foreach (var (name, value) in props) {
Expand Down
5 changes: 5 additions & 0 deletions src/RestSharp/RestClientOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
public bool UseDefaultCredentials { get; set; }

/// <summary>
/// Set to true if you need the Content-Type not to have the charset
/// </summary>
public bool DisableCharset { get; set; }

#if NETSTANDARD
public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip;
Expand Down
13 changes: 0 additions & 13 deletions test/RestSharp.IntegrationTests/AsyncTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Net;
using RestSharp.IntegrationTests.Fixtures;
using RestSharp.Tests.Shared.Fixtures;

namespace RestSharp.IntegrationTests;

Expand Down Expand Up @@ -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");
Expand Down
20 changes: 18 additions & 2 deletions test/RestSharp.IntegrationTests/Fixtures/TestServer.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -54,4 +68,6 @@ public async Task Stop() {
}
}

public record TestServerResponse(string Name, string Value);
record TestServerResponse(string Name, string Value);

record ContentResponse(string Content);
49 changes: 49 additions & 0 deletions test/RestSharp.IntegrationTests/PutTests.cs
Original file line number Diff line number Diff line change
@@ -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<TestRequest, TestRequest>("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);
Loading

0 comments on commit 8b388fb

Please sign in to comment.