Skip to content

Commit

Permalink
#102 Many more variations on query param asserting, other test utilit…
Browse files Browse the repository at this point in the history
…y methods, HttpCall.Url is now a Flurl.Url object
  • Loading branch information
tmenier committed Nov 24, 2016
1 parent 68c5251 commit bd8ef06
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 51 deletions.
59 changes: 49 additions & 10 deletions Test/Flurl.Test.Shared/Http/TestingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public async Task can_setup_multiple_responses() {
.RespondWith("two")
.RespondWith("three");

HttpTest.ShouldNotHaveMadeACall();

await "http://www.api.com/1".GetAsync();
await "http://www.api.com/2".GetAsync();
await "http://www.api.com/3".GetAsync();
Expand All @@ -61,27 +63,64 @@ public async Task can_setup_multiple_responses() {
Assert.AreEqual("two", await calls[1].Response.Content.ReadAsStringAsync());
Assert.AreEqual("three", await calls[2].Response.Content.ReadAsStringAsync());

HttpTest.ShouldHaveMadeACall();
HttpTest.ShouldHaveCalled("http://www.api.com/*").WithVerb(HttpMethod.Get).Times(3);
HttpTest.ShouldNotHaveCalled("http://www.otherapi.com/*");
}

[Test]
public async Task can_assert_query_params() {
await "http://www.api.com?x=111&y=222&z=333".GetAsync();

HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParam("x");
HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParam("y", 222);
HttpTest.ShouldHaveCalled("*").WithQueryParam("z", "*3");
HttpTest.ShouldHaveCalled("*").WithQueryParams(new { z = 333, y = 222 });
await "http://www.api.com?x=111&y=222&z=333#abcd".GetAsync();

HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParams();
HttpTest.ShouldHaveMadeACall().WithQueryParam("x");
HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParams("z", "y");
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("y", 222);
HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParamValue("z", "*3");
HttpTest.ShouldHaveMadeACall().WithQueryParamValues(new { z = 333, y = 222 });

// without
HttpTest.ShouldHaveCalled("http://www.api.com*").WithoutQueryParam("w");
HttpTest.ShouldHaveMadeACall().WithoutQueryParams("t", "u", "v");
HttpTest.ShouldHaveCalled("http://www.api.com*").WithoutQueryParamValue("x", 112);
HttpTest.ShouldHaveMadeACall().WithoutQueryParamValues(new { x = 112, y = 223, z = 666 });

// failures
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithQueryParam("w"));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("y", 223));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("z", "*4"));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithQueryParamValues(new { x = 111, y = 666 }));

Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParam("w"));
HttpTest.ShouldHaveMadeACall().WithoutQueryParams());
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithoutQueryParam("x"));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithoutQueryParams("z", "y"));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveCalled("http://www.api.com*").WithQueryParam("y", 223));
HttpTest.ShouldHaveMadeACall().WithoutQueryParamValue("y", 222));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithoutQueryParamValue("z", "*3"));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveMadeACall().WithoutQueryParamValues(new { z = 333, y = 222 }));
}

[Test]
public async Task can_assert_multiple_occurances_of_query_param() {
await "http://www.api.com?x=1&x=2&x=3&y=4#abcd".GetAsync();

HttpTest.ShouldHaveMadeACall().WithQueryParam("x");
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("x", new[] { 2, 1 }); // order shouldn't matter
HttpTest.ShouldHaveMadeACall().WithQueryParamValues(new { x = new[] { 3, 2, 1 } }); // order shouldn't matter

Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveCalled("*").WithQueryParam("z", "*4"));
HttpTest.ShouldHaveMadeACall().WithQueryParamValue("x", new[] { 1, 2, 4 }));
Assert.Throws<HttpCallAssertException>(() =>
HttpTest.ShouldHaveCalled("*").WithQueryParams(new { x = 111, y = 666 }));
HttpTest.ShouldHaveMadeACall().WithQueryParamValues(new { x = new[] { 1, 2, 4 } }));
}

