aspnetcore/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProvid...

765 lines
28 KiB
C#

// 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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Routing.Constraints;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Description
{
public class DefaultApiDescriptionProviderTest
{
[Fact]
public void GetApiDescription_IgnoresNonReflectedActionDescriptor()
{
// Arrange
var action = new ActionDescriptor();
action.SetProperty(new ApiDescriptionActionData());
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Empty(descriptions);
}
[Fact]
public void GetApiDescription_IgnoresActionWithoutApiExplorerData()
{
// Arrange
var action = new ControllerActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Empty(descriptions);
}
[Fact]
public void GetApiDescription_PopulatesActionDescriptor()
{
// Arrange
var action = CreateActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Same(action, description.ActionDescriptor);
}
[Fact]
public void GetApiDescription_PopulatesGroupName()
{
// Arrange
var action = CreateActionDescriptor();
action.GetProperty<ApiDescriptionActionData>().GroupName = "Customers";
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal("Customers", description.GroupName);
}
[Fact]
public void GetApiDescription_HttpMethodIsNullWithoutConstraint()
{
// Arrange
var action = CreateActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Null(description.HttpMethod);
}
[Fact]
public void GetApiDescription_CreatesMultipleDescriptionsForMultipleHttpMethods()
{
// Arrange
var action = CreateActionDescriptor();
action.ActionConstraints = new List<IActionConstraintMetadata>()
{
new HttpMethodConstraint(new string[] { "PUT", "POST" }),
new HttpMethodConstraint(new string[] { "GET" }),
};
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Equal(3, descriptions.Count);
Assert.Single(descriptions, d => d.HttpMethod == "PUT");
Assert.Single(descriptions, d => d.HttpMethod == "POST");
Assert.Single(descriptions, d => d.HttpMethod == "GET");
}
// This is a test for the placeholder behavior - see #886
[Fact]
public void GetApiDescription_PopulatesParameters()
{
// Arrange
var action = CreateActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
IsOptional = true,
ParameterType = typeof(int),
},
new ParameterDescriptor()
{
BinderMetadata = new FromBodyAttribute(),
Name = "username",
ParameterType = typeof(string),
}
};
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(2, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id");
Assert.NotNull(id.ModelMetadata);
Assert.True(id.IsOptional);
Assert.Same(action.Parameters[0], id.ParameterDescriptor);
Assert.Equal(ApiParameterSource.Query, id.Source);
Assert.Equal(typeof(int), id.Type);
var username = Assert.Single(description.ParameterDescriptions, p => p.Name == "username");
Assert.NotNull(username.ModelMetadata);
Assert.False(username.IsOptional);
Assert.Same(action.Parameters[1], username.ParameterDescriptor);
Assert.Equal(ApiParameterSource.Body, username.Source);
Assert.Equal(typeof(string), username.Type);
}
[Theory]
[InlineData("api/products/{id}", false, null, null)]
[InlineData("api/products/{id?}", true, null, null)]
[InlineData("api/products/{id=5}", true, null, "5")]
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int=5}", true, null, "5")]
[InlineData("api/products/{*id}", false, null, null)]
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
public void GetApiDescription_PopulatesParameters_ThatAppearOnlyOnRouteTemplate(
string template,
bool isOptional,
Type constraintType,
object defaultValue)
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.IsOptional);
Assert.Equal("id", parameter.Name);
Assert.Null(parameter.ParameterDescriptor);
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(parameter.Constraints));
}
if (defaultValue != null)
{
Assert.Equal(defaultValue, parameter.DefaultValue);
}
else
{
Assert.Null(parameter.DefaultValue);
}
}
[Theory]
[InlineData("api/products/{id}", false, null, null)]
[InlineData("api/products/{id?}", true, null, null)]
[InlineData("api/products/{id=5}", true, null, "5")]
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")]
[InlineData("api/products/{*id}", false, null, null)]
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
public void GetApiDescription_PopulatesParametersThatAppearOnRouteTemplate_AndHaveAssociatedParameterDescriptor(
string template,
bool isOptional,
Type constraintType,
object defaultValue)
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
Name = "id",
IsOptional = true,
ParameterType = typeof(int),
};
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.IsOptional);
Assert.Equal("id", parameter.Name);
Assert.Equal(parameterDescriptor, parameter.ParameterDescriptor);
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(parameter.Constraints));
}
if (defaultValue != null)
{
Assert.Equal(defaultValue, parameter.DefaultValue);
}
else
{
Assert.Null(parameter.DefaultValue);
}
}
[Theory]
[InlineData("api/products/{id}", false, null, null)]
[InlineData("api/products/{id?}", true, null, null)]
[InlineData("api/products/{id=5}", true, null, "5")]
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")]
[InlineData("api/products/{*id}", false, null, null)]
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
public void GetApiDescription_CreatesDifferentParameters_IfParameterDescriptorIsFromBody(
string template,
bool isOptional,
Type constraintType,
object defaultValue)
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
BinderMetadata = new FromBodyAttribute(),
Name = "id",
IsOptional = false,
ParameterType = typeof(int),
};
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var bodyParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Body);
Assert.False(bodyParameter.IsOptional);
Assert.Equal("id", bodyParameter.Name);
Assert.Equal(parameterDescriptor, bodyParameter.ParameterDescriptor);
var pathParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Path);
Assert.Equal(isOptional, pathParameter.IsOptional);
Assert.Equal("id", pathParameter.Name);
Assert.Null(pathParameter.ParameterDescriptor);
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(pathParameter.Constraints));
}
if (defaultValue != null)
{
Assert.Equal(defaultValue, pathParameter.DefaultValue);
}
else
{
Assert.Null(pathParameter.DefaultValue);
}
}
[Theory]
[InlineData("api/products/{id}", false, false)]
[InlineData("api/products/{id}", true, false)]
[InlineData("api/products/{id?}", false, false)]
[InlineData("api/products/{id?}", true, true)]
[InlineData("api/products/{id=5}", false, false)]
[InlineData("api/products/{id=5}", true, true)]
public void GetApiDescription_ParameterFromPathAndDescriptor_IsOptionalOnly_IfBothAreOptional(
string template,
bool isDescriptorParameterOptional,
bool expectedOptional)
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
Name = "id",
IsOptional = isDescriptorParameterOptional,
ParameterType = typeof(int),
};
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(expectedOptional, parameter.IsOptional);
}
[Theory]
[InlineData("api/Products/{id}", "api/Products/{id}")]
[InlineData("api/Products/{id?}", "api/Products/{id}")]
[InlineData("api/Products/{id:int}", "api/Products/{id}")]
[InlineData("api/Products/{id:int?}", "api/Products/{id}")]
[InlineData("api/Products/{*id}", "api/Products/{id}")]
[InlineData("api/Products/{*id:int}", "api/Products/{id}")]
[InlineData("api/Products/{id1}-{id2:int}", "api/Products/{id1}-{id2}")]
[InlineData("api/{id1}/{id2?}/{id3:int}/{id4:int?}/{*id5:int}", "api/{id1}/{id2}/{id3}/{id4}/{id5}")]
public void GetApiDescription_PopulatesRelativePath(string template, string relativePath)
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo();
action.AttributeRouteInfo.Template = template;
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(relativePath, description.RelativePath);
}
[Fact]
public void GetApiDescription_DetectsMultipleParameters_OnTheSameSegment()
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo();
action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2:int}";
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
Assert.Equal(ApiParameterSource.Path, id1.Source);
Assert.Empty(id1.Constraints);
var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
Assert.Equal(ApiParameterSource.Path, id2.Source);
Assert.IsType<IntRouteConstraint>(Assert.Single(id2.Constraints));
}
[Fact]
public void GetApiDescription_DetectsMultipleParameters_OnDifferentSegments()
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo();
action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2}/{id3:int}/{id4:int?}/{*id5:int}";
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
Assert.Single(description.ParameterDescriptions, p => p.Name == "id3");
Assert.Single(description.ParameterDescriptions, p => p.Name == "id4");
Assert.Single(description.ParameterDescriptions, p => p.Name == "id5");
}
[Fact]
public void GetApiDescription_PopulatesResponseType_WithProduct()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Product), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Fact]
public void GetApiDescription_PopulatesResponseType_WithTaskOfProduct()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Product), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Theory]
[InlineData(nameof(ReturnsObject))]
[InlineData(nameof(ReturnsActionResult))]
[InlineData(nameof(ReturnsJsonResult))]
[InlineData(nameof(ReturnsTaskOfObject))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenUnknown(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Null(description.ResponseType);
Assert.Null(description.ResponseModelMetadata);
Assert.Empty(description.SupportedResponseFormats);
}
[Theory]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsTask))]
public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenVoid(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(void), description.ResponseType);
Assert.Null(description.ResponseModelMetadata);
Assert.Empty(description.SupportedResponseFormats);
}
[Theory]
[InlineData(nameof(ReturnsObject))]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsActionResult))]
[InlineData(nameof(ReturnsJsonResult))]
[InlineData(nameof(ReturnsTaskOfObject))]
[InlineData(nameof(ReturnsTask))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
var filter = new ContentTypeAttribute("text/*")
{
Type = typeof(Order)
};
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Order), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Fact]
public void GetApiDescription_IncludesResponseFormats()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(4, description.SupportedResponseFormats.Count);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Single(formats, f => f.MediaType.RawValue == "application/json");
Assert.Single(formats, f => f.MediaType.RawValue == "text/xml");
Assert.Single(formats, f => f.MediaType.RawValue == "application/xml");
}
[Fact]
public void GetApiDescription_IncludesResponseFormats_FilteredByAttribute()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(new ContentTypeAttribute("text/*"), FilterScope.Action));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(2, description.SupportedResponseFormats.Count);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Single(formats, f => f.MediaType.RawValue == "text/xml");
}
[Fact]
public void GetApiDescription_IncludesResponseFormats_FilteredByType()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsObject));
var filter = new ContentTypeAttribute("text/*")
{
Type = typeof(Order)
};
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
var formatters = CreateFormatters();
// This will just format Order
formatters[0].SupportedTypes.Add(typeof(Order));
// This will just format Product
formatters[1].SupportedTypes.Add(typeof(Product));
// Act
var descriptions = GetApiDescriptions(action, formatters);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(1, description.SupportedResponseFormats.Count);
Assert.Equal(typeof(Order), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Same(formatters[0], formats[0].Formatter);
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action)
{
return GetApiDescriptions(action, CreateFormatters());
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(
ActionDescriptor action,
List<MockFormatter> formatters)
{
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
var formattersProvider = new Mock<IOutputFormattersProvider>(MockBehavior.Strict);
formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters);
var constraintResolver = new Mock<IInlineConstraintResolver>();
constraintResolver.Setup(c => c.ResolveConstraint("int"))
.Returns(new IntRouteConstraint());
var modelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
modelMetadataProvider
.Setup(mmp => mmp.GetMetadataForType(null, It.IsAny<Type>()))
.Returns((Func<object> accessor, Type type) =>
{
return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null);
});
var provider = new DefaultApiDescriptionProvider(
formattersProvider.Object,
constraintResolver.Object,
modelMetadataProvider.Object);
provider.Invoke(context, () => { });
return context.Results;
}
private List<MockFormatter> CreateFormatters()
{
// Include some default formatters that look reasonable, some tests will override this.
var formatters = new List<MockFormatter>()
{
new MockFormatter(),
new MockFormatter(),
};
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json"));
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
return formatters;
}
private ControllerActionDescriptor CreateActionDescriptor(string methodName = null)
{
var action = new ControllerActionDescriptor();
action.SetProperty(new ApiDescriptionActionData());
action.MethodInfo = GetType().GetMethod(
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
return action;
}
private object ReturnsObject()
{
return null;
}
private void ReturnsVoid()
{
}
private IActionResult ReturnsActionResult()
{
return null;
}
private JsonResult ReturnsJsonResult()
{
return null;
}
private Task<Product> ReturnsTaskOfProduct()
{
return null;
}
private Task<object> ReturnsTaskOfObject()
{
return null;
}
private Task ReturnsTask()
{
return null;
}
private Task<IActionResult> ReturnsTaskOfActionResult()
{
return null;
}
private Task<JsonResult> ReturnsTaskOfJsonResult()
{
return null;
}
private Product ReturnsProduct()
{
return null;
}
private class Product
{
}
private class Order
{
}
private class MockFormatter : OutputFormatter
{
public List<Type> SupportedTypes { get; } = new List<Type>();
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
{
throw new NotImplementedException();
}
protected override bool CanWriteType(Type declaredType, Type actualType)
{
if (SupportedTypes.Count == 0)
{
return true;
}
else if ((actualType ?? declaredType) == null)
{
return false;
}
else
{
return SupportedTypes.Contains(actualType ?? declaredType);
}
}
}
private class ContentTypeAttribute : Attribute, IFilter, IApiResponseMetadataProvider
{
public ContentTypeAttribute(string mediaType)
{
ContentTypes.Add(MediaTypeHeaderValue.Parse(mediaType));
}
public List<MediaTypeHeaderValue> ContentTypes { get; } = new List<MediaTypeHeaderValue>();
public Type Type { get; set; }
public void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes)
{
contentTypes.Clear();
foreach (var contentType in ContentTypes)
{
contentTypes.Add(contentType);
}
}
}
}
}