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
197 changes: 197 additions & 0 deletions Activout.RestClient.Test/DictionaryParameterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
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 DictionaryParameterTests(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<DictionaryParameterTests>())
.With(_mockHttp.ToHttpClient())
.BaseUri(BaseUri);
}

[Fact]
public async Task TestQueryParamDictionary()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var queryParams = new Dictionary<string, string>
{
["param1"] = "value1",
["param2"] = "value2"
};

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

// act
await service.TestQueryParamDictionary(queryParams);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestFormParamDictionary()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var formParams = new Dictionary<string, string>
{
["field1"] = "value1",
["field2"] = "value2"
};

_mockHttp
.When(HttpMethod.Post, "https://example.com/api/test")
.WithFormData("field1", "value1")
.WithFormData("field2", "value2")
.Respond("application/json", "{}");

// act
await service.TestFormParamDictionary(formParams);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestHeaderParamDictionary()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var headers = new Dictionary<string, string>
{
["X-Custom-Header1"] = "value1",
["X-Custom-Header2"] = "value2"
};

_mockHttp
.When("https://example.com/api/test")
.WithHeaders("X-Custom-Header1", "value1")
.WithHeaders("X-Custom-Header2", "value2")
.Respond("application/json", "{}");

// act
await service.TestHeaderParamDictionary(headers);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestMixedDictionaryAndRegularParams()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var queryParams = new Dictionary<string, string>
{
["param1"] = "value1",
["param2"] = "value2"
};

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

// act
await service.TestMixedParams(queryParams, "singleValue");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestEmptyDictionary()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var emptyParams = new Dictionary<string, string>();

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

// act
await service.TestQueryParamDictionary(emptyParams);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public async Task TestNullDictionaryValues()
{
// arrange
var service = CreateRestClientBuilder().Build<ITestService>();
var paramsWithNull = new Dictionary<string, string>
{
["param1"] = "value1",
["param2"] = null
};

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

// act
await service.TestQueryParamDictionary(paramsWithNull);

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}

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

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

// act
await service.TestRegularParam("regularValue");

// assert
_mockHttp.VerifyNoOutstandingExpectation();
}
}

public interface ITestService
{
[Get("test")]
Task TestQueryParamDictionary([QueryParam] Dictionary<string, string> queryParams);

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

[Get("test")]
Task TestHeaderParamDictionary([HeaderParam] Dictionary<string, string> headers);

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

[Get("test")]
Task TestRegularParam([QueryParam("regularParam")] string regularParam);
}
49 changes: 43 additions & 6 deletions Activout.RestClient/Implementation/RequestHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
Expand Down Expand Up @@ -295,20 +296,56 @@ private CancellationToken GetParams(
}
else if (attribute is QueryParamAttribute queryParamAttribute)
{
queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" +
Uri.EscapeDataString(stringValue));
if (rawValue is IDictionary dictionary)
{
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));
}
}
else
{
queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" +
Uri.EscapeDataString(stringValue));
}
handled = true;
}
else if (attribute is FormParamAttribute formParamAttribute)
{
formParams.Add(new KeyValuePair<string, string>(formParamAttribute.Name ?? parameterName,
stringValue));
if (rawValue is IDictionary dictionary)
{
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));
}
}
else
{
formParams.Add(new KeyValuePair<string, string>(formParamAttribute.Name ?? parameterName,
stringValue));
}
handled = true;
}
else if (attribute is HeaderParamAttribute headerParamAttribute)
{
headers.AddOrReplaceHeader(headerParamAttribute.Name ?? parameterName, stringValue,
headerParamAttribute.Replace);
if (rawValue is IDictionary dictionary)
{
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);
}
}
else
{
headers.AddOrReplaceHeader(headerParamAttribute.Name ?? parameterName, stringValue,
headerParamAttribute.Replace);
}
handled = true;
}
}
Expand Down