Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
29 changes: 29 additions & 0 deletions Source/Mockolate/Web/HttpQueryParameterValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Mockolate.Web;

/// <summary>
/// The value of an HTTP query parameter.
/// </summary>
public class HttpQueryParameterValue
{
private readonly string _value;

/// <inheritdoc cref="HttpQueryParameterValue" />
public HttpQueryParameterValue(string value)
{
_value = value;
}

/// <summary>
/// Checks whether the given query parameter value matches this value.
/// </summary>
public virtual bool Matches(string parameterValue)
=> _value.Equals(parameterValue);

/// <summary>
/// Implicitly converts a string to an <see cref="HttpQueryParameterValue" />.
/// </summary>
public static implicit operator HttpQueryParameterValue(string value)
{
return new HttpQueryParameterValue(value);
}
}
43 changes: 36 additions & 7 deletions Source/Mockolate/Web/ItExtensions.Uri.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,24 @@ public interface IUriParameter : IParameter<Uri?>
IUriParameter WithPath(string pathPattern);

/// <summary>
/// Expects the <see cref="Uri.Query" /> to match the given <paramref name="queryPattern" /> supporting wildcards.
/// Expects the <see cref="Uri.Query" /> to contain the parameters in the <paramref name="queryString" />.
/// </summary>
IUriParameter WithQuery(string queryPattern);
/// <remarks>
/// Expect parameters to be separated by '&amp;' and the key-value pairs of the parameters to be separated by '='.
/// The order of the parameters is ignored.
/// </remarks>
IUriParameter WithQuery(string queryString);

/// <summary>
/// Expects the <see cref="Uri.Query" /> to contain the given <paramref name="key" />-<paramref name="value" />
/// pair.
/// </summary>
IUriParameter WithQuery(string key, HttpQueryParameterValue value);

/// <summary>
/// Expects the <see cref="Uri.Query" /> to contain the given query <paramref name="parameters" />.
/// </summary>
IUriParameter WithQuery(params IEnumerable<(string Key, HttpQueryParameterValue Value)> parameters);
Comment thread
vbreuss marked this conversation as resolved.
}

private sealed class UriParameter(string? pattern) : IUriParameter, IParameter
Expand All @@ -65,7 +80,7 @@ private sealed class UriParameter(string? pattern) : IUriParameter, IParameter
private string? _hostPattern;
private string? _pathPattern;
private Func<int, bool>? _portPredicate;
private string? _queryPattern;
private HttpQueryMatcher? _query;
private string? _scheme;

#pragma warning disable S3776 // Cognitive Complexity of methods should not be too high
Expand Down Expand Up @@ -110,8 +125,7 @@ public bool Matches(object? value)
return false;
}

if (_queryPattern is not null &&
!Wildcard.Pattern(_queryPattern, true).Matches(WebUtility.UrlDecode(uri.Query)))
if (_query is not null && !_query.Matches(uri))
{
return false;
}
Expand Down Expand Up @@ -165,9 +179,24 @@ public IUriParameter WithPath(string pathPattern)
return this;
}

public IUriParameter WithQuery(string queryPattern)
public IUriParameter WithQuery(string queryString)
{
_query ??= new HttpQueryMatcher();
_query.AddRequiredQueryParameter(queryString);
return this;
}

public IUriParameter WithQuery(string key, HttpQueryParameterValue value)
{
_query ??= new HttpQueryMatcher();
_query.AddRequiredQueryParameter(key, value);
return this;
}

public IUriParameter WithQuery(params IEnumerable<(string Key, HttpQueryParameterValue Value)> parameters)
{
_queryPattern = queryPattern;
_query ??= new HttpQueryMatcher();
_query.AddRequiredQueryParameter(parameters);
return this;
}
}
Expand Down
38 changes: 38 additions & 0 deletions Source/Mockolate/Web/ItExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
#if NETSTANDARD2_0
Expand Down Expand Up @@ -63,6 +64,43 @@ private static bool MatchesHeader(string name, HttpHeaderValue value, HttpHeader
}
}

