File: ToolPackageDownloaderTests.cs
Web Access
Project: ..\..\..\test\Microsoft.DotNet.PackageInstall.Tests\Microsoft.DotNet.PackageInstall.Tests.csproj (Microsoft.DotNet.PackageInstall.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Transactions;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools.Tests.ComponentMocks;
using Microsoft.Extensions.DependencyModel.Tests;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Frameworks;
using NuGet.Versioning;
using NuGet.Configuration;
 
namespace Microsoft.DotNet.PackageInstall.Tests
{
    internal class DotnetEnvironmentTestFixture : IDisposable
    {
        private readonly string _originalPath;
        private const string _PATH_VAR_NAME = "PATH";
 
        public DotnetEnvironmentTestFixture()
        {
            string dotnetRootUnderTest = TestContext.Current.ToolsetUnderTest.DotNetRoot;
            _originalPath = Environment.GetEnvironmentVariable(_PATH_VAR_NAME);
            Environment.SetEnvironmentVariable(_PATH_VAR_NAME, dotnetRootUnderTest + Path.PathSeparator + _originalPath);
        }
 
        public void Dispose() => Environment.SetEnvironmentVariable(_PATH_VAR_NAME, _originalPath);
    }
 
    [Collection(nameof(TestToolBuilderCollection))]
    public class ToolPackageDownloaderTests : SdkTest, IClassFixture<DotnetEnvironmentTestFixture>
    {
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenNugetConfigInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true,
                identiifer: testMockBehaviorIsInSync.ToString());
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenNugetConfigInstallSucceedsInTransaction(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            IToolPackage package = null;
            using (var transactionScope = new TransactionScope(
                TransactionScopeOption.Required,
                TimeSpan.Zero))
            {
                package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                    packageId: TestPackageId,
                    verbosity: TestVerbosity,
                    versionRange: VersionRange.Parse(TestPackageVersion),
                    targetFramework: _testTargetframework,
                    isGlobalTool: true,
                    verifySignatures: false);
 
                transactionScope.Complete();
            }
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenNugetConfigInstallCreatesAnAssetFile(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            /*
              From mytool.dll to project.assets.json
               <root>/packageid/version/packageid/version/tools/framework/rid/mytool.dll
                                       /project.assets.json
             */
            var assetJsonPath = package.Command.Executable
                .GetDirectoryPath()
                .GetParentPath()
                .GetParentPath()
                .GetParentPath()
                .GetParentPath()
                .GetParentPath()
                .WithFile("project.assets.json").Value;
 
            fileSystem.File.Exists(assetJsonPath).Should().BeTrue();
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAConfigFileRootDirectoryPackageInstallSucceedsViaFindingNugetConfigInParentDir(
            bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var subDirectory = testDir.WithSubDirectories("sub");
            fileSystem.Directory.CreateDirectory(subDirectory.Value);
 
            var package = downloader.InstallPackage(
                new PackageLocation(rootConfigDirectory: subDirectory),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAllButNoPackageVersionItReturnLatestStableVersion(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.GetNuGetVersion(
                new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: TestPackageId,
                verbosity: TestVerbosity).version;
 
            package.OriginalVersion.Should().Be(TestPackageVersion);
        }
 
        [Theory]
        [InlineData(false, "1.0.0-rc*", TestPackageVersion)]
        [InlineData(true, "1.0.0-rc*", TestPackageVersion)]
        [InlineData(false, "1.*", TestPackageVersion)]
        [InlineData(true, "1.*", TestPackageVersion)]
        [InlineData(false, TestPackageVersion, TestPackageVersion)]
        [InlineData(true, TestPackageVersion, TestPackageVersion)]
        public void GivenASpecificVersionGetCorrectVersion(bool testMockBehaviorIsInSync, string requestedVersion, string expectedVersion)
        {
 
            var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(emptySource);
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.GetNuGetVersion(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config"),
                    additionalFeeds: new[] { emptySource }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(requestedVersion)).version;
 
            package.OriginalVersion.Should().Be(expectedVersion);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAllButNoPackageVersionItCanInstallThePackage(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(
                new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAllButNoTargetFrameworkItCanDownloadThePackage(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenASourceInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var package = downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenARelativeSourcePathInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            //  CI seems to be getting an old version of the global.tool.console.demo package which targets .NET Core 2.1.  This may fix that
            ToolBuilder.RemovePackageFromGlobalPackages(Log, TestPackageId.ToString(), TestPackageVersion);
 
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var relativePath = Path.GetRelativePath(testDir.Value, source);
 
            Log.WriteLine("Root path: " + testDir.Value);
            Log.WriteLine("Relative path: " + relativePath);
            Log.WriteLine("Current Directory: " + Directory.GetCurrentDirectory());
 
            var package = downloader.InstallPackage(
                new PackageLocation(additionalFeeds: new[] {relativePath}),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAUriSourceInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            //  CI seems to be getting an old version of the global.tool.console.demo package which targets .NET Core 2.1.  This may fix that
            ToolBuilder.RemovePackageFromGlobalPackages(Log, TestPackageId.ToString(), TestPackageVersion);
 
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var package = downloader.InstallPackage(
                new PackageLocation(additionalFeeds: new[] { new Uri(source).AbsoluteUri }), packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAEmptySourceAndNugetConfigInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(emptySource);
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config"),
                    additionalFeeds: new[] { emptySource }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenFailureAfterRestoreInstallWillRollback(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            static void FailedStepAfterSuccessRestore() => throw new GracefulException("simulated error");
 
            Action a = () =>
            {
                using (var t = new TransactionScope(
                    TransactionScopeOption.Required,
                    TimeSpan.Zero))
                {
                    downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        isGlobalTool: true,
                        verifySignatures: false);
 
                    FailedStepAfterSuccessRestore();
                    t.Complete();
                }
            };
 
            a.Should().Throw<GracefulException>().WithMessage("simulated error");
 
            AssertInstallRollBack(fileSystem, store);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenSecondInstallInATransactionTheFirstInstallShouldRollback(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            Action a = () =>
            {
                using (var t = new TransactionScope(
                    TransactionScopeOption.Required,
                    TimeSpan.Zero))
                {
                    Action first = () => downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        isGlobalTool: true,
                        verifySignatures: false);
 
                    first.Should().NotThrow();
 
                    downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        isGlobalTool: true,
                        verifySignatures: false);
 
                    t.Complete();
                }
            };
 
            a.Should().Throw<ToolPackageException>().Where(
                ex => ex.Message ==
                      string.Format(
                          CliStrings.ToolPackageConflictPackageId,
                          TestPackageId,
                          TestPackageVersion));
 
            AssertInstallRollBack(fileSystem, store);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenFailureWhenInstallLocalToolsItWillRollbackPackageVersion(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            static void FailedStepAfterSuccessDownload() => throw new GracefulException("simulated error");
            ISettings settings = Settings.LoadDefaultSettings(Directory.GetCurrentDirectory());
            var localToolDownloadDir = Path.Combine(new DirectoryPath(SettingsUtility.GetGlobalPackagesFolder(settings)).ToString().Trim('"'), TestPackageId.ToString());
            var localToolVersionDir = Path.Combine(localToolDownloadDir, TestPackageVersion.ToString());
 
            if (fileSystem.Directory.Exists(localToolVersionDir))
            {
                fileSystem.Directory.Delete(localToolVersionDir, true);
            }
 
            Action a = () =>
            {
                using (var t = new TransactionScope(
                    TransactionScopeOption.Required,
                    TimeSpan.Zero))
                {
                    downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        verifySignatures: false);
 
                    fileSystem.Directory
                        .Exists(localToolDownloadDir)
                        .Should()
                        .BeTrue();
 
                    fileSystem.Directory
                        .Exists(localToolVersionDir)
                        .Should()
                        .BeTrue();
 
                    FailedStepAfterSuccessDownload();
                    t.Complete();
                }
            };
 
            a.Should().Throw<GracefulException>().WithMessage("simulated error");
            
            fileSystem
            .Directory
                .Exists(localToolDownloadDir)
                .Should()
                .BeTrue();
 
            
            fileSystem
                .Directory
                .Exists(localToolVersionDir)
                .Should()
                .BeFalse();
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenSecondInstallOfLocalToolItShouldNotThrowException(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            Action a = () =>
            {
                using (var t = new TransactionScope(
                    TransactionScopeOption.Required,
                    TimeSpan.Zero))
                {
                    downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        verifySignatures: false);
 
 
                    downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                        packageId: TestPackageId,
                        verbosity: TestVerbosity,
                        versionRange: VersionRange.Parse(TestPackageVersion),
                        targetFramework: _testTargetframework,
                        verifySignatures: false);
 
                    t.Complete();
                }
            };
 
            a();
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenSecondInstallWithoutATransactionTheFirstShouldNotRollback(bool testMockBehaviorIsInSync)
        {
            new RunExeCommand(Log, "dotnet", "nuget", "locals", "all", "--list")
                .Execute().Should().Pass();
 
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var package = downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            Action secondCall = () => downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            reporter.Lines.Should().BeEmpty();
 
            secondCall.Should().Throw<ToolPackageException>().Where(
                ex => ex.Message ==
                      string.Format(
                          CliStrings.ToolPackageConflictPackageId,
                          TestPackageId,
                          TestPackageVersion));
 
            fileSystem
                .Directory
                .Exists(store.Root.WithSubDirectories(TestPackageId.ToString()).Value)
                .Should()
                .BeTrue();
 
            uninstaller.Uninstall(package.PackageDirectory);
 
            fileSystem
                .Directory
                .EnumerateFileSystemEntries(store.Root.WithSubDirectories(ToolPackageStoreAndQuery.StagingDirectory).Value)
                .Should()
                .BeEmpty();
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAnInstalledPackageUninstallRemovesThePackage(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false,
                identiifer: testMockBehaviorIsInSync.ToString());
 
            var package = downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
 
            storeQuery.EnumeratePackages().Should().BeEmpty();
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAnInstalledPackageUninstallRollsbackWhenTransactionFails(bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var package = downloader.InstallPackage(
                new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            using (var scope = new TransactionScope(
                TransactionScopeOption.Required,
                TimeSpan.Zero))
            {
                uninstaller.Uninstall(package.PackageDirectory);
 
                storeQuery.EnumeratePackages().Should().BeEmpty();
            }
 
            package = storeQuery.EnumeratePackageVersions(TestPackageId).First();
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAnInstalledPackageUninstallRemovesThePackageWhenTransactionCommits(
            bool testMockBehaviorIsInSync)
        {
            var source = GetTestLocalFeedPath();
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: false);
 
            var package = downloader.InstallPackage(new PackageLocation(additionalFeeds: new[] { source }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            using (var scope = new TransactionScope(
                TransactionScopeOption.Required,
                TimeSpan.Zero))
            {
                uninstaller.Uninstall(package.PackageDirectory);
                scope.Complete();
            }
 
            storeQuery.EnumeratePackages().Should().BeEmpty();
        }
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void GivenAPackageNameWithDifferentCaseItCanInstallThePackage(bool testMockBehaviorIsInSync)
        {
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config")),
                packageId: new PackageId("GlObAl.TooL.coNsoLe.DemO"),
                verbosity: TestVerbosity,
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [Fact]
        public void GivenARootWithNonAsciiCharacterInstallSucceeds()
        {
            var surrogate = char.ConvertFromUtf32(int.Parse("2A601", NumberStyles.HexNumber));
            string nonAscii = "ab Ṱ̺̺̕o 田中さん åä," + surrogate;
 
            var root = _testAssetsManager.CreateTestDirectory(testName: nonAscii, identifier: "root");
            var reporter = new BufferedReporter();
            var fileSystem = new FileSystemWrapper();
            var store = new ToolPackageStoreAndQuery(new DirectoryPath(root.Path));
 
            var nugetConfigPath = new FilePath(Path.Combine(root.Path, "NuGet.config"));
 
            WriteNugetConfigFile(fileSystem, nugetConfigPath, true);
 
            var testRuntimeJsonPath = Path.Combine(TestContext.Current.ToolsetUnderTest.SdkFolderUnderTest, "RuntimeIdentifierGraph.json");
 
            var downloader = new ToolPackageDownloader(
                store: store,
                testRuntimeJsonPath
                );
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: nugetConfigPath),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse(TestPackageVersion),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, store);
 
            new ToolPackageUninstaller(store).Uninstall(package.PackageDirectory);
        }
 
 
        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        // repro https://github.com/dotnet/cli/issues/9409
        public void GivenAComplexVersionRangeInstallSucceeds(bool testMockBehaviorIsInSync)
        {
            var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(emptySource);
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            var package = downloader.InstallPackage(new PackageLocation(nugetConfig: testDir.WithFile("NuGet.config"),
                    additionalFeeds: new[] { emptySource }),
                packageId: TestPackageId,
                verbosity: TestVerbosity,
                versionRange: VersionRange.Parse("1.0.0-rc*"),
                targetFramework: _testTargetframework,
                isGlobalTool: true,
                verifySignatures: false);
 
            AssertPackageInstall(reporter, fileSystem, package, store, storeQuery);
 
            uninstaller.Uninstall(package.PackageDirectory);
        }
 
        [UnixOnlyTheory]
        [InlineData(false)]
        [InlineData(true)]
        // repro https://github.com/dotnet/cli/issues/10101
        public void GivenAPackageWithCasingAndenUSPOSIXInstallSucceeds(bool testMockBehaviorIsInSync)
        {
 
            var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(emptySource);
 
            var packageId = new PackageId("Global.Tool.Console.Demo.With.Casing");
            var packageVersion = "2.0.4";
 
            var (store, storeQuery, downloader, uninstaller, reporter, fileSystem, testDir) = Setup(
                useMock: testMockBehaviorIsInSync,
                includeLocalFeedInNugetConfig: true);
 
            CultureInfo currentCultureBefore = CultureInfo.CurrentCulture;
            try
            {
                CultureInfo.CurrentCulture = new CultureInfo("en-US-POSIX");
                IToolPackage package = null;
                Action action = () => package = downloader.InstallPackage(
                    new PackageLocation(
                        nugetConfig: testDir.WithFile("NuGet.config"),
                        additionalFeeds: new[] { emptySource }),
                    packageId: packageId,
                    verbosity: TestVerbosity,
                    versionRange: VersionRange.Parse(packageVersion),
                    targetFramework: _testTargetframework,
                    isGlobalTool: true);
 
                action.Should().NotThrow<ToolConfigurationException>();
 
                fileSystem.File.Exists(package.Command.Executable.Value).Should().BeTrue($"{package.Command.Executable.Value} should exist");
 
                uninstaller.Uninstall(package.PackageDirectory);
            }
            finally
            {
                CultureInfo.CurrentCulture = currentCultureBefore;
            }
        }
 
        private static void AssertPackageInstall(
            BufferedReporter reporter,
            IFileSystem fileSystem,
            IToolPackage package,
            IToolPackageStore store,
            IToolPackageStoreQuery storeQuery)
        {
            reporter.Lines.Should().BeEmpty();
 
            package.Id.Should().Be(TestPackageId);
            package.Version.ToNormalizedString().Should().Be(TestPackageVersion);
            package.PackageDirectory.Value.Should().Contain(store.Root.Value);
            package.Frameworks.Should().BeEquivalentTo(TestFrameworks);
 
            storeQuery.EnumeratePackageVersions(TestPackageId)
                .Select(p => p.Version.ToNormalizedString())
                .Should()
                .Equal(TestPackageVersion);
 
            package.Command.Should().NotBeNull();
            fileSystem.File.Exists(package.Command.Executable.Value).Should()
                .BeTrue($"{package.Command.Executable.Value} should exist");
            package.Command.Executable.Value.Should().Contain(store.Root.Value);
        }
 
        private static void AssertInstallRollBack(IFileSystem fileSystem, IToolPackageStore store)
        {
            if (!fileSystem.Directory.Exists(store.Root.Value))
            {
                return;
            }
 
            fileSystem
                .Directory
                .EnumerateFileSystemEntries(store.Root.Value)
                .Should()
                .NotContain(e => Path.GetFileName(e) != ToolPackageStoreAndQuery.StagingDirectory);
 
            fileSystem
                .Directory
                .EnumerateFileSystemEntries(store.Root.WithSubDirectories(ToolPackageStoreAndQuery.StagingDirectory).Value)
                .Should()
                .BeEmpty();
        }
 
        private (IToolPackageStore, IToolPackageStoreQuery, IToolPackageDownloader, IToolPackageUninstaller, BufferedReporter, IFileSystem, DirectoryPath testDir
            ) Setup(
                bool useMock,
                bool includeLocalFeedInNugetConfig,
                [CallerMemberName] string callingMethod = "",
                string identiifer = null)
        {
            var root = new DirectoryPath(_testAssetsManager.CreateTestDirectory(callingMethod, identifier: useMock.ToString() + identiifer).Path);
            var reporter = new BufferedReporter();
 
            IFileSystem fileSystem;
            IToolPackageStore store;
            IToolPackageStoreQuery storeQuery;
            IToolPackageDownloader downloader;
            IToolPackageUninstaller uninstaller;
 
            var nugetConfigPath = new FilePath(Path.Combine(root.Value, "NuGet.config"));
 
            var toolsRoot = root.WithSubDirectories("tools");
 
 
            if (useMock)
            {
                fileSystem = new FileSystemMockBuilder().Build();
                var frameworksMap = new Dictionary<PackageId, IEnumerable<NuGetFramework>>()
                        { {TestPackageId, TestFrameworks } };
 
                WriteNugetConfigFile(fileSystem, nugetConfigPath, includeLocalFeedInNugetConfig);
                var storeAndQuery = new ToolPackageStoreAndQuery(toolsRoot, fileSystem);
                store = storeAndQuery;
                storeQuery = storeAndQuery;
                downloader = new ToolPackageDownloaderMock2(storeAndQuery,
                    runtimeJsonPathForTests: TestContext.GetRuntimeGraphFilePath(),
                    currentWorkingDirectory: root.Value,
                    fileSystem);
 
                uninstaller = new ToolPackageUninstallerMock(fileSystem, storeAndQuery);
            }
            else
            {
                fileSystem = new FileSystemWrapper();
                WriteNugetConfigFile(fileSystem, nugetConfigPath, includeLocalFeedInNugetConfig);
                var toolPackageStore = new ToolPackageStoreAndQuery(toolsRoot);
                store = toolPackageStore;
                storeQuery = toolPackageStore;
                var testRuntimeJsonPath = Path.Combine(TestContext.Current.ToolsetUnderTest.SdkFolderUnderTest, "RuntimeIdentifierGraph.json");
                downloader = new ToolPackageDownloader(store, testRuntimeJsonPath, root.Value);
                uninstaller = new ToolPackageUninstaller(store);
            }
 
            store.Root.Value.Should().Be(Path.GetFullPath(toolsRoot.Value));
 
            return (store, storeQuery, downloader, uninstaller, reporter, fileSystem, root);
        }
 
        private static void WriteNugetConfigFile(IFileSystem fileSystem, FilePath? filePath, bool includeLocalFeedPath)
        {
            if (!filePath.HasValue) return;
 
            fileSystem.Directory.CreateDirectory(filePath.Value.GetDirectoryPath().Value);
 
            fileSystem.File.WriteAllText(filePath.Value.Value, FormatNuGetConfig(
                localFeedPath: includeLocalFeedPath ? GetTestLocalFeedPath() : null));
        }
 
        public static string FormatNuGetConfig(string localFeedPath)
        {
            string localFeed = string.IsNullOrEmpty(localFeedPath)
                ? string.Empty
                : $"<add key=\"Test Source\" value=\"{localFeedPath}\" />";
 
            return $@"<?xml version=""1.0"" encoding=""utf-8""?>
<configuration>
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
{localFeed}
<add key=""dotnet-public"" value=""https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json"" />
<add key=""myget-legacy"" value=""https://pkgs.dev.azure.com/dnceng/public/_packaging/myget-legacy/nuget/v3/index.json"" />
</packageSources>
</configuration>";
        }
 
        private static string GetTestLocalFeedPath() =>
            Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed");
 
        private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker();
        private const string TestPackageVersion = "1.0.4";
        private static readonly PackageId TestPackageId = new("global.tool.console.demo");
        private static readonly IEnumerable<NuGetFramework> TestFrameworks = new NuGetFramework[] { NuGetFramework.Parse(ToolPackageDownloaderMock2.DefaultTargetFramework) };
        private static readonly VerbosityOptions TestVerbosity = new VerbosityOptions();
 
        private readonly TestToolBuilder ToolBuilder;
 
        public ToolPackageDownloaderTests(ITestOutputHelper log, TestToolBuilder toolBuilder) : base(log)
        {
            ToolBuilder = toolBuilder;
        }
    }
}