diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs index ec791aaee0..d2eec34199 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs @@ -46,7 +46,13 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders; } - var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType); + var defaultErrorType = typeof(void); + if (action.Properties.TryGetValue(typeof(ProducesErrorResponseTypeAttribute), out result)) + { + defaultErrorType = ((ProducesErrorResponseTypeAttribute)result).Type; + } + + var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType, defaultErrorType); return apiResponseTypes; } @@ -69,7 +75,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer private ICollection GetApiResponseTypes( IReadOnlyList responseMetadataAttributes, - Type type) + Type type, + Type defaultErrorType) { var results = new Dictionary(); @@ -83,48 +90,39 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer { metadataAttribute.SetContentTypes(contentTypes); - ApiResponseType apiResponseType; + var statusCode = metadataAttribute.StatusCode; - if (metadataAttribute is IApiDefaultResponseMetadataProvider) + var apiResponseType = new ApiResponseType { - apiResponseType = new ApiResponseType + Type = metadataAttribute.Type, + StatusCode = statusCode, + IsDefaultResponse = metadataAttribute is IApiDefaultResponseMetadataProvider, + }; + + if (apiResponseType.Type == typeof(void)) + { + if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created)) { - IsDefaultResponse = true, - Type = metadataAttribute.Type, - }; - } - else if (metadataAttribute.Type == typeof(void) && - type != null && - (metadataAttribute.StatusCode == StatusCodes.Status200OK || metadataAttribute.StatusCode == StatusCodes.Status201Created)) - { - // ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified. - // In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a - // [ProducesResponseType(201)] instead of [ProducesResponseType(201, typeof(Person)] when typeof(Person) can be inferred - // from the return type. - apiResponseType = new ApiResponseType + // ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified. + // In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a + // [ProducesResponseType(201)] instead of [ProducesResponseType(201, typeof(Person)] when typeof(Person) can be inferred + // from the return type. + apiResponseType.Type = type; + } + else if (IsClientError(statusCode) || apiResponseType.IsDefaultResponse) { - StatusCode = metadataAttribute.StatusCode, - Type = type, - }; - } - else if (metadataAttribute.Type != null) - { - apiResponseType = new ApiResponseType - { - StatusCode = metadataAttribute.StatusCode, - Type = metadataAttribute.Type, - }; - } - else - { - continue; + // Use the default error type for "default" responses or 4xx client errors if no response type is specified. + apiResponseType.Type = defaultErrorType; + } } - results[apiResponseType.StatusCode] = apiResponseType; + if (apiResponseType.Type != null) + { + results[apiResponseType.StatusCode] = apiResponseType; + } } } - // Set the default status only when no status has already been set explicitly if (results.Count == 0 && type != null) { @@ -225,5 +223,10 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer return declaredReturnType; } + + private static bool IsClientError(int statusCode) + { + return statusCode >= 400 && statusCode < 500; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs index 5b21f55378..9eeb83769a 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { public class ApiBehaviorApplicationModelProvider : IApplicationModelProvider { + private readonly ProducesErrorResponseTypeAttribute DefaultErrorType = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails)); private readonly ApiBehaviorOptions _apiBehaviorOptions; private readonly IModelMetadataProvider _modelMetadataProvider; private readonly ModelStateInvalidFilter _modelStateInvalidFilter; @@ -105,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal AddMultipartFormDataConsumesAttribute(actionModel); DiscoverApiConvention(actionModel, conventions); + + DiscoverErrorResponseType(actionModel); } } } @@ -274,6 +277,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal } } + internal void DiscoverErrorResponseType(ActionModel actionModel) + { + var errorTypeAttribute = + actionModel.Attributes.OfType().FirstOrDefault() ?? + actionModel.Controller.Attributes.OfType().FirstOrDefault() ?? + actionModel.Controller.ControllerType.Assembly.GetCustomAttribute(); + + if (!_apiBehaviorOptions.SuppressMapClientErrors) + { + // If ClientErrorFactory is being used and the application does not supply a error response type, assume ProblemDetails. + errorTypeAttribute = errorTypeAttribute ?? DefaultErrorType; + } + + if (errorTypeAttribute != null) + { + actionModel.Properties[typeof(ProducesErrorResponseTypeAttribute)] = errorTypeAttribute; + } + } + private bool ParameterExistsInAnyRoute(ActionModel actionModel, string parameterName) { foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel)) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs new file mode 100644 index 0000000000..f74a826e93 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs @@ -0,0 +1,38 @@ +// 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.Mvc.Infrastructure; + +namespace Microsoft.AspNetCore.Mvc +{ + /// + /// Specifies the type returned by default by controllers annotated with . + /// + /// specifies the error model type associated with a + /// for a client error (HTTP Status Code 4xx) when no value is provided. When no value is specified, MVC assumes the + /// client error type to be , if mapping client errors () + /// is used. + /// + /// + /// Use this to configure the default error type if your application uses a custom error type to respond. + /// + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ProducesErrorResponseTypeAttribute : Attribute + { + /// + /// Initializes a new instance of . + /// + /// The error type. + public ProducesErrorResponseTypeAttribute(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + } + + /// + /// Gets the default error type. + /// + public Type Type { get; } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs index 11c9bcb8e0..aee457d5a6 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs @@ -309,6 +309,247 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer }); } + [Fact] + public void GetApiResponseTypes_UsesErrorType_ForClientErrors() + { + // Arrange + var errorType = typeof(InvalidTimeZoneException); + var actionDescriptor = GetControllerActionDescriptor( + typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController), + nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + new ProducesResponseTypeAttribute(404), + new ProducesResponseTypeAttribute(415), + }); + + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(BaseModel), responseType.Type); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(404, responseType.StatusCode); + Assert.Equal(errorType, responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(415, responseType.StatusCode); + Assert.Equal(errorType, responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + + [Fact] + public void GetApiResponseTypes_UsesErrorType_ForDefaultResponse() + { + // Arrange + var errorType = typeof(ProblemDetails); + var actionDescriptor = GetControllerActionDescriptor( + typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController), + nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + new ProducesDefaultResponseTypeAttribute(), + }); + + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(errorType, responseType.Type); + Assert.True(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(BaseModel), responseType.Type); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + + [Fact] + public void GetApiResponseTypes_DoesNotUseErrorType_IfSpecified() + { + // Arrange + var errorType = typeof(InvalidTimeZoneException); + var actionDescriptor = GetControllerActionDescriptor( + typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController), + nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + new ProducesResponseTypeAttribute(typeof(DivideByZeroException), 415), + new ProducesDefaultResponseTypeAttribute(typeof(DivideByZeroException)), + }); + + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(typeof(DivideByZeroException), responseType.Type); + Assert.True(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(BaseModel), responseType.Type); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(415, responseType.StatusCode); + Assert.Equal(typeof(DivideByZeroException), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + + [Fact] + public void GetApiResponseTypes_DoesNotUseErrorType_ForNonClientErrors() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor( + typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController), + nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(201), + new ProducesResponseTypeAttribute(300), + new ProducesResponseTypeAttribute(500), + }); + + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(InvalidTimeZoneException)); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(201, responseType.StatusCode); + Assert.Equal(typeof(BaseModel), responseType.Type); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(300, responseType.StatusCode); + Assert.Equal(typeof(void), responseType.Type); + Assert.Empty(responseType.ApiResponseFormats); + }, + responseType => + { + Assert.Equal(500, responseType.StatusCode); + Assert.Equal(typeof(void), responseType.Type); + Assert.Empty(responseType.ApiResponseFormats); + }); + } + + [Fact] + public void GetApiResponseTypes_AllowsUsingVoid() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor( + typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController), + nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(typeof(InvalidCastException), 400), + new ProducesResponseTypeAttribute(415), + new ProducesDefaultResponseTypeAttribute(), + }); + + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(void)); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.True(responseType.IsDefaultResponse); + Assert.Equal(typeof(void), responseType.Type); + Assert.Empty(responseType.ApiResponseFormats); + }, + responseType => + { + Assert.Equal(400, responseType.StatusCode); + Assert.Equal(typeof(InvalidCastException), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(415, responseType.StatusCode); + Assert.Equal(typeof(void), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Empty(responseType.ApiResponseFormats); + }); + } + private static ApiResponseTypeProvider GetProvider() { var mvcOptions = new MvcOptions diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs index c0c415573f..c4104b9027 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Logging.Abstractions; @@ -20,6 +19,8 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; +[assembly: Microsoft.AspNetCore.Mvc.ProducesErrorResponseType(typeof(InvalidEnumArgumentException))] + namespace Microsoft.AspNetCore.Mvc.Internal { public class ApiBehaviorApplicationModelProviderTest @@ -1042,9 +1043,6 @@ Environment.NewLine + "int b"; var actionModel = new ActionModel( typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), Array.Empty()); - actionModel.Filters.Add(new AuthorizeFilter()); - actionModel.Filters.Add(new ServiceFilterAttribute(typeof(object))); - actionModel.Filters.Add(new ConsumesAttribute("application/xml")); var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; // Act @@ -1060,6 +1058,167 @@ Environment.NewLine + "int b"; }); } + [Fact] + public void DiscoverErrorResponseType_SetsProblemDetails_IfActionHasNoAttributes() + { + // Arrange + var expected = typeof(ProblemDetails); + var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() }); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + Array.Empty()) + { + Controller = controllerModel, + }; + var provider = GetProvider(); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Collection( + actionModel.Properties, + kvp => + { + Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.Equal(expected, value.Type); + }); + } + + [Fact] + public void DiscoverErrorResponseType_DoesNotSetDefaultProblemDetailsResponse_IfSuppressMapClientErrorsIsSet() + { + // Arrange + var expected = typeof(ProblemDetails); + var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() }); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + Array.Empty()) + { + Controller = controllerModel, + }; + var provider = GetProvider(new ApiBehaviorOptions + { + InvalidModelStateResponseFactory = _ => null, + SuppressMapClientErrors = true, + }); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Empty(actionModel.Properties); + } + + [Fact] + public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnControllerAsssembly() + { + // Arrange + var expected = typeof(InvalidEnumArgumentException); + var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new object() }); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + Array.Empty()) + { + Controller = controllerModel, + }; + var provider = GetProvider(); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Collection( + actionModel.Properties, + kvp => + { + Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.Equal(expected, value.Type); + }); + } + + [Fact] + public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnController() + { + // Arrange + var expected = typeof(InvalidTimeZoneException); + var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(expected) }); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + Array.Empty()) + { + Controller = controllerModel, + }; + var provider = GetProvider(); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Collection( + actionModel.Properties, + kvp => + { + Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.Equal(expected, value.Type); + }); + } + + [Fact] + public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnAction() + { + // Arrange + var expected = typeof(InvalidTimeZoneException); + var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(typeof(Guid)) }); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + new[] { new ProducesErrorResponseTypeAttribute(expected) }) + { + Controller = controllerModel, + }; + var provider = GetProvider(); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Collection( + actionModel.Properties, + kvp => + { + Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.Equal(expected, value.Type); + }); + } + + [Fact] + public void DiscoverErrorResponseType_AllowsVoidsType() + { + // Arrange + var expected = typeof(void); + var actionModel = new ActionModel( + typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), + new[] { new ProducesErrorResponseTypeAttribute(expected) }); + var provider = GetProvider(); + + // Act + provider.DiscoverErrorResponseType(actionModel); + + // Assert + Assert.Collection( + actionModel.Properties, + kvp => + { + Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.Equal(expected, value.Type); + }); + } + [Fact] public void OnProvidersExecuting_AddsClientErrorResultFilter() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs index d216ad4aff..6230542cb0 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -1180,9 +1180,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(404, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } @@ -1215,7 +1215,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task ApiConvention_ForMethodWithResponseTypeAttributes() { // Arrange - var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + var expectedMediaTypes = new[] { "application/json" }; // Act var response = await Client.PostAsync( @@ -1236,15 +1236,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(403, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } [Fact] public async Task ApiConvention_ForPostMethodThatMatchesConvention() { + // Arrange + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + // Act var response = await Client.PostAsync( $"ApiExplorerResponseTypeWithApiConventionController/PostTaskOfProduct", @@ -1268,15 +1271,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(400, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } [Fact] public async Task ApiConvention_ForPutActionThatMatchesConvention() { + // Arrange + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + // Act var response = await Client.PutAsync( $"ApiExplorerResponseTypeWithApiConventionController/Put", @@ -1300,21 +1306,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(400, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(404, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } [Fact] public async Task ApiConvention_ForDeleteActionThatMatchesConvention() { + // Arrange + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + // Act var response = await Client.DeleteAsync( $"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync"); @@ -1337,21 +1346,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(400, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(404, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); } [Fact] public async Task ApiConvention_ForActionWtihApiConventionMethod() { + // Arrange + var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; + // Act var response = await Client.PostAsync( "ApiExplorerResponseTypeWithApiConventionController/PostItem", @@ -1371,9 +1383,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }, responseType => { - Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); Assert.Equal(409, responseType.StatusCode); - Assert.Empty(responseType.ResponseFormats); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); }); }