// 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 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 ReflectedActionDescriptor(); // 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().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.MethodConstraints = new List() { 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() { new ParameterDescriptor() { Name = "id", IsOptional = true, ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)), }, new ParameterDescriptor() { Name = "username", BodyParameterInfo = new BodyParameterInfo(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); } // This is a placeholder based on current functionality - see #885 [Fact] public void GetApiDescription_PopluatesRelativePath() { // Arrange var action = CreateActionDescriptor(); action.AttributeRouteInfo = new AttributeRouteInfo(); action.AttributeRouteInfo.Template = "api/Products/{id}"; // Act var descriptions = GetApiDescriptions(action); // Assert var description = Assert.Single(descriptions); Assert.Equal("api/Products/{id}", description.RelativePath); } [Fact] public void GetApiDescription_PopluatesResponseType_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_PopluatesResponseType_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_DoesNotPopluatesResponseInformation_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_DoesNotPopluatesResponseInformation_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_PopluatesResponseInformation_WhenSetByFilter(string methodName) { // Arrange var action = CreateActionDescriptor(methodName); var filter = new ContentTypeAttribute("text/*") { Type = typeof(Order) }; action.FilterDescriptors = new List(); 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(); 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(); 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 GetApiDescriptions(ActionDescriptor action) { return GetApiDescriptions(action, CreateFormatters()); } private IReadOnlyList GetApiDescriptions(ActionDescriptor action, List formatters) { var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action }); var formattersProvider = new Mock(MockBehavior.Strict); formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters); var modelMetadataProvider = new Mock(MockBehavior.Strict); modelMetadataProvider .Setup(mmp => mmp.GetMetadataForType(null, It.IsAny())) .Returns((Func accessor, Type type) => { return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null); }); var provider = new DefaultApiDescriptionProvider(formattersProvider.Object, modelMetadataProvider.Object); provider.Invoke(context, () => { }); return context.Results; } private List CreateFormatters() { // Include some default formatters that look reasonable, some tests will override this. var formatters = new List() { 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 ReflectedActionDescriptor CreateActionDescriptor(string methodName = null) { var action = new ReflectedActionDescriptor(); 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 ReturnsTaskOfProduct() { return null; } private Task ReturnsTaskOfObject() { return null; } private Task ReturnsTask() { return null; } private Task ReturnsTaskOfActionResult() { return null; } private Task ReturnsTaskOfJsonResult() { return null; } private Product ReturnsProduct() { return null; } private class Product { } private class Order { } private class MockFormatter : OutputFormatter { public List SupportedTypes { get; } = new List(); 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 ContentTypes { get; } = new List(); public Type Type { get; set; } public void SetContentTypes(IList contentTypes) { contentTypes.Clear(); foreach (var contentType in ContentTypes) { contentTypes.Add(contentType); } } } } }