File: CommandTests\New\DotnetSlnPostActionTests.cs
Web Access
Project: ..\..\..\test\dotnet.Tests\dotnet.Tests.csproj (dotnet.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.DotNet.Cli.Commands.New.PostActions;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Mocks;
using Microsoft.TemplateEngine.TestHelper;
 
namespace Microsoft.DotNet.Cli.New.Tests
{
    public class DotnetSlnPostActionTests : IClassFixture<EnvironmentSettingsHelper>
    {
        private readonly IEngineEnvironmentSettings _engineEnvironmentSettings;
 
        public DotnetSlnPostActionTests(EnvironmentSettingsHelper environmentSettingsHelper)
        {
            _engineEnvironmentSettings = environmentSettingsHelper.CreateEnvironment(hostIdentifier: GetType().Name, virtualize: true);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionFindSolutionFileAtOutputPath))]
        public void AddProjectToSolutionPostActionFindSolutionFileAtOutputPath()
        {
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string solutionFileFullPath = Path.Combine(targetBasePath, "MySln.sln");
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(solutionFileFullPath, string.Empty);
 
            IReadOnlyList<string> solutionFiles = DotnetSlnPostActionProcessor.FindSolutionFilesAtOrAbovePath(_engineEnvironmentSettings.Host.FileSystem, targetBasePath);
            Assert.Single(solutionFiles);
            Assert.Equal(solutionFileFullPath, solutionFiles[0]);
        }
 
        [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.Linux)] // https://github.com/dotnet/sdk/issues/49923
        public void AddProjectToSolutionPostActionFindSlnxFileAtOutputPath()
        {
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            _engineEnvironmentSettings.Host.VirtualizeDirectory(targetBasePath);
            EnsureParentDirectoriesExist(targetBasePath);
            
            string solutionFileFullPath = Path.Combine(targetBasePath, "MySln.slnx");
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(solutionFileFullPath, string.Empty);
 
            IReadOnlyList<string> solutionFiles = DotnetSlnPostActionProcessor.FindSolutionFilesAtOrAbovePath(_engineEnvironmentSettings.Host.FileSystem, targetBasePath);
            Assert.Single(solutionFiles);
            Assert.Equal(solutionFileFullPath, solutionFiles[0]);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionPrefersSlnOverSlnx))]
        public void AddProjectToSolutionPostActionPrefersSlnOverSlnx()
        {
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            _engineEnvironmentSettings.Host.VirtualizeDirectory(targetBasePath);
            EnsureParentDirectoriesExist(targetBasePath);
            
            string slnFileFullPath = Path.Combine(targetBasePath, "MySln.sln");
            string slnxFileFullPath = Path.Combine(targetBasePath, "MySln.slnx");
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, string.Empty);
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnxFileFullPath, string.Empty);
 
            IReadOnlyList<string> solutionFiles = DotnetSlnPostActionProcessor.FindSolutionFilesAtOrAbovePath(_engineEnvironmentSettings.Host.FileSystem, targetBasePath);
            Assert.Single(solutionFiles);
            Assert.Equal(slnFileFullPath, solutionFiles[0]);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionFindsOneProjectToAdd))]
        public void AddProjectToSolutionPostActionFindsOneProjectToAdd()
        {
            string outputBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            IPostAction postAction = new MockPostAction(default, default, default, default, default!)
            {
                ActionId = DotnetSlnPostActionProcessor.ActionProcessorId,
                Args = new Dictionary<string, string>()
                {
                    { "primaryOutputIndexes", "0" }
                }
            };
 
            ICreationResult creationResult = new MockCreationResult(primaryOutputs: new[] { new MockCreationPath(Path.GetFullPath("outputProj1.csproj")) });
 
            Assert.True(DotnetSlnPostActionProcessor.TryGetProjectFilesToAdd(postAction, creationResult, outputBasePath, out IReadOnlyList<string>? foundProjectFiles));
            Assert.Equal(1, foundProjectFiles?.Count);
            Assert.Equal(creationResult.PrimaryOutputs[0].Path, foundProjectFiles?[0]);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionFindsMultipleProjectsToAdd))]
        public void AddProjectToSolutionPostActionFindsMultipleProjectsToAdd()
        {
            string outputBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            IPostAction postAction = new MockPostAction(default, default, default, default, default!)
            {
                ActionId = DotnetSlnPostActionProcessor.ActionProcessorId,
                Args = new Dictionary<string, string>()
                {
                    { "primaryOutputIndexes", "0; 2" }
                }
            };
 
            ICreationResult creationResult = new MockCreationResult(
                primaryOutputs: new[]
                {
                    new MockCreationPath(Path.GetFullPath("outputProj1.csproj")),
                    new MockCreationPath(Path.GetFullPath("dontFindMe.csproj")),
                    new MockCreationPath(Path.GetFullPath("outputProj2.csproj"))
                });
 
            Assert.True(DotnetSlnPostActionProcessor.TryGetProjectFilesToAdd(postAction, creationResult, outputBasePath, out IReadOnlyList<string>? foundProjectFiles));
            Assert.NotNull(foundProjectFiles);
            Assert.Equal(2, foundProjectFiles.Count);
            Assert.Contains(creationResult.PrimaryOutputs[0].Path, foundProjectFiles.ToList());
            Assert.Contains(creationResult.PrimaryOutputs[2].Path, foundProjectFiles.ToList());
 
            Assert.DoesNotContain(creationResult.PrimaryOutputs[1].Path, foundProjectFiles.ToList());
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionDoesntFindProjectOutOfRange))]
        public void AddProjectToSolutionPostActionDoesntFindProjectOutOfRange()
        {
            IPostAction postAction = new MockPostAction(default, default, default, default, default!)
            {
                ActionId = DotnetSlnPostActionProcessor.ActionProcessorId,
                Args = new Dictionary<string, string>()
                {
                    { "primaryOutputIndexes", "1" }
                }
            };
 
            ICreationResult creationResult = new MockCreationResult(primaryOutputs: new[] { new MockCreationPath("outputProj1.csproj") });
 
            Assert.False(DotnetSlnPostActionProcessor.TryGetProjectFilesToAdd(postAction, creationResult, string.Empty, out IReadOnlyList<string>? foundProjectFiles));
            Assert.Empty(foundProjectFiles);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionFindsMultipleProjectsToAddWithOutputBasePath))]
        public void AddProjectToSolutionPostActionFindsMultipleProjectsToAddWithOutputBasePath()
        {
            string outputBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
 
            IPostAction postAction = new MockPostAction(default, default, default, default, default!)
            {
                ActionId = DotnetSlnPostActionProcessor.ActionProcessorId,
                Args = new Dictionary<string, string>()
                {
                    { "primaryOutputIndexes", "0; 2" }
                }
            };
 
            ICreationResult creationResult = new MockCreationResult(
                primaryOutputs: new[]
                {
                    new MockCreationPath("outputProj1.csproj"),
                    new MockCreationPath("dontFindMe.csproj"),
                    new MockCreationPath("outputProj2.csproj")
                });
            string outputFileFullPath0 = Path.Combine(outputBasePath, creationResult.PrimaryOutputs[0].Path);
            string dontFindMeFullPath1 = Path.Combine(outputBasePath, creationResult.PrimaryOutputs[1].Path);
            string outputFileFullPath2 = Path.Combine(outputBasePath, creationResult.PrimaryOutputs[2].Path);
 
            Assert.True(DotnetSlnPostActionProcessor.TryGetProjectFilesToAdd(postAction, creationResult, outputBasePath, out IReadOnlyList<string>? foundProjectFiles));
            Assert.NotNull(foundProjectFiles);
            Assert.Equal(2, foundProjectFiles.Count);
            Assert.Contains(outputFileFullPath0, foundProjectFiles.ToList());
            Assert.Contains(outputFileFullPath2, foundProjectFiles.ToList());
 
            Assert.DoesNotContain(dontFindMeFullPath1, foundProjectFiles.ToList());
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionPostActionWithoutPrimaryOutputIndexesWithOutputBasePath))]
        public void AddProjectToSolutionPostActionWithoutPrimaryOutputIndexesWithOutputBasePath()
        {
            string outputBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
 
            IPostAction postAction = new MockPostAction(default, default, default, default, default!)
            {
                ActionId = DotnetSlnPostActionProcessor.ActionProcessorId,
                Args = new Dictionary<string, string>()
            };
 
            ICreationResult creationResult = new MockCreationResult(
                primaryOutputs: new[]
                {
                    new MockCreationPath("outputProj1.csproj"),
                    new MockCreationPath("outputProj2.csproj"),
                });
            string outputFileFullPath0 = Path.Combine(outputBasePath, creationResult.PrimaryOutputs[0].Path);
            string outputFileFullPath1 = Path.Combine(outputBasePath, creationResult.PrimaryOutputs[1].Path);
 
            Assert.True(DotnetSlnPostActionProcessor.TryGetProjectFilesToAdd(postAction, creationResult, outputBasePath, out IReadOnlyList<string>? foundProjectFiles));
            Assert.NotNull(foundProjectFiles);
            Assert.Equal(2, foundProjectFiles.Count);
            Assert.Contains(outputFileFullPath0, foundProjectFiles.ToList());
            Assert.Contains(outputFileFullPath1, foundProjectFiles.ToList());
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionCanTargetASingleProjectWithAJsonArray))]
        public void AddProjectToSolutionCanTargetASingleProjectWithAJsonArray()
        {
            var callback = new MockAddProjectToSolutionCallback();
            var actionProcessor = new DotnetSlnPostActionProcessor(callback.AddProjectToSolution);
 
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string slnFileFullPath = Path.Combine(targetBasePath, "MyApp.sln");
            string projFileFullPath = Path.Combine(targetBasePath, "MyApp.csproj");
 
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, "");
 
            var args = new Dictionary<string, string>() { { "projectFiles", "[\"MyApp.csproj\"]" } };
            var postAction = new MockPostAction(default, default, default, default, default!) { ActionId = DotnetSlnPostActionProcessor.ActionProcessorId, Args = args };
 
            MockCreationEffects creationEffects = new MockCreationEffects()
                .WithFileChange(new MockFileChange("./MyApp.csproj", "./MyApp.csproj", ChangeKind.Create));
 
            actionProcessor.Process(
                _engineEnvironmentSettings,
                postAction,
                creationEffects,
                new MockCreationResult(),
                targetBasePath);
 
            Assert.Equal(new[] { projFileFullPath }, callback.Projects);
            Assert.Equal(slnFileFullPath, callback.Solution);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionCanTargetASingleProjectWithTheProjectName))]
        public void AddProjectToSolutionCanTargetASingleProjectWithTheProjectName()
        {
            var callback = new MockAddProjectToSolutionCallback();
            var actionProcessor = new DotnetSlnPostActionProcessor(callback.AddProjectToSolution);
 
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string slnFileFullPath = Path.Combine(targetBasePath, "MyApp.sln");
            string projFileFullPath = Path.Combine(targetBasePath, "MyApp.csproj");
 
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, "");
 
            var args = new Dictionary<string, string>() { { "projectFiles", "MyApp.csproj" } };
            var postAction = new MockPostAction(default, default, default, default, default!) { ActionId = DotnetSlnPostActionProcessor.ActionProcessorId, Args = args };
 
            MockCreationEffects creationEffects = new MockCreationEffects()
                .WithFileChange(new MockFileChange("./MyApp.csproj", "./MyApp.csproj", ChangeKind.Create));
 
            actionProcessor.Process(
                _engineEnvironmentSettings,
                postAction,
                creationEffects,
                new MockCreationResult(),
                targetBasePath);
 
            Assert.Equal(new[] { projFileFullPath }, callback.Projects);
            Assert.Equal(slnFileFullPath, callback.Solution);
        }
 
        [Fact(DisplayName = nameof(AddProjectToSolutionCanPlaceProjectInSolutionRoot))]
        public void AddProjectToSolutionCanPlaceProjectInSolutionRoot()
        {
            var callback = new MockAddProjectToSolutionCallback();
            var actionProcessor = new DotnetSlnPostActionProcessor(callback.AddProjectToSolution);
 
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string slnFileFullPath = Path.Combine(targetBasePath, "MyApp.sln");
            string projFileFullPath = Path.Combine(targetBasePath, "MyApp.csproj");
 
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, "");
 
            var args = new Dictionary<string, string>() {
                { "projectFiles", "MyApp.csproj" },
                { "inRoot", "true" }
            };
            var postAction = new MockPostAction(default, default, default, default, default!) { ActionId = DotnetSlnPostActionProcessor.ActionProcessorId, Args = args };
 
            MockCreationEffects creationEffects = new MockCreationEffects()
                .WithFileChange(new MockFileChange("./MyApp.csproj", "./MyApp.csproj", ChangeKind.Create));
 
            actionProcessor.Process(
                _engineEnvironmentSettings,
                postAction,
                creationEffects,
                new MockCreationResult(),
                targetBasePath);
 
            Assert.True(callback.InRoot);
            Assert.Null(callback.TargetFolder);
        }
 
        [Fact]
        public void AddProjectToSolutionCanPlaceProjectInSolutionFolder()
        {
            var callback = new MockAddProjectToSolutionCallback();
            var actionProcessor = new DotnetSlnPostActionProcessor(callback.AddProjectToSolution);
 
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string slnFileFullPath = Path.Combine(targetBasePath, "MyApp.sln");
            string projFileFullPath = Path.Combine(targetBasePath, "MyApp.csproj");
 
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, "");
 
            var args = new Dictionary<string, string>() {
                { "projectFiles", "MyApp.csproj" },
                { "solutionFolder", "src" }
            };
            var postAction = new MockPostAction(default, default, default, default, default!) { ActionId = DotnetSlnPostActionProcessor.ActionProcessorId, Args = args };
 
            MockCreationEffects creationEffects = new MockCreationEffects()
                .WithFileChange(new MockFileChange("./MyApp.csproj", "./MyApp.csproj", ChangeKind.Create));
 
            actionProcessor.Process(
                _engineEnvironmentSettings,
                postAction,
                creationEffects,
                new MockCreationResult(),
                targetBasePath);
 
            Assert.Null(callback.InRoot);
            Assert.Equal("src", callback.TargetFolder);
        }
 
        [Fact]
        public void AddProjectToSolutionFailsWhenSolutionFolderAndInRootSpecified()
        {
            var callback = new MockAddProjectToSolutionCallback();
            var actionProcessor = new DotnetSlnPostActionProcessor(callback.AddProjectToSolution);
 
            string targetBasePath = _engineEnvironmentSettings.GetTempVirtualizedPath();
            string slnFileFullPath = Path.Combine(targetBasePath, "MyApp.sln");
            string projFileFullPath = Path.Combine(targetBasePath, "MyApp.csproj");
 
            _engineEnvironmentSettings.Host.FileSystem.WriteAllText(slnFileFullPath, "");
 
            var args = new Dictionary<string, string>() {
                { "projectFiles", "MyApp.csproj" },
                { "inRoot", "true" },
                { "solutionFolder", "src" }
            };
            var postAction = new MockPostAction(default, default, default, default, default!) { ActionId = DotnetSlnPostActionProcessor.ActionProcessorId, Args = args };
 
            MockCreationEffects creationEffects = new MockCreationEffects()
                .WithFileChange(new MockFileChange("./MyApp.csproj", "./MyApp.csproj", ChangeKind.Create));
 
            bool result = actionProcessor.Process(
                _engineEnvironmentSettings,
                postAction,
                creationEffects,
                new MockCreationResult(),
                targetBasePath);
 
            Assert.False(result);
        }
 
        private void EnsureParentDirectoriesExist(string targetBasePath)
        {
            // Ensure parent directories exist to avoid DirectoryNotFoundException during traversal
            string? currentPath = targetBasePath;
            while (!string.IsNullOrEmpty(currentPath) && currentPath != Path.GetPathRoot(currentPath))
            {
                if (!_engineEnvironmentSettings.Host.FileSystem.DirectoryExists(currentPath))
                {
                    _engineEnvironmentSettings.Host.FileSystem.CreateDirectory(currentPath);
                }
                currentPath = Path.GetDirectoryName(currentPath);
            }
        }
 
        private class MockAddProjectToSolutionCallback
        {
            public string? Solution { get; private set; }
 
            public IReadOnlyList<string?>? Projects { get; private set; }
 
            public string? TargetFolder { get; private set; }
 
            public bool? InRoot { get; private set; }
 
            public bool AddProjectToSolution(string solution, IReadOnlyList<string?> projects, string? targetFolder, bool? inRoot)
            {
                Solution = solution;
                Projects = projects;
                InRoot = inRoot;
                TargetFolder = targetFolder;
 
                return true;
            }
        }
    }
}