Skip to content

Commit

Permalink
Add AssertionContext (#167)
Browse files Browse the repository at this point in the history
* Add AssertionContext

* Update docs
  • Loading branch information
Hawxy authored Jul 26, 2024
1 parent 1ae5e66 commit fbdc3bc
Show file tree
Hide file tree
Showing 18 changed files with 149 additions and 104 deletions.
14 changes: 8 additions & 6 deletions docs/scenarios/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ failures will be reported out in the Exception message thrown by Alba on Scenari
:::

The Scenario assertions in Alba are completely extensible and you can happily add your own via extension methods - but
please send anything that's generally useful as a pull request to Alba itself;-)
please send anything that's generally useful as a pull request to Alba itself ;-)

The first step is to write your own implementation of this interface:

Expand All @@ -15,7 +15,7 @@ The first step is to write your own implementation of this interface:
```cs
public interface IScenarioAssertion
{
void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex);
void Assert(Scenario scenario, AssertionContext context);
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/IScenarioAssertion.cs#L5-L10' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_iscenarioassertion' title='Start of snippet'>anchor</a></sup>
Expand All @@ -35,19 +35,21 @@ internal sealed class BodyContainsAssertion : IScenarioAssertion
Text = text;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var body = ex.ReadBody(context);
// Context has this useful extension to read the body as a string.
// This will bake the body contents into the exception message to make debugging easier.
var body = context.ReadBodyAsString();
if (!body.Contains(Text))
{
// Add the failure message to the exception. This exception only
// gets thrown if there are failures.
ex.Add($"Expected text '{Text}' was not found in the response body");
context.AddFailure($"Expected text '{Text}' was not found in the response body");
}
}
}
```
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/Assertions/BodyContainsAssertion.cs#L5-L26' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_bodycontainsassertion' title='Start of snippet'>anchor</a></sup>
<sup><a href='https://github.com/JasperFx/alba/blob/master/src/Alba/Assertions/BodyContainsAssertion.cs#L5-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_bodycontainsassertion' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Once you have your assertion class, you can apply it to a scenario through an extension method against the
Expand Down
2 changes: 1 addition & 1 deletion src/Alba.Testing/Assertions/AssertionRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static ScenarioAssertionException Run(IScenarioAssertion assertion,
var stream = context.Response.Body;
stream.Position = 0;

assertion.Assert(null, context, ex);
assertion.Assert(null, new AssertionContext(context, ex));

return ex;
}
Expand Down
20 changes: 14 additions & 6 deletions src/Alba.Testing/ScenarioAssertionExceptionTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Shouldly;
using System;
using System.IO;
using Microsoft.AspNetCore.Http;
using Shouldly;
using Xunit;

namespace Alba.Testing
Expand Down Expand Up @@ -37,16 +40,21 @@ public void all_messages_are_in_the_ex_message()
public void show_the_body_in_the_message_if_set()
{
var ex = new ScenarioAssertionException();
var ctx = new DefaultHttpContext();
ctx.Response.Body = new MemoryStream();
var body = "<html></html>";
using var sw = new StreamWriter(ctx.Response.Body);
sw.Write(body);
sw.Flush();

var context = new AssertionContext(ctx, ex);
ex.Add("You stink!");

ex.Message.ShouldNotContain("Actual body text was:");

ex.Body = "<html></html>";



context.ReadBodyAsString();
ex.Message.ShouldContain("Actual body text was:");
ex.Message.ShouldContain(ex.Body);
ex.Message.ShouldContain(body);
}
}
}
65 changes: 65 additions & 0 deletions src/Alba/AssertionContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using Alba.Internal;
using Microsoft.AspNetCore.Http;

namespace Alba;

