File: NuGet\NuGetPackageOperationConfigurationTests.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.Xml.Linq;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Configuration;
 
namespace Aspire.Cli.Tests.NuGet;
 
public class NuGetPackageOperationConfigurationTests(ITestOutputHelper outputHelper)
{
    private static readonly byte[] s_sourceIdentityKey = Enumerable.Range(0, 32).Select(static value => (byte)value).ToArray();
 
    [Fact]
    public async Task StableChannelUsesAmbientConfigurationWithoutGeneratingNuGetOrgConfig()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        WriteAmbientConfig(workspace.WorkspaceRoot);
        var service = CreateService();
 
        using var configuration = await service.CreatePackageOperationConfigurationAsync(
            workspace.WorkspaceRoot,
            [new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)],
            restrictToSelectedSources: false,
            TestContext.Current.CancellationToken);
 
        Assert.Equal(workspace.WorkspaceRoot.FullName, configuration.EffectiveWorkingDirectory.FullName);
        Assert.Null(configuration.ConfigurationFile);
        Assert.Null(configuration.ExplicitConfigFile);
    }
 
    [Fact]
    public async Task ChannelOverlayPreservesAmbientFallbackWithoutAddingNuGetOrg()
    {
        const string channelSource = "https://example.test/aspire/v3/index.json";
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        WriteAmbientConfig(workspace.WorkspaceRoot);
        var service = CreateService();
 
        string overlayDirectory;
        using (var configuration = await service.CreatePackageOperationConfigurationAsync(
            workspace.WorkspaceRoot,
            [
                new PackageMapping("Aspire*", channelSource),
                new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)
            ],
            restrictToSelectedSources: false,
            TestContext.Current.CancellationToken))
        {
            Assert.NotNull(configuration.ConfigurationFile);
            Assert.Null(configuration.ExplicitConfigFile);
            Assert.Equal(workspace.WorkspaceRoot.FullName, configuration.EffectiveWorkingDirectory.Parent!.FullName);
 
            overlayDirectory = configuration.EffectiveWorkingDirectory.FullName;
            var document = XDocument.Load(configuration.ConfigurationFile.FullName);
            var sources = document.Root!
                .Element("packageSources")!
                .Elements("add")
                .Select(element => (string)element.Attribute("value")!)
                .ToArray();
            Assert.Equal([channelSource], sources);
 
            var mappings = document.Root!
                .Element("packageSourceMapping")!
                .Elements("packageSource")
                .ToDictionary(
                    element => (string)element.Attribute("key")!,
                    element => element.Elements("package")
                        .Select(package => (string)package.Attribute("pattern")!)
                        .ToArray());
            Assert.Equal([PackageMapping.AllPackages], mappings["internal"]);
            Assert.Equal(["Aspire*"], mappings["aspire-package-search"]);
        }
 
        Assert.False(Directory.Exists(overlayDirectory));
    }
 
    [Theory]
    [InlineData(false, "http://example.test/private/v3/index.json")]
    [InlineData(true, "http://example.test/private/v3/index.json")]
    [InlineData(false, "HTTP://EXAMPLE.TEST/private/v3/index.json")]
    [InlineData(true, "HTTP://EXAMPLE.TEST/private/v3/index.json")]
    [InlineData(false, "http://example.test:80/private/v3/index.json")]
    [InlineData(true, "http://example.test:80/private/v3/index.json")]
    [InlineData(false, "http://example.test/private/./v3/index.json")]
    [InlineData(true, "http://example.test/private/./v3/index.json")]
    [InlineData(false, "http://example.test/private/v3/%69ndex.json")]
    [InlineData(true, "http://example.test/private/v3/%69ndex.json")]
    public async Task SourceOverridePreservesSelectedAliasCredentialsAndTransportSettings(bool disabled, string sourceOverride)
    {
        const string sourceUrl = "http://example.test/private/v3/index.json";
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var ambientPath = Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config");
        var ambientConfig = $"""
            <configuration>
              <config>
                <add key="signatureValidationMode" value="require" />
              </config>
              <packageSources>
                <clear />
                <add key="private" value="{sourceUrl}" protocolVersion="3" allowInsecureConnections="true" disableTLSCertificateValidation="true" />
                <add key="unrelated" value="https://example.test/unrelated/v3/index.json" />
              </packageSources>
              <auditSources>
                <clear />
                <add key="audit" value="https://example.test/audit/v3/index.json" />
              </auditSources>
              <trustedSigners>
                <clear />
                <author name="trusted-author">
                  <certificate fingerprint="{new string('A', 64)}" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
                </author>
              </trustedSigners>
              <packageSourceCredentials>
                <private>
                  <add key="Username" value="test-user" />
                  <add key="ClearTextPassword" value="test-password" />
                </private>
              </packageSourceCredentials>
              <disabledPackageSources>
                {(disabled ? """<add key="private" value="true" />""" : "")}
              </disabledPackageSources>
              <packageSourceMapping>
                <packageSource key="unrelated">
                  <package pattern="CommunityToolkit.Aspire.Hosting.Custom" />
                </packageSource>
              </packageSourceMapping>
            </configuration>
            """;
        File.WriteAllText(ambientPath, ambientConfig);
 
        using var configuration = await CreateService().CreatePackageOperationConfigurationAsync(
            workspace.WorkspaceRoot,
            PackageSourceOverrideMappings.CreateForSourceOnlyOperations(sourceOverride),
            restrictToSelectedSources: true,
            TestContext.Current.CancellationToken);
 
        Assert.Null(configuration.ExplicitConfigFile);
        Assert.Equal(workspace.WorkspaceRoot.FullName, configuration.EffectiveWorkingDirectory.Parent!.FullName);
        var settings = Settings.LoadDefaultSettings(configuration.EffectiveWorkingDirectory.FullName);
        var selectedSource = Assert.Single(new PackageSourceProvider(settings).LoadPackageSources(), source => source.IsEnabled);
        Assert.Equal("private", selectedSource.Name);
        Assert.Equal(sourceUrl, selectedSource.Source);
        Assert.True(selectedSource.AllowInsecureConnections);
        Assert.True(selectedSource.DisableTLSCertificateValidation);
        Assert.Equal(3, selectedSource.ProtocolVersion);
        Assert.NotNull(selectedSource.Credentials);
        Assert.Equal("test-user", selectedSource.Credentials.Username);
        Assert.Equal("test-password", selectedSource.Credentials.Password);
        Assert.Equal("require", SettingsUtility.GetConfigValue(settings, "signatureValidationMode"));
        Assert.Equal(["audit"], new PackageSourceProvider(settings).LoadAuditSources().Select(source => source.Name));
        var signer = Assert.Single(settings.GetSection("trustedSigners")!.Items.OfType<TrustedSignerItem>());
        Assert.Equal("trusted-author", signer.Name);
        var mapping = PackageSourceMapping.GetPackageSourceMapping(settings);
        Assert.Equal(["private"], mapping.GetConfiguredPackageSources("Aspire.Hosting.Redis"));
        Assert.Equal(["private"], mapping.GetConfiguredPackageSources("CommunityToolkit.Aspire.Hosting.Custom"));
        Assert.Equal(ambientConfig, File.ReadAllText(ambientPath));
 
        var overlay = XDocument.Load(configuration.ConfigurationFile!.FullName);
        Assert.Equal(
            ["disabledPackageSources", "packageSourceMapping"],
            overlay.Root!.Elements().Select(element => element.Name.LocalName).Order(StringComparer.Ordinal));
    }
 
    [Fact]
    public async Task SourceOverrideAddsAndRestrictsANewSource()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        WriteAmbientConfig(workspace.WorkspaceRoot);
        const string sourceUrl = "https://example.test/selected/v3/index.json";
 
        using var configuration = await CreateService().CreatePackageOperationConfigurationAsync(
            workspace.WorkspaceRoot,
            PackageSourceOverrideMappings.CreateForSourceOnlyOperations(sourceUrl),
            restrictToSelectedSources: true,
            TestContext.Current.CancellationToken);
 
        var settings = Settings.LoadDefaultSettings(configuration.EffectiveWorkingDirectory.FullName);
        var source = Assert.Single(new PackageSourceProvider(settings).LoadPackageSources(), source => source.IsEnabled);
        Assert.Equal("aspire-package-search", source.Name);
        Assert.Equal(sourceUrl, source.Source);
        Assert.Null(source.Credentials);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task AllWritersMaterializeTheComposedSourceRestriction(bool restricted)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        WriteAmbientConfig(workspace.WorkspaceRoot);
        var service = CreateService();
        var configuration = service.BuildConfiguration(
            workspace.WorkspaceRoot,
            workloadId: "writer-test",
            PackageSourceOverrideMappings.CreateForSourceOnlyOperations("https://example.test/selected/v3/index.json"),
            restrictToSelectedSources: restricted,
            hasAuthoritativeAspirePolicy: true,
            cancellationToken: TestContext.Current.CancellationToken);
 
        var overlay = configuration.Overlay;
        Assert.NotNull(overlay);
        Assert.Equal(restricted, overlay.ClearDisabledPackageSources);
        Assert.Equal(restricted ? ["internal"] : [], overlay.DisabledPackageSourceKeys);
        Assert.Equal(
            restricted ? ["aspire-writer-test"] : ["aspire-writer-test", "internal"],
            overlay.PackageSourceMappings.Select(mapping => mapping.SourceKey).Order(StringComparer.Ordinal));
 
        var persistentPath = Path.Combine(workspace.CreateDirectory("persistent").FullName, "NuGet.Config");
        service.WriteNuGetConfig(configuration, persistentPath, globalPackagesFolder: null, TestContext.Current.CancellationToken);
        using var temporary = await service.WriteTemporaryOverlayAsync(
            configuration,
            workspace.WorkspaceRoot,
            globalPackagesFolder: null,
            TestContext.Current.CancellationToken);
        Assert.NotNull(temporary);
        Assert.Equal(XDocument.Load(persistentPath).ToString(), XDocument.Load(temporary.ConfigFile.FullName).ToString());
 
        await temporary.RegenerateAsync(path =>
            service.WriteNuGetConfig(configuration, path, globalPackagesFolder: null, TestContext.Current.CancellationToken));
        Assert.Equal(XDocument.Load(persistentPath).ToString(), XDocument.Load(temporary.ConfigFile.FullName).ToString());
    }
 
    private static BundleNuGetService CreateService()
    {
        var nuGetClient = new NuGetClient(
            new TestFeatures(),
            new TestEnvironment(),
            NullLogger<NuGetClient>.Instance);
        var service = new BundleNuGetService(
            NullLogger<BundleNuGetService>.Instance,
            nuGetClient)
        {
            SourceIdentityKeyFactory = static () => s_sourceIdentityKey
        };
        return service;
    }
 
    private static void WriteAmbientConfig(DirectoryInfo directory)
    {
        File.WriteAllText(
            Path.Combine(directory.FullName, "NuGet.Config"),
            """
            <configuration>
              <packageSources>
                <clear />
                <add key="internal" value="https://example.test/internal/v3/index.json" />
              </packageSources>
            </configuration>
            """);
    }
}