Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Activout.RestClient.Test/DictionaryParameterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task TestQueryParamDictionary()

_mockHttp
.When("https://example.com/api/test")
.WithQueryString("param1=value1&param2=value2")
.WithExactQueryString("param1=value1&param2=value2")
.Respond("application/json", "{}");

// act
Expand Down Expand Up @@ -108,7 +108,7 @@ public async Task TestMixedDictionaryAndRegularParams()

_mockHttp
.When("https://example.com/api/test")
.WithQueryString("param1=value1&param2=value2&singleParam=singleValue")
.WithExactQueryString("param1=value1&param2=value2&singleParam=singleValue")
.Respond("application/json", "{}");

// act
Expand Down Expand Up @@ -149,7 +149,7 @@ public async Task TestNullDictionaryValues()

_mockHttp
.When("https://example.com/api/test")
.WithQueryString("param1=value1&param2=")
.WithExactQueryString("param1=value1")
.Respond("application/json", "{}");

// act
Expand All @@ -167,7 +167,7 @@ public async Task TestBackwardCompatibilityWithNonDictionaryParams()

_mockHttp
.When("https://example.com/api/test")
.WithQueryString("regularParam=regularValue")
.WithExactQueryString("regularParam=regularValue")
.Respond("application/json", "{}");

// act
Expand Down
255 changes: 255 additions & 0 deletions Activout.RestClient.Test/NullParameterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using RichardSzalay.MockHttp;
using Xunit;
using Xunit.Abstractions;

namespace Activout.RestClient.Test;

public class NullParameterTests(ITestOutputHelper outputHelper)
{
private const string BaseUri = "https://example.com/api";

private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory();
private readonly MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler();
private readonly ILoggerFactory _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper);

private IRestClientBuilder CreateRestClientBuilder()
{
return _restClientFactory.CreateBuilder()
.With(_loggerFactory.CreateLogger<NullParameterTests>())
.With(_mockHttp.ToHttpClient())
.BaseUri(BaseUri);
}

[Fact]
public async Task TestEmptyStringQueryParam_ShouldAddParameter()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: empty string should still be added as parameter
_mockHttp
.When("https://example.com/api/test")
.WithExactQueryString("param=")
.Respond("application/json", "{}");

// act
await service.TestQueryParam("");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestEmptyStringFormParam_ShouldAddParameter()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: empty string should still be added as form parameter
_mockHttp
.When(HttpMethod.Post, "https://example.com/api/test")
.WithExactFormData([
new KeyValuePair<string, string>("param", "")
])
.Respond("application/json", "{}");

// act
await service.TestFormParam("");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestEmptyStringHeaderParam_ShouldAddHeader()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: empty string should still be added as header
_mockHttp
.When("https://example.com/api/test")
.WithHeaders("X-Custom-Header", "")
.Respond("application/json", "{}");

// act
await service.TestHeaderParam("");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestMixedNullAndValidQueryParams_ShouldOnlyAddValidParams()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: only the valid parameter should be added, null param should be skipped
_mockHttp
.When("https://example.com/api/test")
.WithExactQueryString("validParam=validValue")
.Respond("application/json", "{}");

// act
await service.TestMixedQueryParams(null, "validValue");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestMixedNullAndValidFormParams_ShouldOnlyAddValidParams()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: only the valid form parameter should be added, null param should be skipped
_mockHttp
.When(HttpMethod.Post, "https://example.com/api/test")
.WithExactFormData([
new KeyValuePair<string, string>("validParam", "validValue")
])
.Respond("application/json", "{}");

// act
await service.TestMixedFormParams(null, "validValue");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestMixedNullAndValidHeaderParams_ShouldOnlyAddValidParams()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();

// expect: only the valid header should be added, null param should be skipped
_mockHttp
.When("https://example.com/api/test")
.WithHeaders("X-Valid-Header", "validValue")
.With(req => !req.Headers.Contains("X-Null-Header"))
.Respond("application/json", "{}");

// act
await service.TestMixedHeaderParams(null, "validValue");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestNullValueInQueryDictionary_SkipsNullValues()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();
var queryParams = new Dictionary<string, string>
{
["param1"] = "value1",
["param2"] = null, // This should be skipped (already working correctly)
["param3"] = "value3"
};

// Dictionary handling already works correctly - only non-null values are added
_mockHttp
.When("https://example.com/api/test")
.WithExactQueryString("param1=value1&param3=value3")
.Respond("application/json", "{}");

// act
await service.TestNullDictionaryValues(queryParams);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestNullValueInFormDictionary_SkipsNullValues()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();
var formParams = new Dictionary<string, string>
{
["field1"] = "value1",
["field2"] = null, // This should be skipped (already working correctly)
["field3"] = "value3"
};

