Skip to content

Commit

Permalink
Merge pull request #165 from dncsvr/issue/dont-ensure-success
Browse files Browse the repository at this point in the history
Issue / Don't Ensure Success
  • Loading branch information
dncsvr authored Aug 23, 2024
2 parents 3de1b0b + 249068d commit 8747085
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<Response> Send(Request request,

if (allowErrorResponse)
{
return new(res.StatusCode.ToStatusCode(), content);
return new(res.StatusCode, content);
}

try
Expand All @@ -49,6 +49,6 @@ public async Task<Response> Send(Request request,
throw new ClientException(content, ex);
}

return new(res.StatusCode.ToStatusCode(), content);
return new(res.StatusCode, content);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Baked.Testing;
using Moq;
using Newtonsoft.Json;
using System.Net;
using System.Reflection;

namespace Baked.Communication.Mock;
Expand All @@ -10,7 +11,7 @@ public class DefaultResponseBuilder
static readonly MethodInfo _setupClient = typeof(DefaultResponseBuilder).GetMethod(nameof(SetupClient), BindingFlags.Static | BindingFlags.NonPublic)
?? throw new("SetupClient<T> should have existed");

static void SetupClient<T>(Mock<IClient<T>> mock, List<(string response, StatusCode statusCode, Func<Request, bool> when)> setups)
static void SetupClient<T>(Mock<IClient<T>> mock, List<(string response, HttpStatusCode statusCode, Func<Request, bool> when)> setups)
where T : class
{
foreach (var (response, statusCode, when) in setups)
Expand All @@ -20,20 +21,20 @@ static void SetupClient<T>(Mock<IClient<T>> mock, List<(string response, StatusC
}
}

readonly Dictionary<Type, List<(string? response, StatusCode statusCode, Func<Request, bool> when)>> _setups = [];
readonly Dictionary<Type, List<(string? response, HttpStatusCode statusCode, Func<Request, bool> when)>> _setups = [];

public void ForClient<T>(object response,
StatusCode? statusCode = default,
HttpStatusCode? statusCode = default,
Func<Request, bool>? when = default
) where T : class =>
ForClient<T>(JsonConvert.SerializeObject(response), statusCode, when);

public void ForClient<T>(string responseString,
StatusCode? statusCode = default,
HttpStatusCode? statusCode = default,
Func<Request, bool>? when = default
) where T : class
{
statusCode ??= StatusCode.Success;
statusCode ??= HttpStatusCode.OK;
when ??= _ => true;

if (!_setups.TryGetValue(typeof(T), out var setups))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Baked.Communication.Mock;
using Baked.Testing;
using Moq;
using System.Net;

namespace Baked;

Expand All @@ -16,14 +17,12 @@ public static IClient<T> TheClient<T>(this Mocker mockMe,
string? urlEndsWith = default,
object? response = default,
string? responseString = default,
StatusCode? statusCode = default,
HttpStatusCode? statusCode = default,
Exception? throws = default,
List<object>? responses = default,
bool? noResponse = default
)
{
statusCode ??= StatusCode.Success;

var mock = Moq.Mock.Get(mockMe.Spec.GiveMe.The<IClient<T>>());

var setup = () => mock.Setup(c =>
Expand All @@ -41,19 +40,23 @@ public static IClient<T> TheClient<T>(this Mocker mockMe,
}
else if (response is not null)
{
setup().ReturnsAsync(new Response(statusCode.GetValueOrDefault(), response.ToJsonString()));
setup().ReturnsAsync(new Response(statusCode ?? HttpStatusCode.OK, response.ToJsonString()));
}
else if (responseString is not null)
{
setup().ReturnsAsync(new Response(statusCode.GetValueOrDefault(), responseString));
setup().ReturnsAsync(new Response(statusCode ?? HttpStatusCode.OK, responseString));
}
else if (responses is not null)
{
setup().ReturnsAsync(responses.Select(r => new Response(statusCode.GetValueOrDefault(), r.ToJsonString())).ToArray());
setup().ReturnsAsync(responses.Select(r => new Response(statusCode ?? HttpStatusCode.OK, r.ToJsonString())).ToArray());
}
else if (noResponse == true)
{
setup().ReturnsAsync(new Response(statusCode.GetValueOrDefault(), string.Empty));
setup().ReturnsAsync(new Response(statusCode ?? HttpStatusCode.OK, string.Empty));
}
else if (statusCode is not null)
{
setup().ReturnsAsync(new Response(statusCode.Value, "{}"));
}

return mock.Object;
Expand All @@ -78,10 +81,10 @@ public static void VerifySent<T>(this IClient<T> client,
(content == default || new Content(content, null).Equals(r.Content)) &&
(contentContains == default || r.Content != null && r.Content.Body.Contains(contentContains)) &&
(form == default || new Content(form).Equals(r.Content)) &&
(!header.HasValue || r.Headers[header.GetValueOrDefault().key] == header.GetValueOrDefault().value) &&
(!header.HasValue || (r.Headers.ContainsKey(header.GetValueOrDefault().key) && r.Headers[header.GetValueOrDefault().key] == header.GetValueOrDefault().value)) &&
(excludesHeader == default || !r.Headers.ContainsKey(excludesHeader))
),
allowErrorResponse.GetValueOrDefault()
It.Is<bool>(aer => allowErrorResponse == default || aer == allowErrorResponse.GetValueOrDefault())
),
times is null ? Times.AtLeastOnce() : Times.Exactly(times.Value)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,4 @@ public static string ToFormBody(this Dictionary<string, string> parameters) =>

public static string ToQueryString(this Dictionary<string, string> parameters) =>
string.Join("&", parameters.Select(pair => $"{pair.Key}={WebUtility.UrlEncode(pair.Value)}"));

public static StatusCode ToStatusCode(this HttpStatusCode httStatusCode) =>
(int)httStatusCode switch
{
var c when c < 400 => StatusCode.Success,
var c when c < 500 => StatusCode.Handled,
_ => StatusCode.Unhandled
};
}
6 changes: 3 additions & 3 deletions src/recipe/Baked.Recipe.Service/Communication/Response.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
using Newtonsoft.Json;
using System.Net;

namespace Baked.Communication;

public record Response(
StatusCode StatusCode,
HttpStatusCode StatusCode,
string Content
)
{
public bool HasContent => !string.IsNullOrWhiteSpace(Content);
public bool IsSuccess => StatusCode == StatusCode.Success;
public bool IsError => StatusCode is StatusCode.Handled or StatusCode.Unhandled;
public bool IsSuccess => (int)StatusCode < 400;

public dynamic? GetContentAsObject(
JsonSerializerSettings? settings = default
Expand Down
8 changes: 0 additions & 8 deletions src/recipe/Baked.Recipe.Service/Communication/StatusCode.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Baked.Test.Communication;
using System.Net;

namespace Baked.Test.Communication;

public class MockingClients : TestServiceSpec
{
Expand All @@ -7,7 +9,7 @@ public async Task Mock_communication_allows_default_response_for_a_client()
{
var client = MockMe.TheClient<ExternalSamples>();

var response = await client.Send(new(UrlOrPath: string.Empty, Method: HttpMethod.Post));
var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.ShouldNotBeNull();
response.Content.ShouldBe("\"test result\""); // this reponse result is configured through Communication.Mock feature in TestServiceSpec
Expand All @@ -18,11 +20,11 @@ public async Task Mock_communication_allows_conditioned_default_responses_for_a_
{
var client = MockMe.TheClient<InternalSamples>();

var response1 = await client.Send(new(UrlOrPath: "path1", Method: HttpMethod.Post));
var response1 = await client.Send(new("path1", HttpMethod.Post));
response1.ShouldNotBeNull();
response1.Content.ShouldBe("\"path1 response\""); // this reponse result is configured through Communication.Mock feature in TestServiceSpec

var response2 = await client.Send(new(UrlOrPath: "path2", Method: HttpMethod.Post));
var response2 = await client.Send(new("path2", HttpMethod.Post));
response2.ShouldNotBeNull();
response2.Content.ShouldBe("\"path2 response\""); // this reponse result is configured through Communication.Mock feature in TestServiceSpec
}
Expand All @@ -32,9 +34,84 @@ public async Task Response_of_a_client_can_be_set_through_mock_helper()
{
var client = MockMe.TheClient<ExternalSamples>(response: "overridden response");

var response = await client.Send(new(UrlOrPath: string.Empty, Method: HttpMethod.Post));
var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.ShouldNotBeNull();
response.Content.ShouldBe("\"overridden response\"");
}

[Test]
public async Task Mock_helper_can_configure_individual_response_per_path()
{
MockMe.TheClient<ExternalSamples>(path: "path1", response: new { content = "Response 1" });
var client = MockMe.TheClient<ExternalSamples>(path: "path2", response: new { content = "Response 2" });

var response = await client.Send(new("path2", HttpMethod.Post));

response.Content.ShouldBe(new { content = "Response 2" }.ToJsonString());
}

[Test]
public async Task Mock_helper_can_configure_sequence_of_responses()
{
var client = MockMe.TheClient<ExternalSamples>(responses: [new { content = "Response 1" }, new { content = "Response 2" }]);

var responseOne = await client.Send(new(string.Empty, HttpMethod.Post));
var responseTwo = await client.Send(new(string.Empty, HttpMethod.Post));

responseOne.Content.ShouldBe(new { content = "Response 1" }.ToJsonString());
responseTwo.Content.ShouldBe(new { content = "Response 2" }.ToJsonString());
}

[Test]
public async Task Mock_helper_can_configure_client_to_throw_exception()
{
var client = MockMe.TheClient<ExternalSamples>(throws: new Exception());

var task = client.Send(new(string.Empty, HttpMethod.Post));

await task.ShouldThrowAsync<Exception>();
}

[Test]
public async Task Mock_helper_can_configures_OK_as_default_response_code()
{
var client = MockMe.TheClient<ExternalSamples>(noResponse: true);

var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Test]
public async Task Mock_helper_can_configure_response_status_code_as_non_success()
{
var client = MockMe.TheClient<ExternalSamples>(statusCode: HttpStatusCode.NotFound);

var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
}

[Test]
public async Task Mock_helper_can_configure_non_success_response_with_error_content()
{
var client = MockMe.TheClient<ExternalSamples>(statusCode: HttpStatusCode.BadRequest, responseString: "Invalid Request");

var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
response.Content.ShouldBe("Invalid Request");
}

[Test]
public async Task Mock_helper_does_not_configure_mock_when_no_response_parameters_are_set()
{
MockMe.TheClient<ExternalSamples>(responseString: "response");
var client = MockMe.TheClient<ExternalSamples>();

var response = await client.Send(new(string.Empty, HttpMethod.Post));

response.Content.ShouldBe("response");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Baked.Communication;

namespace Baked.Test.Communication;

public class VerifyingClients : TestServiceSpec
{
[Test]
public async Task Verify_includes_parameters_only_when_given()
{
var client = MockMe.TheClient<ExternalSamples>();

await client.Send(new("path", HttpMethod.Get));
await client.Send(new("path", HttpMethod.Get), allowErrorResponse: true);
await client.Send(new Request("path", HttpMethod.Get).AddAuthorization("token"));
await client.Send(new("path2", HttpMethod.Get));
await client.Send(new($"{GiveMe.AUrl()}", HttpMethod.Post, Content: new(new { content = "content" })));
await client.Send(new($"{GiveMe.AUrl()}", HttpMethod.Post, Content: new(new { content = "another content" })));
await client.Send(new($"{GiveMe.AUrl()}", HttpMethod.Post, Content: new(new() { { "Key", "Value" } })));

client.VerifySent(path: "path", times: 3);
client.VerifySent(url: "path2", times: 1);
client.VerifySent(allowErrorResponse: true, times: 1);
client.VerifySent(method: HttpMethod.Get, times: 4);
client.VerifySent(method: HttpMethod.Post, times: 3);
client.VerifySent(content: new { content = "content" }, times: 1);
client.VerifySent(contentContains: "content", times: 2);
client.VerifySent(header: ("Authorization", "token"), times: 1);
client.VerifySent(excludesHeader: "Authorization", times: 6);
client.VerifySent(form: new() { { "Key", "Value" } }, times: 1);
}

[Test]
public async Task Verify_must_satisfy_all_parameters_when_given()
{
var client = MockMe.TheClient<ExternalSamples>();

await client.Send(new Request("path", HttpMethod.Get).AddAuthorization("token"), allowErrorResponse: true);
await client.Send(new Request("path", HttpMethod.Post, Content: new(new { content = "another content" })).AddAuthorization("token"), allowErrorResponse: true);
await client.Send(new Request("path", HttpMethod.Post, Content: new(new { content = "content" })).AddAuthorization("token"), allowErrorResponse: false);
await client.Send(new Request("path", HttpMethod.Post, Content: new(new { content = "content" })).AddAuthorization("another token"), allowErrorResponse: true);
await client.Send(new Request("another/path", HttpMethod.Post, Content: new(new { content = "content" })).AddAuthorization("token"), allowErrorResponse: true);
await client.Send(new Request("path", HttpMethod.Post, Content: new(new { content = "content" })).AddAuthorization("token"), allowErrorResponse: true);

client.VerifySent(path: "path", method: HttpMethod.Post, header: ("Authorization", "token"), content: new { content = "content" }, allowErrorResponse: true, times: 1);
}
}
11 changes: 11 additions & 0 deletions unreleased.md
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
# Unreleased

## Improvements

- Change `Response.StatusCode` property type to `HttpStatusCode`
- `VerifySent` now have `allowErrorResponse` parameter as optional
- `MockMe.ThClient<T>` can now set response with only `statusCode` parameter

## Bugfixes

- `VerifySent` was throwing key not found exception when header key was not
present, fixed

0 comments on commit 8747085

Please sign in to comment.