Allow Implicit 200 status codes to match Ok result
This commit is contained in:
parent
c73b13f544
commit
f7da3503d6
|
|
@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
internal readonly struct DeclaredApiResponseMetadata
|
internal readonly struct DeclaredApiResponseMetadata
|
||||||
{
|
{
|
||||||
public static DeclaredApiResponseMetadata ImplicitResponse { get; } =
|
public static DeclaredApiResponseMetadata ImplicitResponse { get; } =
|
||||||
new DeclaredApiResponseMetadata(statusCode: 0, attributeData: null, attributeSource: null, @implicit: true, @default: false);
|
new DeclaredApiResponseMetadata(statusCode: 200, attributeData: null, attributeSource: null, @implicit: true, @default: false);
|
||||||
|
|
||||||
public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource)
|
public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource)
|
||||||
{
|
{
|
||||||
|
|
@ -41,8 +41,16 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
|
||||||
public IMethodSymbol AttributeSource { get; }
|
public IMethodSymbol AttributeSource { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>True</c> if this <see cref="DeclaredApiResponseMetadata" /> is the implicit 200 associated with an
|
||||||
|
/// action specifying no metadata.
|
||||||
|
/// </summary>
|
||||||
public bool IsImplicit { get; }
|
public bool IsImplicit { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>True</c> if this <see cref="DeclaredApiResponseMetadata" /> is from a <c>ProducesDefaultResponseTypeAttribute</c>.
|
||||||
|
/// Matches all failure (400 and above) status codes.
|
||||||
|
/// </summary>
|
||||||
public bool IsDefault { get; }
|
public bool IsDefault { get; }
|
||||||
|
|
||||||
internal static bool Contains(IList<DeclaredApiResponseMetadata> declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
|
internal static bool Contains(IList<DeclaredApiResponseMetadata> declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
|
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
|
||||||
=> RunNoDiagnosticsAreReturned();
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public Task NoDiagnosticsAreReturned_ForOkResultReturningAction()
|
||||||
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
|
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
|
||||||
=> RunNoDiagnosticsAreReturned();
|
=> RunNoDiagnosticsAreReturned();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
{
|
||||||
|
public class DeclaredApiResponseMetadataTest
|
||||||
|
{
|
||||||
|
private readonly ReturnStatementSyntax ReturnStatement = SyntaxFactory.ReturnStatement();
|
||||||
|
private readonly AttributeData AttributeData = new TestAttributeData();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsImplicit_AndActualMetadataReturns200()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIs200_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(200, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <example>
|
||||||
|
/// [ProducesResponseType(201)]
|
||||||
|
/// public IActionResult SomeAction => new Model();
|
||||||
|
/// </example>
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIs201_AndActualMetadataIsDefault()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <example>
|
||||||
|
/// [ProducesResponseType(201)]
|
||||||
|
/// public IActionResult SomeAction => Ok(new Model());
|
||||||
|
/// </example>
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIs201_AndActualMetadataIs200()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataAndActualMetadataHaveSameStatusCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(302, AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(400)]
|
||||||
|
[InlineData(409)]
|
||||||
|
[InlineData(500)]
|
||||||
|
public void Matches_ReturnsTrue_IfDeclaredMetadataIsDefault_AndActualMetadataIsErrorStatusCode(int actualStatusCode)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsNotErrorStatusCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Matches_ReturnsFalse_IfDeclaredMetadataIsDefault_AndActualMetadataIsDefaultResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||||
|
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var matches = declaredMetadata.Matches(actualMetadata);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestAttributeData : AttributeData
|
||||||
|
{
|
||||||
|
protected override INamedTypeSymbol CommonAttributeClass => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override IMethodSymbol CommonAttributeConstructor => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override SyntaxReference CommonApplicationSyntaxReference => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override ImmutableArray<TypedConstant> CommonConstructorArguments => throw new System.NotImplementedException();
|
||||||
|
|
||||||
|
protected override ImmutableArray<KeyValuePair<string, TypedConstant>> CommonNamedArguments => throw new System.NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -433,6 +435,95 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResult()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResult);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.False(metadata.IsDefaultResponse);
|
||||||
|
Assert.Equal(200, metadata.StatusCode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningModel);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.True(metadata.IsDefaultResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||||
|
var methodName = nameof(TryGetActualResponseMetadataController.ActionReturningNotFoundAndModel);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var (success, responseMetadatas, testSource) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(success);
|
||||||
|
Assert.Collection(
|
||||||
|
responseMetadatas,
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.False(metadata.IsDefaultResponse);
|
||||||
|
Assert.Equal(204, metadata.StatusCode);
|
||||||
|
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation());
|
||||||
|
|
||||||
|
},
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.True(metadata.IsDefaultResponse);
|
||||||
|
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(bool result, IList<ActualApiResponseMetadata> responseMetadatas, TestSource testSource)> TryGetActualResponseMetadata(string typeName, string methodName)
|
||||||
|
{
|
||||||
|
var testSource = MvcTestSource.Read(GetType().Name, "TryGetActualResponseMetadataTests");
|
||||||
|
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||||
|
|
||||||
|
var compilation = await GetCompilation("TryGetActualResponseMetadataTests");
|
||||||
|
|
||||||
|
var type = compilation.GetTypeByMetadataName(typeName);
|
||||||
|
var method = (IMethodSymbol)type.GetMembers(methodName).First();
|
||||||
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
|
var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||||
|
var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||||
|
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||||
|
|
||||||
|
var result = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas);
|
||||||
|
|
||||||
|
return (result, responseMetadatas, testSource);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
public class NoDiagnosticsAreReturned_ForOkResultReturningAction : ControllerBase
|
||||||
|
{
|
||||||
|
public async Task<ActionResult<IEnumerable<NoDiagnosticsAreReturned_ForOkResultReturningAction>>> Action()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<NoDiagnosticsAreReturned_ForOkResultReturningActionModel>();
|
||||||
|
|
||||||
|
return Ok(models);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NoDiagnosticsAreReturned_ForOkResultReturningActionModel { }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||||
|
{
|
||||||
|
public class TryGetActualResponseMetadataController : ControllerBase
|
||||||
|
{
|
||||||
|
public async Task<ActionResult<IEnumerable<TryGetActualResponseMetadataModel>>> ActionWithActionResultOfTReturningOkResult()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<TryGetActualResponseMetadataModel>();
|
||||||
|
|
||||||
|
return Ok(models);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ActionResult<IEnumerable<TryGetActualResponseMetadataModel>>> ActionWithActionResultOfTReturningModel()
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
var models = new List<TryGetActualResponseMetadataModel>();
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ActionResult<TryGetActualResponseMetadataModel>> ActionReturningNotFoundAndModel(int id)
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
if (id == 0)
|
||||||
|
{
|
||||||
|
/*MM1*/return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*MM2*/return new TryGetActualResponseMetadataModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TryGetActualResponseMetadataModel { }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue