Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue / Don't Ensure Success #165

Merged
merged 8 commits into from
Aug 23, 2024
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
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