Adding a pattern for returning 'unhandled' exception information via

middleware.

This should be used where posssible instead of throwing an exception and
catching in a test, as that only works in memory.
This commit is contained in:
Ryan Nowak 2014-12-10 17:46:18 -08:00
parent 1a617eb533
commit 6390bad0d3
20 changed files with 268 additions and 74 deletions

View File

@ -21,12 +21,16 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var expectedMessage = "No service for type 'ActivatorWebSite.CannotBeActivatedController+FakeType' " +
"has been registered.";
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync("http://localhost/CannotBeActivated/Index"));
Assert.Equal(expectedMessage, ex.Message);
var response = await client.GetAsync("http://localhost/CannotBeActivated/Index");
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(expectedMessage, exception.ExceptionMessage);
}
[Fact]
@ -162,9 +166,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
"has been registered.";
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => client.GetAsync("http://localhost/View/ConsumeCannotBeActivatedComponent"));
Assert.Equal(expectedMessage, ex.Message);
var response = await client.GetAsync("http://localhost/View/ConsumeCannotBeActivatedComponent");
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(expectedMessage, exception.ExceptionMessage);
}
}
}

View File

@ -100,9 +100,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request));
Assert.Equal("The anti-forgery token could not be decrypted.", ex.Message);
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal("The anti-forgery token could not be decrypted.", exception.ExceptionMessage);
}
[Fact]
@ -127,9 +130,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request));
Assert.Equal("The anti-forgery token could not be decrypted.", ex.Message);
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal("The anti-forgery token could not be decrypted.", exception.ExceptionMessage);
}
[Fact]
@ -162,9 +168,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request));
Assert.Equal("The anti-forgery cookie token and form field token do not match.", ex.Message);
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal("The anti-forgery cookie token and form field token do not match.", exception.ExceptionMessage);
}
[Fact]
@ -189,9 +198,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request));
Assert.Equal("The required anti-forgery cookie \"__RequestVerificationToken\" is not present.", ex.Message);
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal("The required anti-forgery cookie \"__RequestVerificationToken\" is not present.", exception.ExceptionMessage);
}
[Fact]
@ -214,10 +226,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
request.Content = new FormUrlEncodedContent(nameValueCollection);
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request));
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal("The required anti-forgery form field \"__RequestVerificationToken\" is not present.",
ex.Message);
exception.ExceptionMessage);
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
/// <summary>
/// Information about an exception that occured on the server side of a functional
/// test.
/// </summary>
public class ExceptionInfo
{
public string ExceptionMessage { get; set; }
public string ExceptionType { get; set; }
}
}

View File

@ -124,8 +124,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
var url = "http://localhost/RandomNumber/GetAuthorizedRandomNumber";
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(url));
// Act
var response = await client.GetAsync(url);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
}
[Fact]
@ -152,8 +156,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
var url = "http://localhost/RandomNumber/GetHalfOfModifiedRandomNumber?randomNumber=3";
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(() => client.GetAsync(url));
// Act
var response = await client.GetAsync(url);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
}
[Fact]
@ -465,9 +473,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
await Assert.ThrowsAsync<InvalidProgramException>(
() => client.GetAsync("http://localhost/Home/ThrowingResultFilter"));
// Act
var response = await client.GetAsync("http://localhost/Home/ThrowingResultFilter");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidProgramException).FullName, exception.ExceptionType);
}
// Action Filter throws.
@ -496,9 +507,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
await Assert.ThrowsAsync<InvalidProgramException>(
() => client.GetAsync("http://localhost/Home/ThrowingAuthorizationFilter"));
// Act
var response = await client.GetAsync("http://localhost/Home/ThrowingAuthorizationFilter");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidProgramException).FullName, exception.ExceptionType);
}
// Exception Filter throws.

View File

