File: Integration\Playwright\Infrastructure\DashboardServerFixture.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.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 Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Model;
using Aspire.Hosting;
using Aspire.DashboardService.Proto.V1;
using Google.Protobuf.WellKnownTypes;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure;
 
public class DashboardServerFixture : IAsyncLifetime
{
    // Keep tests sharing this fixture sequential through browser-context disposal so a previous
    // Blazor circuit cannot continue using fixture services after the next test starts.
    internal SemaphoreSlim TestGate { get; } = new(1, 1);
 
    public Dictionary<string, string?> Configuration { get; }
 
    public DashboardWebApplication DashboardApp { get; private set; } = null!;
 
    // Can't have multiple fixtures when one is generic. Workaround by nesting playwright fixture.
    public PlaywrightFixture PlaywrightFixture { get; }
 
    protected virtual IReadOnlyList<ResourceViewModel>? Resources => null;
 
    protected virtual void ConfigureServices(IServiceCollection services)
    {
    }
 
    public DashboardServerFixture()
    {
        PlaywrightFixture = new PlaywrightFixture();
 
        Configuration = new Dictionary<string, string?>
        {
            [DashboardConfigNames.DashboardFrontendUrlName.ConfigKey] = "http://127.0.0.1:0",
            [DashboardConfigNames.DashboardOtlpHttpUrlName.ConfigKey] = "http://127.0.0.1:0",
            [DashboardConfigNames.DashboardOtlpAuthModeName.ConfigKey] = nameof(OtlpAuthMode.Unsecured),
            [DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = nameof(FrontendAuthMode.Unsecured)
        };
    }
 
    public async ValueTask InitializeAsync()
    {
        await PlaywrightFixture.InitializeAsync();
 
        DashboardApp = CreateDashboardApp(Configuration, Resources, ConfigureServices);
 
        await DashboardApp.StartAsync();
 
        if (Resources is not null)
        {
            var writer = DashboardApp.Services.GetRequiredService<IResourceRepositoryWriter>();
            await writer.ReplaceResourcesAsync(Resources.Select(CreateResource).ToList());
        }
    }
 
    internal static DashboardWebApplication CreateDashboardApp(
        IReadOnlyDictionary<string, string?> configuration,
        IReadOnlyList<ResourceViewModel>? resources = null,
        Action<IServiceCollection>? configureServices = null)
    {
        const string aspireDashboardAssemblyName = "Aspire.Dashboard";
        var currentAssemblyName = Assembly.GetExecutingAssembly().GetName().Name!;
        var currentAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
        var aspireAssemblyDirectory = currentAssemblyDirectory.Replace(currentAssemblyName, aspireDashboardAssemblyName);
 
        var config = new ConfigurationManager().AddInMemoryCollection(configuration).Build();
 
        // Add services to the container.
        return new DashboardWebApplication(
            options: new WebApplicationOptions
            {
                EnvironmentName = "Development",
                ContentRootPath = aspireAssemblyDirectory,
                WebRootPath = Path.Combine(aspireAssemblyDirectory, "wwwroot"),
                ApplicationName = aspireDashboardAssemblyName,
            },
            preConfigureBuilder: builder =>
            {
                builder.Configuration.AddConfiguration(config);
                var dashboardClient = new MockDashboardClient(resources);
                builder.Services.AddSingleton<IDashboardClient>(dashboardClient);
                builder.Services.AddSingleton<IRepositoryFactory>(
                    services => new MockRepositoryFactory(services, dashboardClient));
                configureServices?.Invoke(builder.Services);
            });
    }
 
    private static Resource CreateResource(ResourceViewModel resource)
    {
        var result = new Resource
        {
            Name = resource.Name,
            DisplayName = resource.DisplayName,
            ResourceType = resource.ResourceType,
            Uid = resource.Uid,
            State = resource.State ?? string.Empty,
            StateStyle = resource.StateStyle ?? string.Empty
        };
 
        if (resource.CreationTimeStamp is { } creationTimeStamp)
        {
            result.CreatedAt = Timestamp.FromDateTime(creationTimeStamp.ToUniversalTime());
        }
        if (resource.StartTimeStamp is { } startTimeStamp)
        {
            result.StartedAt = Timestamp.FromDateTime(startTimeStamp.ToUniversalTime());
        }
        if (resource.StopTimeStamp is { } stopTimeStamp)
        {
            result.StoppedAt = Timestamp.FromDateTime(stopTimeStamp.ToUniversalTime());
        }
 
        result.Urls.AddRange(resource.Urls.Select(url => new Url
        {
            EndpointName = url.EndpointName ?? string.Empty,
            FullUrl = url.Url.AbsoluteUri,
            IsInternal = url.IsInternal,
            IsInactive = url.IsInactive,
            DisplayProperties = new UrlDisplayProperties
            {
                DisplayName = url.DisplayProperties.DisplayName,
                SortOrder = url.DisplayProperties.SortOrder
            }
        }));
 
        return result;
    }
 
    public async ValueTask DisposeAsync()
    {
        await PlaywrightFixture.DisposeAsync();
        await DashboardApp.DisposeAsync();
    }
}