// Dictionary handling already works correctly - only non-null values are added
_mockHttp
.When(HttpMethod.Post, "https://example.com/api/test")
.WithExactFormData([
new KeyValuePair<string, string>("field1", "value1"),
new KeyValuePair<string, string>("field3", "value3")
])
.Respond("application/json", "{}");

// act
await service.TestNullFormDictionaryValues(formParams);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestNullValueInHeaderDictionary_SkipsNullValues()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestNullService>();
var headers = new Dictionary<string, string>
{
["X-Header-1"] = "value1",
["X-Header-2"] = null, // This should be skipped (already working correctly)
["X-Header-3"] = "value3"
};

// Dictionary handling already works correctly - only non-null values are added
_mockHttp
.When("https://example.com/api/test")
.WithHeaders("X-Header-1", "value1")
.WithHeaders("X-Header-3", "value3")
.With(req => !req.Headers.Contains("X-Header-2"))
.Respond("application/json", "{}");

// act
await service.TestNullHeaderDictionaryValues(headers);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}
}

public interface ITestNullService
{
[Get("test")]
Task TestQueryParam([QueryParam("param")] string param);

[Post("test")]
Task TestFormParam([FormParam("param")] string param);

[Get("test")]
Task TestHeaderParam([HeaderParam("X-Custom-Header")] string param);

[Get("test")]
Task TestMixedQueryParams([QueryParam("nullParam")] string nullParam, [QueryParam("validParam")] string validParam);

[Post("test")]
Task TestMixedFormParams([FormParam("nullParam")] string nullParam, [FormParam("validParam")] string validParam);

[Get("test")]
Task TestMixedHeaderParams([HeaderParam("X-Null-Header")] string nullHeader, [HeaderParam("X-Valid-Header")] string validHeader);

[Get("test")]
Task TestNullDictionaryValues([QueryParam] Dictionary<string, string> queryParams);

[Post("test")]
Task TestNullFormDictionaryValues([FormParam] Dictionary<string, string> formParams);

[Get("test")]
Task TestNullHeaderDictionaryValues([HeaderParam] Dictionary<string, string> headers);
}
42 changes: 25 additions & 17 deletions Activout.RestClient/Implementation/RequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,15 @@ private void SetHeaders(HttpRequestMessage request, List<KeyValuePair<string, ob
}

private CancellationToken GetParams(
IReadOnlyList<object> args,
IDictionary<string, object> pathParams,
ICollection<string> queryParams,
ICollection<KeyValuePair<string, string>> formParams,
object[] args,
Dictionary<string, object> pathParams,
List<string> queryParams,
List<KeyValuePair<string, string>> formParams,
List<KeyValuePair<string, object>> headers,
List<Part<HttpContent>> parts)
{
var cancellationToken = CancellationToken.None;


for (var i = 0; i < _parameters.Length; i++)
{
var rawValue = args[i];
Expand Down Expand Up @@ -300,12 +299,15 @@ private CancellationToken GetParams(
{
foreach (DictionaryEntry entry in dictionary)
{
var key = entry.Key?.ToString() ?? string.Empty;
var value = entry.Value?.ToString() ?? string.Empty;
queryParams.Add(Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value));
var key = entry.Key?.ToString();
var value = entry.Value?.ToString();
if (key != null && value != null)
{
queryParams.Add(Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value));
}
}
}
else
else if (rawValue != null)
{
queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" +
Uri.EscapeDataString(stringValue));
Expand All @@ -318,12 +320,15 @@ private CancellationToken GetParams(
{
foreach (DictionaryEntry entry in dictionary)
{
var key = entry.Key?.ToString() ?? string.Empty;
var value = entry.Value?.ToString() ?? string.Empty;
formParams.Add(new KeyValuePair<string, string>(key, value));
var key = entry.Key?.ToString();
var value = entry.Value?.ToString();
if (key != null && value != null)
{
formParams.Add(new KeyValuePair<string, string>(key, value));
}
}
}
else
else if (rawValue != null)
{
formParams.Add(new KeyValuePair<string, string>(formParamAttribute.Name ?? parameterName,
stringValue));
Expand All @@ -336,12 +341,15 @@ private CancellationToken GetParams(
{
foreach (DictionaryEntry entry in dictionary)
{
var key = entry.Key?.ToString() ?? string.Empty;
var value = entry.Value?.ToString() ?? string.Empty;
headers.AddOrReplaceHeader(key, value, headerParamAttribute.Replace);
var key = entry.Key?.ToString();
var value = entry.Value?.ToString();
if (key != null && value != null)
{
headers.AddOrReplaceHeader(key, value, headerParamAttribute.Replace);
}
}
}
else
else if (rawValue != null)
{
headers.AddOrReplaceHeader(headerParamAttribute.Name ?? parameterName, stringValue,
headerParamAttribute.Replace);
Expand Down