[Test]
Expand Down
9 changes: 5 additions & 4 deletions src/Flurl.Http.Shared/HttpCall.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using Flurl.Http.Configuration;
Expand All @@ -13,9 +11,12 @@ namespace Flurl.Http
/// </summary>
public class HttpCall
{
private readonly Lazy<Url> _url;

internal HttpCall(HttpRequestMessage request, FlurlHttpSettings settings) {
Request = request;
Settings = settings;
_url = new Lazy<Url>(() => new Url(Request.RequestUri.AbsoluteUri));
}

/// <summary>
Expand Down Expand Up @@ -65,9 +66,9 @@ internal HttpCall(HttpRequestMessage request, FlurlHttpSettings settings) {
public TimeSpan? Duration => EndedUtc - StartedUtc;

/// <summary>
/// Absolute URI being called.
/// The URL being called.
/// </summary>
public string Url => Request.RequestUri.AbsoluteUri;
public Url Url => _url.Value;

/// <summary>
/// True if a response was received, regardless of whether it is an error status.
Expand Down
36 changes: 21 additions & 15 deletions src/Flurl.Http.Shared/Testing/HttpCallAssertException.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;

namespace Flurl.Http.Testing
{
Expand All @@ -11,21 +12,26 @@ public class HttpCallAssertException : Exception
/// <summary>
/// Initializes a new instance of the <see cref="HttpCallAssertException"/> class.
/// </summary>
/// <param name="urlPattern">The URL pattern.</param>
/// <param name="expectedCalls">The expected calls.</param>
/// <param name="actualCalls">The actual calls.</param>
public HttpCallAssertException(string urlPattern, int? expectedCalls, int actualCalls) : base(BuildMessage(urlPattern, expectedCalls, actualCalls)) { }
/// <param name="conditions">The expected call conditions.</param>
/// <param name="expectedCalls">The expected number of calls.</param>
/// <param name="actualCalls">The actual number calls.</param>
public HttpCallAssertException(IList<string> conditions, int? expectedCalls, int actualCalls) : base(BuildMessage(conditions, expectedCalls, actualCalls)) { }

private static string BuildMessage(string urlPattern, int? expectedCalls, int actualCalls) {
if (expectedCalls == null)
return $"Expected call to {urlPattern} was not made.";

return new StringBuilder()
.Append("Expected ").Append(expectedCalls.Value)
.Append(expectedCalls == 1 ? " call" : " calls")
.Append(" to ").Append(urlPattern).Append(" but ").Append(actualCalls)
.Append(actualCalls == 1 ? " call was made." : " calls were made.")
.ToString();
private static string BuildMessage(IList<string> conditions, int? expectedCalls, int actualCalls) {
var expected =
(expectedCalls == null) ? "any calls to be made" :
(expectedCalls == 0) ? "no calls to be made" :
(expectedCalls == 1) ? "1 call to be made" :
expectedCalls + " calls to be made";
var actual =
(actualCalls == 0) ? "no matching calls were made" :
(actualCalls == 1) ? "1 matching call was made" :
actualCalls + " matching calls were made";
if (conditions.Any())
expected += " with " + string.Join(" and ", conditions);
else
actual = actual.Replace(" matching", "");
return $"Expected {expected}, but {actual}.";
}
}
}
129 changes: 109 additions & 20 deletions src/Flurl.Http.Shared/Testing/HttpCallAssertion.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.Http;
Expand All @@ -16,9 +17,9 @@ namespace Flurl.Http.Testing
public class HttpCallAssertion
{
private readonly bool _negate;
private readonly IList<string> _expectedConditions = new List<string>();

private IList<HttpCall> _calls;
private string _urlPattern;

/// <param name="loggedCalls">Set of calls (usually from HttpTest.CallLog) to assert against.</param>
/// <param name="negate">if true, assertions pass when calls matching criteria were NOT made.</param>
Expand All @@ -45,7 +46,11 @@ public void Times(int expectedCount) {
/// </summary>
/// <param name="urlPattern">Can contain * wildcard.</param>
public HttpCallAssertion WithUrlPattern(string urlPattern) {
_urlPattern = urlPattern; // this will land in the exception message when we assert, which is the only reason for capturing it
if (urlPattern == "*") {
Assert();
return this;
}
_expectedConditions.Add($"URL pattern {urlPattern}");
return With(c => MatchesPattern(c.Url, urlPattern));
}

Expand All @@ -55,38 +60,110 @@ public HttpCallAssertion WithUrlPattern(string urlPattern) {
/// <param name="name">The query parameter name.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParam(string name) {
return With(c => new Url(c.Url).QueryParams.Any(q => q.Name == name));
_expectedConditions.Add($"query parameter {name}");
return With(c => c.Url.QueryParams.Any(q => q.Name == name));
}

/// <summary>
/// Asserts whether calls were made containing the given query parameter name/value.
/// Asserts whether calls were made NOT containing the given query parameter.
/// </summary>
/// <param name="name">The query parameter name.</param>
/// <returns></returns>
public HttpCallAssertion WithoutQueryParam(string name) {
_expectedConditions.Add($"no query parameter {name}");
return Without(c => c.Url.QueryParams.Any(q => q.Name == name));
}

/// <summary>
/// Asserts whether calls were made containing all the given query parameters (regardless of their values).
/// </summary>
/// <param name="names">The query parameter names.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParams(params string[] names) {
if (!names.Any()) {
_expectedConditions.Add("any query parameters");
return With(c => c.Url.QueryParams.Any());
}
return names.Select(WithQueryParam).LastOrDefault() ?? this;
}

/// <summary>
/// Asserts whether calls were made NOT containing ANY of the given query parameters.
/// </summary>
/// <param name="names">The query parameter names.</param>
/// <returns></returns>
public HttpCallAssertion WithoutQueryParams(params string[] names) {
if (!names.Any()) {
_expectedConditions.Add("no query parameters");
return Without(c => c.Url.QueryParams.Any());
}
return names.Select(WithoutQueryParam).LastOrDefault() ?? this;
}

/// <summary>
/// Asserts whether calls were made containing the given query parameter name and value.
/// </summary>
/// <param name="name">The query parameter name.</param>
/// <param name="value">The query parameter value. Can contain * wildcard.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParamValue(string name, object value) {
if (value is IEnumerable && !(value is string)) {
foreach (var val in (IEnumerable)value)
WithQueryParamValue(name, val);
return this;
}
_expectedConditions.Add($"query parameter {name}={value}");
return With(c => c.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value)));
}

