File: Utils\DevCertsCheckTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Aspire.Cli.Certificates;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Utils.EnvironmentChecker;
using Microsoft.AspNetCore.Certificates.Generation;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.Utils;
 
public class DevCertsCheckTests
{
    private const int MinVersion = CertificateManager.CurrentAspNetCoreCertificateVersion;
 
    private static DevCertInfo CreateDevCertInfo(CertificateManager.TrustLevel trustLevel, string thumbprint, int version)
    {
        var now = DateTimeOffset.UtcNow;
        return new DevCertInfo
        {
            TrustLevel = trustLevel,
            Thumbprint = thumbprint,
            Version = version,
            ValidityNotBefore = now.AddDays(-30),
            ValidityNotAfter = now.AddDays(335),
            Subject = "CN=localhost",
            IsHttpsDevelopmentCertificate = true,
            IsExportable = true
        };
    }
 
    private static DevCertInfo CreateDevCertInfo(CertificateManager.TrustLevel trustLevel, X509Certificate2 certificate, int version)
    {
        return new DevCertInfo
        {
            TrustLevel = trustLevel,
            Thumbprint = certificate.Thumbprint,
            Version = version,
            ValidityNotBefore = certificate.NotBefore,
            ValidityNotAfter = certificate.NotAfter,
            Subject = certificate.Subject,
            IsHttpsDevelopmentCertificate = true,
            IsExportable = true
        };
    }
 
