// Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Transactions; using FluentAssertions; using Microsoft.DotNet.Tools.Test.Utilities; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Tools; using Microsoft.DotNet.Tools.Install.Tool; using Microsoft.DotNet.Tools.Tests.ComponentMocks; using Microsoft.Extensions.DependencyModel.Tests; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; using Xunit; namespace Microsoft.DotNet.ToolPackage.Tests { public class ToolPackageInstallerTests : TestBase { [Theory] [InlineData(false)] [InlineData(true)] public void GivenNoFeedInstallFailsWithException(bool testMockBehaviorIsInSync) { var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: new MockFeed[0]); Action a = () => installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework); a.ShouldThrow().WithMessage(LocalizableStrings.ToolInstallationRestoreFailed); reporter.Lines.Count.Should().Be(1); reporter.Lines[0].Should().Contain(TestPackageId.ToString()); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenOfflineFeedInstallSucceeds(bool testMockBehaviorIsInSync) { var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, offlineFeed: new DirectoryPath(GetTestLocalFeedPath()), feeds: GetOfflineMockFeed()); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAEmptySourceAndOfflineFeedInstallSucceeds(bool testMockBehaviorIsInSync) { var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(emptySource); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, offlineFeed: new DirectoryPath(GetTestLocalFeedPath()), feeds: GetOfflineMockFeed()); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {emptySource}); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenNugetConfigInstallSucceeds(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenNugetConfigInstallSucceedsInTransaction(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); IToolPackage package = null; using (var transactionScope = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); transactionScope.Complete(); } AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenNugetConfigInstallCreatesAnAssetFile(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); AssertPackageInstall(reporter, fileSystem, package, store); /* From mytool.dll to project.assets.json /packageid/version/packageid/version/tools/framework/rid/mytool.dll /project.assets.json */ var assetJsonPath = package.Commands[0].Executable .GetDirectoryPath() .GetParentPath() .GetParentPath() .GetParentPath() .GetParentPath() .GetParentPath() .WithFile("project.assets.json").Value; fileSystem.File.Exists(assetJsonPath).Should().BeTrue(); package.Uninstall(); } [Fact] public void GivenAConfigFileRootDirectoryPackageInstallSucceeds() { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup(useMock: false); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, rootConfigDirectory: nugetConfigPath.GetDirectoryPath()); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAllButNoPackageVersionItCanInstallThePackage(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); var package = installer.InstallPackage( packageId: TestPackageId, targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAllButNoTargetFrameworkItCanDownloadThePackage(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), nugetConfig: nugetConfigPath); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenASourceInstallSucceeds(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAEmptySourceAndNugetConfigInstallSucceeds(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var emptySource = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); Directory.CreateDirectory(emptySource); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(emptySource)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath, additionalFeeds: new[] {emptySource}); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenFailedRestoreInstallWillRollback(bool testMockBehaviorIsInSync) { var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync); Action a = () => { using (var t = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { installer.InstallPackage(new PackageId("non.existent.package.id")); t.Complete(); } }; a.ShouldThrow().WithMessage(LocalizableStrings.ToolInstallationRestoreFailed); AssertInstallRollBack(fileSystem, store); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenFailureAfterRestoreInstallWillRollback(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); void FailedStepAfterSuccessRestore() => throw new GracefulException("simulated error"); Action a = () => { using (var t = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); FailedStepAfterSuccessRestore(); t.Complete(); } }; a.ShouldThrow().WithMessage("simulated error"); AssertInstallRollBack(fileSystem, store); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenSecondInstallInATransactionTheFirstInstallShouldRollback(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); Action a = () => { using (var t = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { Action first = () => installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); first.ShouldNotThrow(); installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); t.Complete(); } }; a.ShouldThrow().Where( ex => ex.Message == string.Format( CommonLocalizableStrings.ToolPackageConflictPackageId, TestPackageId, TestPackageVersion)); AssertInstallRollBack(fileSystem, store); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenSecondInstallWithoutATransactionTheFirstShouldNotRollback(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); AssertPackageInstall(reporter, fileSystem, package, store); Action secondCall = () => installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); reporter.Lines.Should().BeEmpty(); secondCall.ShouldThrow().Where( ex => ex.Message == string.Format( CommonLocalizableStrings.ToolPackageConflictPackageId, TestPackageId, TestPackageVersion)); fileSystem .Directory .Exists(store.Root.WithSubDirectories(TestPackageId.ToString()).Value) .Should() .BeTrue(); package.Uninstall(); fileSystem .Directory .EnumerateFileSystemEntries(store.Root.WithSubDirectories(ToolPackageStore.StagingDirectory).Value) .Should() .BeEmpty(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAnInstalledPackageUninstallRemovesThePackage(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); store.EnumeratePackages().Should().BeEmpty(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAnInstalledPackageUninstallRollsbackWhenTransactionFails(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); AssertPackageInstall(reporter, fileSystem, package, store); using (var scope = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { package.Uninstall(); store.EnumeratePackages().Should().BeEmpty(); } package = store.EnumeratePackageVersions(TestPackageId).First(); AssertPackageInstall(reporter, fileSystem, package, store); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAnInstalledPackageUninstallRemovesThePackageWhenTransactionCommits(bool testMockBehaviorIsInSync) { var source = GetTestLocalFeedPath(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForSource(source)); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse(TestPackageVersion), targetFramework: _testTargetframework, additionalFeeds: new[] {source}); AssertPackageInstall(reporter, fileSystem, package, store); using (var scope = new TransactionScope( TransactionScopeOption.Required, TimeSpan.Zero)) { package.Uninstall(); scope.Complete(); } store.EnumeratePackages().Should().BeEmpty(); } [Theory] [InlineData(false)] [InlineData(true)] public void GivenAPackageNameWithDifferentCaseItCanInstallThePackage(bool testMockBehaviorIsInSync) { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var (store, installer, reporter, fileSystem) = Setup( useMock: testMockBehaviorIsInSync, feeds: GetMockFeedsForConfigFile(nugetConfigPath)); var package = installer.InstallPackage( packageId: new PackageId("GlObAl.TooL.coNsoLe.DemO"), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } [Fact] public void GivenANuGetDiagnosticMessageItShouldNotContainTheTempProject() { var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); var tempProject = GetUniqueTempProjectPathEachTest(); var (store, installer, reporter, fileSystem) = Setup( useMock: false, tempProject: tempProject); var package = installer.InstallPackage( packageId: TestPackageId, versionRange: VersionRange.Parse("1.0.*"), targetFramework: _testTargetframework, nugetConfig: nugetConfigPath); reporter.Lines.Should().NotBeEmpty(); reporter.Lines.Should().Contain(l => l.Contains("warning")); reporter.Lines.Should().NotContain(l => l.Contains(tempProject.Value)); reporter.Lines.Clear(); AssertPackageInstall(reporter, fileSystem, package, store); package.Uninstall(); } private static void AssertPackageInstall( BufferedReporter reporter, IFileSystem fileSystem, IToolPackage package, IToolPackageStore store) { reporter.Lines.Should().BeEmpty(); package.Id.Should().Be(TestPackageId); package.Version.ToNormalizedString().Should().Be(TestPackageVersion); package.PackageDirectory.Value.Should().Contain(store.Root.Value); store.EnumeratePackageVersions(TestPackageId) .Select(p => p.Version.ToNormalizedString()) .Should() .Equal(TestPackageVersion); package.Commands.Count.Should().Be(1); fileSystem.File.Exists(package.Commands[0].Executable.Value).Should().BeTrue($"{package.Commands[0].Executable.Value} should exist"); package.Commands[0].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) != ToolPackageStore.StagingDirectory); fileSystem .Directory .EnumerateFileSystemEntries(store.Root.WithSubDirectories(ToolPackageStore.StagingDirectory).Value) .Should() .BeEmpty(); } private static FilePath GetUniqueTempProjectPathEachTest() { var tempProjectDirectory = new DirectoryPath(Path.GetTempPath()).WithSubDirectories(Path.GetRandomFileName()); var tempProjectPath = tempProjectDirectory.WithFile(Path.GetRandomFileName() + ".csproj"); return tempProjectPath; } private static IEnumerable GetMockFeedsForConfigFile(FilePath nugetConfig) { return new MockFeed[] { new MockFeed { Type = MockFeedType.ExplicitNugetConfig, Uri = nugetConfig.Value, Packages = new List { new MockFeedPackage { PackageId = TestPackageId.ToString(), Version = TestPackageVersion } } } }; } private static IEnumerable GetMockFeedsForSource(string source) { return new MockFeed[] { new MockFeed { Type = MockFeedType.ImplicitAdditionalFeed, Uri = source, Packages = new List { new MockFeedPackage { PackageId = TestPackageId.ToString(), Version = TestPackageVersion } } } }; } private static IEnumerable GetOfflineMockFeed() { return new MockFeed[] { new MockFeed { Type = MockFeedType.ImplicitAdditionalFeed, Uri = GetTestLocalFeedPath(), Packages = new List { new MockFeedPackage { PackageId = TestPackageId.ToString(), Version = TestPackageVersion } } } }; } private static (IToolPackageStore, IToolPackageInstaller, BufferedReporter, IFileSystem) Setup( bool useMock, IEnumerable feeds = null, FilePath? tempProject = null, DirectoryPath? offlineFeed = null) { var root = new DirectoryPath(Path.Combine(Path.GetFullPath(TempRoot.Root), Path.GetRandomFileName())); var reporter = new BufferedReporter(); IFileSystem fileSystem; IToolPackageStore store; IToolPackageInstaller installer; if (useMock) { fileSystem = new FileSystemMockBuilder().Build(); store = new ToolPackageStoreMock(root, fileSystem); installer = new ToolPackageInstallerMock( fileSystem: fileSystem, store: store, projectRestorer: new ProjectRestorerMock( fileSystem: fileSystem, reporter: reporter, feeds: feeds)); } else { fileSystem = new FileSystemWrapper(); store = new ToolPackageStore(root); installer = new ToolPackageInstaller( store: store, projectRestorer: new ProjectRestorer(reporter), tempProject: tempProject ?? GetUniqueTempProjectPathEachTest(), offlineFeed: offlineFeed ?? new DirectoryPath("does not exist")); } return (store, installer, reporter, fileSystem); } private static FilePath WriteNugetConfigFileToPointToTheFeed() { var nugetConfigName = "nuget.config"; var tempPathForNugetConfigWithWhiteSpace = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + " " + Path.GetRandomFileName()); Directory.CreateDirectory(tempPathForNugetConfigWithWhiteSpace); NuGetConfig.Write( directory: tempPathForNugetConfigWithWhiteSpace, configname: nugetConfigName, localFeedPath: GetTestLocalFeedPath()); return new FilePath(Path.GetFullPath(Path.Combine(tempPathForNugetConfigWithWhiteSpace, nugetConfigName))); } 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 PackageId("global.tool.console.demo"); } }