private sealed class HttpQueryMatcher
{
private readonly List<(string Name, HttpQueryParameterValue Value)> _requiredQueryParameters = [];

public void AddRequiredQueryParameter(string name, HttpQueryParameterValue value)
=> _requiredQueryParameters.Add((name, value));

public void AddRequiredQueryParameter(IEnumerable<(string Name, HttpQueryParameterValue Value)> queryParameters)
=> _requiredQueryParameters.AddRange(queryParameters);

public void AddRequiredQueryParameter(string queryParameters)
=> _requiredQueryParameters.AddRange(
ParseQueryParameters(queryParameters)
.Select(pair => (pair.Key, new HttpQueryParameterValue(pair.Value))));

public bool Matches(Uri uri)
{
List<(string Key, string Value)> queryParameters = ParseQueryParameters(uri.Query).ToList();
return _requiredQueryParameters.All(requiredParameter
=> queryParameters.Any(p
=> p.Key == requiredParameter.Name &&
requiredParameter.Value.Matches(p.Value)));
}

private static IEnumerable<(string Key, string Value)> ParseQueryParameters(string input)
=> input.TrimStart('?')
.Split('&')
.Select(pair => pair.Split('=', 2))
.Where(pair => pair.Length > 0 && !string.IsNullOrWhiteSpace(pair[0]))
Comment thread
vbreuss marked this conversation as resolved.
.Select(pair =>
(
WebUtility.UrlDecode(pair[0]),
pair.Length == 2 ? WebUtility.UrlDecode(pair[1]) : ""
)
);
}

/// <summary>
/// Further expectations on the <see cref="HttpContent" />.
/// </summary>
Expand Down
12 changes: 11 additions & 1 deletion Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,12 @@ namespace Mockolate.Web
public virtual bool Matches(string headerValue) { }
public static Mockolate.Web.HttpHeaderValue op_Implicit(string value) { }
}
public class HttpQueryParameterValue
{
public HttpQueryParameterValue(string value) { }
public virtual bool Matches(string parameterValue) { }
public static Mockolate.Web.HttpQueryParameterValue op_Implicit(string value) { }
}
public static class ItExtensions
{
public interface IBinaryContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpContentParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>
Expand Down Expand Up @@ -1885,7 +1891,11 @@ namespace Mockolate.Web
Mockolate.Web.ItExtensions.IUriParameter WithHost(string hostPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPath(string pathPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPort(int port);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryPattern);
Mockolate.Web.ItExtensions.IUriParameter WithQuery([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpQueryParameterValue>> parameters);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryString);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string key, Mockolate.Web.HttpQueryParameterValue value);
}
extension(Mockolate.It _)
{
Expand Down
12 changes: 11 additions & 1 deletion Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1844,6 +1844,12 @@ namespace Mockolate.Web
public virtual bool Matches(string headerValue) { }
public static Mockolate.Web.HttpHeaderValue op_Implicit(string value) { }
}
public class HttpQueryParameterValue
{
public HttpQueryParameterValue(string value) { }
public virtual bool Matches(string parameterValue) { }
public static Mockolate.Web.HttpQueryParameterValue op_Implicit(string value) { }
}
public static class ItExtensions
{
public interface IBinaryContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpContentParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>
Expand Down Expand Up @@ -1884,7 +1890,11 @@ namespace Mockolate.Web
Mockolate.Web.ItExtensions.IUriParameter WithHost(string hostPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPath(string pathPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPort(int port);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryPattern);
Mockolate.Web.ItExtensions.IUriParameter WithQuery([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpQueryParameterValue>> parameters);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryString);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string key, Mockolate.Web.HttpQueryParameterValue value);
}
extension(Mockolate.It _)
{
Expand Down
12 changes: 11 additions & 1 deletion Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,12 @@ namespace Mockolate.Web
public virtual bool Matches(string headerValue) { }
public static Mockolate.Web.HttpHeaderValue op_Implicit(string value) { }
}
public class HttpQueryParameterValue
{
public HttpQueryParameterValue(string value) { }
public virtual bool Matches(string parameterValue) { }
public static Mockolate.Web.HttpQueryParameterValue op_Implicit(string value) { }
}
public static class ItExtensions
{
public interface IBinaryContentParameter : Mockolate.Parameters.IParameter<System.Net.Http.HttpContent?>, Mockolate.Web.ItExtensions.IHttpContentParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>, Mockolate.Web.ItExtensions.IHttpHeaderParameter<Mockolate.Web.ItExtensions.IBinaryContentParameter>
Expand Down Expand Up @@ -1825,7 +1831,11 @@ namespace Mockolate.Web
Mockolate.Web.ItExtensions.IUriParameter WithHost(string hostPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPath(string pathPattern);
Mockolate.Web.ItExtensions.IUriParameter WithPort(int port);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryPattern);
Mockolate.Web.ItExtensions.IUriParameter WithQuery([System.Runtime.CompilerServices.TupleElementNames(new string[] {
"Key",
"Value"})] System.Collections.Generic.IEnumerable<System.ValueTuple<string, Mockolate.Web.HttpQueryParameterValue>> parameters);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string queryString);
Mockolate.Web.ItExtensions.IUriParameter WithQuery(string key, Mockolate.Web.HttpQueryParameterValue value);
}
extension(Mockolate.It _)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using Mockolate.Web;