    private static X509Certificate2 CreateCertificate()
    {
        using var key = RSA.Create(2048);
        var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
        return X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert));
    }
 
    private static DevCertsCheck CreateCheck(TestCertificateToolRunner toolRunner, IEnvironment environment, TestProcessExecutionFactory? processExecutionFactory = null) =>
        new(NullLogger<DevCertsCheck>.Instance, toolRunner, environment, processExecutionFactory ?? CreateOpenSslProcessExecutionFactory());
 
    private static TestProcessExecutionFactory CreateOpenSslProcessExecutionFactory(string hash = "12345678") =>
        new()
        {
            AsyncAttemptCallback = (_, _, _) => Task.FromResult((0, (string?)hash))
        };
 
    private static string CreateCertUtil(DirectoryInfo directory)
    {
        var certUtilPath = Path.Combine(directory.FullName, CertificateHelpers.CertUtilCommand);
        File.WriteAllText(certUtilPath, "");
        if (!OperatingSystem.IsWindows())
        {
            File.SetUnixFileMode(certUtilPath, UnixFileMode.UserRead | UnixFileMode.UserExecute);
        }
 
        return certUtilPath;
    }
 
    private static string CreateOpenSsl(DirectoryInfo directory, string hash)
    {
        var openSslPath = OperatingSystem.IsWindows()
            ? Path.Combine(directory.FullName, "openssl.cmd")
            : Path.Combine(directory.FullName, "openssl");
 
        var contents = OperatingSystem.IsWindows()
            ? $"""
                @echo off
                if "%1"=="x509" (
                  echo {hash}
                  exit /b 0
                )
                exit /b 1
                """
            : $"""
                #!/bin/sh
                if [ "$1" = "x509" ]; then
                  echo {hash}
                  exit 0
                fi
                exit 1
                """;
        File.WriteAllText(openSslPath, contents);
        if (!OperatingSystem.IsWindows())
        {
            File.SetUnixFileMode(openSslPath, UnixFileMode.UserRead | UnixFileMode.UserExecute);
        }
 
        return openSslPath;
    }
 
    private static string GetOpenSslCertificateFileName(X509Certificate2 certificate) =>
        $"aspnetcore-localhost-{certificate.Thumbprint}.pem";
 
    private static void WriteOpenSslCertificateCache(DirectoryInfo trustDirectory, X509Certificate2 certificate, string hashEntryName = "12345678.0")
    {
        var certificatePem = certificate.ExportCertificatePem();
        File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificatePem);
        File.WriteAllText(Path.Combine(trustDirectory.FullName, hashEntryName), certificatePem);
    }
 
    [Fact]
    public void EvaluateCertificateResults_NoCertificates_ReturnsWarning()
    {
        var results = DevCertsCheck.EvaluateCertificateResults([], new HostEnvironment());
 
        var devCertsResult = Assert.Single(results);
        Assert.Equal(DevCertsCheck.CheckName, devCertsResult.Name);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("No HTTPS development certificate found", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_MultipleCerts_AllTrusted_ReturnsPass()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "CCCC3333DDDD4444", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Pass, devCertsResult.Status);
        Assert.Contains("trusted", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_MultipleCerts_NoneTrusted_ReturnsWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.None, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.None, "CCCC3333DDDD4444", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("none are trusted", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_MultipleCerts_SomeUntrusted_ReturnsWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.None, "CCCC3333DDDD4444", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("Multiple HTTPS development certificates found", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_SingleCert_Trusted_ReturnsPass()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Pass, devCertsResult.Status);
        Assert.Contains("trusted", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_SingleCert_Untrusted_ReturnsWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.None, "AAAA1111BBBB2222", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("not trusted", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_SingleCert_PartiallyTrusted_ReturnsWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Partial, "AAAA1111BBBB2222", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("partially trusted", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_OldTrustedCert_ReturnsVersionWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion - 1),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        Assert.Equal(2, results.Count);
        var versionResult = Assert.Single(results, r => r.Name == DevCertsCheck.VersionCheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, versionResult.Status);
        Assert.Contains("older version", versionResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_MultipleCerts_AllTrusted_NoVersionWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "CCCC3333DDDD4444", MinVersion + 1),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        // Should only have the pass result, no version warning
        var devCertsResult = Assert.Single(results);
        Assert.Equal(DevCertsCheck.CheckName, devCertsResult.Name);
        Assert.Equal(EnvironmentCheckStatus.Pass, devCertsResult.Status);
    }
 
    [Fact]
    public void EvaluateCertificateResults_MultipleCerts_AllPartiallyTrusted_ReturnsPass()
    {
        // Partially trusted counts as trusted (not None), so all certs are "trusted"
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Partial, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.Partial, "CCCC3333DDDD4444", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        // Should not have a "Multiple certs" warning since all are trusted
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.NotEqual(EnvironmentCheckStatus.Warning, devCertsResult.Status);
    }
 
    [Fact]
    public void EvaluateCertificateResults_ThreeCerts_TwoTrustedOneNot_ReturnsWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "CCCC3333DDDD4444", MinVersion),
            CreateDevCertInfo(CertificateManager.TrustLevel.None, "EEEE5555FFFF6666", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, devCertsResult.Status);
        Assert.Contains("3 certificates", devCertsResult.Message);
    }
 
    [Fact]
    public void EvaluateCertificateResults_PassResult_IncludesMetadata()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
        };
 
        var results = DevCertsCheck.EvaluateCertificateResults(certs, new HostEnvironment());
 
        var devCertsResult = Assert.Single(results, r => r.Name == DevCertsCheck.CheckName);
        Assert.NotNull(devCertsResult.Metadata);
        Assert.True(devCertsResult.Metadata.ContainsKey("certificates"));
 
        var certificates = devCertsResult.Metadata["certificates"]!.AsArray();
        Assert.Single(certificates);
 
        var certNode = certificates[0]!.AsObject();
        Assert.Equal("AAAA1111BBBB2222", certNode["thumbprint"]!.GetValue<string>());
        Assert.Equal(MinVersion, certNode["version"]!.GetValue<int>());
        Assert.Equal("full", certNode["trustLevel"]!.GetValue<string>());
        Assert.NotNull(certNode["notBefore"]);
        Assert.NotNull(certNode["notAfter"]);
    }
 
    [Fact]
    public void EvaluateCertificateResults_NoCertificates_DoesNotIncludeMetadata()
    {
        var results = DevCertsCheck.EvaluateCertificateResults([], new HostEnvironment());
 
        var devCertsResult = Assert.Single(results);
        Assert.Null(devCertsResult.Metadata);
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithoutCertUtil_ReturnsCertUtilWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
        };
        var toolRunner = new TestCertificateToolRunner
        {
            CheckHttpCertificateCallback = () => new CertificateTrustResult
            {
                HasCertificates = true,
                TrustLevel = CertificateManager.TrustLevel.Full,
                Certificates = certs
            }
        };
        var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
        {
            ["PATH"] = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N"))
        });
        var check = CreateCheck(toolRunner, environment);
 
        var results = await check.CheckAsync();
 
        var certUtilResult = Assert.Single(results, r => r.Name == DevCertsCheck.CertUtilCheckName);
        Assert.Equal(EnvironmentCheckStatus.Warning, certUtilResult.Status);
        Assert.Contains("certutil", certUtilResult.Message);
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithCertUtil_DoesNotReturnCertUtilWarning()
    {
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.CertUtilCheckName);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithMatchingOpenSslCertificateCache_DoesNotReturnOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            if (!OperatingSystem.IsWindows())
            {
                CreateOpenSsl(tempDirectory, "12345678");
            }
 
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            WriteOpenSslCertificateCache(trustDirectory, certificate);
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")]
    public async Task CheckAsync_LinuxWithFailedOpenSslHashProbeAndMatchingHashStyleEntry_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            CreateOpenSsl(tempDirectory, "12345678");
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            WriteOpenSslCertificateCache(trustDirectory, certificate, "87654321.0");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var processExecutionFactory = new TestProcessExecutionFactory
            {
                AsyncAttemptCallback = (_, _, _) => Task.FromResult((1, (string?)null))
            };
            var check = CreateCheck(toolRunner, environment, processExecutionFactory);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("subject-hash", cacheResult.Details);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")]
    public async Task CheckAsync_LinuxWithOpenSslHashProbeStartFailure_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            CreateOpenSsl(tempDirectory, "12345678");
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            WriteOpenSslCertificateCache(trustDirectory, certificate, "12345678.0");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var processExecutionFactory = new TestProcessExecutionFactory
            {
                CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) =>
                    new TestProcessExecution(fileName, args, env, options, (_, _, _) => Task.FromResult((0, (string?)"12345678")), () => 1)
                    {
                        StartException = new InvalidOperationException("openssl failed to start")
                    }
            };
            var check = CreateCheck(toolRunner, environment, processExecutionFactory);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("subject-hash", cacheResult.Details);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")]
    public async Task CheckAsync_LinuxWithMissingOpenSslHashEntry_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            CreateOpenSsl(tempDirectory, "12345678");
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem());
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("subject-hash", cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
            Assert.DoesNotContain("Install openssl", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")]
    public async Task CheckAsync_LinuxWithCallerCanceledOpenSslHashProbeAndKillFailure_ThrowsOperationCanceledException()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
        using var cancellationTokenSource = new CancellationTokenSource();
 
        try
        {
            CreateCertUtil(tempDirectory);
            CreateOpenSsl(tempDirectory, "12345678");
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem());
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var processExecutionFactory = new TestProcessExecutionFactory
            {
                CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) =>
                    new TestProcessExecution(fileName, args, env, options, async (_, _, cancellationToken) =>
                    {
                        await cancellationTokenSource.CancelAsync();
                        await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
                        return (0, (string?)"12345678");
                    }, () => 1)
                    {
                        KillCallback = _ => throw new NotSupportedException("Process tree termination is unavailable.")
                    }
            };
            var check = CreateCheck(toolRunner, environment, processExecutionFactory);
 
            await Assert.ThrowsAnyAsync<OperationCanceledException>(() => check.CheckAsync(cancellationTokenSource.Token));
 
            var processExecution = Assert.IsType<TestProcessExecution>(Assert.Single(processExecutionFactory.CreatedExecutions));
            Assert.Equal(1, processExecution.KillCount);
            Assert.True(processExecution.KilledEntireProcessTree);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    [SkipOnPlatform(TestPlatforms.Windows, "The synthetic openssl command uses a POSIX shell script.")]
    public async Task CheckAsync_LinuxWithOpenSslHashProbeFailureAndKillFailure_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            CreateOpenSsl(tempDirectory, "12345678");
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem());
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var processExecutionFactory = new TestProcessExecutionFactory
            {
                CreateExecutionWithFileNameCallback = (fileName, args, env, workingDirectory, options) =>
                    new TestProcessExecution(fileName, args, env, options, (_, _, _) => throw new IOException("openssl pipe failed"), () => 1)
                    {
                        KillCallback = _ => throw new NotSupportedException("Process tree termination is unavailable.")
                    }
            };
            var check = CreateCheck(toolRunner, environment, processExecutionFactory);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("subject-hash", cacheResult.Details);
 
            var processExecution = Assert.IsType<TestProcessExecution>(Assert.Single(processExecutionFactory.CreatedExecutions));
            Assert.Equal(1, processExecution.KillCount);
            Assert.True(processExecution.KilledEntireProcessTree);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithMissingOpenSslHashEntryAndNoOpenSsl_DoesNotReturnOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), certificate.ExportCertificatePem());
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithMissingOpenSslCertificateCacheDirectory_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Path.Combine(tempDirectory.FullName, "missing-trust");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithMissingOpenSslCertificateCacheEntry_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithCorruptOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            var corruptCertificateFileName = GetOpenSslCertificateFileName(certificate);
            File.WriteAllText(Path.Combine(trustDirectory.FullName, corruptCertificateFileName), "not a certificate");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(corruptCertificateFileName, cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithUntrustedCertificateAndCorruptOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            var corruptCertificateFileName = GetOpenSslCertificateFileName(certificate);
            File.WriteAllText(Path.Combine(trustDirectory.FullName, corruptCertificateFileName), "not a certificate");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.None, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.None,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(corruptCertificateFileName, cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithUnrelatedCorruptOpenSslCertificateCache_DoesNotReturnOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            WriteOpenSslCertificateCache(trustDirectory, certificate);
            File.WriteAllText(Path.Combine(trustDirectory.FullName, "unrelated.pem"), "not a certificate");
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            Assert.DoesNotContain(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_LinuxWithStaleOpenSslCertificateCache_ReturnsOpenSslCertificateCacheWarning()
    {
        using var certificate = CreateCertificate();
        using var staleCertificate = CreateCertificate();
        var tempDirectory = Directory.CreateTempSubdirectory();
 
        try
        {
            CreateCertUtil(tempDirectory);
            var trustDirectory = Directory.CreateDirectory(Path.Combine(tempDirectory.FullName, "trust"));
            File.WriteAllText(Path.Combine(trustDirectory.FullName, GetOpenSslCertificateFileName(certificate)), staleCertificate.ExportCertificatePem());
 
            var certs = new List<DevCertInfo>
            {
                CreateDevCertInfo(CertificateManager.TrustLevel.Full, certificate, MinVersion),
            };
            var toolRunner = new TestCertificateToolRunner
            {
                CheckHttpCertificateCallback = () => new CertificateTrustResult
                {
                    HasCertificates = true,
                    TrustLevel = CertificateManager.TrustLevel.Full,
                    Certificates = certs
                }
            };
            var environment = TestEnvironment.CreateLinux(new Dictionary<string, string?>
            {
                ["PATH"] = tempDirectory.FullName,
                [CertificateHelpers.DevCertsOpenSslCertDirEnvVar] = trustDirectory.FullName
            });
            var check = CreateCheck(toolRunner, environment);
 
            var results = await check.CheckAsync();
 
            var cacheResult = Assert.Single(results, r => r.Name == DevCertsCheck.OpenSslCertificateCacheCheckName);
            Assert.Equal(EnvironmentCheckStatus.Warning, cacheResult.Status);
            Assert.Contains(certificate.Thumbprint, cacheResult.Details);
            Assert.Contains("aspire certs clean", cacheResult.Fix);
        }
        finally
        {
            tempDirectory.Delete(recursive: true);
        }
    }
 
    [Fact]
    public async Task CheckAsync_NonLinux_DoesNotReadEnvironmentVariablesForCertUtilWarning()
    {
        var certs = new List<DevCertInfo>
        {
            CreateDevCertInfo(CertificateManager.TrustLevel.Full, "AAAA1111BBBB2222", MinVersion),
        };
        var toolRunner = new TestCertificateToolRunner
        {
            CheckHttpCertificateCallback = () => new CertificateTrustResult
            {
                HasCertificates = true,
                TrustLevel = CertificateManager.TrustLevel.Full,
                Certificates = certs
            }
        };
        var environment = TestEnvironment.CreateMacOS(new Dictionary<string, string?>
        {
            ["PATH"] = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N"))
        });
        var check = CreateCheck(toolRunner, environment);
 
        var results = await check.CheckAsync();
 
        Assert.Equal(0, environment.GetEnvironmentVariablesCallCount);
        Assert.Collection(results, result => Assert.Equal(DevCertsCheck.CheckName, result.Name));
    }
}