public sealed class AssertionContext
{
private readonly ScenarioAssertionException _assertionException;
public HttpContext HttpContext { get; }
public AssertionContext(HttpContext httpContext, ScenarioAssertionException assertionException)
{
HttpContext = httpContext;
_assertionException = assertionException;
}

/// <summary>
/// Add an assertion failure message
/// </summary>
/// <param name="message"></param>
public void AddFailure(string message) => _assertionException.Add(message);

private string? _body;

/// <summary>
/// Reads the response body and returns it as a string
/// </summary>
/// <returns>A string with the content of the body</returns>
[MemberNotNull(nameof(_body))]
public string ReadBodyAsString()
{
// Hardening for GH-95
try
{
var stream = HttpContext.Response.Body;
if (_body == null)
{
if (stream.CanSeek)
{
stream.Position = 0;
}

_body = Encoding.UTF8.GetString(stream.ReadAllBytes());

// reset the position so users can do follow up activities without tripping up.
if (stream.CanSeek)
{
stream.Position = 0;
}

_assertionException.AddBody(_body);
}
}
catch (Exception)
{
_body = string.Empty;
}

return _body;
}


}
8 changes: 5 additions & 3 deletions src/Alba/Assertions/BodyContainsAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ public BodyContainsAssertion(string text)
Text = text;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var body = ex.ReadBody(context);
// Context has this useful extension to read the body as a string.
// This will bake the body contents into the exception message to make debugging easier.
var body = context.ReadBodyAsString();
if (!body.Contains(Text))
{
// Add the failure message to the exception. This exception only
// gets thrown if there are failures.
ex.Add($"Expected text '{Text}' was not found in the response body");
context.AddFailure($"Expected text '{Text}' was not found in the response body");
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Alba/Assertions/BodyDoesNotContainAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public BodyDoesNotContainAssertion(string text)
Text = text;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var body = ex.ReadBody(context);
var body = context.ReadBodyAsString();
if (body.Contains(Text))
{
ex.Add($"Text '{Text}' should not be found in the response body");
context.AddFailure($"Text '{Text}' should not be found in the response body");
}
}
}
6 changes: 3 additions & 3 deletions src/Alba/Assertions/BodyTextAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ public BodyTextAssertion(string text)
Text = text;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var body = ex.ReadBody(context);
var body = context.ReadBodyAsString();
if (!body.Equals(Text))
{
ex.Add($"Expected the content to be '{Text}'");
context.AddFailure($"Expected the content to be '{Text}'");
}
}
}
8 changes: 4 additions & 4 deletions src/Alba/Assertions/HasSingleHeaderValueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ public HasSingleHeaderValueAssertion(string headerKey)
_headerKey = headerKey;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var values = context.Response.Headers[_headerKey];
var values = context.HttpContext.Response.Headers[_headerKey];

