Skip to content

Commit 3455849

Browse files
committed
Improved the way the WithBodyXXX overloads are reported
1 parent 7cd2893 commit 3455849

File tree

4 files changed

+127
-73
lines changed

4 files changed

+127
-73
lines changed

Mockly.Specs/HttpMockSpecs.cs

Lines changed: 107 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.Net;
34
using System.Net.Http;
45
using System.Text;
@@ -147,7 +148,7 @@ public async Task Can_match_path_with_pipe_character()
147148
// Arrange
148149
var mock = new HttpMock();
149150
var key = $"{Guid.NewGuid()}|{Guid.NewGuid()}";
150-
151+
151152
mock.ForDelete()
152153
.WithPath($"IncomeRelations/{key}")
153154
.RespondsWithStatus(HttpStatusCode.OK);
@@ -166,7 +167,7 @@ public async Task Can_match_query_with_pipe_character()
166167
// Arrange
167168
var mock = new HttpMock();
168169
var filter = "status=active|pending";
169-
170+
170171
mock.ForGet()
171172
.WithPath("api/items")
172173
.WithQuery($"filter={filter}")
@@ -480,6 +481,28 @@ public async Task Can_match_body_against_a_wildcard_pattern()
480481
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
481482
}
482483

484+
[Fact]
485+
public async Task Will_report_the_wildcard()
486+
{
487+
// Arrange
488+
var mock = new HttpMock();
489+
490+
mock.ForPost()
491+
.WithPath("/api/test")
492+
.WithBody("*something*")
493+
.RespondsWithStatus(HttpStatusCode.NoContent);
494+
495+
var client = mock.GetClient();
496+
497+
// Act
498+
var action = async () => await client.PostAsync("https://localhost/api/mismatch",
499+
new StringContent("a body with something in it"));
500+
501+
// Assert
502+
await action.Should().ThrowAsync<UnexpectedRequestException>()
503+
.WithMessage("*where body matches wildcard pattern*something*");
504+
}
505+
483506
[Fact]
484507
public async Task Can_match_the_body_against_a_json_string_ignoring_layout()
485508
{
@@ -507,6 +530,27 @@ public async Task Can_match_the_body_against_a_json_string_ignoring_layout()
507530
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
508531
}
509532

533+
[Fact]
534+
public async Task Will_report_the_expected_json()
535+
{
536+
// Arrange
537+
var mock = new HttpMock();
538+
539+
mock.ForPost()
540+
.WithPath("/api/json")
541+
.WithBodyMatchingJson("{\"name\": \"John\", \"age\": 30}")
542+
.RespondsWithStatus(HttpStatusCode.NoContent);
543+
544+
var client = mock.GetClient();
545+
546+
// Act
547+
var action = async () => await client.PostAsync("https://localhost/api/wrongroute", new StringContent(""));
548+
549+
// Assert
550+
await action.Should().ThrowAsync<UnexpectedRequestException>()
551+
.WithMessage("*body matches JSON {\"name\": \"John\", \"age\": 30}*");
552+
}
553+
510554
[Fact]
511555
public async Task Throws_for_a_body_that_cannot_be_parsed_as_json()
512556
{
@@ -555,6 +599,28 @@ public async Task Can_match_body_against_a_regex_pattern()
555599
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
556600
}
557601