@ -0,0 +1,48 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using Microsoft.AspNet.Mvc.TestConfiguration;
using Xunit;
using Xunit.Sdk;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public static class HttpResponseMessageExceptions
{
public static ExceptionInfo GetServerException(this HttpResponseMessage response)
{
if (response.StatusCode != HttpStatusCode.InternalServerError)
{
throw new AssertActualExpectedException(
HttpStatusCode.InternalServerError,
response.StatusCode,
"A server-side exception should be returned as a 500.");
}
var headers = response.Headers;
IEnumerable<string> exceptionMessageHeader;
IEnumerable<string> exceptionTypeHeader;
if (!headers.TryGetValues(ErrorReporterMiddleware.ExceptionMessageHeader, out exceptionMessageHeader))
{
throw new XunitException(
"No value for the '" + ErrorReporterMiddleware.ExceptionMessageHeader + "' header.");
}
if (!headers.TryGetValues(ErrorReporterMiddleware.ExceptionTypeHeader, out exceptionTypeHeader))
{
throw new XunitException(
"No value for the '" + ErrorReporterMiddleware.ExceptionTypeHeader + "' header.");
}
return new ExceptionInfo()
{
ExceptionMessage = Assert.Single(exceptionMessageHeader),
ExceptionType = Assert.Single(exceptionTypeHeader),
};
}
}
}

View File

@ -42,15 +42,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => client.GetAsync("http://localhost/area-withoutexists/Users")
);
// Act
var response = await client.GetAsync("http://localhost/area-withoutexists/Users");
// Assert
var exception = response.GetServerException();
Assert.Equal("The view 'Index' was not found." +
" The following locations were searched:\r\n/Areas/Users/Views/Home/Index.cshtml\r\n" +
"/Areas/Users/Views/Shared/Index.cshtml\r\n/Views/Shared/Index.cshtml.",
ex.Message);
" The following locations were searched:__/Areas/Users/Views/Home/Index.cshtml__" +
"/Areas/Users/Views/Shared/Index.cshtml__/Views/Shared/Index.cshtml.",
exception.ExceptionMessage);
}
[Fact]

View File

@ -128,12 +128,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FromBodyParametersThrows"));
// Act
var response = await client.GetAsync("http://localhost/FromAttributes/FromBodyParametersThrows");
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
@ -143,12 +146,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FromBodyParameterAndPropertyThrows"));
// Act
var response = await client.GetAsync("http://localhost/FromAttributes/FromBodyParameterAndPropertyThrows");
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
@ -158,12 +164,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FormAndBody_AsParameters_Throws"));
// Act
var response = await client.GetAsync("http://localhost/FromAttributes/FormAndBody_AsParameters_Throws");
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
@ -173,12 +182,15 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/FromAttributes/FormAndBody_Throws"));
// Act
var response = await client.GetAsync("http://localhost/FromAttributes/FormAndBody_Throws");
Assert.Equal("More than one parameter and/or property is bound to the HTTP request's content.",
ex.Message);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(
"More than one parameter and/or property is bound to the HTTP request's content.",
exception.ExceptionMessage);
}
[Fact]
@ -930,13 +942,18 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var client = server.CreateClient();
Expression<Func<User, object>> expression = model => model.Address.Country;
var expected = string.Format(
"The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType);
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
client.GetAsync("http://localhost/TryUpdateModel/GetUserAsync_WithChainedProperties?id=123"));
Assert.Equal(string.Format("The passed expression of expression node type '{0}' is invalid." +
" Only simple member access expressions for model properties are supported.",
expression.Body.NodeType),
ex.Message);
var response = await client.GetAsync("http://localhost/TryUpdateModel/GetUserAsync_WithChainedProperties?id=123");
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
Assert.Equal(expected, exception.ExceptionMessage);
}
[Fact]

View File

@ -925,11 +925,11 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var expectedMessage = "The supplied route name 'DuplicateRoute' is ambiguous and matched more than one route.";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await client.GetAsync(url));
var response = await client.GetAsync(url);
// Assert
Assert.Equal(expectedMessage, ex.Message);
var exception = response.GetServerException();
Assert.Equal(expectedMessage, exception.ExceptionMessage);
}
[Fact]

View File

@ -577,8 +577,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var request = new HttpRequestMessage(new HttpMethod("POST"), "http://localhost/api/Admin/Test?name=mario");
// Act & Assert
await Assert.ThrowsAsync<AmbiguousActionException>(async () => await client.SendAsync(request));
// Act
var response = await client.SendAsync(request);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(AmbiguousActionException).FullName, exception.ExceptionType);
}
[Theory]

View File

@ -50,9 +50,12 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
"<SampleInt>" + sampleInputInt.ToString() + "</SampleInt></DummyClass>";
var content = new StringContent(input, Encoding.UTF8, "application/xml");
// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await client.PostAsync("http://localhost/Home/Index", content));
// Act
var response = await client.PostAsync("http://localhost/Home/Index", content);
// Assert
var exception = response.GetServerException();
Assert.Equal(typeof(InvalidOperationException).FullName, exception.ExceptionType);
}
}
}

View File

@ -22,6 +22,9 @@ namespace ActivatorWebSite
services.AddScoped<ViewService, ViewService>();
});
// Used to report exceptions that MVC doesn't handle
app.UseErrorReporter();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{

View File

@ -17,6 +17,8 @@ namespace AntiForgeryWebSite
services.AddMvc(configuration);
});
app.UseErrorReporter();
app.UseMvc(routes =>
{
routes.MapRoute("ActionAsMethod", "{controller}/{action}",

View File

@ -28,6 +28,8 @@ namespace FiltersWebSite
});
});
app.UseErrorReporter();
app.UseMvc();
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Constraints;
@ -11,8 +10,6 @@ namespace InlineConstraints
{
public class Startup
{
public Action<IRouteBuilder> RouteCollectionProvider { get; set; }
public void Configure(IApplicationBuilder app)
{
var configuration = app.GetTestConfiguration();
@ -22,6 +19,8 @@ namespace InlineConstraints
services.AddMvc(configuration);
});
app.UseErrorReporter();
app.UseMvc(routes =>
{
routes.MapRoute("StoreId",

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.TestConfiguration;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
@ -18,5 +19,10 @@ namespace Microsoft.AspNet.Builder
return configuration;
}
public static IApplicationBuilder UseErrorReporter(this IApplicationBuilder app)
{
return app.Use(next => new ErrorReporterMiddleware(next).Invoke);
}
}
}

View File

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Http;
namespace Microsoft.AspNet.Mvc.TestConfiguration
{
/// <summary>
/// A middleware that reports errors via header values. Useful for tests that want to verify
/// an exception that goes unhandled by the MVC part of the stack.
/// </summary>
public class ErrorReporterMiddleware
{
public static readonly string ExceptionMessageHeader = "ExceptionMessage";
public static readonly string ExceptionTypeHeader = "ExceptionType";
private readonly RequestDelegate _next;
public ErrorReporterMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception exception)
{
if (context.Response.HeadersSent)
{
throw;
}
else
{
context.Response.StatusCode = 500;
var escapedMessage = exception.Message.Replace('\r', '_').Replace('\n', '_');
context.Response.Headers.Add(ExceptionTypeHeader, new string[] { exception.GetType().FullName });
context.Response.Headers.Add(ExceptionMessageHeader, new string[] { escapedMessage });
}
}
}
}
}

View File

@ -30,6 +30,8 @@ namespace ModelBindingWebSite
services.AddSingleton<ITestService, TestService>();
});
app.UseErrorReporter();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{

View File

@ -20,6 +20,8 @@ namespace RoutingWebSite
services.AddScoped<TestResponseGenerator>();
});
app.UseErrorReporter();
app.UseMvc(routes =>
{
routes.MapRoute("areaRoute",

View File

@ -19,6 +19,8 @@ namespace WebApiCompatShimWebSite
services.AddWebApiConventions();
});
app.UseErrorReporter();
app.UseMvc(routes =>
{
// This route can't access any of our webapi controllers

View File

@ -27,6 +27,8 @@ namespace XmlSerializerWebSite
});
});
app.UseErrorReporter();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{