switch (values.Count)
{
case 0:
ex.Add(
context.AddFailure(
$"Expected a single header value of '{_headerKey}', but no values were found on the response");
break;
case 1:
Expand All @@ -28,7 +28,7 @@ public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionExce

default:
var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");
ex.Add($"Expected a single header value of '{_headerKey}', but found multiple values on the response: {valueText}");
context.AddFailure($"Expected a single header value of '{_headerKey}', but found multiple values on the response: {valueText}");
break;
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/Alba/Assertions/HeaderExistsAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ public HeaderExistsAssertion(string headerKey)
_headerKey = headerKey;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var values = context.Response.Headers[_headerKey];
var values = context.HttpContext.Response.Headers[_headerKey];

if (values.Count == 0)
{
ex.Add($"Expected header '{_headerKey}' to be present but no values were found on the response.");
context.AddFailure($"Expected header '{_headerKey}' to be present but no values were found on the response.");
}

}
Expand Down
10 changes: 5 additions & 5 deletions src/Alba/Assertions/HeaderMatchAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ public HeaderMatchAssertion(string headerKey, Regex regex)
_regex = regex;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var values = context.Response.Headers[_headerKey];
var values = context.HttpContext.Response.Headers[_headerKey];

switch (values.Count)
{
case 0:
ex.Add($"Expected a single header value of '{_headerKey}' matching '{_regex}', but no values were found on the response");
context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but no values were found on the response");
break;

case 1:
var actual = values.Single();
if (_regex.IsMatch(actual) == false)
{
ex.Add($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual value was '{actual}'");
context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual value was '{actual}'");
}
break;

default:
var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");
ex.Add($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual values were {valueText}");
context.AddFailure($"Expected a single header value of '{_headerKey}' matching '{_regex}', but the actual values were {valueText}");
break;
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/Alba/Assertions/HeaderMultiValueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ public HeaderMultiValueAssertion(string headerKey, IEnumerable<string> expected)
_expected = expected.ToList();
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var values = context.Response.Headers[_headerKey];
var values = context.HttpContext.Response.Headers[_headerKey];
var expectedText = _expected.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");

switch (values.Count)
{
case 0:
ex.Add($"Expected header values of '{_headerKey}'={expectedText}, but no values were found on the response.");
context.AddFailure($"Expected header values of '{_headerKey}'={expectedText}, but no values were found on the response.");
break;

default:
if (!_expected.All(x => values.Contains(x)))
{
var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");
ex.Add($"Expected header values of '{_headerKey}'={expectedText}, but the actual values were {valueText}.");
context.AddFailure($"Expected header values of '{_headerKey}'={expectedText}, but the actual values were {valueText}.");
}
break;
}
Expand Down
10 changes: 5 additions & 5 deletions src/Alba/Assertions/HeaderValueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,27 @@ public HeaderValueAssertion(string headerKey, string expected)
_expected = expected;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var values = context.Response.Headers[_headerKey];
var values = context.HttpContext.Response.Headers[_headerKey];

switch (values.Count)
{
case 0:
ex.Add($"Expected a single header value of '{_headerKey}'='{_expected}', but no values were found on the response");
context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but no values were found on the response");
break;

case 1:
var actual = values.Single();
if (actual != _expected)
{
ex.Add($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual value was '{actual}'");
context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual value was '{actual}'");
}
break;

default:
var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");
ex.Add($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual values were {valueText}");
context.AddFailure($"Expected a single header value of '{_headerKey}'='{_expected}', but the actual values were {valueText}");
break;
}

Expand Down
6 changes: 3 additions & 3 deletions src/Alba/Assertions/NoHeaderValueAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ public NoHeaderValueAssertion(string headerKey)
_headerKey = headerKey;
}

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var headers = context.Response.Headers;
var headers = context.HttpContext.Response.Headers;
if (headers.ContainsKey(_headerKey))
{
var values = headers[_headerKey];
var valueText = values.Select(x => "'" + x + "'").Aggregate((s1, s2) => $"{s1}, {s2}");
ex.Add($"Expected no value for header '{_headerKey}', but found values {valueText}");
context.AddFailure($"Expected no value for header '{_headerKey}', but found values {valueText}");
}
}
}
8 changes: 4 additions & 4 deletions src/Alba/Assertions/RedirectAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ public RedirectAssertion(string expected, bool permanent)
public string Expected { get; }
public bool Permanent { get; }

public void Assert(Scenario scenario, HttpContext context, ScenarioAssertionException ex)
public void Assert(Scenario scenario, AssertionContext context)
{
var location = context.Response.Headers["Location"];
var location = context.HttpContext.Response.Headers.Location;
if (!string.Equals(location, Expected, StringComparison.OrdinalIgnoreCase))
{
ex.Add($"Expected to be redirected to '{Expected}' but was '{location}'.");
context.AddFailure($"Expected to be redirected to '{Expected}' but was '{location}'.");
}

new StatusCodeAssertion(Permanent ? 301 : 302).Assert(scenario, context, ex);
new StatusCodeAssertion(Permanent ? 301 : 302).Assert(scenario, context);
}
}
Loading

0 comments on commit fbdc3bc

Please sign in to comment.