602+
[Fact]
603+
public async Task Will_report_the_regex_for_mismatching_request()
604+
{
605+
// Arrange
606+
var mock = new HttpMock();
607+
608+
mock.ForPost()
609+
.WithPath("/api/test")
610+
.WithBodyMatchingRegex(".*something.*")
611+
.RespondsWithStatus(HttpStatusCode.NoContent);
612+
613+
var client = mock.GetClient();
614+
615+
// Act
616+
var action = async () => await client.PostAsync("https://localhost/api/wrongroute",
617+
new StringContent("a body with something in it"));
618+
619+
// Assert
620+
await action.Should().ThrowAsync<UnexpectedRequestException>()
621+
.WithMessage("*body matches regex .*something.**");
622+
}
623+
558624
[Fact]
559625
public async Task Can_prevent_the_matcher_from_prefetching_the_body()
560626
{
@@ -583,6 +649,29 @@ await client.PostAsync("https://localhost/api/test",
583649
request.Should().NotBeNull();
584650
request.Body.Should().BeNull();
585651
}
652+
653+
[Fact]
654+
public async Task Can_use_multiple_matches_combined()
655+
{
656+
// Arrange
657+
var mock = new HttpMock();
658+
659+
mock.ForPost()
660+
.WithPath("/api/test")
661+
.WithBody("*something*")
662+
.WithBodyMatchingRegex(".*else.*")
663+
.RespondsWithStatus(HttpStatusCode.NoContent);
664+
665+
var client = mock.GetClient();
666+
667+
// Act
668+
var action = async () => await client.PostAsync("https://localhost/api/test",
669+
new StringContent("a body with something in it"));
670+
671+
// Assert
672+
await action.Should().ThrowAsync<UnexpectedRequestException>()
673+
.WithMessage("*body matches wildcard pattern \"*something*\" or body matches regex .*else.*");
674+
}
586675
}
587676

588677
public class WhenCollectingRequests
@@ -770,14 +859,14 @@ await act.Should().ThrowAsync<UnexpectedRequestException>()
770859
GET https://localhost/fnv_collectiveschemes(111)
771860
772861
Closest matching mock:
773-
GET https://*/fnv_collectiveschemes(123*)
862+
GET https://localhost:443/fnv_collectiveschemes(123*)
774863
775864
Registered mocks:
776-
- GET https://*/fnv_collectiveschemes
777-
- POST https://*/fnv_collectiveschemes
778-
- GET https://*/fnv_collectiveschemes(123*)
779-
- GET https://*/fnv_collectiveschemes(123*) (1 custom matcher(s)) where (request => request.Uri?.Query == "?$count=1")
780-
- GET https://*/fnv_collectiveschemes(456)
865+
- GET https://localhost:443/fnv_collectiveschemes
866+
- POST https://localhost:443/fnv_collectiveschemes
867+
- GET https://localhost:443/fnv_collectiveschemes(123*)
868+
- GET https://localhost:443/fnv_collectiveschemes(123*) where request => request.Uri?.Query == "?$count=1"
869+
- GET https://localhost:443/fnv_collectiveschemes(456)
781870
""");
782871
}
783872

@@ -804,7 +893,7 @@ await act.Should().ThrowAsync<UnexpectedRequestException>()
804893
GET https://localhost/fnv_collectiveschemes(111)
805894
806895
Registered mocks:
807-
- GET https://*/fnv_collectiveschemes
896+
- GET https://localhost:443/fnv_collectiveschemes
808897
""");
809898
}
810899
}
@@ -1512,7 +1601,7 @@ public async Task Can_respond_with_stream_content()
15121601
{
15131602
// Arrange
15141603
var mock = new HttpMock();
1515-
var stream = new System.IO.MemoryStream([1, 2, 3, 4, 5]);
1604+
var stream = new MemoryStream([1, 2, 3, 4, 5]);
15161605
var content = new StreamContent(stream);
15171606

15181607
mock.ForGet()
@@ -1553,7 +1642,11 @@ public UserBuilder WithName(string name)
15531642

15541643
public object Build()
15551644
{
1556-
return new { Id = id, Name = name };
1645+
return new
1646+
{
1647+
Id = id,
1648+
Name = name
1649+
};
15571650
}
15581651
}
15591652

@@ -1619,17 +1712,7 @@ public async Task Can_use_response_builder_with_odata_result_single_item()
16191712

16201713
// Assert
16211714
response.StatusCode.Should().Be(HttpStatusCode.OK);
1622-
await response.Should().BeEquivalentTo(new
1623-
{
1624-
value = new[]
1625-
{
1626-
new
1627-
{
1628-
Id = 789,
1629-
Name = "Bob Johnson"
1630-
}
1631-
}
1632-
});
1715+
await response.Should().BeEquivalentTo(new { value = new[] { new { Id = 789, Name = "Bob Johnson" } } });
16331716
}
16341717

