Skip to content
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
1 change: 1 addition & 0 deletions Source/Testably.Expectations/Core/Formatting/Formatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Formatter
new BooleanFormatter(),
new StringFormatter(),
new TypeFormatter(),
new HttpStatusCodeFormatter(),
new CollectionFormatter(),
new NumberFormatter<int>(),
new NumberFormatter<uint>(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Net;
using System.Text;

namespace Testably.Expectations.Core.Formatting.Formatters;

internal class HttpStatusCodeFormatter : FormatterBase<HttpStatusCode>
{
/// <inheritdoc />
public override void Format(HttpStatusCode value, StringBuilder stringBuilder,
FormattingOptions options)
{
stringBuilder.Append($"{(int)value} {value}");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Testably.Expectations.Core.Constraints;
using Testably.Expectations.Core.Formatting;

// ReSharper disable once CheckNamespace
namespace Testably.Expectations;

public static partial class ThatHttpResponseMessageExtensions
{
private readonly struct HasStatusCodeConstraint(HttpStatusCode expected)
: IAsyncConstraint<HttpResponseMessage>
{
public async Task<ConstraintResult> IsMetBy(HttpResponseMessage? actual)
{
if (actual == null)
{
return new ConstraintResult.Failure<HttpResponseMessage?>(actual, ToString(),
"found <null>");
}

if (actual.StatusCode == expected)
{
return new ConstraintResult.Success<HttpResponseMessage?>(actual, ToString());
}

string formattedResponse = await HttpResponseMessageFormatter.Format(actual, " ");
return new ConstraintResult.Failure<HttpResponseMessage?>(actual, ToString(),
$"found {Formatter.Format(actual.StatusCode)}:{Environment.NewLine}{formattedResponse}");
}

public override string ToString()
=> $"has StatusCode {Formatter.Format(expected)}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Testably.Expectations.Core.Constraints;
using Testably.Expectations.Core.Formatting;

// ReSharper disable once CheckNamespace
namespace Testably.Expectations;

public static partial class ThatHttpResponseMessageExtensions
{
private readonly struct HasStatusCodeRangeConstraint(Func<int, bool> predicate, string expectation)
: IAsyncConstraint<HttpResponseMessage>
{
public async Task<ConstraintResult> IsMetBy(HttpResponseMessage? actual)
{
if (actual == null)
{
return new ConstraintResult.Failure<HttpResponseMessage?>(actual, ToString(),
"found <null>");
}

if (predicate((int)actual.StatusCode))
{
return new ConstraintResult.Success<HttpResponseMessage?>(actual, ToString());
}

string formattedResponse = await HttpResponseMessageFormatter.Format(actual, " ");
return new ConstraintResult.Failure<HttpResponseMessage?>(actual, ToString(),
$"found {Formatter.Format(actual.StatusCode)}:{Environment.NewLine}{formattedResponse}");
}

public override string ToString()
=> expectation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Testably.Expectations.Core.Helpers;

// ReSharper disable once CheckNamespace
namespace Testably.Expectations;

public static partial class ThatHttpResponseMessageExtensions
{
private static class HttpResponseMessageFormatter
{
public static async Task<string> Format(HttpResponseMessage response, string indentation)
{
StringBuilder messageBuilder = new StringBuilder();

messageBuilder.Append(indentation)
.Append("HTTP/").Append(response.Version)
.Append(" ").Append((int)response.StatusCode).Append(" ").Append(response.StatusCode)
.AppendLine();

AppendHeaders(messageBuilder, response.Headers, indentation);
await AppendContent(messageBuilder, response.Content, indentation);

HttpRequestMessage? request = response.RequestMessage;
if (request == null)
{
messageBuilder.Append(indentation).AppendLine("The originating request was <null>");
}
else
{
messageBuilder.Append(indentation).AppendLine("The originating request was:");
messageBuilder.Append(indentation).Append(indentation).Append(request.Method.ToString().ToUpper()).Append(" ")
.Append(request.RequestUri).Append(" HTTP ").Append(request.Version).AppendLine();

AppendHeaders(messageBuilder, request.Headers, indentation);
if (request.Content != null)
{
await AppendContent(messageBuilder, request.Content, indentation + indentation);
}
}

return messageBuilder.ToString().TrimEnd();
}

private static async Task AppendContent(StringBuilder messageBuilder,
HttpContent content,
string indentation)
{
if (content is StringContent)
{
var stringContent = await content.ReadAsStringAsync();
messageBuilder.AppendLine(stringContent.Indent(indentation));
}
else if (content is FormUrlEncodedContent)
{
var stringContent = await content.ReadAsStringAsync();
messageBuilder.AppendLine(stringContent.Indent(indentation));
}
else
{
messageBuilder.Append(indentation).AppendLine("Content is binary");
}
}

private static void AppendHeaders(
StringBuilder messageBuilder,
HttpHeaders headers,
string indentation)
{
foreach (KeyValuePair<string, IEnumerable<string>> header in headers
.OrderBy(x => x.Key == "Content-Length"))
{
foreach (string headerValue in header.Value)
{
messageBuilder.Append(indentation).Append(indentation)
.Append(header.Key).Append(": ").AppendLine(headerValue);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using Testably.Expectations.Core;
using Testably.Expectations.Core.Formatting;
using Testably.Expectations.Core.Helpers;
using Testably.Expectations.Core.Results;

Expand All @@ -12,6 +14,34 @@ namespace Testably.Expectations;
/// </summary>
public static partial class ThatHttpResponseMessageExtensions
{
/// <summary>
/// Verifies that the response has a status code different to <paramref name="unexpected" />
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
DoesNotHaveStatusCode(
this That<HttpResponseMessage?> source,
HttpStatusCode unexpected,
[CallerArgumentExpression("unexpected")]
string doNotPopulateThisValue = "")
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(statusCode => statusCode != (int)unexpected,
$"has StatusCode different to {Formatter.Format(unexpected)}"),
b => b.AppendMethod(nameof(DoesNotHaveStatusCode), doNotPopulateThisValue)),
source);

/// <summary>
/// Verifies that the response has a client error status code (4xx)
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
HasClientError(
this That<HttpResponseMessage?> source)
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(
statusCode => statusCode >= 400 && statusCode < 500,
"has client error (status code 4xx)"),
b => b.AppendMethod(nameof(HasClientError))),
source);

/// <summary>
/// Verifies that the string content is equal to <paramref name="expected" />
/// </summary>
Expand All @@ -25,4 +55,69 @@ public static partial class ThatHttpResponseMessageExtensions
b => b.AppendMethod(nameof(HasContent), doNotPopulateThisValue)),
source,
expected);

/// <summary>
/// Verifies that the response has a client or server error status code (4xx or 5xx)
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
HasError(
this That<HttpResponseMessage?> source)
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(
statusCode => statusCode >= 400 && statusCode < 600,
"has an error (status code 4xx or 5xx)"),
b => b.AppendMethod(nameof(HasError))),
source);

/// <summary>
/// Verifies that the response has a server error status code (5xx)
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
HasServerError(
this That<HttpResponseMessage?> source)
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(
statusCode => statusCode >= 500 && statusCode < 600,
"has server error (status code 5xx)"),
b => b.AppendMethod(nameof(HasServerError))),
source);

/// <summary>
/// Verifies that the response has a status code equal to <paramref name="expected" />
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
HasStatusCode(
this That<HttpResponseMessage?> source,
HttpStatusCode expected,
[CallerArgumentExpression("expected")] string doNotPopulateThisValue = "")
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeConstraint(expected),
b => b.AppendMethod(nameof(HasContent), doNotPopulateThisValue)),
source);

/// <summary>
/// Verifies that the response has a redirection status code (3xx)
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
IsRedirection(
this That<HttpResponseMessage?> source)
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(
statusCode => statusCode >= 300 && statusCode < 400,
"is redirection (status code 3xx)"),
b => b.AppendMethod(nameof(IsRedirection))),
source);

