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
28 changes: 28 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,34 @@ The related `Newtonsoft.Json.JsonSerializerSettings` used to configure the Newto
NewtonsoftJsonSerializerConfig.Options.Converters.Add(new YesNoBooleanJsonConverter());
```

### Response Formatting

The assertion failure messages include a readable rendering of the HTTP response. By default, only the first `10 * 128 * 1024` characters of the response content are printed, the rest being replaced by a warning message. To change this limit globally, set the `ResponseFormatterOptions`:

```csharp
FluentAssertionsWebConfig.ResponseFormatterOptions = new HttpResponseFormatterOptions
{
MaximumReadableBytes = 4 * 1024
};
```
or

```csharp
AwesomeAssertionsWebConfig.ResponseFormatterOptions = new HttpResponseFormatterOptions
{
MaximumReadableBytes = 4 * 1024
};
```

The change must be done before the test is run, like for the serializer configuration above. The global options only apply to the messages produced by **FluentAssertions.Web** and **AwesomeAssertions.Web**. The **HttpMessageFormatter** library itself keeps no global state and accepts the same `HttpResponseFormatterOptions` parameter object per call:

```csharp
var formatted = response.Format(new HttpResponseFormatterOptions
{
MaximumReadableBytes = 4 * 1024
});
```

## Full API

| *HttpResponseMessageAssertions* | Contains a number of methods to assert that an HttpResponseMessage is in the expected state related to the HTTP content. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<PackageReference Include="System.Text.Json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HttpMessageFormatter\HttpMessageFormatter.csproj" />
</ItemGroup>

<PropertyGroup>
<DefineConstants>$(DefineConstants);AAV</DefineConstants>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ public void Format(object value,
FormatChild formatChild)
{
var response = (HttpResponseMessage)value;
var formatted = response.Format();
#if AAV
var options = AwesomeAssertionsWebConfig.ResponseFormatterOptions;
#else
var options = FluentAssertionsWebConfig.ResponseFormatterOptions;
#endif
var formatted = response.Format(options);

formattedGraph.AddFragment(formatted);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
<ItemGroup>
<PackageReference Include="System.Text.Json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HttpMessageFormatter\HttpMessageFormatter.csproj" />
</ItemGroup>
</Project>
31 changes: 28 additions & 3 deletions src/FluentAssertions.Web.Types/FluentAssertionsWebConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// ReSharper disable once CheckNamespace
using HttpMessageFormatter;

// ReSharper disable once CheckNamespace
#if AAV
namespace AwesomeAssertions;
#else
Expand All @@ -18,11 +20,20 @@ public static class FluentAssertionsWebConfig
#endif
{
private static ISerializer? _serializer;
private static HttpResponseFormatterOptions? _responseFormatterOptions;

#if AAV
static AwesomeAssertionsWebConfig() => Serializer = new SystemTextJsonSerializer();
static AwesomeAssertionsWebConfig()
{
Serializer = new SystemTextJsonSerializer();
ResponseFormatterOptions = new HttpResponseFormatterOptions();
}
#else
static FluentAssertionsWebConfig() => Serializer = new SystemTextJsonSerializer();
static FluentAssertionsWebConfig()
{
Serializer = new SystemTextJsonSerializer();
ResponseFormatterOptions = new HttpResponseFormatterOptions();
}
#endif

/// <summary>
Expand All @@ -34,4 +45,18 @@ public static ISerializer Serializer

set => _serializer = value ?? throw new ArgumentNullException(nameof(value), "Serializer cannot be null.");
}

/// <summary>
/// The options used when formatting an HTTP response message into a readable string,
/// for example in the assertion failure messages.
/// They control, for instance, how many characters of the content are printed before the content
/// is considered too large. These options do not change the assertions themselves, only the way
/// the HTTP messages are formatted.
/// </summary>
public static HttpResponseFormatterOptions ResponseFormatterOptions
{
get => _responseFormatterOptions ?? throw new InvalidOperationException("ResponseFormatterOptions cannot be null");

set => _responseFormatterOptions = value ?? throw new ArgumentNullException(nameof(value), "ResponseFormatterOptions cannot be null.");
}
}
34 changes: 22 additions & 12 deletions src/HttpMessageFormatter/HttpResponseFormatterExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,33 @@ public static class HttpResponseFormatterExtensions
/// <param name="response">The HTTP response message to format.</param>
/// <returns>A formatted string representation of the HTTP response.</returns>
public static string Format(this HttpResponseMessage response)
=> Format(response, null);

/// <summary>
/// Formats an HTTP response message into a readable string representation using the given <paramref name="options"/>.
/// </summary>
/// <param name="response">The HTTP response message to format.</param>
/// <param name="options">The options controlling the formatting, like the maximum number of content characters to print.
/// When <see langword="null"/>, the default options are used.</param>
/// <returns>A formatted string representation of the HTTP response.</returns>
public static string Format(this HttpResponseMessage response, HttpResponseFormatterOptions? options)
{
var messageBuilder = new StringBuilder();
messageBuilder.AppendLine();
messageBuilder.AppendLine();
messageBuilder.AppendLine("The HTTP response was:");

Func<Task> contentResolver = async () => await AppendHttpResponseMessage(messageBuilder, response);
Func<Task> contentResolver = async () => await AppendHttpResponseMessage(messageBuilder, response, options);
contentResolver.ExecuteInDefaultSynchronizationContext();

return messageBuilder.ToString();
}

private static async Task AppendHttpResponseMessage(StringBuilder messageBuilder, HttpResponseMessage response)
private static async Task AppendHttpResponseMessage(StringBuilder messageBuilder, HttpResponseMessage response, HttpResponseFormatterOptions? options)
{
try
{
await AppendResponse(messageBuilder, response);
await AppendResponse(messageBuilder, response, options);
}
catch (Exception e)
{
Expand All @@ -40,7 +50,7 @@ private static async Task AppendHttpResponseMessage(StringBuilder messageBuilder
}
try
{
await AppendRequest(messageBuilder, response);
await AppendRequest(messageBuilder, response, options);
}
catch (Exception e)
{
Expand All @@ -49,15 +59,15 @@ private static async Task AppendHttpResponseMessage(StringBuilder messageBuilder
}
}

private static async Task AppendResponse(StringBuilder messageBuilder, HttpResponseMessage response)
private static async Task AppendResponse(StringBuilder messageBuilder, HttpResponseMessage response, HttpResponseFormatterOptions? options)
{
AppendProtocolAndStatusCode(messageBuilder, response);
Appender.AppendHeaders(messageBuilder, response.GetHeaders());
AppendContentLength(messageBuilder, response.Content);
await AppendResponseContent(messageBuilder, response);
await AppendResponseContent(messageBuilder, response, options);
}

private static async Task AppendRequest(StringBuilder messageBuilder, HttpResponseMessage response)
private static async Task AppendRequest(StringBuilder messageBuilder, HttpResponseMessage response, HttpResponseFormatterOptions? options)
{
messageBuilder.AppendLine();
messageBuilder.AppendLine();
Expand All @@ -75,10 +85,10 @@ private static async Task AppendRequest(StringBuilder messageBuilder, HttpRespon
Appender.AppendHeaders(messageBuilder, request.GetHeaders());
AppendContentLength(messageBuilder, request.Content);

await AppendRequestContent(messageBuilder, request.Content);
await AppendRequestContent(messageBuilder, request.Content, options);
}

private static async Task AppendResponseContent(StringBuilder messageBuilder, HttpResponseMessage response)
private static async Task AppendResponseContent(StringBuilder messageBuilder, HttpResponseMessage response, HttpResponseFormatterOptions? options)
{
var content = response.Content;
if (content == null)
Expand All @@ -88,7 +98,7 @@ private static async Task AppendResponseContent(StringBuilder messageBuilder, Ht

var processors = new List<IContentProcessor>();
processors.Add(new InternalServerErrorProcessor(response, content));
processors.AddRange(ProcessorsRunner.CommonProcessors(content));
processors.AddRange(ProcessorsRunner.CommonProcessors(content, options));

var contentBuilder = await ProcessorsRunner.RunProcessors(processors);
messageBuilder.AppendLine();
Expand All @@ -109,9 +119,9 @@ internal static IEnumerable<KeyValuePair<string, IEnumerable<string>>> GetHeader
return request.Headers.Union(requestContentHeaders);
}

private static async Task AppendRequestContent(StringBuilder messageBuilder, HttpContent content)
private static async Task AppendRequestContent(StringBuilder messageBuilder, HttpContent content, HttpResponseFormatterOptions? options)
{
await Appender.AppendContent(messageBuilder, content, false);
await Appender.AppendContent(messageBuilder, content, false, options);
}

private static void AppendProtocolAndStatusCode(StringBuilder messageBuilder, HttpResponseMessage response)
Expand Down
32 changes: 32 additions & 0 deletions src/HttpMessageFormatter/HttpResponseFormatterOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using HttpMessageFormatter.Internal;

namespace HttpMessageFormatter;

/// <summary>
/// Parameter object with the options used when formatting an <see cref="HttpResponseMessage"/>.
/// </summary>
public class HttpResponseFormatterOptions
{
private int _maximumReadableBytes = ContentFormatterOptions.MaximumReadableBytes;

/// <summary>
/// The maximum number of characters read from the content and printed into the formatted output.
/// When the content is longer, only a part of it is printed together with a warning message.
/// Defaults to 10 * 128 * 1024 (1,310,720) characters.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">The value is less than one.</exception>
public int MaximumReadableBytes
{
get => _maximumReadableBytes;

set
{
if (value < 1)
{
throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(MaximumReadableBytes)} must be greater than zero.");
}

_maximumReadableBytes = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ public static void AppendHeaders(StringBuilder messageBuilder, IEnumerable<KeyVa
}
}

public static async Task AppendContent(StringBuilder contentBuilder, HttpContent content, bool appendLineBeforeContent)
public static async Task AppendContent(StringBuilder contentBuilder, HttpContent content, bool appendLineBeforeContent, HttpResponseFormatterOptions? options = null)
{
var partContentBuilder = await ProcessorsRunner.RunProcessors(ProcessorsRunner.CommonProcessors(content));
var partContentBuilder = await ProcessorsRunner.RunProcessors(ProcessorsRunner.CommonProcessors(content, options));

if (partContentBuilder.Length > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ internal class BinaryProcessor : ProcessorBase
{
private readonly HttpContent? _httpContent;

public BinaryProcessor(HttpContent? httpContent)
public BinaryProcessor(HttpContent? httpContent, HttpResponseFormatterOptions? options = null)
: base(options)
{
_httpContent = httpContent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ internal class FallbackProcessor : ProcessorBase
{
private readonly HttpContent? _httpContent;

public FallbackProcessor(HttpContent? httpContent)
public FallbackProcessor(HttpContent? httpContent, HttpResponseFormatterOptions? options = null)
: base(options)
{
_httpContent = httpContent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ internal class InternalServerErrorProcessor : ProcessorBase
private readonly HttpResponseMessage? _httpResponseMessage;
private readonly HttpContent? _httpContent;

// the exception details are extracted as they are and are not limited by the readable bytes limit
public InternalServerErrorProcessor(HttpResponseMessage? httpResponseMessage, HttpContent? httpContent)
: base(null)
{
_httpResponseMessage = httpResponseMessage;
_httpContent = httpContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ internal class JsonProcessor : ProcessorBase
private static readonly JavaScriptEncoder JavaScriptEncoder = JavaScriptEncoder.Create(UnicodeRanges.All);
private readonly HttpContent? _httpContent;

public JsonProcessor(HttpContent? httpContent)
public JsonProcessor(HttpContent? httpContent, HttpResponseFormatterOptions? options = null)
: base(options)
{
_httpContent = httpContent;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ internal class MultipartProcessor : ProcessorBase
{
private readonly HttpContent? _httpContent;

public MultipartProcessor(HttpContent? httpContent)
public MultipartProcessor(HttpContent? httpContent, HttpResponseFormatterOptions? options = null)
: base(options)
{
_httpContent = httpContent;
}
Expand Down Expand Up @@ -52,7 +53,7 @@ protected override async Task Handle(StringBuilder contentBuilder)

Appender.AppendHeaders(contentBuilder, content.Headers);

await Appender.AppendContent(contentBuilder, content, true);
await Appender.AppendContent(contentBuilder, content, true, Options);
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ namespace HttpMessageFormatter.Internal.ContentProcessors;

internal abstract class ProcessorBase : IContentProcessor
{
protected ProcessorBase(HttpResponseFormatterOptions? options) => Options = options;

protected HttpResponseFormatterOptions? Options { get; }

public async Task GetContentInfo(StringBuilder contentBuilder)
{
if (!CanHandle())
Expand All @@ -22,14 +26,15 @@ protected void AppendContentWithinLimits(StringBuilder contentBuilder, string? c
return;
}

var maximumReadableBytes = Options?.MaximumReadableBytes ?? ContentFormatterOptions.MaximumReadableBytes;
var toAppend = content;
var contentLength = content.Length;

if (contentLength >= ContentFormatterOptions.MaximumReadableBytes)
if (contentLength >= maximumReadableBytes)
{
contentBuilder.AppendLine();
contentBuilder.AppendLine(ContentFormatterOptions.WarningMessageWhenContentIsTooLarge);
toAppend = content!.Substring(0, ContentFormatterOptions.MaximumReadableBytes);
toAppend = content!.Substring(0, maximumReadableBytes);
}

contentBuilder.Append(toAppend);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ public static async Task<StringBuilder> RunProcessors(IEnumerable<IContentProces
return contentBuilder;
}

public static IReadOnlyCollection<IContentProcessor> CommonProcessors(HttpContent content) => new IContentProcessor[]
public static IReadOnlyCollection<IContentProcessor> CommonProcessors(HttpContent content, HttpResponseFormatterOptions? options = null) => new IContentProcessor[]
{
new JsonProcessor(content),
new BinaryProcessor(content),
new MultipartProcessor(content),
new FallbackProcessor(content)
new JsonProcessor(content, options),
new BinaryProcessor(content, options),
new MultipartProcessor(content, options),
new FallbackProcessor(content, options)
};
}
16 changes: 15 additions & 1 deletion src/HttpMessageFormatter/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,18 @@ public class HttpResponseFormatterExtensionsTests
}
}

```
```

### Limiting The Printed Content

By default, only the first 10 * 128 * 1024 characters of the content are printed. Pass an
`HttpResponseFormatterOptions` parameter object to change this limit:

```csharp
using HttpMessageFormatter;

var formatted = subject.Format(new HttpResponseFormatterOptions
{
MaximumReadableBytes = 4 * 1024
});
```
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\AwesomeAssertions.Web\AwesomeAssertions.Web.csproj" />
<ProjectReference Include="..\..\src\AwesomeAssertions.Web.Types\AwesomeAssertions.Web.Types.csproj" />
<ProjectReference Include="..\..\src\AwesomeAssertions.HttpMessageFormatter\AwesomeAssertions.HttpMessageFormatter.csproj" />
<ProjectReference Include="..\..\src\AwesomeAssertions.Web.Serializers.NewtonsoftJson\AwesomeAssertions.Web.Serializers.NewtonsoftJson.csproj" />
</ItemGroup>

Expand Down
Loading