File: NuGet\NuGetConfigurationQueryTests.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 Aspire.Cli.NuGet;
using Aspire.Cli.DotNet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Tests.TestServices;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.NuGet;
 
public class NuGetConfigurationQueryTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData(PackageChannelNames.Daily, false)]
    [InlineData(PackageChannelNames.Daily, true)]
    [InlineData(PackageChannelNames.Staging, false)]
    [InlineData(PackageChannelNames.Staging, true)]
    [InlineData(PackageChannelNames.Stable, false)]
    [InlineData(PackageChannelNames.Stable, true)]
    public async Task ChannelPolicy_PreservesAmbientFeedsAndRetiresOwnedSources(string channelName, bool hasMappings)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appHostDirectory = workspace.CreateDirectory("AppHost");
        var oldHive = workspace.CreateDirectory(".aspire/hives/pr-old/packages").FullName;
        const string customerSource = "https://pkgs.dev.azure.com/contoso/team/_packaging/aspire-company/nuget/v3/index.json";
        const string privateSource = "https://packages.example.com/v3/index.json";
        const string sharedSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json";
        const string oldStagingSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-abcdef12/nuget/v3/index.json";
        const string newStagingSource = "https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-microsoft-aspire-12345678/nuget/v3/index.json";
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config");
        var original = $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="customer" value="{{customerSource}}" />
                <add key="private" value="{{privateSource}}" />
                <add key="shared" value="{{sharedSource}}" />
                <add key="public" value="{{PackageSources.NuGetOrg}}" />
                <add key="old-staging" value="{{oldStagingSource}}" />
                <add key="old-hive" value="{{oldHive}}" />
              </packageSources>
              {{(hasMappings ? """
              <packageSourceMapping>
                <packageSource key="customer"><package pattern="*" /></packageSource>
                <packageSource key="private"><package pattern="*" /></packageSource>
                <packageSource key="shared"><package pattern="*" /></packageSource>
                <packageSource key="public"><package pattern="*" /></packageSource>
                <packageSource key="old-staging"><package pattern="*" /></packageSource>
                <packageSource key="old-hive"><package pattern="*" /></packageSource>
              </packageSourceMapping>
              """ : "")}}
            </configuration>
            """;
        await File.WriteAllTextAsync(configPath, original);
        var service = NuGetTestHelper.CreateService();
        var selectedSource = channelName switch
        {
            PackageChannelNames.Daily => sharedSource,
            PackageChannelNames.Staging => newStagingSource,
            _ => PackageSources.NuGetOrg
        };
        var channel = PackageChannel.CreateExplicitChannel(
            channelName, channelName == PackageChannelNames.Stable ? PackageChannelQuality.Stable : PackageChannelQuality.Both,
            channelName == PackageChannelNames.Stable
                ? [new PackageMapping("*", PackageSources.NuGetOrg)]
                : [new PackageMapping("Aspire*", selectedSource), new PackageMapping("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
        var configuration = channelName == PackageChannelNames.Stable
            ? NuGetConfigurationBuilder.BuildStableAppHostConfiguration(
                service.GetNuGetSettings(appHostDirectory.FullName, TestContext.Current.CancellationToken),
                "test", channel)
            : service.BuildChannelConfiguration(
                appHostDirectory, "test", channel, packageSourceOverride: null,
                nugetServiceIndexOverride: null, TestContext.Current.CancellationToken);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Equal(["old-hive", "old-staging"], overlay.RetiredSourceKeys.Order(StringComparer.Ordinal));
        using var preview = await service.CreateConfigurationPreviewAsync(
            appHostDirectory, configuration, globalPackagesFolder: null, TestContext.Current.CancellationToken);
        var ambientSources = new[] { customerSource, privateSource, sharedSource, PackageSources.NuGetOrg }
            .Order(StringComparer.Ordinal).ToArray();
        string[] aspireSources = channelName == PackageChannelNames.Stable ? ambientSources : [selectedSource];
        var previewDirectory = preview.EffectiveWorkingDirectory.FullName;
 
        Assert.Equal(ambientSources, NuGetTestHelper.GetEligiblePackageSources(previewDirectory, "Contoso.Package"));
        Assert.Equal(aspireSources, NuGetTestHelper.GetEligiblePackageSources(previewDirectory, "Aspire.Hosting"));
        Assert.Equal(original, await File.ReadAllTextAsync(configPath));
 
        var candidate = await new DotNetAppHostNuGetConfigMerger(service).PrepareAsync(
            workspace.WorkspaceRoot, configuration, createIfMissing: false,
            globalPackagesFolder: null, TestContext.Current.CancellationToken);
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
 
        Assert.Equal(ambientSources, NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Contoso.Package"));
        Assert.Equal(aspireSources, NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Aspire.Hosting"));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task ChannelPolicy_RetiresUnselectedHivesBeforePersistence(bool hasMappings)
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var appHostDirectory = workspace.CreateDirectory("AppHost");
        var oldHive = workspace.CreateDirectory(".aspire/hives/pr-old/packages").FullName;
        var newHive = workspace.CreateDirectory(".aspire/hives/pr-new/packages").FullName;
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config");
        var original = $$"""
            <configuration>
              <packageSources>
                <clear />
                <add key="old-hive" value="{{oldHive}}" />
                <add key="private" value="https://private.example/v3/index.json" />
              </packageSources>
              {{(hasMappings ? """
              <packageSourceMapping>
                <packageSource key="old-hive"><package pattern="Aspire*" /></packageSource>
                <packageSource key="private"><package pattern="Contoso.*" /></packageSource>
              </packageSourceMapping>
              """ : "")}}
            </configuration>
            """;
        await File.WriteAllTextAsync(configPath, original);
        var service = CreateService();
        var configuration = service.BuildConfiguration(
            appHostDirectory, "test", [new PackageMapping("Aspire*", newHive)],
            restrictToSelectedSources: false, hasAuthoritativeAspirePolicy: true,
            cancellationToken: TestContext.Current.CancellationToken);
        using var preview = await service.CreateConfigurationPreviewAsync(
            appHostDirectory, configuration, globalPackagesFolder: null, TestContext.Current.CancellationToken);
        var previewDirectory = preview.EffectiveWorkingDirectory.FullName;
 
        Assert.Equal([newHive], NuGetTestHelper.GetEligiblePackageSources(previewDirectory, "Aspire.Hosting"));
        Assert.Equal(
            hasMappings ? [] : ["https://private.example/v3/index.json"],
            NuGetTestHelper.GetEligiblePackageSources(previewDirectory, "Example.Dependency"));
        Assert.Equal(["https://private.example/v3/index.json"], NuGetTestHelper.GetEligiblePackageSources(previewDirectory, "Contoso.Package"));
        Assert.Equal(original, await File.ReadAllTextAsync(configPath));
 
        var candidate = await new DotNetAppHostNuGetConfigMerger(service).PrepareAsync(
            workspace.WorkspaceRoot, configuration, createIfMissing: false,
            globalPackagesFolder: null, TestContext.Current.CancellationToken);
        Assert.NotNull(candidate);
        await DotNetAppHostNuGetConfigMerger.ApplyAsync(candidate, TestContext.Current.CancellationToken);
        Assert.Equal([newHive], NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Aspire.Hosting"));
        Assert.Equal(
            hasMappings ? [] : ["https://private.example/v3/index.json"],
            NuGetTestHelper.GetEligiblePackageSources(appHostDirectory.FullName, "Example.Dependency"));
        var persistedSources = new global::NuGet.Configuration.PackageSourceProvider(
            global::NuGet.Configuration.Settings.LoadDefaultSettings(appHostDirectory.FullName)).LoadPackageSources();
        Assert.Equal(
            new[] { "https://private.example/v3/index.json", newHive }.Order(StringComparer.Ordinal),
            persistedSources.Select(static source => source.Source).Order(StringComparer.Ordinal));
    }
 
    [Fact]
    public async Task ChannelPolicy_CanChangeGeneratedSourceWithoutReusingItsCredentials()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var directory = workspace.WorkspaceRoot;
        await File.WriteAllTextAsync(
            Path.Combine(directory.FullName, "NuGet.Config"),
            """
            <configuration>
              <packageSources>
                <clear />
                <add key="aspire-test" value="https://old.example/v3/index.json" />
              </packageSources>
              <packageSourceCredentials>
                <aspire-test>
                  <add key="Username" value="test-user" />
                  <add key="ClearTextPassword" value="test-password" />
                </aspire-test>
              </packageSourceCredentials>
              <packageSourceMapping>
                <packageSource key="aspire-test"><package pattern="Aspire*" /></packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var service = CreateService();
        var configuration = service.BuildConfiguration(
            directory, "test", [new PackageMapping("Aspire*", "https://new.example/v3/index.json")],
            restrictToSelectedSources: false, hasAuthoritativeAspirePolicy: true,
            cancellationToken: TestContext.Current.CancellationToken);
        using var preview = await service.CreateConfigurationPreviewAsync(
            directory, configuration, globalPackagesFolder: null, TestContext.Current.CancellationToken);
        var previewSettings = global::NuGet.Configuration.Settings.LoadDefaultSettings(preview.EffectiveWorkingDirectory.FullName);
        var selectedSource = new global::NuGet.Configuration.PackageSourceProvider(previewSettings).LoadPackageSources()
            .Single(static source => source.IsEnabled);
 
        Assert.Equal("aspire-test-0", selectedSource.Name);
        Assert.Null(selectedSource.Credentials);
        Assert.Equal(["https://new.example/v3/index.json"], NuGetTestHelper.GetEligiblePackageSources(preview.EffectiveWorkingDirectory.FullName, "Aspire.Hosting"));
        await new DotNetAppHostNuGetConfigMerger(service).CreateOrUpdateAsync(
            directory, configuration, createIfMissing: false, globalPackagesFolder: null,
            confirmationCallback: null, TestContext.Current.CancellationToken);
        Assert.Equal(["https://new.example/v3/index.json"], NuGetTestHelper.GetEligiblePackageSources(directory.FullName, "Aspire.Hosting"));
    }
 
    [Fact]
    public void IsPackageSourceMappingEnabled_UsesNuGetConfigHierarchy()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var projectDirectory = workspace.CreateDirectory("AppHost");
        var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "NuGet.Config");
        File.WriteAllText(
            configPath,
            """
            <configuration>
              <packageSourceMapping>
                <packageSource key="private">
                  <package pattern="Aspire*" />
                </packageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var service = CreateService();
 
        var enabled = service.IsPackageSourceMappingEnabled(
            projectDirectory,
            TestContext.Current.CancellationToken);
 
        Assert.True(enabled);
    }
 
    [Fact]
    public void IsPackageSourceMappingEnabled_MatchesNuGetSectionAndItemSemantics()
    {
        using var workspace = TemporaryWorkspace.CreateForCli(outputHelper);
        var nonCanonicalDirectory = workspace.CreateDirectory("noncanonical");
        var mappingThenClearDirectory = workspace.CreateDirectory("mapping-then-clear");
        var clearThenMappingDirectory = workspace.CreateDirectory("clear-then-mapping");
 
        File.WriteAllText(
            Path.Combine(nonCanonicalDirectory.FullName, "NuGet.Config"),
            """
            <configuration>
              <PackageSourceMapping>
                <packageSource key="private">
                  <package pattern="*" />
                </packageSource>
              </PackageSourceMapping>
              <packageSourceMapping>
                <clear />
              </packageSourceMapping>
            </configuration>
            """);
        File.WriteAllText(
            Path.Combine(mappingThenClearDirectory.FullName, "NuGet.Config"),
            """
            <configuration>
              <packageSourceMapping>
                <packageSource key="private">
                  <package pattern="*" />
                </packageSource>
                <clear />
              </packageSourceMapping>
            </configuration>
            """);
        File.WriteAllText(
            Path.Combine(clearThenMappingDirectory.FullName, "NuGet.Config"),
            """
            <configuration>
              <packageSourceMapping>
                <clear />
                <PackageSource key="private">
                  <package pattern="*" />
                </PackageSource>
              </packageSourceMapping>
            </configuration>
            """);
        var service = CreateService();
 
        Assert.False(service.IsPackageSourceMappingEnabled(
            nonCanonicalDirectory,
            TestContext.Current.CancellationToken));
        Assert.False(service.IsPackageSourceMappingEnabled(
            mappingThenClearDirectory,
            TestContext.Current.CancellationToken));
        Assert.True(service.IsPackageSourceMappingEnabled(
            clearThenMappingDirectory,
            TestContext.Current.CancellationToken));
    }
 
    private static BundleNuGetService CreateService()
    {
        var nuGetClient = new NuGetClient(
            new TestFeatures(),
            new TestEnvironment(),
            NullLogger<NuGetClient>.Instance);
        var bundleNuGetService = new BundleNuGetService(
            NullLogger<BundleNuGetService>.Instance,
            nuGetClient);
        return bundleNuGetService;
    }
}