diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs new file mode 100644 index 0000000000..aea31ae127 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtActionResult.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation. 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.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// An that returns a Accepted (202) response with a Location header. + /// + public class AcceptedAtActionResult : ObjectResult + { + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public AcceptedAtActionResult( + string actionName, + string controllerName, + object routeValues, + object value) + : base(value) + { + ActionName = actionName; + ControllerName = controllerName; + RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); + StatusCode = StatusCodes.Status202Accepted; + } + + /// + /// Gets or sets the used to generate URLs. + /// + public IUrlHelper UrlHelper { get; set; } + + /// + /// Gets or sets the name of the action to use for generating the URL. + /// + public string ActionName { get; set; } + + /// + /// Gets or sets the name of the controller to use for generating the URL. + /// + public string ControllerName { get; set; } + + /// + /// Gets or sets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; set; } + + /// + public override void OnFormatting(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + base.OnFormatting(context); + + var request = context.HttpContext.Request; + + var urlHelper = UrlHelper; + if (urlHelper == null) + { + var services = context.HttpContext.RequestServices; + urlHelper = services.GetRequiredService().GetUrlHelper(context); + } + + var url = urlHelper.Action( + ActionName, + ControllerName, + RouteValues, + request.Scheme, + request.Host.ToUriComponent()); + + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException(Resources.NoRoutesMatched); + } + + context.HttpContext.Response.Headers[HeaderNames.Location] = url; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs new file mode 100644 index 0000000000..bfde81d3ae --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedAtRouteResult.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation. 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.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// An that returns a Accepted (202) response with a Location header. + /// + public class AcceptedAtRouteResult : ObjectResult + { + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public AcceptedAtRouteResult(object routeValues, object value) + : this(routeName: null, routeValues: routeValues, value: value) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public AcceptedAtRouteResult( + string routeName, + object routeValues, + object value) + : base(value) + { + RouteName = routeName; + RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); + StatusCode = StatusCodes.Status202Accepted; + } + + /// + /// Gets or sets the used to generate URLs. + /// + public IUrlHelper UrlHelper { get; set; } + + /// + /// Gets or sets the name of the route to use for generating the URL. + /// + public string RouteName { get; set; } + + /// + /// Gets or sets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; set; } + + /// + public override void OnFormatting(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + base.OnFormatting(context); + + var urlHelper = UrlHelper; + if (urlHelper == null) + { + var services = context.HttpContext.RequestServices; + urlHelper = services.GetRequiredService().GetUrlHelper(context); + } + + var url = urlHelper.Link(RouteName, RouteValues); + + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException(Resources.NoRoutesMatched); + } + + context.HttpContext.Response.Headers[HeaderNames.Location] = url; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs new file mode 100644 index 0000000000..ce6f7cb72f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/AcceptedResult.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. 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.AspNetCore.Http; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// An that returns an Accepted (202) response with a Location header. + /// + public class AcceptedResult : ObjectResult + { + private string _location; + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + public AcceptedResult() + : base(value: null) + { + StatusCode = StatusCodes.Status202Accepted; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored. + /// The value to format in the entity body. + public AcceptedResult(string location, object value) + : base(value) + { + Location = location; + StatusCode = StatusCodes.Status202Accepted; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored + /// It is an optional paramater and may be null + /// The value to format in the entity body. + public AcceptedResult(Uri locationUri, object value) + : base(value) + { + if (locationUri == null) + { + throw new ArgumentNullException(nameof(locationUri)); + } + + if (locationUri.IsAbsoluteUri) + { + Location = locationUri.AbsoluteUri; + } + else + { + Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); + } + + StatusCode = StatusCodes.Status202Accepted; + } + + /// + /// Gets or sets the location at which the status of the requested content can be monitored. + /// + public string Location + { + get + { + return _location; + } + set + { + _location = value; + } + } + + /// + public override void OnFormatting(ActionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + base.OnFormatting(context); + + if (!string.IsNullOrEmpty(Location)) + { + context.HttpContext.Response.Headers[HeaderNames.Location] = Location; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index 64aef4bd5d..5fc7b1257d 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -910,6 +910,223 @@ namespace Microsoft.AspNetCore.Mvc return new CreatedAtRouteResult(routeName, routeValues, value); } + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted() + { + return new AcceptedResult(); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted(object value) + { + return new AcceptedResult(location: null, value: value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The optional URI with the location at which the status of requested content can be monitored. + /// May be null. + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new AcceptedResult(locationUri: uri, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The optional URI with the location at which the status of requested content can be monitored. + /// May be null. + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted(string uri) + { + return new AcceptedResult(location: uri, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The URI with the location at which the status of requested content can be monitored. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted(string uri, object value) + { + return new AcceptedResult(uri, value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The URI with the location at which the status of requested content can be monitored. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedResult Accepted(Uri uri, object value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + return new AcceptedResult(locationUri: uri, value: value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction(string actionName) + { + return AcceptedAtAction(actionName, routeValues: null, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName) + { + return AcceptedAtAction(actionName, controllerName, routeValues: null, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, object value) + { + return AcceptedAtAction(actionName, routeValues: null, value: value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, object routeValues) + { + return AcceptedAtAction(actionName, controllerName, routeValues, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The route data to use for generating the URL. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, object routeValues, object value) + { + return AcceptedAtAction(actionName, controllerName: null, routeValues: routeValues, value: value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the action to use for generating the URL. + /// The name of the controller to use for generating the URL. + /// The route data to use for generating the URL. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedAtActionResult AcceptedAtAction( + string actionName, + string controllerName, + object routeValues, + object value) + { + return new AcceptedAtActionResult(actionName, controllerName, routeValues, value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The route data to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtRouteResult AcceptedAtRoute(object routeValues) + { + return AcceptedAtRoute(routeName: null, routeValues: routeValues, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the route to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName) + { + return AcceptedAtRoute(routeName, routeValues: null, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the route to use for generating the URL. + ///The route data to use for generating the URL. + /// The created for the response. + [NonAction] + public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues) + { + return AcceptedAtRoute(routeName, routeValues, value: null); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The route data to use for generating the URL. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedAtRouteResult AcceptedAtRoute(object routeValues, object value) + { + return AcceptedAtRoute(routeName: null, routeValues: routeValues, value: value); + } + + /// + /// Creates a object that produces an Accepted (202) response. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The optional content value to format in the entity body; may be null. + /// The created for the response. + [NonAction] + public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues, object value) + { + return new AcceptedAtRouteResult(routeName, routeValues, value); + } + /// /// Creates a . /// diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs new file mode 100644 index 0000000000..e42c8712d5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtActionResultTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class AcceptedAtActionResultTests + { + public static TheoryData ValuesData + { + get + { + return new TheoryData + { + null, + "Test string", + new object(), + }; + } + } + + [Theory] + [MemberData(nameof(ValuesData))] + public void Constructor_InitializesStatusCodeAndValue(object value) + { + // Arrange + var url = "testAction"; + + // Act + var result = new AcceptedAtActionResult( + actionName: url, + controllerName: null, + routeValues: null, + value: value); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(value, result.Value); + } + + [Theory] + [MemberData(nameof(ValuesData))] + public async Task ExecuteResultAsync_SetsObjectValueOfFormatter(object value) + { + // Arrange + var url = "testAction"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + object actual = null; + formatter.Setup(f => f.WriteAsync(It.IsAny())) + .Callback((OutputFormatterWriteContext context) => actual = context.Object) + .Returns(Task.FromResult(0)); + + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(url); + + // Act + var result = new AcceptedAtActionResult( + actionName: url, + controllerName: null, + routeValues: null, + value: value); + + result.UrlHelper = urlHelper; + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Same(value, actual); + } + + [Fact] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() + { + // Arrange + var expectedUrl = "testAction"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(expectedUrl); + + // Act + var result = new AcceptedAtActionResult( + actionName: expectedUrl, + controllerName: null, + routeValues: null, + value: null); + + result.UrlHelper = urlHelper; + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_ThrowsIfActionUrlIsNull() + { + // Arrange + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(returnValue: null); + + // Act + var result = new AcceptedAtActionResult( + actionName: null, + controllerName: null, + routeValues: null, + value: null); + + result.UrlHelper = urlHelper; + + // Assert + await ExceptionAssert.ThrowsAsync(() => + result.ExecuteResultAsync(actionContext), + "No route matches the supplied values."); + } + + private static ActionContext GetActionContext(HttpContext httpContext) + { + var routeData = new RouteData(); + routeData.Routers.Add(Mock.Of()); + + return new ActionContext( + httpContext, + routeData, + new ActionDescriptor()); + } + + private static HttpContext GetHttpContext(Mock formatter) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(formatter); + return httpContext; + } + + private static Mock CreateMockFormatter() + { + var formatter = new Mock + { + CallBase = true + }; + formatter.Setup(f => f.CanWriteResult(It.IsAny())).Returns(true); + + return formatter; + } + + private static IServiceProvider CreateServices(Mock formatter) + { + var options = new TestOptionsManager(); + options.Value.OutputFormatters.Add(formatter.Object); + var services = new ServiceCollection(); + services.AddSingleton(new ObjectResultExecutor( + options, + new TestHttpResponseStreamWriterFactory(), + NullLoggerFactory.Instance)); + + return services.BuildServiceProvider(); + } + + private static IUrlHelper GetMockUrlHelper(string returnValue) + { + var urlHelper = new Mock(); + urlHelper.Setup(o => o.Action(It.IsAny())).Returns(returnValue); + + return urlHelper.Object; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs new file mode 100644 index 0000000000..d085d22581 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedAtRouteResultTests.cs @@ -0,0 +1,201 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc +{ + public class AcceptedAtRouteResultTests + { + public static TheoryData ValuesData + { + get + { + return new TheoryData + { + null, + "Test string", + new object(), + }; + } + } + + [Theory] + [MemberData(nameof(ValuesData))] + public void Constructor_InitializesStatusCodeAndValue(object value) + { + // Arrange & Act + var result = new AcceptedAtRouteResult( + routeName: null, + routeValues: null, + value: value); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(value, result.Value); + } + + [Theory] + [MemberData(nameof(ValuesData))] + public async Task ExecuteResultAsync_SetsObjectValueOfFormatter(object value) + { + // Arrange + var url = "testAction"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + object actual = null; + formatter.Setup(f => f.WriteAsync(It.IsAny())) + .Callback((OutputFormatterWriteContext context) => actual = context.Object) + .Returns(Task.FromResult(0)); + + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(url); + var routeValues = new RouteValueDictionary(new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }); + + // Act + var result = new AcceptedAtRouteResult( + routeName: "sample", + routeValues: routeValues, + value: value); + result.UrlHelper = urlHelper; + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Same(value, actual); + } + + public static TheoryData AcceptedAtRouteData + { + get + { + return new TheoryData + { + null, + new Dictionary() + { + { "hello", "world" } + }, + new RouteValueDictionary( + new Dictionary() + { + { "test", "case" }, + { "sample", "route" } + }), + }; + } + } + + [Theory] + [MemberData(nameof(AcceptedAtRouteData))] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object values) + { + // Arrange + var expectedUrl = "testAction"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(expectedUrl); + + // Act + var result = new AcceptedAtRouteResult(routeValues: values, value: null); + result.UrlHelper = urlHelper; + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() + { + // Arrange + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + var urlHelper = GetMockUrlHelper(returnValue: null); + + // Act + var result = new AcceptedAtRouteResult( + routeName: null, + routeValues: new Dictionary(), + value: null); + + result.UrlHelper = urlHelper; + + // Assert + await ExceptionAssert.ThrowsAsync(() => + result.ExecuteResultAsync(actionContext), + "No route matches the supplied values."); + } + + private static ActionContext GetActionContext(HttpContext httpContext) + { + var routeData = new RouteData(); + routeData.Routers.Add(Mock.Of()); + + return new ActionContext( + httpContext, + routeData, + new ActionDescriptor()); + } + + private static HttpContext GetHttpContext(Mock formatter) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(formatter); + return httpContext; + } + + private static Mock CreateMockFormatter() + { + var formatter = new Mock + { + CallBase = true + }; + formatter.Setup(f => f.CanWriteResult(It.IsAny())).Returns(true); + + return formatter; + } + + private static IServiceProvider CreateServices(Mock formatter) + { + var options = new TestOptionsManager(); + options.Value.OutputFormatters.Add(formatter.Object); + var services = new ServiceCollection(); + services.AddSingleton(new ObjectResultExecutor( + options, + new TestHttpResponseStreamWriterFactory(), + NullLoggerFactory.Instance)); + + return services.BuildServiceProvider(); + } + + private static IUrlHelper GetMockUrlHelper(string returnValue) + { + var urlHelper = new Mock(); + urlHelper.Setup(o => o.Link(It.IsAny(), It.IsAny())).Returns(returnValue); + + return urlHelper.Object; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs new file mode 100644 index 0000000000..9ac5493fb9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/AcceptedResultTests.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation. 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.Buffers; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Core.Test +{ + public class AcceptedResultTests + { + public static TheoryData ValuesData + { + get + { + return new TheoryData + { + null, + "Test string", + new object(), + }; + } + } + + [Theory] + [MemberData(nameof(ValuesData))] + public void Constructor_InitializesStatusCodeAndValue(object value) + { + // Arrange & Act + var result = new AcceptedResult("testlocation", value); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(value, result.Value); + } + + [Theory] + [MemberData(nameof(ValuesData))] + public async Task ExecuteResultAsync_SetsObjectValueOfFormatter(object value) + { + // Arrange + var location = "/test/"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + object actual = null; + formatter.Setup(f => f.WriteAsync(It.IsAny())) + .Callback((OutputFormatterWriteContext context) => actual = context.Object) + .Returns(Task.FromResult(0)); + + var actionContext = GetActionContext(httpContext); + + // Act + var result = new AcceptedResult(location, value); + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Same(value, actual); + } + + [Fact] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() + { + // Arrange + var location = "/test/"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + + // Act + var result = new AcceptedResult(location, "testInput"); + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_OverwritesLocationHeader() + { + // Arrange + var location = "/test/"; + var formatter = CreateMockFormatter(); + var httpContext = GetHttpContext(formatter); + var actionContext = GetActionContext(httpContext); + httpContext.Response.Headers["Location"] = "/different/location/"; + + // Act + var result = new AcceptedResult(location, "testInput"); + await result.ExecuteResultAsync(actionContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } + + private static ActionContext GetActionContext(HttpContext httpContext) + { + var routeData = new RouteData(); + routeData.Routers.Add(Mock.Of()); + return new ActionContext( + httpContext, + routeData, + new ActionDescriptor()); + } + + private static HttpContext GetHttpContext(Mock formatter) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(formatter); + return httpContext; + } + + private static Mock CreateMockFormatter() + { + var formatter = new Mock + { + CallBase = true + }; + formatter.Setup(f => f.CanWriteResult(It.IsAny())).Returns(true); + + return formatter; + } + + private static IServiceProvider CreateServices(Mock formatter) + { + var options = new TestOptionsManager(); + options.Value.OutputFormatters.Add(formatter.Object); + var services = new ServiceCollection(); + services.AddSingleton(new ObjectResultExecutor( + options, + new TestHttpResponseStreamWriterFactory(), + NullLoggerFactory.Instance)); + + return services.BuildServiceProvider(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs index b494e1be75..c2ea012596 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs @@ -566,6 +566,185 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test Assert.Equal(expected, result.RouteValues); } + [Fact] + public void Accepted_SetsStatusCode() + { + // Arrange + var controller = new TestableController(); + + // Act + var result = controller.Accepted(); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + } + + [Fact] + public void Accepted_SetsValue() + { + // Arrange + var controller = new TestableController(); + var value = new object(); + + // Act + var result = controller.Accepted(value); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(value, result.Value); + } + + [Fact] + public void Accepted_StringUri_SetsAcceptedLocation() + { + // Arrange + var controller = new TestableController(); + var uri = "http://test/url"; + + // Act + var result = controller.Accepted(uri); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(uri, result.Location); + } + + [Fact] + public void Accepted_AbsoluteUri_SetsAcceptedLocation() + { + // Arrange + var controller = new TestableController(); + var uri = new Uri("http://test/url"); + + // Act + var result = controller.Accepted(uri); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri.OriginalString, result.Location); + } + + [Fact] + public void Accepted_RelativeUri_SetsAcceptedLocation() + { + // Arrange + var controller = new TestableController(); + var uri = new Uri("/test/url", UriKind.Relative); + + // Act + var result = controller.Accepted(uri); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(uri.OriginalString, result.Location); + } + + [Fact] + public void AcceptedAtAction_SetsActionName() + { + // Arrange + var controller = new TestableController(); + + // Act + var result = controller.AcceptedAtAction("SampleAction"); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal("SampleAction", result.ActionName); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("SampleController")] + public void AcceptedAtAction_SetsActionController(string controllerName) + { + // Arrange + var controller = new TestableController(); + + // Act + var result = controller.AcceptedAtAction("SampleAction", controllerName); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal("SampleAction", result.ActionName); + Assert.Equal(controllerName, result.ControllerName); + } + + [Fact] + public void AcceptedAtAction_SetsActionControllerRouteValues() + { + // Arrange + var controller = new TestableController(); + var expected = new Dictionary + { + { "test", "case" }, + { "sample", "route" }, + }; + + // Act + var result = controller.AcceptedAtAction( + "SampleAction", + "SampleController", + new RouteValueDictionary(expected)); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal("SampleAction", result.ActionName); + Assert.Equal("SampleController", result.ControllerName); + Assert.Equal(expected, result.RouteValues); + } + + [Fact] + public void AcceptedAtRoute_SetsRouteValues() + { + // Arrange + var controller = new TestableController(); + var expected = new Dictionary + { + { "test", "case" }, + { "sample", "route" }, + }; + + // Act + var result = controller.AcceptedAtRoute(new RouteValueDictionary(expected)); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Equal(expected, result.RouteValues); + } + + [Fact] + public void AcceptedAtRoute_SetsRouteNameAndValues() + { + // Arrange + var controller = new TestableController(); + var routeName = "SampleRoute"; + var expected = new Dictionary + { + { "test", "case" }, + { "sample", "route" }, + }; + + // Act + var result = controller.AcceptedAtRoute(routeName, new RouteValueDictionary(expected)); + + // Assert + Assert.IsType(result); + Assert.Equal(StatusCodes.Status202Accepted, result.StatusCode); + Assert.Same(routeName, result.RouteName); + Assert.Equal(expected, result.RouteValues); + } + [Fact] public void File_WithContents() { @@ -1107,7 +1286,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test // Arrange var modelName = "mymodel"; - Func propertyFilter = (m) => + Func propertyFilter = (m) => string.Equals(m.PropertyName, "Include1", StringComparison.OrdinalIgnoreCase) || string.Equals(m.PropertyName, "Include2", StringComparison.OrdinalIgnoreCase); @@ -1207,7 +1386,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test // Arrange var modelName = "mymodel"; - Func propertyFilter = (m) => + Func propertyFilter = (m) => string.Equals(m.PropertyName, "Include1", StringComparison.OrdinalIgnoreCase) || string.Equals(m.PropertyName, "Include2", StringComparison.OrdinalIgnoreCase); @@ -1446,7 +1625,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test var binder = new StubModelBinder(); var controller = GetController(binder, valueProvider: null); controller.ObjectValidator = new DefaultObjectValidator( - controller.MetadataProvider, + controller.MetadataProvider, new[] { provider.Object }); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs index 95ee71bedb..a7db806384 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerUnitTestabilityTests.cs @@ -103,6 +103,26 @@ namespace Microsoft.AspNetCore.Mvc Assert.Equal(StatusCodes.Status201Created, createdResult.StatusCode); } + [Theory] + [InlineData("/Accepted_1", "AcceptedBody")] + [InlineData("/Accepted_2", null)] + public void ControllerAccepted_InvokedInUnitTests(string uri, string content) + { + // Arrange + var controller = new TestabilityController(); + + // Act + var result = controller.Accepted_Action(uri, content); + + // Assert + Assert.NotNull(result); + + var acceptedResult = Assert.IsType(result); + Assert.Equal(uri, acceptedResult.Location); + Assert.Equal(content, acceptedResult.Value); + Assert.Equal(StatusCodes.Status202Accepted, acceptedResult.StatusCode); + } + [Fact] public void ControllerFileContent_InvokedInUnitTests() { @@ -335,6 +355,42 @@ namespace Microsoft.AspNetCore.Mvc Assert.Null(createdAtRouteResult.Value); } + [Fact] + public void ControllerAcceptedAtRoute_InvokedInUnitTests() + { + // Arrange + var controller = new TestabilityController(); + var routeName = "RouteName_1"; + var routeValues = new Dictionary() { { "route", "sample" } }; + var value = new { Value = "Value_1" }; + + // Act + var result = controller.AcceptedAtRoute_Action(routeName, routeValues, value); + + // Assert + Assert.NotNull(result); + + var acceptedAtRouteResult = Assert.IsType(result); + Assert.Equal(routeName, acceptedAtRouteResult.RouteName); + Assert.Single(acceptedAtRouteResult.RouteValues); + Assert.Equal("sample", acceptedAtRouteResult.RouteValues["route"]); + Assert.Same(value,acceptedAtRouteResult.Value); + + // Arrange + controller = new TestabilityController(); + + // Act + result = controller.AcceptedAtRoute_Action(null, null, null); + + // Assert + Assert.NotNull(result); + + acceptedAtRouteResult = Assert.IsType(result); + Assert.Null(acceptedAtRouteResult.RouteName); + Assert.Null(acceptedAtRouteResult.RouteValues); + Assert.Null(acceptedAtRouteResult.Value); + } + [Fact] public void ControllerCreatedAtAction_InvokedInUnitTests() { @@ -374,6 +430,45 @@ namespace Microsoft.AspNetCore.Mvc Assert.Null(createdAtActionResult.RouteValues); } + [Fact] + public void ControllerAcceptedAtAction_InvokedInUnitTests() + { + // Arrange + var controller = new TestabilityController(); + var actionName = "ActionName_1"; + var controllerName = "ControllerName_1"; + var routeValues = new Dictionary() { { "route", "sample" } }; + var value = new { Value = "Value_1" }; + + // Act + var result = controller.AcceptedAtAction_Action(actionName, controllerName, routeValues, value); + + // Assert + Assert.NotNull(result); + + var acceptedAtActionResult = Assert.IsType(result); + Assert.Equal(actionName, acceptedAtActionResult.ActionName); + Assert.Equal(controllerName, acceptedAtActionResult.ControllerName); + Assert.Single(acceptedAtActionResult.RouteValues); + Assert.Equal("sample", acceptedAtActionResult.RouteValues["route"]); + Assert.Same(value, acceptedAtActionResult.Value); + + // Arrange + controller = new TestabilityController(); + + // Act + result = controller.AcceptedAtAction_Action(null, null, null, null); + + // Assert + Assert.NotNull(result); + + acceptedAtActionResult = Assert.IsType(result); + Assert.Null(acceptedAtActionResult.ActionName); + Assert.Null(acceptedAtActionResult.ControllerName); + Assert.Null(acceptedAtActionResult.Value); + Assert.Null(acceptedAtActionResult.RouteValues); + } + [Fact] public void ControllerRedirectToRoute_InvokedInUnitTests() { @@ -628,6 +723,11 @@ namespace Microsoft.AspNetCore.Mvc return Created(uri, data); } + public IActionResult Accepted_Action(string uri, object data) + { + return Accepted(uri, data); + } + public IActionResult FileContent_Action(string content, string contentType, string fileName) { var contentArray = Encoding.UTF8.GetBytes(content); @@ -675,6 +775,16 @@ namespace Microsoft.AspNetCore.Mvc return CreatedAtRoute(routeName, routeValues, value); } + public IActionResult AcceptedAtAction_Action(string actionName, string controllerName, object routeValues, object value) + { + return AcceptedAtAction(actionName, controllerName, routeValues, value); + } + + public IActionResult AcceptedAtRoute_Action(string routeName, object routeValues, object value) + { + return AcceptedAtRoute(routeName, routeValues, value); + } + public IActionResult HttpBadRequest_Action() { return BadRequest();