16351718
[Fact]
@@ -1648,17 +1731,7 @@ public async Task Can_use_response_builder_with_odata_result_and_status_code()
16481731

16491732
// Assert
16501733
response.StatusCode.Should().Be(HttpStatusCode.Accepted);
1651-
await response.Should().BeEquivalentTo(new
1652-
{
1653-
value = new[]
1654-
{
1655-
new
1656-
{
1657-
Id = 999,
1658-
Name = "Alice Brown"
1659-
}
1660-
}
1661-
});
1734+
await response.Should().BeEquivalentTo(new { value = new[] { new { Id = 999, Name = "Alice Brown" } } });
16621735
}
16631736

16641737
[Fact]
@@ -1740,13 +1813,7 @@ public async Task Can_use_response_builders_with_odata_result_and_context()
17401813
// Assert
17411814
response.StatusCode.Should().Be(HttpStatusCode.OK);
17421815
content.Should().Contain("\"@odata.context\":\"" + context + "\"");
1743-
await response.Should().BeEquivalentTo(new
1744-
{
1745-
value = new[]
1746-
{
1747-
new { Id = 100, Name = "Context User" }
1748-
}
1749-
});
1816+
await response.Should().BeEquivalentTo(new { value = new[] { new { Id = 100, Name = "Context User" } } });
17501817
}
17511818
}
17521819
}

Mockly/HttpMock.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ private async Task ThrowDetailedException(RequestInfo request)
292292
{
293293
messageBuilder.AppendLine();
294294
messageBuilder.AppendLine("Closest matching mock:");
295-
messageBuilder.Append($" {closestMock.ToDetailedString()}");
295+
messageBuilder.Append($" {closestMock}");
296296
messageBuilder.AppendLine();
297297
}
298298

@@ -308,7 +308,7 @@ private async Task ThrowDetailedException(RequestInfo request)
308308
foreach (RequestMock mock in mocks)
309309
{
310310
messageBuilder.Append(" - ");
311-
messageBuilder.AppendLine(mock.ToDetailedString());
311+
messageBuilder.AppendLine(mock.ToString());
312312
}
313313
}
314314

Mockly/RequestMock.cs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public class RequestMock
5656
/// </summary>
5757
public async Task<bool> Matches(RequestInfo request)
5858
{
59+
NormalizeHostPatternOnce();
60+
5961
// Check HTTP method
6062
if (!request.Method.Equals(Method))
6163
{
@@ -74,7 +76,6 @@ public async Task<bool> Matches(RequestInfo request)
7476
// Check host pattern if specified
7577
if (HostPattern != null && request.Uri != null)
7678
{
77-
NormalizeHostPattern();
7879
var host = request.Uri.Host + ":" + request.Uri.Port;
7980
if (!MatchesPattern(host, HostPattern))
8081
{
@@ -130,7 +131,7 @@ public async Task<bool> Matches(RequestInfo request)
130131
/// Normalizes the host pattern by appending the default port if missing
131132
/// based on the scheme (443 for HTTPS, 80 for HTTP), unless the pattern is a wildcard.
132133
/// </summary>
133-
private void NormalizeHostPattern()
134+
private void NormalizeHostPatternOnce()
134135
{
135136
if (!hostPatternNormalized && HostPattern is not null && HostPattern != "*")
136137
{
@@ -286,24 +287,6 @@ public CapturedRequest TrackRequest(RequestInfo request)
286287
/// This method is intended for diagnostics and exception messages only and is
287288
/// not part of the public API.
288289
/// </remarks>
289-
internal string ToDetailedString()
290-
{
291-
var route = ToString();
292-
293-
if (!CustomMatchers.Any())
294-
{
295-
return route;
296-
}
297-
298-
var matcherDescriptions = string.Join(" or ", CustomMatchers.Select(m => "(" + m + ")"));
299-
return $"{route} where {matcherDescriptions}";
300-
}
301-
302-
/// <summary>
303-
/// Returns a string describing the exact route pattern this mock will match.
304-
/// Wildcards (*) indicate unspecified components.
305-
/// Example: GET https://api.*.com/users/*?*
306-
/// </summary>
307290
public override string ToString()
308291
{
309292
string method = Method.Method ?? "*";
@@ -326,12 +309,14 @@ public override string ToString()
326309
query = QueryPattern.StartsWith("?", StringComparison.Ordinal) ? QueryPattern : "?" + QueryPattern;
327310
}
328311

329-
string route = $"{method} {scheme}://{host}{path}{query}";
330-
if (CustomMatchers.Any())
312+
var route = $"{method} {scheme}://{host}{path}{query}";
313+
314+
if (!CustomMatchers.Any())
331315
{
332-
route += $" ({CustomMatchers.Count()} custom matcher(s))";
316+
return route;
333317
}
334318

335-
return route;
319+
var matcherDescriptions = string.Join(" or ", CustomMatchers);
320+
return $"{route} where {matcherDescriptions}";
336321
}
337322
}

Mockly/RequestMockBuilder.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public RequestMockBuilder WithoutQuery()
131131
/// </param>
132132
public RequestMockBuilder WithBodyMatchingRegex([StringSyntax(StringSyntaxAttribute.Regex)] string regex)
133133
{
134-
return With(request => request.Body is not null && Regex.IsMatch(request.Body, regex));
134+
return With(request => request.Body is not null && Regex.IsMatch(request.Body, regex), $"body matches regex {regex}");
135135
}
136136

137137
/// <summary>
@@ -174,7 +174,7 @@ public RequestMockBuilder WithBodyMatchingJson(string json)
174174
{
175175
throw new RequestMatchingException("Could not parse the request body as JSON", jsonException);
176176
}
177-
}, $"Body matches JSON: {json}");
177+
}, $"body matches JSON {json}");
178178
}
179179

