Browse Source

Error simulation (#545)

* Error simulation

* Improved tests.

* Increase test verbosity

* Disable slow test.
pull/546/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
0fe15cbf10
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Dockerfile
  2. 73
      backend/src/Squidex.Web/ApiExceptionConverter.cs
  3. 4
      backend/src/Squidex.Web/ErrorDto.cs
  4. 53
      backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs
  5. 85
      backend/src/Squidex/wwwroot/scripts/editor-json-schema.html
  6. 1
      backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs
  7. 172
      backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs

2
Dockerfile

@ -27,7 +27,7 @@ RUN dotnet restore
COPY backend .
# Test Backend
RUN dotnet test --no-restore --filter Category!=Dependencies
RUN dotnet test --no-restore --filter Category!=Dependencies -v n
# Publish
RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION

73
backend/src/Squidex.Web/ApiExceptionConverter.cs

@ -34,11 +34,20 @@ namespace Squidex.Web
[500] = "https://tools.ietf.org/html/rfc7231#section-6.6.1"
};
public static (ErrorDto Error, bool WellKnown) ToErrorDto(int statusCode, HttpContext? httpContext)
{
var error = new ErrorDto { StatusCode = statusCode };
Enrich(httpContext, error);
return (error, true);
}
public static (ErrorDto Error, bool WellKnown) ToErrorDto(this ProblemDetails problem, HttpContext? httpContext)
{
Guard.NotNull(problem, nameof(problem));
var error = new ErrorDto { Message = problem.Title, StatusCode = problem.Status };
var error = CreateError(problem.Status ?? 500, problem.Title);
Enrich(httpContext, error);
@ -60,10 +69,12 @@ namespace Squidex.Web
{
error.TraceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
if (error.StatusCode.HasValue)
if (error.StatusCode == 0)
{
error.Type = Links.GetOrDefault(error.StatusCode.Value);
error.StatusCode = 500;
}
error.Type = Links.GetOrDefault(error.StatusCode);
}
private static (ErrorDto Error, bool WellKnown) CreateError(Exception exception)
@ -71,64 +82,38 @@ namespace Squidex.Web
switch (exception)
{
case ValidationException ex:
return (new ErrorDto
{
StatusCode = 400,
Message = ex.Summary,
Details = ToDetails(ex)
}, true);
return (CreateError(400, ex.Summary, ToDetails(ex)), true);
case DomainObjectNotFoundException _:
return (new ErrorDto
{
StatusCode = 404,
Message = null!
}, true);
return (CreateError(404), true);
case DomainObjectVersionException _:
return (new ErrorDto
{
StatusCode = 412,
Message = exception.Message
}, true);
return (CreateError(412, exception.Message), true);
case DomainForbiddenException _:
return (new ErrorDto
{
StatusCode = 403,
Message = exception.Message
}, true);
return (CreateError(403, exception.Message), true);
case DomainException _:
return (new ErrorDto
{
StatusCode = 400,
Message = exception.Message
}, true);
return (CreateError(400, exception.Message), true);
case SecurityException _:
return (new ErrorDto
{
StatusCode = 403,
Message = "Forbidden"
}, false);
return (CreateError(403), false);
case DecoderFallbackException _:
return (new ErrorDto
{
StatusCode = 400,
Message = exception.Message
}, true);
return (CreateError(400, exception.Message), true);
default:
return (new ErrorDto
{
StatusCode = 500,
Message = "Server Error"
}, false);
return (CreateError(500), false);
}
}
private static ErrorDto CreateError(int status, string? message = null, string[]? details = null)
{
var error = new ErrorDto { StatusCode = status, Message = message, Details = details };
return error;
}
private static string[] ToDetails(ValidationException ex)
{
return ex.Errors.Select(e =>

4
backend/src/Squidex.Web/ErrorDto.cs

@ -13,7 +13,7 @@ namespace Squidex.Web
{
[Required]
[Display(Description = "Error message.")]
public string Message { get; set; }
public string? Message { get; set; }
[Display(Description = "The optional trace id.")]
public string? TraceId { get; set; }
@ -25,6 +25,6 @@ namespace Squidex.Web
public string[]? Details { get; set; }
[Display(Description = "Status code of the http response.")]
public int? StatusCode { get; set; } = 400;
public int StatusCode { get; set; } = 400;
}
}

53
backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs

@ -7,7 +7,12 @@
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
@ -15,27 +20,69 @@ namespace Squidex.Web.Pipeline
{
public sealed class RequestExceptionMiddleware : IMiddleware
{
private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();
private static readonly RouteData EmptyRouteData = new RouteData();
private readonly IActionResultExecutor<ObjectResult> resultWriter;
private readonly ISemanticLog log;
public RequestExceptionMiddleware(ISemanticLog log)
public RequestExceptionMiddleware(IActionResultExecutor<ObjectResult> resultWriter, ISemanticLog log)
{
Guard.NotNull(resultWriter, nameof(resultWriter));
Guard.NotNull(log, nameof(log));
this.resultWriter = resultWriter;
this.log = log;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out var statusCode) && IsErrorStatusCode(statusCode))
{
var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context);
await WriteErrorAsync(context, error);
return;
}
try
{
await next(context);
}
catch (Exception ex)
{
log.LogError(ex, w => w.WriteProperty("messag", "An unexpected exception has occurred."));
log.LogError(ex, w => w.WriteProperty("message", "An unexpected exception has occurred."));
if (!context.Response.HasStarted)
{
var (error, _) = ex.ToErrorDto(context);
await WriteErrorAsync(context, error);
}
}
if (IsErrorStatusCode(context.Response.StatusCode) && !context.Response.HasStarted)
{
var (error, _) = ApiExceptionConverter.ToErrorDto(context.Response.StatusCode, context);
context.Response.StatusCode = 500;
await WriteErrorAsync(context, error);
}
}
private async Task WriteErrorAsync(HttpContext context, ErrorDto error)
{
var actionRouteData = context.GetRouteData() ?? EmptyRouteData;
var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor);
await resultWriter.ExecuteAsync(actionContext, new ObjectResult(error)
{
StatusCode = error.StatusCode
});
}
private static bool IsErrorStatusCode(int statusCode)
{
return statusCode >= 400 && statusCode < 600;
}
}
}

