File: GenerateBindingRedirects_Tests.cs
Web Access
Project: ..\..\..\src\Tasks.UnitTests\Microsoft.Build.Tasks.UnitTests.csproj (Microsoft.Build.Tasks.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.UnitTests;
using Microsoft.Build.Utilities;
using Shouldly;
using Xunit;
using Xunit.Abstractions;
 
#nullable disable
 
namespace Microsoft.Build.Tasks.UnitTests
{
    public class GenerateBindingRedirectsTests : IDisposable
    {
        private readonly ITestOutputHelper _output;
        private readonly TestEnvironment _env;
 
        public GenerateBindingRedirectsTests(ITestOutputHelper output)
        {
            _output = output;
            _env = TestEnvironment.Create(output);
        }
 
        public void Dispose()
        {
            _env.Dispose();
        }
 
        /// <summary>
        /// In this case,
        /// - A valid redirect information is provided for <see cref="GenerateBindingRedirects"/> task.
        /// Expected:
        /// - Task should create a target app.config with specified redirect information.
        /// Rationale:
        /// - The only goal for <see cref="GenerateBindingRedirects"/> task is to add specified redirects to the output app.config.
        /// </summary>
        [Fact]
        public void TargetAppConfigShouldContainsBindingRedirects()
        {
            // Arrange
            // Current appConfig is empty
            string appConfigFile = WriteAppConfigRuntimeSection(string.Empty);
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.ExecuteResult.ShouldBeTrue();
            redirectResults.TargetAppConfigContent.ShouldContain("<assemblyIdentity name=\"System\" publicKeyToken=\"b77a5c561934e089\" culture=\"neutral\" />");
            redirectResults.TargetAppConfigContent.ShouldContain("newVersion=\"40.0.0.0\"");
        }
 
        /// <summary>
        /// In this case,
        /// - A valid redirect information is provided for <see cref="GenerateBindingRedirects"/> task and app.config is not empty.
        /// Expected:
        /// - Task should create a target app.config with specified redirect information.
        /// Rationale:
        /// - The only goal for <see cref="GenerateBindingRedirects"/> task is to add specified redirects to the output app.config.
        /// </summary>
        [Fact]
        public void TargetAppConfigShouldContainsBindingRedirectsFromAppConfig()
        {
            // Arrange
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"">
  <dependentAssembly>
    <assemblyIdentity name=""MyAssembly""
                  publicKeyToken = ""14a739be0244c389""
                  culture = ""Neutral""/>
    <bindingRedirect oldVersion= ""1.0.0.0""
                  newVersion = ""5.0.0.0"" />
  </dependentAssembly>
</assemblyBinding>");
 
            TaskItemMock redirect = new TaskItemMock("MyAssembly, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='14a739be0244c389'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.TargetAppConfigContent.ShouldContain("MyAssembly");
            redirectResults.TargetAppConfigContent.ShouldContain("<bindingRedirect oldVersion=\"0.0.0.0-40.0.0.0\" newVersion=\"40.0.0.0\"");
        }
 
        /// <summary>
        /// In this case,
        /// - An app.config is passed in with two dependentAssembly elements
        /// Expected:
        /// - Both redirects appears in the output app.config
        /// Rationale:
        /// - assemblyBinding could have more than one dependentAssembly elements and <see cref="GenerateBindingRedirects"/>
        ///   should respect that.
        /// </summary>
        [Fact]
        public void GenerateBindingRedirectsFromTwoDependentAssemblySections()
        {
            // Arrange
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<loadFromRemoteSources enabled=""true""/>
  <assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"" >
    <dependentAssembly>
      <assemblyIdentity name=""Microsoft.ServiceBus"" publicKeyToken =""31bf3856ad364e35"" culture =""neutral"" />
      <bindingRedirect oldVersion=""2.0.0.0-3.0.0.0"" newVersion =""2.2.0.0"" />
    </dependentAssembly>
    <probing privatePath=""VSO14"" />
    <dependentAssembly>
      <assemblyIdentity name=""System.Web.Http"" publicKeyToken =""31bf3856ad364e35"" culture =""neutral"" />
      <bindingRedirect oldVersion=""4.0.0.0-6.0.0.0"" newVersion =""4.0.0.0"" />
    </dependentAssembly>
    <dependentAssembly>
      <assemblyIdentity name=""Microsoft.TeamFoundation.Common"" publicKeyToken =""b03f5f7f11d50a3a"" culture =""neutral"" />
      <codeBase version=""11.0.0.0"" href =""Microsoft.TeamFoundation.Common.dll"" />
      <codeBase version=""14.0.0.0"" href =""VSO14\Microsoft.TeamFoundation.Common.dll"" />
    </dependentAssembly>
  </assemblyBinding>");
 
            TaskItemMock serviceBusRedirect = new TaskItemMock("Microsoft.ServiceBus, Version=2.0.0.0, Culture=Neutral, PublicKeyToken='31bf3856ad364e35'", "41.0.0.0");
            TaskItemMock webHttpRedirect = new TaskItemMock("System.Web.Http, Version=4.0.0.0, Culture=Neutral, PublicKeyToken='31bf3856ad364e35'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, serviceBusRedirect, webHttpRedirect);
 
            // Assert
            redirectResults.ExecuteResult.ShouldBeTrue();
            // Naive check that target app.config contains custom redirects.
            // Output config should have max versions for both serviceBus and webhttp assemblies.
            redirectResults.TargetAppConfigContent.ShouldContain($"oldVersion=\"0.0.0.0-{serviceBusRedirect.MaxVersion}\"");
            redirectResults.TargetAppConfigContent.ShouldContain($"newVersion=\"{serviceBusRedirect.MaxVersion}\"");
 
            redirectResults.TargetAppConfigContent.ShouldContain($"oldVersion=\"0.0.0.0-{webHttpRedirect.MaxVersion}\"");
            redirectResults.TargetAppConfigContent.ShouldContain($"newVersion=\"{webHttpRedirect.MaxVersion}\"");
 
            XElement targetAppConfig = XElement.Parse(redirectResults.TargetAppConfigContent);
            targetAppConfig.Descendants()
                .Count(e => e.Name.LocalName.Equals("assemblyBinding", StringComparison.OrdinalIgnoreCase))
                .ShouldBe(1);
            // "Binding redirects should not add additional assemblyBinding sections into the target app.config: " + targetAppConfig
 
            // Log file should contains a warning when GenerateBindingRedirects updates existing app.config entries
            redirectResults.Engine.AssertLogContains("MSB3836");
        }
 
        /// <summary>
        /// In this case,
        /// - An app.config is passed in that has dependentAssembly section with probing element but without
        ///   assemblyIdentity or bindingRedirect elements.
        /// Expected:
        /// - No warning
        /// Rationale:
        /// - In initial implementation such app.config was considered invalid and MSB3835 was issued.
        ///   But due to MSDN documentation, dependentAssembly could have only probing element without any other elements inside.
        /// </summary>
        [Fact]
        public void AppConfigWithProbingPathAndWithoutDependentAssemblyShouldNotProduceWarningsBug1161241()
        {
            // Arrange
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"">
   <probing privatePath = 'bin;bin2\subbin;bin3'/>
</assemblyBinding>");
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.Engine.Errors.ShouldBe(0); // "Unexpected errors. Engine log: " + redirectResults.Engine.Log
            redirectResults.Engine.Warnings.ShouldBe(0); // "Unexpected errors. Engine log: " + redirectResults.Engine.Log
        }
 
        /// <summary>
        /// In this case,
        /// - An app.config is passed in that has empty assemblyBinding section.
        /// Expected:
        /// - No warning
        /// Rationale:
        /// - In initial implementation such app.config was considered invalid and MSB3835 was issued.
        ///   But due to MSDN documentation, dependentAssembly could have only probing element without any other elements inside.
        /// </summary>
        [Fact]
        public void AppConfigWithEmptyAssemblyBindingShouldNotProduceWarnings()
        {
            // Arrange
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"" appliesTo=""v1.0.3705"">
</assemblyBinding>");
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.Engine.Errors.ShouldBe(0);
            redirectResults.Engine.Warnings.ShouldBe(0);
        }
 
        /// <summary>
        /// In this case,
        /// - An app.config is passed in that has dependentAssembly section with assemblyIdentity but without bindingRedirect
        /// Expected:
        /// - No warning
        /// Rationale:
        /// - Due to app.config xsd schema this is a valid configuration.
        /// </summary>
        [Fact]
        public void DependentAssemblySectionWithoutBindingRedirectShouldNotProduceWarnings()
        {
            // Arrange
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"" appliesTo=""v1.0.3705"">
      <dependentAssembly>
        <assemblyIdentity name=""Microsoft.TeamFoundation.Common"" publicKeyToken =""b03f5f7f11d50a3a"" culture =""neutral"" />
        <codeBase version=""11.0.0.0"" href =""Microsoft.TeamFoundation.Common.dll"" />
        <codeBase version=""14.0.0.0"" href =""VSO14\Microsoft.TeamFoundation.Common.dll"" />
      </dependentAssembly>
 </assemblyBinding>");
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.Engine.Errors.ShouldBe(0);
            redirectResults.Engine.Warnings.ShouldBe(0);
        }
 
        /// <summary>
        /// In this case,
        /// - An app.config is passed in but dependentAssembly element is empty.
        /// Expected:
        /// - MSB3835
        /// Rationale:
        /// - Due to MSDN documentation, assemblyBinding element should always have a dependentAssembly subsection.
        /// </summary>
        [Fact]
        public void AppConfigInvalidIfDependentAssemblyNodeIsEmpty()
        {
            // Construct the app.config.
            string appConfigFile = WriteAppConfigRuntimeSection(
@"<assemblyBinding xmlns=""urn:schemas-microsoft-com:asm.v1"">
  <dependentAssembly>
  </dependentAssembly>
</assemblyBinding>");
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            // Act
            var redirectResults = GenerateBindingRedirects(appConfigFile, null, redirect);
 
            // Assert
            redirectResults.Engine.AssertLogContains("MSB3835");
        }
 
        [Fact]
        public void AppConfigWhenFilePlacedInLocationWithGB18030Characters()
        {
            using (TestEnvironment env = TestEnvironment.Create())
            {
                TransientTestFolder rootTestFolder = env.CreateFolder(); 
                TransientTestFolder testFolder = env.CreateFolder(Path.Combine(rootTestFolder.Path, "\uD873\uDD02\u9FA8\u82D8\u722B\u9EA4\u03C5\u33D1\uE038\u486B\u0033"));
                string appConfigContents = WriteAppConfigRuntimeSection(string.Empty, testFolder);
                string outputAppConfigFile = env.ExpectFile(".config").Path;
 
                TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
                _ = Should.NotThrow(() => GenerateBindingRedirects(appConfigContents, outputAppConfigFile, redirect));
            }
        }
 
        [Fact]
        public void AppConfigFileNotSavedWhenIdentical()
        {
            string appConfigFile = WriteAppConfigRuntimeSection(string.Empty);
            string outputAppConfigFile = _env.ExpectFile(".config").Path;
 
            TaskItemMock redirect = new TaskItemMock("System, Version=10.0.0.0, Culture=Neutral, PublicKeyToken='b77a5c561934e089'", "40.0.0.0");
 
            var redirectResults = GenerateBindingRedirects(appConfigFile, outputAppConfigFile, redirect);
 
            // Verify it ran correctly
            redirectResults.ExecuteResult.ShouldBeTrue();
            redirectResults.TargetAppConfigContent.ShouldContain("<assemblyIdentity name=\"System\" publicKeyToken=\"b77a5c561934e089\" culture=\"neutral\" />");
            redirectResults.TargetAppConfigContent.ShouldContain("newVersion=\"40.0.0.0\"");
 
            var oldTimestamp = DateTime.Now.Subtract(TimeSpan.FromDays(30));
 
            File.SetCreationTime(outputAppConfigFile, oldTimestamp);
            File.SetLastWriteTime(outputAppConfigFile, oldTimestamp);
 
            // Make sure it's old
            File.GetCreationTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5));
            File.GetLastWriteTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5));
 
            // Re-run the task
            var redirectResults2 = GenerateBindingRedirects(appConfigFile, outputAppConfigFile, redirect);
 
            // Verify it ran correctly and that it's still old
            redirectResults2.ExecuteResult.ShouldBeTrue();
            redirectResults2.TargetAppConfigContent.ShouldContain("<assemblyIdentity name=\"System\" publicKeyToken=\"b77a5c561934e089\" culture=\"neutral\" />");
            redirectResults2.TargetAppConfigContent.ShouldContain("newVersion=\"40.0.0.0\"");
 
            File.GetCreationTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5));
            File.GetLastWriteTime(outputAppConfigFile).ShouldBe(oldTimestamp, TimeSpan.FromSeconds(5));
        }
 
        private BindingRedirectsExecutionResult GenerateBindingRedirects(string appConfigFile, string targetAppConfigFile,
            params ITaskItem[] suggestedRedirects)
        {
            // Create the engine.
            MockEngine engine = new MockEngine(_output);
 
            string outputAppConfig = string.IsNullOrEmpty(targetAppConfigFile) ? _env.ExpectFile(".config").Path : targetAppConfigFile;
 
            GenerateBindingRedirects bindingRedirects = new GenerateBindingRedirects
            {
                BuildEngine = engine,
                SuggestedRedirects = suggestedRedirects ?? Array.Empty<ITaskItem>(),
                AppConfigFile = new TaskItem(appConfigFile),
                OutputAppConfigFile = new TaskItem(outputAppConfig)
            };
 
            bool executionResult = bindingRedirects.Execute();
 
            return new BindingRedirectsExecutionResult
            {
                ExecuteResult = executionResult,
                Engine = engine,
                SourceAppConfigContent = File.ReadAllText(appConfigFile),
                TargetAppConfigContent = File.ReadAllText(outputAppConfig),
                TargetAppConfigFilePath = outputAppConfig
            };
        }
 
        private string WriteAppConfigRuntimeSection(
            string runtimeSection,
            TransientTestFolder transientTestFolder = null)
        {
            string formatString =
@"<configuration>
  <runtime>
     {0}
  </runtime>
</configuration>";
            string appConfigContents = string.Format(formatString, runtimeSection);
 
            string appConfigFile = _env.CreateFile(transientTestFolder ?? new TransientTestFolder(), ".config").Path;
            File.WriteAllText(appConfigFile, appConfigContents);
            return appConfigFile;
        }
 
        /// <summary>
        /// Helper class that contains execution results for <see cref="GenerateBindingRedirects"/>.
        /// </summary>
        private sealed class BindingRedirectsExecutionResult
        {
            public MockEngine Engine { get; set; }
 
            public string SourceAppConfigContent { get; set; }
 
            public string TargetAppConfigContent { get; set; }
 
            public bool ExecuteResult { get; set; }
 
            public string TargetAppConfigFilePath { get; set; }
        }
 
        /// <summary>
        /// Mock implementation of the <see cref="ITaskItem"/>.
        /// </summary>
        private sealed class TaskItemMock : ITaskItem
        {
            public TaskItemMock(string assemblyName, string maxVersion)
            {
                ((ITaskItem)this).ItemSpec = assemblyName;
                MaxVersion = maxVersion;
            }
 
            public string MaxVersion { get; }
 
            string ITaskItem.ItemSpec { get; set; }
 
            ICollection ITaskItem.MetadataNames { get; }
 
            int ITaskItem.MetadataCount { get; }
 
            string ITaskItem.GetMetadata(string metadataName)
            {
                return MaxVersion;
            }
 
            void ITaskItem.SetMetadata(string metadataName, string metadataValue)
            {
                throw new NotImplementedException();
            }
 
            void ITaskItem.RemoveMetadata(string metadataName)
            {
                throw new NotImplementedException();
            }
 
            void ITaskItem.CopyMetadataTo(ITaskItem destinationItem)
            {
                throw new NotImplementedException();
            }
 
            IDictionary ITaskItem.CloneCustomMetadata()
            {
                throw new NotImplementedException();
            }
        }
    }
}