180180
/// <summary>
@@ -187,7 +187,9 @@ public RequestMockBuilder WithBodyMatchingJson(string json)
187187
/// <returns>The current <see cref="RequestMockBuilder"/> instance, updated with the specified body matching condition.</returns>
188188
public RequestMockBuilder WithBody(string wildcardPattern)
189189
{
190-
return With(request => request.Body is not null && request.Body.MatchesWildcard(wildcardPattern));
190+
return With(
191+
request => request.Body is not null && request.Body.MatchesWildcard(wildcardPattern),
192+
$"body matches wildcard pattern \"{wildcardPattern}\"");
191193
}
192194

193195
/// <summary>
@@ -527,8 +529,8 @@ public RequestMockResponseBuilder RespondsWithEmptyContent(HttpStatusCode status
527529
/// </summary>
528530
/// <param name="content">The HTTP content to include in the response.</param>
529531
/// <remarks>
530-
/// Note: The same <paramref name="content"/> instance is used for all matching requests.
531-
/// If the mock will be called multiple times, consider using the <see cref="RespondsWith(Func{RequestInfo, HttpResponseMessage})"/>
532+
/// Note: The same <paramref name="content"/> instance is used for all matching requests.
533+
/// If the mock will be called multiple times, consider using the <see cref="RespondsWith(Func{RequestInfo, HttpResponseMessage})"/>
532534
/// overload to create a new content instance for each request.
533535
/// </remarks>
534536
public RequestMockResponseBuilder RespondsWith(HttpContent content)
@@ -542,8 +544,8 @@ public RequestMockResponseBuilder RespondsWith(HttpContent content)
542544
/// <param name="statusCode">The HTTP status code for the response.</param>
543545
/// <param name="content">The HTTP content to include in the response.</param>
544546
/// <remarks>
545-
/// Note: The same <paramref name="content"/> instance is used for all matching requests.
546-
/// If the mock will be called multiple times, consider using the <see cref="RespondsWith(Func{RequestInfo, HttpResponseMessage})"/>
547+
/// Note: The same <paramref name="content"/> instance is used for all matching requests.
548+
/// If the mock will be called multiple times, consider using the <see cref="RespondsWith(Func{RequestInfo, HttpResponseMessage})"/>
547549
/// overload to create a new content instance for each request.
548550
/// </remarks>
549551
public RequestMockResponseBuilder RespondsWith(HttpStatusCode statusCode, HttpContent content)

0 commit comments

Comments
 (0)