File: Services\OpenApiDocumentServiceTestsBase.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.Reflection;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Microsoft.OpenApi.Models;
using Moq;
using Xunit.Sdk;
using static Microsoft.AspNetCore.OpenApi.Tests.OpenApiOperationGeneratorTests;
 
public abstract class OpenApiDocumentServiceTestBase
{
    public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
        => await VerifyOpenApiDocument(builder, new OpenApiOptions(), verifyOpenApiDocument, cancellationToken);
 
    public static async Task VerifyOpenApiDocument(IEndpointRouteBuilder builder, OpenApiOptions openApiOptions, Action<OpenApiDocument> verifyOpenApiDocument, CancellationToken cancellationToken = default)
    {
        var documentService = CreateDocumentService(builder, openApiOptions);
        var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider, cancellationToken);
        verifyOpenApiDocument(document);
    }
 
    public static async Task VerifyOpenApiDocument(ActionDescriptor action, Action<OpenApiDocument> verifyOpenApiDocument)
    {
        var builder = CreateBuilder();
        var documentService = CreateDocumentService(builder, action);
        var scopedService = ((TestServiceProvider)builder.ServiceProvider).CreateScope();
        var document = await documentService.GetOpenApiDocumentAsync(scopedService.ServiceProvider);
        verifyOpenApiDocument(document);
    }
 
    internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuilder builder, ActionDescriptor actionDescriptor)
    {
        var context = new ApiDescriptionProviderContext([actionDescriptor]);
        var hostEnvironment = new HostEnvironment
        {
            ApplicationName = nameof(OpenApiDocumentServiceTests)
        };
 
        var options = new MvcOptions
        {
            OutputFormatters = { new TestJsonOutputFormatter() }
        };
        var optionsAccessor = Options.Create(options);
 
        var constraintResolver = new Mock<IInlineConstraintResolver>();
        constraintResolver.Setup(c => c.ResolveConstraint("int"))
            .Returns(new IntRouteConstraint());
 
        var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
 
        var routeOptions = new RouteOptions();
        routeOptions.ConstraintMap["regex"] = typeof(RegexInlineRouteConstraint);
 
        var provider = new DefaultApiDescriptionProvider(
            optionsAccessor,
            constraintResolver.Object,
            modelMetadataProvider,
            new ActionResultTypeMapper(),
            Options.Create(routeOptions));
        provider.OnProvidersExecuting(context);
        provider.OnProvidersExecuted(context);
 
        var apiDescriptionGroupCollectionProvider = CreateApiDescriptionGroupCollectionProvider(context.Results);
 
        var openApiOptions = new Mock<IOptionsMonitor<OpenApiOptions>>();
        openApiOptions.Setup(o => o.Get(It.IsAny<string>())).Returns(new OpenApiOptions());
 
        var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, openApiOptions.Object);
        ((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
        var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider, new OpenApiTestServer());
        ((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
 
        return documentService;
    }
 
    private class TestJsonOutputFormatter : TextOutputFormatter
    {
        public TestJsonOutputFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
 
            SupportedEncodings.Add(Encoding.UTF8);
        }
 
        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            return Task.FromResult(0);
        }
    }
 
    internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuilder builder, OpenApiOptions openApiOptions)
    {
        var context = new ApiDescriptionProviderContext([]);
 
        var endpointDataSource = builder.DataSources.OfType<EndpointDataSource>().Single();
        var hostEnvironment = new HostEnvironment
        {
            ApplicationName = nameof(OpenApiDocumentServiceTests)
        };
        var options = new Mock<IOptionsMonitor<OpenApiOptions>>();
        options.Setup(o => o.Get(It.IsAny<string>())).Returns(openApiOptions);
 
        var provider = CreateEndpointMetadataApiDescriptionProvider(endpointDataSource);
        provider.OnProvidersExecuting(context);
        provider.OnProvidersExecuted(context);
 
        var apiDescriptionGroupCollectionProvider = CreateApiDescriptionGroupCollectionProvider(context.Results);
        var jsonOptions = builder.ServiceProvider.GetService<IOptions<Microsoft.AspNetCore.Http.Json.JsonOptions>>() ?? Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions());
 
        var schemaService = new OpenApiSchemaService("Test", jsonOptions, builder.ServiceProvider, options.Object);
        ((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
        var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider, new OpenApiTestServer());
        ((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
 
        return documentService;
    }
 
    public static IApiDescriptionGroupCollectionProvider CreateApiDescriptionGroupCollectionProvider(IList<ApiDescription> apiDescriptions = null)
    {
        var apiDescriptionGroup = new ApiDescriptionGroup("testGroupName", (apiDescriptions ?? Array.Empty<ApiDescription>()).AsReadOnly());
        var apiDescriptionGroupCollection = new ApiDescriptionGroupCollection([apiDescriptionGroup], 1);
        var apiDescriptionGroupCollectionProvider = new Mock<IApiDescriptionGroupCollectionProvider>();
        apiDescriptionGroupCollectionProvider.Setup(p => p.ApiDescriptionGroups).Returns(apiDescriptionGroupCollection);
        return apiDescriptionGroupCollectionProvider.Object;
    }
 
    private static EndpointMetadataApiDescriptionProvider CreateEndpointMetadataApiDescriptionProvider(EndpointDataSource endpointDataSource)
    {
        var options = new RouteOptions();
        options.ConstraintMap["regex"] = typeof(RegexInlineRouteConstraint);
 
        return new EndpointMetadataApiDescriptionProvider(
            endpointDataSource,
            new HostEnvironment { ApplicationName = nameof(OpenApiDocumentServiceTests) },
            new DefaultParameterPolicyFactory(Options.Create(options), new TestServiceProvider()),
            new ServiceProviderIsService());
    }
 
    internal static TestEndpointRouteBuilder CreateBuilder(IServiceCollection serviceCollection = null)
    {
        var serviceProvider = new TestServiceProvider();
        serviceProvider.SetInternalServiceProvider(serviceCollection ?? new ServiceCollection());
        return new TestEndpointRouteBuilder(new ApplicationBuilder(serviceProvider));
    }
 
    internal class TestEndpointRouteBuilder : IEndpointRouteBuilder
    {
        public TestEndpointRouteBuilder(IApplicationBuilder applicationBuilder)
        {
            ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder));
            DataSources = new List<EndpointDataSource>();
        }
 
        public IApplicationBuilder ApplicationBuilder { get; }
 
        public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
 
        public ICollection<EndpointDataSource> DataSources { get; }
 
        public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices;
    }
 
    public ControllerActionDescriptor CreateActionDescriptor(string methodName = null, Type controllerType = null)
    {
        var action = new ControllerActionDescriptor();
        action.SetProperty(new ApiDescriptionActionData());
 
        if (controllerType != null)
        {
            action.MethodInfo = controllerType.GetMethod(
                methodName ?? "ReturnsObject",
                BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
 
            action.ControllerTypeInfo = controllerType.GetTypeInfo();
            action.BoundProperties = new List<ParameterDescriptor>();
 
            foreach (var property in controllerType.GetProperties())
            {
                var bindingInfo = BindingInfo.GetBindingInfo(property.GetCustomAttributes().OfType<object>());
                if (bindingInfo != null)
                {
                    action.BoundProperties.Add(new ParameterDescriptor()
                    {
                        BindingInfo = bindingInfo,
                        Name = property.Name,
                        ParameterType = property.PropertyType,
                    });
                }
            }
        }
        else
        {
            action.MethodInfo = GetType().GetMethod(
                methodName ?? "ReturnsObject",
                BindingFlags.Instance | BindingFlags.NonPublic);
        }
 
        action.AttributeRouteInfo = new()
        {
            Template = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Template,
            Name = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Name,
            Order = action.MethodInfo.GetCustomAttribute<RouteAttribute>()?.Order ?? 0,
        };
        action.RouteValues.Add("controller", "Test");
        action.RouteValues.Add("action", action.MethodInfo.Name);
        action.ActionConstraints = [new HttpMethodActionConstraint(["GET"])];
        action.EndpointMetadata = [..action.MethodInfo.GetCustomAttributes()];
        if (controllerType is not null)
        {
            foreach (var attribute in controllerType.GetCustomAttributes())
            {
                action.EndpointMetadata.Add(attribute);
            }
        }
 
        action.Parameters = [];
        foreach (var parameter in action.MethodInfo.GetParameters())
        {
            action.Parameters.Add(new ControllerParameterDescriptor()
            {
                Name = parameter.Name,
                ParameterType = parameter.ParameterType,
                BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes().OfType<object>()),
                ParameterInfo = parameter
            });
        }
 
        return action;
    }
 
    private class TestServiceProvider : IServiceProvider, IKeyedServiceProvider, IServiceScope, IServiceScopeFactory
    {
        public static TestServiceProvider Instance { get; } = new TestServiceProvider();
        private IKeyedServiceProvider _serviceProvider;
        internal OpenApiDocumentService TestDocumentService { get; set; }
        internal OpenApiSchemaStore TestSchemaStoreService { get; } = new OpenApiSchemaStore();
        internal OpenApiSchemaService TestSchemaService { get; set; }
 
        public IServiceProvider ServiceProvider => this;
 
        public void SetInternalServiceProvider(IServiceCollection serviceCollection)
        {
            serviceCollection.AddKeyedSingleton<OpenApiSchemaStore>("Test");
            serviceCollection.Configure<OpenApiOptions>("Test", options =>
            {
                options.DocumentName = "Test";
            });
            _serviceProvider = serviceCollection.BuildServiceProvider();
        }
 
        public IServiceScope CreateScope()
        {
            var scope = _serviceProvider.CreateScope();
            // Mock the inner service provider which can contain non-singleton services
            // that have been configured in the service collection this TestServiceProvider
            // was instantiated to.
            if (scope.ServiceProvider is IKeyedServiceProvider scopedServiceProvider)
            {
                _serviceProvider = scopedServiceProvider;
            }
            // Return this instance as the scope so we can continue to resolve
            // our mocked types.
            return this;
        }
 
        public object GetKeyedService(Type serviceType, object serviceKey)
        {
            if (serviceType == typeof(OpenApiDocumentService))
            {
                return TestDocumentService;
            }
            if (serviceType == typeof(OpenApiSchemaService))
            {
                return TestSchemaService;
            }
            if (serviceType == typeof(OpenApiSchemaStore))
            {
                return TestSchemaStoreService;
            }
 
            return _serviceProvider.GetKeyedService(serviceType, serviceKey);
        }
 
        public object GetRequiredKeyedService(Type serviceType, object serviceKey)
        {
            if (serviceType == typeof(OpenApiDocumentService))
            {
                return TestDocumentService;
            }
            if (serviceType == typeof(OpenApiSchemaService))
            {
                return TestSchemaService;
            }
            if (serviceType == typeof(OpenApiSchemaStore))
            {
                return TestSchemaStoreService;
            }
 
            return _serviceProvider.GetRequiredKeyedService(serviceType, serviceKey);
        }
 
        public object GetService(Type serviceType)
        {
            if (serviceType == typeof(IOptions<RouteHandlerOptions>))
            {
                return Options.Create(new RouteHandlerOptions());
            }
 
            return _serviceProvider.GetService(serviceType);
        }
 
        public void Dispose() { }
    }
 
    internal class OpenApiTestServer(string[] addresses = null) : IServer
    {
        public IFeatureCollection Features => GenerateFeatures();
 
        public void Dispose()
        {
            return;
        }
 
        internal virtual IFeatureCollection GenerateFeatures()
        {
            var features = new FeatureCollection();
            features.Set<IServerAddressesFeature>(new TestServerAddressesFeature { Addresses = addresses });
            return features;
        }
 
        public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull => Task.CompletedTask;
 
        public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    }
 
    private class TestServerAddressesFeature : IServerAddressesFeature
    {
        public ICollection<string> Addresses { get; set; }
        public bool PreferHostingUrls { get; set; }
    }
}