// 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.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Analyzer.Testing; using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Xunit; namespace Microsoft.AspNetCore.Mvc.Analyzers { public class SymbolApiResponseMetadataProviderTest { private static readonly string Namespace = typeof(SymbolApiResponseMetadataProviderTest).Namespace; [Fact] public async Task GetResponseMetadata_ReturnsEmptySequence_IfNoAttributesArePresent_ForGetAction() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.GetPerson)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => Assert.True(metadata.IsImplicit)); } [Fact] public async Task GetResponseMetadata_ReturnsEmptySequence_IfNoAttributesArePresent_ForPostAction() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.PostPerson)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => Assert.True(metadata.IsImplicit)); } [Fact] public async Task GetResponseMetadata_IgnoresProducesAttribute() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesAttribute)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => Assert.True(metadata.IsImplicit)); } [Fact] public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructor() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Equal(method, metadata.AttributeSource); }); } [Fact] public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructorWithResponseType() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(202, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Equal(method, metadata.AttributeSource); }); } [Fact] public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructorAndProperty() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructorAndProperty)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(203, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Equal(method, metadata.AttributeSource); }); } [Fact] public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeAndTypeIsSpecifiedInConstructorAndProperty() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Equal(method, metadata.AttributeSource); }); } [Fact] public async Task GetResponseMetadata_ReturnsValueFromCustomProducesResponseType() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }); } [Fact] public async Task GetResponseMetadata_ReturnsValuesFromApiConventionMethodAttribute() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_ReturnsValuesFromApiConventionMethodAttribute)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(200, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }, metadata => { Assert.Equal(404, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }, metadata => { Assert.True(metadata.IsDefault); }); } [Fact] public async Task GetResponseMetadata_WIthProducesResponseTypeAndApiConventionMethod() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_WIthProducesResponseTypeAndApiConventionMethod)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(204, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }); } [Fact] public async Task GetResponseMetadata_IgnoresCustomResponseTypeMetadataProvider() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomApiResponseMetadataProvider)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => Assert.True(metadata.IsImplicit)); } [Fact] public Task GetResponseMetadata_IgnoresAttributesWithIncorrectStatusCodeType() { return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues( nameof(GetResponseMetadata_ControllerActionWithAttributes), nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType)); } [Fact] public Task GetResponseMetadata_IgnoresDerivedAttributesWithoutPropertyOnConstructorArguments() { return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues( nameof(GetResponseMetadata_ControllerActionWithAttributes), nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithoutArguments)); } private async Task GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues(string typeName, string methodName) { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{typeName}"); var method = (IMethodSymbol)controller.GetMembers(methodName).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( result, metadata => { Assert.Equal(200, metadata.StatusCode); Assert.Same(method, metadata.AttributeSource); }); } [Fact] public Task GetStatusCode_ReturnsValueFromConstructor() { // Arrange var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor); var expected = 201; // Act & Assert return GetStatusCodeTest(actionName, expected); } [Fact] public Task GetStatusCode_ReturnsValueFromProperty() { // Arrange var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty); var expected = 201; // Act & Assert return GetStatusCodeTest(actionName, expected); } [Fact] public Task GetStatusCode_ReturnsValueFromConstructor_WhenTypeIsSpecified() { // Arrange var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor); var expected = 202; // Act & Assert return GetStatusCodeTest(actionName, expected); } [Fact] public Task GetStatusCode_Returns200_IfTypeIsNotInteger() { // Arrange var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType); var expected = 200; // Act & Assert return GetStatusCodeTest(actionName, expected); } [Fact] public Task GetStatusCode_ReturnsValueFromDerivedAttributes() { // Arrange var actionName = nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments); var expected = 201; // Act & Assert return GetStatusCodeTest(actionName, expected); } private async Task GetStatusCodeTest(string actionName, int expected) { var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(actionName).First(); var attribute = method.GetAttributes().First(); var statusCode = SymbolApiResponseMetadataProvider.GetStatusCode(attribute); Assert.Equal(expected, statusCode); } [Fact] public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingStatusCodeConstants() { // Arrange var compilation = await GetCompilation("GetDefaultStatusCodeTest"); var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingStatusCodesConstants).FullName).GetAttributes()[0]; // Act var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute); // Assert Assert.Equal(412, actual); } [Fact] public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingHttpStatusCast() { // Arrange var compilation = await GetCompilation("GetDefaultStatusCodeTest"); var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingHttpStatusCodeCast).FullName).GetAttributes()[0]; // Act var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute); // Assert Assert.Equal(302, actual); } [Fact] public async Task InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound() { // Arrange & Act var source = @" using Microsoft.AspNetCore.Mvc; namespace Microsoft.AspNetCore.Mvc.Analyzers { [ApiController] public class InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound : ControllerBase { public IActionResult Get(int id) { return new DoesNotExist(id); } } }"; var actualResponseMetadata = await RunInspectReturnStatementSyntax(source, nameof(InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound)); // Assert Assert.Null(actualResponseMetadata); } [Fact] public async Task InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult() { // Arrange & Act var actualResponseMetadata = await RunInspectReturnStatementSyntax(); // Assert Assert.NotNull(actualResponseMetadata); Assert.Equal(401, actualResponseMetadata.Value.StatusCode); } [Fact] public async Task InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult() { // Arrange & Act var actualResponseMetadata = await RunInspectReturnStatementSyntax(); // Assert Assert.NotNull(actualResponseMetadata); Assert.True(actualResponseMetadata.Value.IsDefaultResponse); } private async Task RunInspectReturnStatementSyntax([CallerMemberName]string test = null) { // Arrange var testSource = MvcTestSource.Read(GetType().Name, test); return await RunInspectReturnStatementSyntax(testSource.Source, test); } private async Task RunInspectReturnStatementSyntax(string source, string test) { var project = DiagnosticProject.Create(GetType().Assembly, new[] { source }); var compilation = await project.GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}"); var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree; var method = (IMethodSymbol)returnType.GetMembers().First(); var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); var returnStatement = methodSyntax.DescendantNodes().OfType().First(); return SymbolApiResponseMetadataProvider.InspectReturnStatementSyntax( symbolCache, compilation.GetSemanticModel(syntaxTree), returnStatement, CancellationToken.None); } private Task GetResponseMetadataCompilation() => GetCompilation("GetResponseMetadataTests"); private Task GetCompilation(string test) { var testSource = MvcTestSource.Read(GetType().Name, test); var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); return project.GetCompilationAsync(); } } }