RepairInterop.cs (18408B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="RepairInterop.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace AppInstallerCLIE2ETests.Interop 8 { 9 using System; 10 using System.IO; 11 using System.Linq; 12 using System.Text; 13 using System.Threading.Tasks; 14 using AppInstallerCLIE2ETests.Helpers; 15 using Microsoft.Management.Deployment; 16 using Microsoft.Management.Deployment.Projection; 17 using NUnit.Framework; 18 19 /// <summary> 20 /// Repair Interop Tests for COM/WinRT Interop classes. 21 /// </summary> 22 [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.InProcess), Category = nameof(InstanceInitializersSource.InProcess))] 23 [TestFixtureSource(typeof(InstanceInitializersSource), nameof(InstanceInitializersSource.OutOfProcess), Category = nameof(InstanceInitializersSource.OutOfProcess))] 24 public class RepairInterop : BaseInterop 25 { 26 private string installDir; 27 private PackageManager packageManager; 28 private PackageCatalogReference compositeSource; 29 30 /// <summary> 31 /// Initializes a new instance of the <see cref="RepairInterop"/> class. 32 /// </summary> 33 /// <param name="initializer">Initializer.</param> 34 public RepairInterop(IInstanceInitializer initializer) 35 : base(initializer) 36 { 37 } 38 39 /// <summary> 40 /// Test setup. 41 /// </summary> 42 [SetUp] 43 public void Init() 44 { 45 this.packageManager = this.TestFactory.CreatePackageManager(); 46 this.installDir = TestCommon.GetRandomTestDir(); 47 48 // Create a composite source 49 var options = this.TestFactory.CreateCreateCompositePackageCatalogOptions(); 50 var testSource = this.packageManager.GetPackageCatalogByName(Constants.TestSourceName); 51 Assert.NotNull(testSource, $"{Constants.TestSourceName} cannot be null"); 52 options.Catalogs.Add(testSource); 53 options.CompositeSearchBehavior = CompositeSearchBehavior.AllCatalogs; 54 this.compositeSource = this.packageManager.CreateCompositePackageCatalog(options); 55 } 56 57 /// <summary> 58 /// Test Repair MSI Installer. 59 /// </summary> 60 /// <returns>A <see cref="Task"/> representing the asynchronous unit test. </returns> 61 [Test] 62 public async Task RepairMSIInstaller() 63 { 64 string msiInstallerPackageId = "AppInstallerTest.TestMsiRepair"; 65 var searchResult = await this.FindAndInstallPackage(msiInstallerPackageId, this.installDir, null); 66 67 // Note: The 'msiexec repair' command requires the original installer file to be present at the location registered in the ARP (Add/Remove Programs). 68 // In our test scenario, the MSI installer file is initially placed in a temporary location and then deleted, which can cause the repair operation to fail. 69 // To work around this, we copy the installer file to the ARP source directory before running the repair command. 70 // A more permanent solution would be to modify the MSI installer to cache the installer file in a known location and register that location as the installer source. 71 // This would allow the 'msiexec repair' command to function as expected. 72 string installerSourceDir = TestCommon.CopyInstallerFileToARPInstallSourceDirectory(TestCommon.GetTestDataFile("AppInstallerTestMsiInstallerV2.msi"), Constants.MsiInstallerProductCode, true); 73 74 // Repair the package 75 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); 76 77 // Cleanup 78 Assert.True(TestCommon.VerifyTestMsiInstalledAndCleanup(this.installDir)); 79 80 if (installerSourceDir != null && Directory.Exists(installerSourceDir)) 81 { 82 Directory.Delete(installerSourceDir, true); 83 } 84 } 85 86 /// <summary> 87 /// Test Repair MSIX Installer. 88 /// </summary> 89 /// <returns>representing the asynchronous unit test.</returns> 90 [Test] 91 public async Task RepairNonStoreMSIXPackage() 92 { 93 string msixPackageId = "AppInstallerTest.TestMsixInstaller"; 94 var searchResult = await this.FindAndInstallPackage(msixPackageId, this.installDir, null); 95 96 // Repair the package 97 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); 98 99 // Cleanup 100 Assert.True(TestCommon.VerifyTestMsixInstalledAndCleanup()); 101 } 102 103 /// <summary> 104 /// Test MSIX non-store package repair with machine scope. 105 /// </summary> 106 /// <returns>representing the asynchronous unit test.</returns> 107 [Test] 108 public async Task RepairNonStoreMsixPackageWithMachineScope() 109 { 110 var findPackages = this.FindAllPackages(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.ContainsCaseInsensitive, "Microsoft.Paint"); 111 112 if (findPackages == null || findPackages.Count == 0) 113 { 114 Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe can't be found."); 115 } 116 117 var searchResult = findPackages.First(); 118 119 if (searchResult == null || searchResult.CatalogPackage == null || searchResult.CatalogPackage.InstalledVersion == null) 120 { 121 Assert.Ignore("Test skipped as Microsoft.Paint_8wekyb3d8bbwe is not installed."); 122 } 123 124 // Repair the package 125 var repairOptions = this.TestFactory.CreateRepairOptions(); 126 repairOptions.PackageRepairScope = PackageRepairScope.System; 127 128 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_INSTALL_SYSTEM_NOT_SUPPORTED); 129 } 130 131 /// <summary> 132 /// Test repair of a Burn installer that has a "modify" repair behavior specified in the manifest. 133 /// </summary> 134 /// <returns>representing the asynchronous unit test.</returns> 135 [Test] 136 public async Task RepairBurnInstallerWithModifyBehavior() 137 { 138 var replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestModifyRepair", useHKLM: true); 139 var searchResult = await this.FindAndInstallPackage(Constants.ModifyRepairInstaller, this.installDir, replaceInstallerArguments.ToString()); 140 141 // Repair the package 142 var repairOptions = this.TestFactory.CreateRepairOptions(); 143 repairOptions.PackageRepairMode = PackageRepairMode.Silent; 144 145 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.Ok); 146 147 // Cleanup 148 Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Modify Repair operation")); 149 } 150 151 /// <summary> 152 /// Tests the repair operation of a Burn installer that was installed in user scope but is being repaired in an admin context. 153 /// </summary> 154 /// <returns>representing the asynchronous unit test.</returns> 155 [Test] 156 public async Task RepairBurnInstallerInAdminContextWithUserScopeInstall() 157 { 158 if (this.TestFactory.Context != ClsidContext.InProc) 159 { 160 Assert.Ignore("Test is only applicable for InProc context."); 161 } 162 163 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestUserScopeInstallRepairInAdminContext"); 164 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestUserScopeInstallRepairInAdminContext", this.installDir, replaceInstallerArguments); 165 166 // Repair the package 167 var repairOptions = this.TestFactory.CreateRepairOptions(); 168 repairOptions.PackageRepairMode = PackageRepairMode.Silent; 169 170 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED); 171 172 // Cleanup 173 TestCommon.CleanupTestExeAndDirectory(this.installDir); 174 } 175 176 /// <summary> 177 /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoModify ARP flag set. 178 /// </summary> 179 /// <returns>representing the asynchronous unit test.</returns> 180 [Test] 181 public async Task RepairBurnInstallerWithModifyBehaviorAndNoModifyFlag() 182 { 183 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestModifyRepairWithNoModify", useHKLM: true, noModify: true); 184 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestModifyRepairWithNoModify", this.installDir, replaceInstallerArguments); 185 186 // Repair the package 187 var repairOptions = this.TestFactory.CreateRepairOptions(); 188 repairOptions.PackageRepairMode = PackageRepairMode.Silent; 189 190 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); 191 192 // Cleanup 193 TestCommon.CleanupTestExeAndDirectory(this.installDir); 194 } 195 196 /// <summary> 197 /// Tests the scenario where the repair operation is not supported for Portable Installer type. 198 /// </summary> 199 /// <returns>representing the asynchronous unit test.</returns> 200 [Test] 201 public async Task RepairOperationNotSupportedForPortableInstaller() 202 { 203 string installDir = TestCommon.GetPortablePackagesDirectory(); 204 string packageId, commandAlias, fileName, packageDirName, productCode; 205 packageId = "AppInstallerTest.TestPortableExe"; 206 packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier; 207 commandAlias = fileName = "AppInstallerTestExeInstaller.exe"; 208 209 var searchResult = await this.FindAndInstallPackage(packageId, null, null); 210 211 // Repair the package 212 var repairOptions = this.TestFactory.CreateRepairOptions(); 213 repairOptions.PackageRepairMode = PackageRepairMode.Silent; 214 215 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, repairOptions, RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); 216 217 // If no location specified, default behavior is to create a package directory with the name "{packageId}_{sourceId}" 218 TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true); 219 } 220 221 /// <summary> 222 /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest. 223 /// </summary> 224 /// <returns>representing the asynchronous unit test.</returns> 225 [Test] 226 public async Task RepairExeInstallerWithUninstallerBehavior() 227 { 228 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "UninstallerRepair", useHKLM: true); 229 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.UninstallerRepair", this.installDir, replaceInstallerArguments); 230 231 // Repair the package 232 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); 233 234 // Cleanup 235 Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Uninstaller Repair operation")); 236 } 237 238 /// <summary> 239 /// Test repair of a Exe installer that has a "uninstaller" repair behavior specified in the manifest and NoRepair ARP flag set. 240 /// </summary> 241 /// <returns>representing the asynchronous unit test.</returns> 242 [Test] 243 public async Task RepairExeInstallerWithUninstallerBehaviorAndNoRepairFlag() 244 { 245 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "UninstallerRepairWithNoRepair", useHKLM: true, noRepair: true); 246 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.UninstallerRepairWithNoRepair", this.installDir, replaceInstallerArguments); 247 248 // Repair the package 249 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.RepairError, Constants.ErrorCode.ERROR_REPAIR_NOT_SUPPORTED); 250 251 // Cleanup 252 TestCommon.CleanupTestExeAndDirectory(this.installDir); 253 } 254 255 /// <summary> 256 /// Test repair of a Nullsoft installer that has a "uninstaller" repair behavior specified in the manifest. 257 /// </summary> 258 /// <returns>representing the asynchronous unit test.</returns> 259 [Test] 260 public async Task RepairNullsoftInstallerWithUninstallerBehavior() 261 { 262 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "NullsoftUninstallerRepair", useHKLM: true); 263 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.NullsoftUninstallerRepair", this.installDir, replaceInstallerArguments.ToString()); 264 265 // Repair the package 266 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); 267 268 // Cleanup 269 Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Uninstaller Repair operation")); 270 } 271 272 /// <summary> 273 /// Test repair of a Inno installer that has a "installer" repair behavior specified in the manifest. 274 /// </summary> 275 /// <returns>representing the asynchronous unit test.</returns> 276 [Test] 277 public async Task RepairInnoInstallerWithInstallerRepairBehavior() 278 { 279 string replaceInstallerArguments = this.GetReplacementArguments(this.installDir, "2.0.0.0", "TestInstallerRepair", useHKLM: true); 280 var searchResult = await this.FindAndInstallPackage("AppInstallerTest.TestInstallerRepair", this.installDir, replaceInstallerArguments); 281 282 // Repair the package 283 await this.RepairPackageAndValidateStatus(searchResult.CatalogPackage, this.TestFactory.CreateRepairOptions(), RepairResultStatus.Ok); 284 285 // Cleanup 286 Assert.True(TestCommon.VerifyTestExeRepairCompletedAndCleanup(this.installDir, "Installer Repair operation")); 287 } 288 289 private async Task<MatchResult> FindAndInstallPackage(string packageId, string installDir, string replacementInstallerArguments) 290 { 291 // Find a package 292 var searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); 293 294 // Configure installation 295 var installOptions = this.TestFactory.CreateInstallOptions(); 296 installOptions.PackageInstallMode = PackageInstallMode.Silent; 297 298 if (!string.IsNullOrEmpty(installDir)) 299 { 300 installOptions.PreferredInstallLocation = installDir; 301 } 302 303 if (!string.IsNullOrEmpty(replacementInstallerArguments)) 304 { 305 installOptions.ReplacementInstallerArguments = replacementInstallerArguments; 306 } 307 308 // Install the package 309 var installResult = await this.packageManager.InstallPackageAsync(searchResult.CatalogPackage, installOptions); 310 Assert.AreEqual(InstallResultStatus.Ok, installResult.Status); 311 312 // Find package again, but this time it should be detected as installed 313 searchResult = this.FindOnePackage(this.compositeSource, PackageMatchField.Id, PackageFieldMatchOption.Equals, packageId); 314 Assert.NotNull(searchResult.CatalogPackage.InstalledVersion); 315 316 // Return the search result 317 return searchResult; 318 } 319 320 private async Task RepairPackageAndValidateStatus(CatalogPackage package, RepairOptions repairOptions, RepairResultStatus expectedStatus, int expectedErrorCode = 0) 321 { 322 var repairResult = await this.packageManager.RepairPackageAsync(package, repairOptions); 323 Assert.AreEqual(expectedStatus, repairResult.Status); 324 325 if (expectedStatus != RepairResultStatus.Ok) 326 { 327 Assert.AreEqual(expectedErrorCode, repairResult.ExtendedErrorCode.HResult); 328 } 329 } 330 331 private string GetReplacementArguments(string installDir, string version, string displayName, bool useHKLM = false, bool noModify = false, bool noRepair = false) 332 { 333 var replacementArguments = new StringBuilder($"/InstallDir {installDir} /Version {version} /DisplayName {displayName}"); 334 335 // Machine scope install. 336 if (useHKLM) 337 { 338 replacementArguments.Append($" /UseHKLM"); 339 } 340 341 // Instructs test installer to set NoModify ARP flag. 342 if (noModify) 343 { 344 replacementArguments.Append($" /NoModify"); 345 } 346 347 // Instructs test installer to set NoRepair ARP flag. 348 if (noRepair) 349 { 350 replacementArguments.Append($" /NoRepair"); 351 } 352 353 return replacementArguments.ToString(); 354 } 355 } 356 }