namespace Mockolate.Tests.Web;

public sealed partial class ItExtensionsTests
{
public sealed partial class IsUriTests
{
public sealed class ForHttpsTests
Comment thread
vbreuss marked this conversation as resolved.
Outdated
{
[Theory]
[InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", true)]
[InlineData("HTTP://www.aweXpect.com/foo/bar?x=123&y=234", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", false)]
public async Task ForHttp_ShouldVerifyHttpScheme(string uri, bool expectMatch)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.GetAsync(It.IsUri().ForHttp())
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.GetAsync(uri, CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}

[Theory]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", true)]
[InlineData("HTTPS://www.aweXpect.com/foo/bar?x=123&y=234", true)]
[InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", false)]
public async Task ForHttps_ShouldVerifyHttpsScheme(string uri, bool expectMatch)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.GetAsync(It.IsUri().ForHttps())
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.GetAsync(uri, CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using Mockolate.Web;

namespace Mockolate.Tests.Web;

public sealed partial class ItExtensionsTests
{
public sealed partial class IsUriTests
{
public sealed class WithHostTests
{
[Theory]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "www.awexpect.com", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*awexpect*", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*aweXpect*", true)]
[InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", "mockolate.com", false)]
public async Task ShouldVerifyHost(string uri, string hostPattern, bool expectMatch)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.GetAsync(It.IsUri().WithHost(hostPattern))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.GetAsync(uri, CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using Mockolate.Web;

namespace Mockolate.Tests.Web;

public sealed partial class ItExtensionsTests
{
public sealed partial class IsUriTests
{
public sealed class WithPathTests
{
[Theory]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "/foo/bar", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*foo*", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*FOO*", true)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", "*bar*", true)]
[InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", "*baz*", false)]
public async Task ShouldVerifyPath(string uri, string pathPattern, bool expectMatch)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.GetAsync(It.IsUri().WithPath(pathPattern))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.GetAsync(uri, CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Net;
using System.Net.Http;
using System.Threading;
using Mockolate.Web;

namespace Mockolate.Tests.Web;

public sealed partial class ItExtensionsTests
{
public sealed partial class IsUriTests
{
public sealed class WithPortTests
{
[Theory]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", 443, true)]
[InlineData("http://www.aweXpect.com/foo/bar?x=123&y=234", 80, true)]
[InlineData("https://www.aweXpect.com:8080/foo/bar?x=123&y=234", 8080, true)]
[InlineData("https://www.aweXpect.com:442/foo/bar?x=123&y=234", 443, false)]
[InlineData("https://www.aweXpect.com/foo/bar?x=123&y=234", 442, false)]
public async Task ShouldVerifyPort(string uri, int port, bool expectMatch)
{
HttpClient httpClient = Mock.Create<HttpClient>();
httpClient.SetupMock.Method
.GetAsync(It.IsUri().WithPort(port))
.ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));

HttpResponseMessage result = await httpClient.GetAsync(uri, CancellationToken.None);

await That(result.StatusCode)
.IsEqualTo(expectMatch ? HttpStatusCode.OK : HttpStatusCode.NotImplemented);
}
}
}
}
Loading
Loading