85
backend/src/Squidex/wwwroot/scripts/editor-json-schema.html

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@rjsf/core/dist/react-jsonschema-form.js"></script>
<link rel="stylesheet" type="text/css" href="https://cloud.squidex.io/build/app.css">
<style>
body {
background: none;
}
#root__title {
display: none;
}
</style>
</head>
<body>
<div id="editor"></div>
<script>
const Form = JSONSchemaForm.default;
const field = new SquidexFormField();
function Editor() {
var stateDisabled = React.useState(false); // Use ES5 language features only.
var stateValue = React.useState(undefined);
var stateSchema = React.useState(undefined);
React.useEffect(function () {
field.onDisabled(function (disabled) {
stateDisabled[1](disabled);
});
field.onValueChanged(function (value) {
stateValue[1](value);
});
if (window.location.hash && window.location.hash.length > 1) {
fetch(window.location.hash.substr(1))
.then(x => x.json())
.then(x => stateSchema[1](x));
}
}, []);
const doBlur = React.useCallback(function () {
field.touched();
}, []);
const doChange = React.useCallback(function (editor) {
if (editor.errors.length === 0) {
field.valueChanged(editor.formData);
}
}, []);
if (!stateSchema[0]) {
return null;
}
return (
React.createElement(Form, {
formData: stateValue[0],
disabled: stateDisabled[0],
onChange: doChange,
onBlur: doBlur,
schema: stateSchema[0],
children: [] // Hide submit button
})
);
}
ReactDOM.render(React.createElement(Editor), document.getElementById("editor"));
</script>
</body>
</html>

1
backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs

@ -14,6 +14,7 @@ using Xunit;
namespace Squidex.Infrastructure.Orleans
{
[Trait("Category", "Dependencies")]
public class PubSubTests
{
[Fact]

172
backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs

@ -0,0 +1,172 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Elasticsearch.Net;
using FakeItEasy;
using GraphQL;
using Grpc.Core.Logging;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Web.Pipeline
{
public class RequestExceptionMiddlewareTests
{
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IActionResultExecutor<ObjectResult> resultWriter = A.Fake<IActionResultExecutor<ObjectResult>>();
private readonly IHttpResponseFeature responseFeature = A.Fake<IHttpResponseFeature>();
private readonly HttpContext httpContext = new DefaultHttpContext();
private readonly RequestDelegate next;
private readonly RequestExceptionMiddleware sut;
private bool isNextCalled;
public RequestExceptionMiddlewareTests()
{
next = new RequestDelegate(context =>
{
isNextCalled = true;
return Task.CompletedTask;
});
httpContext.Features.Set(responseFeature);
sut = new RequestExceptionMiddleware(resultWriter, log);
}
[Fact]
public async Task Should_write_test_error_if_valid_status_code()
{
httpContext.Request.QueryString = new QueryString("?error=412");
await sut.InvokeAsync(httpContext, next);
Assert.False(isNextCalled);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._,
A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto)))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_test_error_if_invalid_status_code()
{
httpContext.Request.QueryString = new QueryString("?error=hello");
await sut.InvokeAsync(httpContext, next);
Assert.True(isNextCalled);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_test_error_if_invalid_error_status_code()
{
httpContext.Request.QueryString = new QueryString("?error=99");
await sut.InvokeAsync(httpContext, next);
Assert.True(isNextCalled);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_handle_exception()
{
var failingNext = new RequestDelegate(context =>
{
throw new InvalidOperationException();
});
await sut.InvokeAsync(httpContext, failingNext);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._,
A<ObjectResult>.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto)))
.MustHaveHappened();
}
[Fact]
public async Task Should_log_exception()
{
var ex = new InvalidOperationException();
var failingNext = new RequestDelegate(context =>
{
throw ex;
});
await sut.InvokeAsync(httpContext, failingNext);
A.CallTo(() => log.Log(SemanticLogLevel.Error, ex, A<LogFormatter>._))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_handle_exception_if_response_body_written()
{
A.CallTo(() => responseFeature.HasStarted)
.Returns(true);
var failingNext = new RequestDelegate(context =>
{
throw new InvalidOperationException();
});
await sut.InvokeAsync(httpContext, failingNext);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_handle_error_status_code()
{
var failingNext = new RequestDelegate(context =>
{
context.Response.StatusCode = 412;
return Task.CompletedTask;
});
await sut.InvokeAsync(httpContext, failingNext);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._,
A<ObjectResult>.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto)))
.MustHaveHappened();
}
[Fact]
public async Task Should_not_handle_error_status_code_if_response_body_written()
{
A.CallTo(() => responseFeature.HasStarted)
.Returns(true);
var failingNext = new RequestDelegate(context =>
{
context.Response.StatusCode = 412;
return Task.CompletedTask;
});
await sut.InvokeAsync(httpContext, failingNext);
A.CallTo(() => resultWriter.ExecuteAsync(A<ActionContext>._, A<ObjectResult>._))
.MustNotHaveHappened();
}
}
}
Loading…
Cancel
Save