File: Integration\OpenApiDocumentIntegrationTests.cs
Web Access
Project: src\src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests\Microsoft.AspNetCore.OpenApi.Tests.csproj (Microsoft.AspNetCore.OpenApi.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Reader;
 
[UsesVerify]
public sealed class OpenApiDocumentIntegrationTests(SampleAppFixture fixture) : IClassFixture<SampleAppFixture>
{
    public static TheoryData<string, OpenApiSpecVersion> OpenApiDocuments()
    {
        OpenApiSpecVersion[] versions =
        [
            OpenApiSpecVersion.OpenApi3_0,
            OpenApiSpecVersion.OpenApi3_1,
        ];
 
        var testCases = new TheoryData<string, OpenApiSpecVersion>();
 
        foreach (var version in versions)
        {
            testCases.Add("v1", version);
            testCases.Add("v2", version);
            testCases.Add("controllers", version);
            testCases.Add("responses", version);
            testCases.Add("forms", version);
            testCases.Add("schemas-by-ref", version);
            testCases.Add("xml", version);
        }
 
        return testCases;
    }
 
    [Theory]
    [MemberData(nameof(OpenApiDocuments))]
    public async Task VerifyOpenApiDocument(string documentName, OpenApiSpecVersion version)
    {
        var json = await GetOpenApiDocument(documentName, version);
        var baseSnapshotsDirectory = SkipOnHelixAttribute.OnHelix()
            ? Path.Combine(Environment.GetEnvironmentVariable("HELIX_WORKITEM_ROOT"), "Integration", "snapshots")
            : "snapshots";
        var outputDirectory = Path.Combine(baseSnapshotsDirectory, version.ToString());
        await Verify(json)
            .UseDirectory(outputDirectory)
            .UseParameters(documentName);
    }
 
    [Theory]
    [MemberData(nameof(OpenApiDocuments))]
    public async Task OpenApiDocumentIsValid(string documentName, OpenApiSpecVersion version)
    {
        var json = await GetOpenApiDocument(documentName, version);
 
        var actual = OpenApiDocument.Parse(json, format: "json");
 
        Assert.NotNull(actual);
        Assert.NotNull(actual.Document);
        Assert.NotNull(actual.Diagnostic);
        Assert.NotNull(actual.Diagnostic.Errors);
        Assert.Empty(actual.Diagnostic.Errors);
 
        var ruleSet = ValidationRuleSet.GetDefaultRuleSet();
 
        var errors = actual.Document.Validate(ruleSet);
        Assert.Empty(errors);
    }
 
    [Theory] // See https://github.com/dotnet/aspnetcore/issues/63090
    [MemberData(nameof(OpenApiDocuments))]
    public async Task OpenApiDocumentReferencesAreValid(string documentName, OpenApiSpecVersion version)
    {
        var json = await GetOpenApiDocument(documentName, version);
 
        var result = OpenApiDocument.Parse(json, format: "json");
 
        var document = result.Document;
        var documentNode = JsonNode.Parse(json);
 
        var ruleName = "OpenApiDocumentReferencesAreValid";
        var rule = new ValidationRule<OpenApiDocument>(ruleName, (context, item) =>
        {
            var visitor = new OpenApiSchemaReferenceVisitor(ruleName, context, documentNode);
 
            var walker = new OpenApiWalker(visitor);
            walker.Walk(item);
        });
 
        var ruleSet = new ValidationRuleSet();
        ruleSet.Add(typeof(OpenApiDocument), rule);
 
        var errors = document.Validate(ruleSet);
 
        Assert.Empty(errors);
    }
 
    private async Task<string> GetOpenApiDocument(string documentName, OpenApiSpecVersion version)
    {
        var documentService = fixture.Services.GetRequiredKeyedService<OpenApiDocumentService>(documentName);
        var scopedServiceProvider = fixture.Services.CreateScope();
        var document = await documentService.GetOpenApiDocumentAsync(scopedServiceProvider.ServiceProvider);
 
        return await document.SerializeAsJsonAsync(version);
    }
 
    private sealed class OpenApiSchemaReferenceVisitor(
        string ruleName,
        IValidationContext context,
        JsonNode document) : OpenApiVisitorBase
    {
        public override void Visit(IOpenApiReferenceHolder referenceHolder)
        {
            if (referenceHolder is OpenApiSchemaReference { Reference.IsLocal: true } reference)
            {
                ValidateSchemaReference(reference);
            }
        }
 
        public override void Visit(IOpenApiSchema schema)
        {
            if (schema is OpenApiSchemaReference { Reference.IsLocal: true } reference)
            {
                ValidateSchemaReference(reference);
            }
        }
 
        private void ValidateSchemaReference(OpenApiSchemaReference reference)
        {
            try
            {
                if (reference.RecursiveTarget is not null)
                {
                    return;
                }
            }
            catch (InvalidOperationException ex)
            {
                // Thrown if a circular reference is detected
                context.Enter($"{PathString[2..]}/{OpenApiSchemaKeywords.RefKeyword}");
                context.CreateError(ruleName, ex.Message);
                context.Exit();
 
                return;
            }
 
            var id = reference.Reference.ReferenceV3;
 
            if (id is { Length: > 0 } && !IsValidSchemaReference(id, document))
            {
                var isValid = false;
 
                // Sometimes ReferenceV3 is not a valid JSON pointer, but the $ref
                // associated with it still points to a valid location in the document.
                // In these cases, we need to find it manually to verify that fact before
                // generating a warning that the schema reference is indeed invalid.
                var parent = Find(PathString, document);
                var @ref = parent[OpenApiSchemaKeywords.RefKeyword];
                var path = PathString[2..]; // Trim off the leading "#/" as the context is already at the root
 
                if (@ref is not null && @ref.GetValueKind() is System.Text.Json.JsonValueKind.String &&
                    @ref.GetValue<string>() is { Length: > 0 } refId)
                {
                    id = refId;
                    path += $"/{OpenApiSchemaKeywords.RefKeyword}";
                    isValid = IsValidSchemaReference(id, document);
                }
 
                if (!isValid)
                {
                    context.Enter(path);
                    context.CreateWarning(ruleName, $"The schema reference '{id}' does not point to an existing schema.");
                    context.Exit();
                }
            }
 
            static bool IsValidSchemaReference(string id, JsonNode baseNode)
                => Find(id, baseNode) is not null;
 
            static JsonNode Find(string id, JsonNode baseNode)
            {
                var pointer = new JsonPointer(id.Replace("#/", "/"));
                return pointer.Find(baseNode);
            }
        }
    }
}