diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/HttpNotFoundObjectResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/HttpNotFoundObjectResult.cs new file mode 100644 index 0000000000..ebfa3fe3ee --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/HttpNotFoundObjectResult.cs @@ -0,0 +1,23 @@ +// 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 Microsoft.AspNet.WebUtilities; + +namespace Microsoft.AspNet.Mvc +{ + /// + /// An that when executed will produce a Not Found (404) response. + /// + public class HttpNotFoundObjectResult : ObjectResult + { + /// + /// Creates a new instance. + /// + /// The value to format in the entity body. + public HttpNotFoundObjectResult(object value) + : base(value) + { + StatusCode = StatusCodes.Status404NotFound; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Controller.cs b/src/Microsoft.AspNet.Mvc.Core/Controller.cs index 60cd3021ba..5a0030650a 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controller.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controller.cs @@ -692,6 +692,16 @@ namespace Microsoft.AspNet.Mvc return new HttpNotFoundResult(); } + /// + /// Creates an that produces a Not Found (404) response. + /// + /// The created for the response. + [NonAction] + public virtual HttpNotFoundObjectResult HttpNotFound(object value) + { + return new HttpNotFoundObjectResult(value); + } + /// /// Creates an that produces a Bad Request (400) response. /// diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/HttpNotFoundObjectResultTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/HttpNotFoundObjectResultTest.cs new file mode 100644 index 0000000000..10da7cdd66 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ActionResults/HttpNotFoundObjectResultTest.cs @@ -0,0 +1,139 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Core; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.OptionsModel; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc +{ + public class HttpNotFoundObjectResultTest + { + [Fact] + public void HttpNotFoundObjectResult_InitializesStatusCode() + { + // Arrange & act + var notFound = new HttpNotFoundObjectResult(null); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + } + + [Fact] + public void HttpNotFoundObjectResult_InitializesStatusCodeAndResponseContent() + { + // Arrange & act + var notFound = new HttpNotFoundObjectResult("Test Content"); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + Assert.Equal("Test Content", notFound.Value); + } + + [Fact] + public async Task HttpNotFoundObjectResult_ExecuteSuccessful() + { + // Arrange + var input = "Test Content"; + var stream = new MemoryStream(); + + var httpResponse = new Mock(); + var tempContentType = string.Empty; + httpResponse.SetupProperty(o => o.ContentType); + httpResponse.SetupGet(r => r.Body).Returns(stream); + + var actionContext = CreateMockActionContext(httpResponse.Object); + var notFound = new HttpNotFoundObjectResult(input); + + // Act + await notFound.ExecuteResultAsync(actionContext); + + // Assert + httpResponse.VerifySet(r => r.StatusCode = StatusCodes.Status404NotFound); + Assert.Equal(input.Length, httpResponse.Object.Body.Length); + } + + private static ActionContext CreateMockActionContext( + HttpResponse response = null, + string requestAcceptHeader = "application/*", + string requestContentType = "application/json", + string requestAcceptCharsetHeader = "", + bool respectBrowserAcceptHeader = false) + { + var formatters = new IOutputFormatter[] { new StringOutputFormatter(), new JsonOutputFormatter() }; + var httpContext = new Mock(); + if (response != null) + { + httpContext.Setup(o => o.Response).Returns(response); + } + + var content = "{name: 'Person Name', Age: 'not-an-age'}"; + var contentBytes = Encoding.UTF8.GetBytes(content); + + var request = new DefaultHttpContext().Request; + request.Headers["Accept-Charset"] = requestAcceptCharsetHeader; + request.Headers["Accept"] = requestAcceptHeader; + request.ContentType = requestContentType; + request.Body = new MemoryStream(contentBytes); + + httpContext.Setup(o => o.Request).Returns(request); + httpContext.Setup(o => o.RequestServices).Returns(GetServiceProvider()); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOutputFormattersProvider))) + .Returns(new TestOutputFormatterProvider(formatters)); + + var options = new Mock>(); + options.SetupGet(o => o.Options) + .Returns(new MvcOptions() + { + RespectBrowserAcceptHeader = respectBrowserAcceptHeader + }); + httpContext.Setup(o => o.RequestServices.GetService(typeof(IOptions))) + .Returns(options.Object); + + return new ActionContext(httpContext.Object, new RouteData(), new ActionDescriptor()); + } + + private static IServiceProvider GetServiceProvider() + { + var optionsSetup = new MvcOptionsSetup(); + var options = new MvcOptions(); + optionsSetup.Configure(options); + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Options).Returns(options); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddInstance>(optionsAccessor.Object); + return serviceCollection.BuildServiceProvider(); + } + + private class TestOutputFormatterProvider : IOutputFormattersProvider + { + private readonly IEnumerable _formatters; + + public TestOutputFormatterProvider(IEnumerable formatters) + { + _formatters = formatters; + } + + public IReadOnlyList OutputFormatters + { + get + { + return _formatters.ToList(); + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs index f200ccd6ec..0e442cdb1e 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerTests.cs @@ -657,6 +657,21 @@ namespace Microsoft.AspNet.Mvc.Test Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); } + [Fact] + public void HttpNotFound_SetsStatusCodeAndResponseContent() + { + // Arrange + var controller = new TestableController(); + + // Act + var result = controller.HttpNotFound("Test Content"); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode); + Assert.Equal("Test Content", result.Value); + } + [Fact] public void BadRequest_SetsStatusCode() { diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerUnitTestabilityTests.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerUnitTestabilityTests.cs index 2933aa5130..e676754cdc 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ControllerUnitTestabilityTests.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ControllerUnitTestabilityTests.cs @@ -214,6 +214,23 @@ namespace Microsoft.AspNet.Mvc Assert.Equal(StatusCodes.Status404NotFound, httpNotFoundResult.StatusCode); } + [Fact] + public void ControllerHttpNotFoundObject_InvokedInUnitTests() + { + // Arrange + var controller = new TestabilityController(); + + // Act + var result = controller.HttpNotFoundObject_Action("Test Content"); + + // Assert + Assert.NotNull(result); + + var httpNotFoundObjectResult = Assert.IsType(result); + Assert.Equal(StatusCodes.Status404NotFound, httpNotFoundObjectResult.StatusCode); + Assert.Equal("Test Content", httpNotFoundObjectResult.Value); + } + [Fact] public void ControllerHttpBadRequest_InvokedInUnitTests() { @@ -621,6 +638,11 @@ namespace Microsoft.AspNet.Mvc { return HttpNotFound(); } + + public IActionResult HttpNotFoundObject_Action(object value) + { + return HttpNotFound(value); + } } private class MyModel diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ActionResultTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActionResultTests.cs index 2e926dc14b..813e9cd4c0 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ActionResultTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ActionResultTests.cs @@ -284,5 +284,45 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); } + + [Fact] + public async Task HttpNotFoundObjectResult_NoResponseContent() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var request = new HttpRequestMessage( + HttpMethod.Get, + "http://localhost/ActionResultsVerification/GetNotFoundObjectResult"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task HttpNotFoundObjectResult_WithResponseContent() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + var input = "{\"SampleInt\":10}"; + + var request = new HttpRequestMessage( + HttpMethod.Post, + "http://localhost/ActionResultsVerification/GetNotFoundObjectResultWithContent"); + request.Content = new StringContent(input, Encoding.UTF8, "application/json"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + Assert.Equal("{\"SampleInt\":10,\"SampleString\":\"Foo\"}", await response.Content.ReadAsStringAsync()); + } } } \ No newline at end of file diff --git a/test/WebSites/ActionResultsWebSite/Controllers/ActionResultsVerificationController.cs b/test/WebSites/ActionResultsWebSite/Controllers/ActionResultsVerificationController.cs index e951a6b420..8fddb320c3 100644 --- a/test/WebSites/ActionResultsWebSite/Controllers/ActionResultsVerificationController.cs +++ b/test/WebSites/ActionResultsWebSite/Controllers/ActionResultsVerificationController.cs @@ -85,6 +85,16 @@ namespace ActionResultsWebSite return result; } + public IActionResult GetNotFoundObjectResult() + { + return HttpNotFound(null); + } + + public IActionResult GetNotFoundObjectResultWithContent() + { + return HttpNotFound(CreateDummy()); + } + public DummyClass GetDummy(int id) { return CreateDummy();