File: Dcp\DcpHostNotificationTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Globalization;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Resources;
using Aspire.Hosting.Tests.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Time.Testing;
 
namespace Aspire.Hosting.Tests.Dcp;
 
#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
[Trait("Partition", "4")]
public sealed class DcpHostNotificationTests
{
    private static Locations CreateTestLocations()
    {
        var directoryService = new FileSystemService(new ConfigurationBuilder().Build());
        return new Locations(directoryService);
    }
 
    [Fact]
    public void DcpHost_WithIInteractionService_CanBeConstructed()
    {
        // Arrange
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService();
        var locations = CreateTestLocations();
        var applicationModel = new DistributedApplicationModel(new ResourceCollection());
        var timeProvider = new FakeTimeProvider();
 
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        // Act & Assert - should not throw
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        Assert.NotNull(dcpHost);
    }
 
    [Fact]
    public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.Contains("docker", interaction.Message);
        Assert.Contains(string.Format(CultureInfo.InvariantCulture, InteractionStrings.ContainerRuntimeUnhealthyMessage, "docker"), interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
    }
 
    [Fact]
    public async Task DcpHost_WithUntrustedDeveloperCertificate_ShowsNotificationAndLogsWarning()
    {
        // Arrange
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = CreateApplicationModelWithHttpsEndpoint();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedMessage, interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning && w.Message is not null && w.Message.Contains("aka.ms/aspire/devcerts", StringComparison.Ordinal));
    }
 
    [Fact]
    public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed and running - healthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = true,
                    Error = null
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        // Use a short timeout to check that no notification is sent
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification should be sent
        }
 
        // Assert - no notification should be shown for healthy runtime
        Assert.False(hasInteraction);
    }
 
    [Fact]
    public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = false }; // Dashboard disabled
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        // Use a short timeout to check that no notification is sent
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification should be sent when dashboard is disabled
        }
 
        // Assert - no notification should be shown when dashboard is disabled
        Assert.False(hasInteraction);
    }
 
    [Fact]
    public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "podman", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container installed but not running - unhealthy state
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Podman is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.Contains("podman", interaction.Message);
        Assert.Contains(InteractionStrings.ContainerRuntimePodmanAdvice, interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Null(notificationOptions.LinkUrl); // No specific link for Podman
    }
 
    [Fact]
    public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhenRuntimeBecomesHealthy()
    {
        // Arrange - this test verifies that the notification is cancelled when runtime becomes healthy
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Initially unhealthy
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = true,
                    Running = false,
                    Error = "Docker daemon is not running"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        // Use ReadAsync with timeout to wait for the notification
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
        
        // Assert - Verify notification was shown initially
        Assert.Equal(InteractionStrings.ContainerRuntimeUnhealthyTitle, interaction.Title);
        Assert.False(interaction.CancellationToken.IsCancellationRequested); // Should not be cancelled yet
 
        // Simulate container runtime becoming healthy
        dependencyCheckService.DcpInfoResult = new DcpInfo
        {
            Containers = new DcpContainersInfo
            {
                Installed = true,
                Running = true,
                Error = null
            }
        };
 
        // Advance time by 5 seconds to trigger the next polling cycle
        timeProvider.Advance(TimeSpan.FromSeconds(5));
 
        // Assert - The notification should now be cancelled
        using (var testTimeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
        using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(interaction.CancellationToken, testTimeoutCts.Token))
        {
            await Assert.ThrowsAsync<TaskCanceledException>(() => Task.Delay(-1, linkedCts.Token));
        }
    }
 
    [Fact]
    public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWithoutPolling()
    {
        // Arrange
        using var app = CreateAppWithContainers();
        var applicationModel = app.Services.GetRequiredService<DistributedApplicationModel>();
        
        var loggerFactory = new NullLoggerFactory();
        var dcpOptions = Options.Create(new DcpOptions { ContainerRuntime = "docker", CliPath = OperatingSystem.IsWindows() ? "cmd.exe" : "/bin/sh" });
        var dependencyCheckService = new TestDcpDependencyCheckService
        {
            // Container not installed
            DcpInfoResult = new DcpInfo
            {
                Containers = new DcpContainersInfo
                {
                    Installed = false,
                    Running = false,
                    Error = "No container runtime found"
                }
            }
        };
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([], false, false, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert
        Assert.Equal(InteractionStrings.ContainerRuntimeNotInstalledTitle, interaction.Title);
        Assert.Contains(InteractionStrings.ContainerRuntimeNotInstalledMessage, interaction.Message);
        Assert.Contains("https://aka.ms/dotnet/aspire/containers", interaction.Message);
        var notificationOptions = Assert.IsType<NotificationInteractionOptions>(interaction.Options);
        Assert.Equal(MessageIntent.Error, notificationOptions.Intent);
        Assert.Equal(InteractionStrings.ContainerRuntimeLinkText, notificationOptions.LinkText);
        Assert.Equal("https://aka.ms/dotnet/aspire/containers", notificationOptions.LinkUrl);
 
        // Verify that no polling is started by ensuring the cancellation token is not cancelled after a delay
        // This tests that the function returns immediately and doesn't start the polling task
        await Task.Delay(TimeSpan.FromMilliseconds(100));
        Assert.False(interaction.CancellationToken.IsCancellationRequested);
    }
 
    private static DistributedApplication CreateAppWithContainers()
    {
        var builder = DistributedApplication.CreateBuilder();
        builder.AddContainer("test-container", "nginx:latest");
        return builder.Build();
    }
 
    private static DistributedApplicationModel CreateApplicationModelWithHttpsEndpoint()
    {
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "https", name: "https"));
        return new DistributedApplicationModel(new ResourceCollection([resource]));
    }
 
    private static DistributedApplicationModel CreateApplicationModelWithHttpEndpoint()
    {
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http"));
        return new DistributedApplicationModel(new ResourceCollection([resource]));
    }
 
    private static X509Certificate2 CreateUntrustedCertificate()
    {
        var searchPaths = new[]
        {
            Path.Combine(Directory.GetCurrentDirectory(), "tests", "Shared", "TestCertificates", "testCert.pfx"),
            Path.Combine(AppContext.BaseDirectory, "shared", "TestCertificates", "testCert.pfx"),
            Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "tests", "Shared", "TestCertificates", "testCert.pfx"))
        };
 
        foreach (var path in searchPaths)
        {
            if (File.Exists(path))
            {
                return new X509Certificate2(path, "testPassword");
            }
        }
 
        throw new FileNotFoundException("Could not locate test certificate file 'testCert.pfx' in expected locations.");
    }
 
    [Fact]
    public async Task DcpHost_WithNoHttpsResources_DoesNotShowCertificateWarning()
    {
        // Arrange - only HTTP endpoints, no HTTPS
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = CreateApplicationModelWithHttpEndpoint();
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert - no notification or warning should be shown
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification
        }
 
        Assert.False(hasInteraction);
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithNoResources_DoesNotShowCertificateWarning()
    {
        // Arrange - empty resource model
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
        var applicationModel = new DistributedApplicationModel(new ResourceCollection());
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert - no notification or warning should be shown for empty model
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification
        }
 
        Assert.False(hasInteraction);
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallback_ShowsCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but no HTTPS endpoint
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test");
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AppHost:Directory"] = appHostDirectory
            })
            .Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert - warning should be shown because HttpsCertificateConfigurationCallbackAnnotation indicates TLS
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallbackDisabledByWithoutHttpsCertificate_DoesNotShowCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateConfigurationCallbackAnnotation but disabled by WithoutHttpsCertificate
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        // This is the state set by WithoutHttpsCertificate()
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            Certificate = null,
            UseDeveloperCertificate = false,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert - no warning because TLS was explicitly disabled
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification
        }
 
        Assert.False(hasInteraction);
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateAnnotationOnly_DoesNotShowCertificateWarning()
    {
        // Arrange - resource has HttpsCertificateAnnotation but NO HttpsCertificateConfigurationCallbackAnnotation
        // HttpsCertificateAnnotation alone has no effect without the callback annotation
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            UseDeveloperCertificate = true,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var configuration = new ConfigurationBuilder().Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        // Assert - no warning because HttpsCertificateAnnotation alone has no effect
        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
        var hasInteraction = false;
        try
        {
            await interactionService.Interactions.Reader.ReadAsync(cts.Token);
            hasInteraction = true;
        }
        catch (OperationCanceledException)
        {
            // Expected - no notification
        }
 
        Assert.False(hasInteraction);
        Assert.DoesNotContain(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    [Fact]
    public async Task DcpHost_WithHttpsCertificateConfigCallbackAndDevCert_ShowsCertificateWarning()
    {
        // Arrange - resource has both HttpsCertificateConfigurationCallbackAnnotation and HttpsCertificateAnnotation
        // with UseDeveloperCertificate = true (not disabled)
        using var certificate = CreateUntrustedCertificate();
        var testSink = new TestSink();
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddProvider(new TestLoggerProvider(testSink));
        });
        var dcpOptions = Options.Create(new DcpOptions());
        var dependencyCheckService = new TestDcpDependencyCheckService();
        var interactionService = new TestInteractionService { IsAvailable = true };
        var locations = CreateTestLocations();
 
        var resource = new ContainerResource("test-resource");
        resource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(_ => Task.CompletedTask));
        resource.Annotations.Add(new HttpsCertificateAnnotation
        {
            UseDeveloperCertificate = true,
        });
        var applicationModel = new DistributedApplicationModel(new ResourceCollection([resource]));
 
        var timeProvider = new FakeTimeProvider();
        var developerCertificateService = new TestDeveloperCertificateService([certificate], false, true, false, latestCertificateIsUntrusted: true);
        var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build());
        var appHostDirectory = Path.Combine(Path.GetTempPath(), "aspire-apphost-test");
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["AppHost:Directory"] = appHostDirectory
            })
            .Build();
 
        var dcpHost = new DcpHost(
            loggerFactory,
            dcpOptions,
            dependencyCheckService,
            interactionService,
            locations,
            applicationModel,
            timeProvider,
            developerCertificateService,
            fileSystemService,
            configuration);
 
        // Act
        await dcpHost.EnsureDevelopmentCertificateTrustAsync(CancellationToken.None).DefaultTimeout();
 
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var interaction = await interactionService.Interactions.Reader.ReadAsync(cts.Token);
 
        // Assert - warning should be shown because TLS is active
        Assert.Equal(InteractionStrings.DeveloperCertificateNotFullyTrustedTitle, interaction.Title);
        Assert.Contains(testSink.Writes, w => w.LogLevel == LogLevel.Warning);
    }
 
    private sealed class TestDcpDependencyCheckService : IDcpDependencyCheckService
    {
        public DcpInfo? DcpInfoResult { get; set; } = new DcpInfo
        {
            VersionString = DcpVersion.Dev.ToString(),
            Version = DcpVersion.Dev,
            Containers = new DcpContainersInfo
            {
                Runtime = "docker",
                Installed = true,
                Running = true
            }
        };
 
        public Task<DcpInfo?> GetDcpInfoAsync(bool force = false, CancellationToken cancellationToken = default)
        {
            return Task.FromResult(DcpInfoResult);
        }
    }
}
 
#pragma warning restore ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.