/// <summary>
/// Asserts whether calls were made NOT containing the given query parameter name and value.
/// </summary>
/// <param name="name">The query parameter name.</param>
/// <param name="value">The query parameter value. Can contain * wildcard.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParam(string name, object value) {
return With(c => new Url(c.Url).QueryParams.Any(q => q.Name == name && MatchesPattern(q.Value.ToString(), value.ToString())));
public HttpCallAssertion WithoutQueryParamValue(string name, object value) {
if (value is IEnumerable && !(value is string)) {
foreach (var val in (IEnumerable)value)
WithoutQueryParamValue(name, val);
return this;
}
_expectedConditions.Add($"no query parameter {name}={value}");
return Without(c => c.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value)));
}

/// <summary>
/// Asserts whether calls were made containing all of the given query parameters.
/// Asserts whether calls were made containing all of the given query parameter values.
/// </summary>
/// <param name="values">Object (usually anonymous) or dictionary that is parsed to name/value query parameters to check for.</param>
/// <returns></returns>
public HttpCallAssertion WithQueryParams(object values) {
return With(c => {
var expected = values.ToKeyValuePairs().Select(kv => $"{kv.Key}={kv.Value}");
var actual = new Url(c.Url).QueryParams.Select(q => $"{q.Name}={q.Value}");
//http://stackoverflow.com/a/333034/62600
return !expected.Except(actual).Any();
});
public HttpCallAssertion WithQueryParamValues(object values) {
return values.ToKeyValuePairs().Select(kv => WithQueryParamValue(kv.Key, kv.Value)).LastOrDefault() ?? this;
}

/// <summary>
/// Asserts whether calls were made NOT containing ANY of the given query parameter values.
/// </summary>
/// <param name="values">Object (usually anonymous) or dictionary that is parsed to name/value query parameters to check for.</param>
/// <returns></returns>
public HttpCallAssertion WithoutQueryParamValues(object values) {
return values.ToKeyValuePairs().Select(kv => WithoutQueryParamValue(kv.Key, kv.Value)).LastOrDefault() ?? this;
}

private bool QueryParamMatches(QueryParameter qp, string name, object value) {
if (qp.Name != name)
return false;
if (value is string)
return MatchesPattern(qp.Value?.ToString(), value?.ToString());
return qp.Value?.ToString() == value?.ToString();
}

/// <summary>
/// Asserts whether calls were made containing given request body or request body pattern.
/// </summary>
/// <param name="bodyPattern">Can contain * wildcard.</param>
public HttpCallAssertion WithRequestBody(string bodyPattern) {
_expectedConditions.Add($"body matching pattern {bodyPattern}");
return With(c => MatchesPattern(c.RequestBody, bodyPattern));
}

Expand All @@ -103,13 +180,15 @@ public HttpCallAssertion WithRequestJson(object body) {
/// Asserts whether calls were made with given HTTP verb.
/// </summary>
public HttpCallAssertion WithVerb(HttpMethod httpMethod) {
_expectedConditions.Add("verb " + httpMethod);
return With(c => c.Request.Method == httpMethod);
}

/// <summary>
/// Asserts whether calls were made with a request body of the given content (MIME) type.
/// </summary>
public HttpCallAssertion WithContentType(string mediaType) {
_expectedConditions.Add("content type " + mediaType);
return With(c => c.Request.Content.Headers.ContentType.MediaType == mediaType);
}

Expand All @@ -118,8 +197,8 @@ public HttpCallAssertion WithContentType(string mediaType) {
/// </summary>
/// <param name="token">Expected token value</param>
/// <returns></returns>
public HttpCallAssertion WithOAuthBearerToken(string token)
{
public HttpCallAssertion WithOAuthBearerToken(string token) {
_expectedConditions.Add("OAuth bearer token " + token);
return With(c => c.Request.Headers.Authorization?.Scheme == "Bearer"
&& c.Request.Headers.Authorization?.Parameter == token);
}
Expand All @@ -130,15 +209,15 @@ public HttpCallAssertion WithOAuthBearerToken(string token)
/// <param name="username">Expected username</param>
/// <param name="password">Expected password</param>
/// <returns></returns>
public HttpCallAssertion WithBasicAuth(string username, string password)
{
public HttpCallAssertion WithBasicAuth(string username, string password) {
_expectedConditions.Add($"basic auth credentials {username}/{password}");
var value = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
return With(c => c.Request.Headers.Authorization?.Scheme == "Basic"
&& c.Request.Headers.Authorization?.Parameter == value);
}

/// <summary>
/// Asserts whether calls were made matching a given predicate function.
/// Asserts whether calls were made matching the given predicate function.
/// </summary>
/// <param name="match">Predicate (usually a lambda expression) that tests an HttpCall and returns a bool.</param>
public HttpCallAssertion With(Func<HttpCall, bool> match) {
Expand All @@ -147,12 +226,22 @@ public HttpCallAssertion With(Func<HttpCall, bool> match) {
return this;
}

/// <summary>
/// Asserts whether calls were made that do NOT match the given predicate function.
/// </summary>
/// <param name="match">Predicate (usually a lambda expression) that tests an HttpCall and returns a bool.</param>
public HttpCallAssertion Without(Func<HttpCall, bool> match) {
_calls = _calls.Where(c => !match(c)).ToList();
Assert();
return this;
}

private void Assert(int? count = null) {
var pass = count.HasValue ? (_calls.Count == count.Value) : _calls.Any();
if (_negate) pass = !pass;

if (!pass)
throw new HttpCallAssertException(_urlPattern, count, _calls.Count);
throw new HttpCallAssertException(_expectedConditions, count, _calls.Count);
}

private bool MatchesPattern(string textToCheck, string pattern) {
Expand Down
Loading

0 comments on commit bd8ef06

Please sign in to comment.