File: NuGet\NuGetConfigurationTests.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.Globalization;
using Aspire.Cli.DotNet;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Tests.TestServices;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Cli.Tests.NuGet;
 
public class NuGetConfigurationTests
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BuildStable_PreservesProtectedChannelSourcesWithNarrowMappings(bool isEnabled)
    {
        var settings = CreateSettings(
        [
            new("aspire-daily", ["Aspire.Hosting.Private", "Private.*"]),
            new("public", ["*"])
        ]) with
        {
            Sources =
            [
                new("aspire-daily", "integration", isEnabled, true, false) { IsIntegrationSource = true },
                new("public", "public", true, false, false)
            ],
            DisabledPackageSourceKeys = isEnabled ? [] : ["aspire-daily"]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = DotNetAppHostNuGetConfigBuilder.BuildStable(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.RetiredSourceKeys);
        Assert.Equal(settings.DisabledPackageSourceKeys, overlay.DisabledPackageSourceKeys);
        Assert.Equal(["Aspire.Hosting.Private", "Private.*"],
            Assert.Single(overlay.PackageSourceMappings, mapping => mapping.SourceKey == "aspire-daily").Patterns);
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Empty(persistence.SourceChanges.RetiredSourceKeys);
        Assert.Empty(persistence.RemovedPackageSourceMappings);
    }
 
    [Theory]
    [InlineData("Aspire*")]
    [InlineData("*")]
    public void BuildStable_RequiresManualConfigurationForAmbiguousIntegrationRouting(string pattern)
    {
        var settings = CreateSettings([new("aspire-daily", [pattern])]) with
        {
            ConfigPaths = ["nuget.config"],
            Sources = [new("aspire-daily", "integration", true, false, false) { IsIntegrationSource = true }]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var exception = Assert.Throws<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigBuilder.BuildStable(settings, "test", channel));
 
        Assert.Equal(string.Format(CultureInfo.CurrentCulture,
            UpdateCommandStrings.ChannelFeedAlreadyConfiguredFormat, "nuget.config", "aspire-daily"), exception.Message);
    }
 
    [Theory]
    [InlineData("Aspire*", "Contoso.*", false)]
    [InlineData("*", "Contoso.*", false)]
    [InlineData("Aspire*", "Aspire.Hosting.Private", false)]
    [InlineData("Aspire*", "Contoso.*", true)]
    [InlineData("Aspire*", "Aspire.Hosting.Private", true)]
    public void BuildStable_RequiresManualConfigurationWhenNarrowerRoutingIsInsufficient(
        string broadPattern, string narrowPattern, bool separateAlias)
    {
        var settings = CreateSettings(separateAlias
            ? [new("aspire-daily", [broadPattern]), new("company", [narrowPattern])]
            : [new("aspire-daily", [broadPattern, narrowPattern])]) with
        {
            ConfigPaths = ["nuget.config"],
            Sources =
            [
                new("aspire-daily", "integration", true, false, false) { IsIntegrationSource = true },
                new("company", "integration", true, false, false) { IsIntegrationSource = true }
            ]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var exception = Assert.Throws<ProjectUpdaterException>(() =>
            DotNetAppHostNuGetConfigBuilder.BuildStable(settings, "test", channel));
 
        Assert.Equal(string.Format(CultureInfo.CurrentCulture,
            UpdateCommandStrings.ChannelFeedAlreadyConfiguredFormat, "nuget.config", "aspire-daily"), exception.Message);
    }
 
    [Theory]
    [InlineData("aspire-integration-team")]
    [InlineData("company")]
    public void BuildStable_PreservesExplicitIntegrationAliasMappings(string sourceKey)
    {
        var settings = CreateSettings([new(sourceKey, ["Aspire*", "*"])]) with
        {
            Sources = [new(sourceKey, "integration", true, false, false) { IsIntegrationSource = true }]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = DotNetAppHostNuGetConfigBuilder.BuildStable(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Equal(["Aspire*", "*"], Assert.Single(overlay.PackageSourceMappings).Patterns);
        Assert.Empty(overlay.RetiredSourceKeys);
        Assert.Empty(Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig).RemovedPackageSourceMappings);
    }
 
    [Theory]
    [InlineData("Contoso.Hosting.One", null)]
    [InlineData("Contoso.Hosting.One", "*")]
    [InlineData("Contoso.Hosting.One", "Aspire*")]
    [InlineData("Aspire.Hosting.Custom", null)]
    [InlineData("Aspire.Hosting.Custom", "*")]
    [InlineData("Aspire.Hosting.Custom", "Aspire*")]
    public void Build_ExactIntegrationSelectionPreservesSdkChannelSources(string packageId, string? sdkPattern)
    {
        const string integrationSource = "https://team.example/v3/index.json";
        const string sdkSourceKey = "aspire-pr-21366";
        var settings = CreateSettings(sdkPattern is null
            ? []
            : [new(sdkSourceKey, [sdkPattern]), new("public", ["*"])]) with
        {
            Sources =
            [
                new(sdkSourceKey, "sdk", true, false, false) { IsCliManaged = true },
                new("public", "public", true, false, false)
            ]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "integration-add", [new(packageId, integrationSource) { SourceKey = "team" }]);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.RetiredSourceKeys);
        Assert.Empty(overlay.DisabledPackageSourceKeys);
        var sdkMapping = Assert.Single(overlay.PackageSourceMappings, mapping => mapping.SourceKey == sdkSourceKey);
        Assert.Equal([sdkPattern ?? "*"], sdkMapping.Patterns);
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Empty(persistence.SourceChanges.RetiredSourceKeys);
        Assert.Empty(persistence.RemovedPackageSourceMappings);
        Assert.Equal([("aspire-integration-team", integrationSource)], persistence.SourceChanges.Sources);
    }
 
    [Theory]
    [InlineData("team")]
    [InlineData("aspire-integration-team")]
    public void Build_NamedFeedSwitchRemovesOnlyThePreviousExactIntegrationMapping(string previousKey)
    {
        const string previousSource = "https://team.example/v3/index.json";
        const string selectedSource = "https://other.example/v3/index.json";
        var settings = CreateSettings(
        [
            new(previousKey, ["Contoso.Hosting.One", "Contoso.Hosting.Keep", "Contoso.Internal.*"]),
            new("public", ["*"]),
            new("sdk", ["Aspire*"])
        ]);
        settings = settings with
        {
            Sources =
            [
                new(previousKey, NuGetSourceIdentity.Compute(previousSource, settings.SourceIdentityKey),
                    true, true, false) { IsIntegrationSource = true },
                new("public", "public", true, false, false),
                new("sdk", "sdk", true, false, false)
            ]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "integration-add", [new("Contoso.Hosting.One", selectedSource) { SourceKey = "other" }]);
 
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        var removed = Assert.Single(persistence.RemovedPackageSourceMappings);
        Assert.Equal(previousKey, removed.SourceKey);
        Assert.Equal(["Contoso.Hosting.One"], removed.Patterns);
        var added = Assert.Single(persistence.PackageSourceMappings);
        Assert.Equal("aspire-integration-other", added.SourceKey);
        Assert.Equal(["Contoso.Hosting.One"], added.Patterns);
        Assert.Empty(persistence.SourceChanges.RetiredSourceKeys);
    }
 
    [Theory]
    [InlineData("Contoso.*", "*", true, false)]
    [InlineData("contoso.*", "*", true, false)]
    [InlineData("Contoso.Hosting.One", "Contoso.*", true, false)]
    [InlineData("*", "Contoso.*", true, true)]
    [InlineData("Contoso.*", "Contoso.Hosting.*", true, true)]
    [InlineData("Contoso.*", "Contoso.Hosting.One", true, true)]
    [InlineData("*", "*", true, true)]
    [InlineData("Contoso.*", "Contoso.*", true, true)]
    [InlineData("Contoso.*", "*", false, true)]
    public void Build_ExistingIntegrationMappingMustUniquelyRouteToSelectedFeed(
        string selectedPattern, string otherPattern, bool selectedSourceEnabled, bool requiresMapping)
    {
        const string selectedSource = "https://selected.example/v3/index.json";
        const string otherSource = "https://other.example/v3/index.json";
        var settings = CreateSettings([new("company-feed", [selectedPattern]), new("other", [otherPattern])]);
        settings = settings with
        {
            Sources =
            [
                new("company-feed", NuGetSourceIdentity.Compute(selectedSource, settings.SourceIdentityKey),
                    selectedSourceEnabled, true, false) { IsIntegrationSource = true },
                new("other", NuGetSourceIdentity.Compute(otherSource, settings.SourceIdentityKey), true, false, false)
            ],
            DisabledPackageSourceKeys = selectedSourceEnabled ? [] : ["company-feed"]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Contoso.Hosting.One", selectedSource) { SourceKey = "team" }]);
 
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Equal(requiresMapping ? ["company-feed"] : [],
            persistence.PackageSourceMappings.Select(static mapping => mapping.SourceKey));
        Assert.Empty(persistence.SourceChanges.Sources);
        Assert.Empty(persistence.RemovedPackageSourceMappings);
    }
 
    [Fact]
    public void Build_ExistingIntegrationMappingCanUseMultipleAliasesForSameFeed()
    {
        const string source = "https://selected.example/v3/index.json";
        var settings = CreateSettings([new("authenticated", ["Contoso.*"]), new("anonymous", ["Contoso.*"])]);
        var identity = NuGetSourceIdentity.Compute(source, settings.SourceIdentityKey);
        settings = settings with
        {
            Sources =
            [
                new("authenticated", identity, true, true, false) { IsIntegrationSource = true },
                new("anonymous", identity, true, false, false) { IsIntegrationSource = true }
            ]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Contoso.Hosting.One", source) { SourceKey = "team" }]);
 
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Equal(["authenticated", "anonymous"], persistence.SelectedSourceKeys);
        Assert.Empty(persistence.PackageSourceMappings);
        Assert.Empty(persistence.SourceChanges.Sources);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BuildStable_PreservesDisabledConfiguredIntegrationSources(bool hasMappings)
    {
        var settings = CreateSettings(hasMappings
            ? [new("aspire-daily", ["Aspire.Hosting.Private"]), new("aspire-staging", ["Aspire*"])]
            : []) with
        {
            Sources =
            [
                new("aspire-daily", "integration", false, false, false) { IsIntegrationSource = true },
                new("aspire-staging", "channel", true, false, false)
            ],
            DisabledPackageSourceKeys = ["aspire-daily"]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = DotNetAppHostNuGetConfigBuilder.BuildStable(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Equal(["aspire-staging"], overlay.RetiredSourceKeys);
        Assert.Equal(["aspire-daily", "aspire-staging"], overlay.DisabledPackageSourceKeys);
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Equal(["aspire-staging"], persistence.SourceChanges.RetiredSourceKeys);
        Assert.Empty(persistence.RemovedPackageSourceMappings);
    }
 
    [Theory]
    [InlineData("Aspire*", false)]
    [InlineData("Aspire.Hosting.Private", true)]
    public void Build_ConfiguredIntegrationMappingsSurviveChannelChangesButAllowExactSelection(string selectedPattern, bool replacesMapping)
    {
        var settings = CreateSettings([new("aspire-daily", ["Aspire.Hosting.Private"])]) with
        {
            Sources =
            [
                new("aspire-daily", "integration", true, false, false) { IsIntegrationSource = true }
            ]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new(selectedPattern, "https://selected.example/v3/index.json")]);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.RetiredSourceKeys);
        Assert.Equal(
            replacesMapping ? ["aspire-test"] : ["aspire-daily", "aspire-test"],
            overlay.PackageSourceMappings.Select(static mapping => mapping.SourceKey));
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Empty(persistence.SourceChanges.RetiredSourceKeys);
        Assert.Equal(replacesMapping ? ["aspire-daily"] : [],
            persistence.RemovedPackageSourceMappings.Select(static mapping => mapping.SourceKey));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void BuildStableAppHostConfiguration_PreservesExistingPublicAliasAndDisabledState(bool isEnabled)
    {
        var settings = CreateSettings(
        [
            new("company", ["*"]),
            new("aspire-test", ["Aspire*"]),
            new("aspire-test-0", ["Aspire*"])
        ]);
        settings = settings with
        {
            Sources =
            [
                new("company", "company", true, false, false),
                new("aspire-test", "daily", true, false, false),
                new("aspire-test-0", NuGetSourceIdentity.Compute(PackageSources.NuGetOrg, settings.SourceIdentityKey), isEnabled, true, false)
            ],
            DisabledPackageSourceKeys = isEnabled ? [] : ["aspire-test-0"]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable,
            [new("*", "https://company.example/v3/index.json")],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = NuGetConfigurationBuilder.BuildStableAppHostConfiguration(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.Sources);
        Assert.Equal(["aspire-test"], overlay.RetiredSourceKeys);
        Assert.Equal(isEnabled ? ["aspire-test"] : ["aspire-test-0", "aspire-test"], overlay.DisabledPackageSourceKeys);
        Assert.Collection(overlay.PackageSourceMappings,
            mapping =>
            {
                Assert.Equal("company", mapping.SourceKey);
                Assert.Equal(["*"], mapping.Patterns);
            },
            mapping =>
            {
                Assert.Equal("aspire-test-0", mapping.SourceKey);
                Assert.Equal(["Aspire*"], mapping.Patterns);
            });
    }
 
    [Theory]
    [InlineData("aspire-test", false)]
    [InlineData("legacy-hive", true)]
    [InlineData("ASPIRE-DAILY", false)]
    [InlineData("aspire-staging", false)]
    [InlineData("aspire-pr-19763", false)]
    [InlineData("aspire-apphost-0123456789abcdef", false)]
    [InlineData("aspire-apphost-0123456789abcdef-0", false)]
    public void BuildStableAppHostConfiguration_PreservesUnrelatedMappingsOnChannelSource(string sourceKey, bool isCliManaged)
    {
        var settings = CreateSettings(
        [
            new("company", ["Aspire.Hosting.Redis", "Company.*"]),
            new(sourceKey, ["Aspire*", "Private.*"])
        ]) with
        {
            Sources =
            [
                new("company", "company", true, true, true),
                new(sourceKey, "channel", true, true, false) { IsCliManaged = isCliManaged }
            ]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = NuGetConfigurationBuilder.BuildStableAppHostConfiguration(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.Sources);
        Assert.Empty(overlay.RetiredSourceKeys);
        Assert.False(overlay.ClearDisabledPackageSources);
        Assert.Collection(overlay.PackageSourceMappings,
            mapping =>
            {
                Assert.Equal("company", mapping.SourceKey);
                Assert.Equal(["Aspire.Hosting.Redis", "Company.*"], mapping.Patterns);
            },
            mapping =>
            {
                Assert.Equal(sourceKey, mapping.SourceKey);
                Assert.Equal(["Private.*"], mapping.Patterns);
            });
    }
 
    [Fact]
    public void BuildStableAppHostConfiguration_DoesNotAddFallbackWhenOnlyChannelSourceExists()
    {
        var settings = CreateSettings([new("aspire-test", ["Aspire*"])]) with
        {
            Sources = [new("aspire-test", "daily", true, false, false)]
        };
        var channel = PackageChannel.CreateExplicitChannel(
            "stable", PackageChannelQuality.Stable, [new("*", PackageSources.NuGetOrg)],
            new FakeNuGetPackageCache(), new TestFeatures(), NullLogger.Instance);
 
        var configuration = NuGetConfigurationBuilder.BuildStableAppHostConfiguration(settings, "test", channel);
 
        var overlay = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Empty(overlay.Sources);
        Assert.Empty(overlay.PackageSourceMappings);
        Assert.Equal(["aspire-test"], overlay.RetiredSourceKeys);
        Assert.Equal(["aspire-test"], overlay.DisabledPackageSourceKeys);
        Assert.True(overlay.ClearPackageSourceMappings);
    }
 
    [Fact]
    public void Build_PersistenceLeavesCompetingUserFeedAlone()
    {
        var settings = CreateSettings(
        [
            new("company", ["Aspire.Hosting.Redis"]),
            new("aspire-staging", ["Aspire*"])
        ]) with
        {
            Sources =
            [
                new("company", "company", true, true, true),
                new("aspire-staging", "staging", true, false, false),
                new("disabled", "disabled", false, false, false)
            ],
            DisabledPackageSourceKeys = ["disabled"]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Aspire*", "https://selected.example/v3/index.json")]);
 
        var operation = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.Equal(["company", "aspire-staging"], operation.RetiredSourceKeys);
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Equal(["aspire-staging"], persistence.SourceChanges.RetiredSourceKeys);
        Assert.False(persistence.SourceChanges.ClearDisabledPackageSources);
        Assert.Empty(persistence.SourceChanges.DisabledPackageSourceKeys);
        Assert.Empty(persistence.SourceChanges.PackageSourceMappings);
        Assert.False(persistence.SourceChanges.ClearPackageSourceMappings);
        var mapping = Assert.Single(persistence.PackageSourceMappings);
        Assert.Equal("aspire-test", mapping.SourceKey);
        Assert.Equal(["Aspire*"], mapping.Patterns);
        Assert.Equal(settings.PackageSourceMappings, configuration.Settings.PackageSourceMappings);
        Assert.Equal(settings.DisabledPackageSourceKeys, configuration.Settings.DisabledPackageSourceKeys);
    }
 
    [Fact]
    public void Build_PersistencePreservesUnrelatedPatternsOnCliFeed()
    {
        var settings = CreateSettings([new("aspire-staging", ["Aspire*", "Private.*"])]) with
        {
            Sources = [new("aspire-staging", "staging", true, false, false)]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Aspire*", "https://selected.example/v3/index.json")]);
 
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Empty(persistence.SourceChanges.RetiredSourceKeys);
        Assert.Empty(persistence.SourceChanges.DisabledPackageSourceKeys);
        var removed = Assert.Single(persistence.RemovedPackageSourceMappings);
        Assert.Equal("aspire-staging", removed.SourceKey);
        Assert.Equal(["Aspire*"], removed.Patterns);
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(false, true)]
    [InlineData(true, false)]
    [InlineData(true, true)]
    public void Build_PersistenceDoesNotWriteDefaultSourceBindings(bool hasAmbientDefaultSource, bool hasAmbientMappings)
    {
        const string channelSource = "https://channel.example/v3/index.json";
        var settings = CreateSettings(hasAmbientMappings ? [new("company-public", ["*"])] : []);
        if (hasAmbientDefaultSource)
        {
            settings = settings with
            {
                Sources =
                [
                    new("company-public", NuGetSourceIdentity.Compute(PackageSources.NuGetOrg, settings.SourceIdentityKey),
                        true, false, false)
                ]
            };
        }
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Aspire*", channelSource), new("*", PackageSources.NuGetOrg)]);
 
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.Equal([("aspire-test", channelSource)], persistence.SourceChanges.Sources);
        if (hasAmbientMappings)
        {
            var mapping = Assert.Single(persistence.PackageSourceMappings);
            Assert.Equal("aspire-test", mapping.SourceKey);
            Assert.Equal(["Aspire*"], mapping.Patterns);
        }
        else
        {
            Assert.Empty(persistence.PackageSourceMappings);
        }
        Assert.Equal(2, configuration.ConfigSources.Count);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void Build_PersistenceDoesNotReenableDisabledChannelOrFallbackSources(bool channelIsDisabled)
    {
        const string channelSource = "https://selected.example/v3/index.json";
        var settings = CreateSettings([new("company", ["*"])]);
        settings = settings with
        {
            Sources =
            [
                new("company", "company", true, false, false),
                new("aspire-test", NuGetSourceIdentity.Compute(channelSource, settings.SourceIdentityKey),
                    !channelIsDisabled, false, false),
                new("nuget.org", NuGetSourceIdentity.Compute(PackageSources.NuGetOrg, settings.SourceIdentityKey),
                    false, false, false)
            ],
            DisabledPackageSourceKeys = channelIsDisabled ? ["aspire-test", "nuget.org"] : ["nuget.org"]
        };
 
        var configuration = DotNetAppHostNuGetConfigBuilder.Build(
            settings, "test", [new("Aspire*", channelSource), new("*", PackageSources.NuGetOrg)]);
 
        var operation = Assert.IsType<NuGetConfigOverlay>(configuration.Overlay);
        Assert.True(operation.ClearDisabledPackageSources);
        Assert.Empty(operation.DisabledPackageSourceKeys);
        var persistence = Assert.IsType<DotNetAppHostNuGetConfig>(configuration.AppHostConfig);
        Assert.False(persistence.SourceChanges.ClearDisabledPackageSources);
        Assert.Empty(persistence.SourceChanges.DisabledPackageSourceKeys);
        Assert.Equal(settings.DisabledPackageSourceKeys, configuration.Settings.DisabledPackageSourceKeys);
    }
 
    [Fact]
    public void HasSourcePolicyChanges_AmbientPolicyIsUnchanged()
    {
        var configuration = new NuGetConfiguration(CreateSettings([]), [], overlay: null);
 
        Assert.False(configuration.HasSourcePolicyChanges);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void HasSourcePolicyChanges_MappingOrderAndCaseAreIgnored(bool clearMappings)
    {
        var settings = CreateSettings(
        [
            new("company", ["Aspire*", "Company.*"]),
            new("public", ["*"])
        ]);
        var overlay = new NuGetConfigOverlay(
            [], [new("PUBLIC", ["*"]), new("COMPANY", ["company.*", "ASPIRE*"])], false, [], null)
        {
            ClearPackageSourceMappings = clearMappings
        };
 
        Assert.False(new NuGetConfiguration(settings, [], overlay).HasSourcePolicyChanges);
    }
 
    [Theory]
    [InlineData("company", "Other.*")]
    [InlineData("other", "Aspire*")]
    public void HasSourcePolicyChanges_ChangedMappingIsDetected(string sourceKey, string pattern)
    {
        var settings = CreateSettings([new("company", ["Aspire*"])]);
        var overlay = new NuGetConfigOverlay([], [new(sourceKey, [pattern])], false, [], null);
 
        Assert.True(new NuGetConfiguration(settings, [], overlay).HasSourcePolicyChanges);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void HasSourcePolicyChanges_EmptyMappingOnlyChangesPolicyWhenCleared(bool clearMappings)
    {
        var settings = CreateSettings([new("company", ["Aspire*"])]);
        var overlay = new NuGetConfigOverlay([], [], false, [], null)
        {
            ClearPackageSourceMappings = clearMappings
        };
 
        Assert.Equal(clearMappings, new NuGetConfiguration(settings, [], overlay).HasSourcePolicyChanges);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void HasSourcePolicyChanges_DisabledSourcesAreComparedByEffectiveState(bool reenableSource)
    {
        var settings = CreateSettings([]) with
        {
            Sources = [new("company", "identity", false, false, false)],
            DisabledPackageSourceKeys = ["company"]
        };
        var overlay = new NuGetConfigOverlay([], [], true, reenableSource ? [] : ["COMPANY"], null);
 
        Assert.Equal(reenableSource, new NuGetConfiguration(settings, [], overlay).HasSourcePolicyChanges);
    }
 
    [Fact]
    public void HasSourcePolicyChanges_NewSourceIsDetected()
    {
        var overlay = new NuGetConfigOverlay([("new", "https://feed.example/v3/index.json")], [], false, [], null);
 
        Assert.True(new NuGetConfiguration(CreateSettings([]), [], overlay).HasSourcePolicyChanges);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void HasSourcePolicyChanges_RetiredSourceOnlyChangesPolicyWhenStillEnabled(bool isEnabled)
    {
        var overlay = new NuGetConfigOverlay([], [], false, [], null) { RetiredSourceKeys = ["old"] };
        var settings = CreateSettings([]) with
        {
            Sources = [new("old", "identity", isEnabled, false, false)]
        };
 
        Assert.Equal(isEnabled, new NuGetConfiguration(settings, [], overlay).HasSourcePolicyChanges);
    }
 
    [Fact]
    public void HasSourcePolicyChanges_CacheFolderIsNotASourcePolicyChange()
    {
        var overlay = new NuGetConfigOverlay([], [], false, [], ".packages");
 
        Assert.False(new NuGetConfiguration(CreateSettings([]), [], overlay).HasSourcePolicyChanges);
    }
 
    private static NuGetSettingsInfo CreateSettings(IReadOnlyList<NuGetPackageSourceMapping> mappings)
        => new([], "ambient", [], [], mappings, [], [], new byte[NuGetSourceIdentity.KeySizeInBytes]);
}