/// <summary>
/// Verifies that the response has a success status code (2xx)
/// </summary>
public static AndOrExpectationResult<HttpResponseMessage, That<HttpResponseMessage?>>
IsSuccessful(
this That<HttpResponseMessage?> source)
=> new(source.ExpectationBuilder.Add(
new HasStatusCodeRangeConstraint(
statusCode => statusCode >= 200 && statusCode < 300,
"is successful (status code 2xx)"),
b => b.AppendMethod(nameof(IsSuccessful))),
source);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
using Xunit.Sdk;

namespace Testably.Expectations.Tests.Specialized.Http;

public sealed partial class ThatHttpResponseMessage
{
public sealed class DoesNotHaveStatusCodeTests
{
[Theory]
[MemberData(nameof(SuccessStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
[MemberData(nameof(RedirectStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
[MemberData(nameof(ClientErrorStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
[MemberData(nameof(ServerErrorStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
public async Task WhenStatusCodeIsUnexpected_ShouldFail(HttpStatusCode statusCode)
{
HttpStatusCode unexpected = statusCode;
HttpResponseMessage sut = ResponseBuilder
.WithStatusCode(statusCode);

async Task Act()
=> await Expect.That(sut).DoesNotHaveStatusCode(unexpected);

await Expect.That(Act).Throws<XunitException>()
.Which.HasMessage(
"*StatusCode different to*Expect.That(sut).DoesNotHaveStatusCode(unexpected)")
.AsWildcard();
}

[Fact]
public async Task WhenStatusCodeDiffersFromExpected_ShouldSucceed()
{
HttpStatusCode unexpected = HttpStatusCode.OK;
HttpResponseMessage sut = ResponseBuilder
.WithStatusCode(HttpStatusCode.BadRequest);

async Task Act()
=> await Expect.That(sut).DoesNotHaveStatusCode(unexpected);

await Expect.That(Act).DoesNotThrow();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
using Xunit.Sdk;

namespace Testably.Expectations.Tests.Specialized.Http;

public sealed partial class ThatHttpResponseMessage
{
public sealed class HasClientErrorTests
{
[Theory]
[MemberData(nameof(ClientErrorStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
public async Task WhenStatusCodeIsExpected_ShouldSucceed(HttpStatusCode statusCode)
{
HttpResponseMessage sut = ResponseBuilder
.WithStatusCode(statusCode);

async Task Act()
=> await Expect.That(sut).HasClientError();

await Expect.That(Act).DoesNotThrow();
}

[Theory]
[MemberData(nameof(SuccessStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
[MemberData(nameof(RedirectStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
[MemberData(nameof(ServerErrorStatusCodes), MemberType = typeof(ThatHttpResponseMessage))]
public async Task WhenStatusCodeIsUnexpected_ShouldFail(HttpStatusCode statusCode)
{
HttpResponseMessage sut = ResponseBuilder
.WithStatusCode(statusCode);

async Task Act()
=> await Expect.That(sut).HasClientError();

await Expect.That(Act).Throws<XunitException>()
.Which.HasMessage(
"*client error (status code 4xx)*Expect.That(sut).HasClientError()")
.AsWildcard();
}
}
}
Loading
Loading