TestCommon.cs (50274B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="TestCommon.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace AppInstallerCLIE2ETests.Helpers 8 { 9 using System; 10 using System.Collections.Generic; 11 using System.Diagnostics; 12 using System.IO; 13 using System.Linq; 14 using System.Management.Automation; 15 using System.Reflection; 16 using System.Security.Principal; 17 using System.Text; 18 using System.Threading; 19 using AppInstallerCLIE2ETests; 20 using AppInstallerCLIE2ETests.PowerShell; 21 using Microsoft.Management.Deployment; 22 using Microsoft.Win32; 23 using NUnit.Framework; 24 25 /// <summary> 26 /// Test common. 27 /// </summary> 28 public static class TestCommon 29 { 30 /// <summary> 31 /// Scope. 32 /// </summary> 33 public enum Scope 34 { 35 /// <summary> 36 /// None. 37 /// </summary> 38 Unknown, 39 40 /// <summary> 41 /// User. 42 /// </summary> 43 User, 44 45 /// <summary> 46 /// Machine. 47 /// </summary> 48 Machine, 49 } 50 51 /// <summary> 52 /// The type of location. 53 /// </summary> 54 public enum TestModuleLocation 55 { 56 /// <summary> 57 /// Current user. 58 /// </summary> 59 CurrentUser, 60 61 /// <summary> 62 /// All users. 63 /// </summary> 64 AllUsers, 65 66 /// <summary> 67 /// Winget module path. 68 /// </summary> 69 WinGetModulePath, 70 71 /// <summary> 72 /// Custom. 73 /// </summary> 74 Custom, 75 76 /// <summary> 77 /// Default winget configure. 78 /// </summary> 79 Default, 80 } 81 82 /// <summary> 83 /// Gets a value indicating whether the current assembly is executing in an administrative context. 84 /// </summary> 85 [System.Diagnostics.CodeAnalysis.SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows only API")] 86 public static bool ExecutingAsAdministrator 87 { 88 get 89 { 90 WindowsIdentity identity = WindowsIdentity.GetCurrent(); 91 WindowsPrincipal principal = new (identity); 92 return principal.IsInRole(WindowsBuiltInRole.Administrator); 93 } 94 } 95 96 /// <summary> 97 /// Gets a value indicating whether the test is running in the CI build. 98 /// </summary> 99 public static bool IsCIEnvironment 100 { 101 get 102 { 103 return Environment.GetEnvironmentVariable("BUILD_BUILDNUMBER") != null; 104 } 105 } 106 107 /// <summary> 108 /// Run winget command. 109 /// </summary> 110 /// <param name="command">Command to run.</param> 111 /// <param name="parameters">Parameters.</param> 112 /// <param name="stdIn">Optional std in.</param> 113 /// <param name="timeOut">Optional timeout.</param> 114 /// <param name="throwOnTimeout">Throw on timeout.</param> 115 /// <returns>The result of the command.</returns> 116 public static RunCommandResult RunAICLICommand(string command, string parameters, string stdIn = null, int timeOut = 60000, bool throwOnTimeout = true) 117 { 118 string correlationParameter = " --correlation " + Guid.NewGuid().ToString(); 119 120 // Don't include correlation when the call has an option ending `--` value. 121 foreach (string part in parameters.Split(' ', StringSplitOptions.TrimEntries)) 122 { 123 if (part == "--") 124 { 125 correlationParameter = string.Empty; 126 } 127 } 128 129 string inputMsg = 130 "AICLI path: " + TestSetup.Parameters.AICLIPath + 131 " Command: " + command + 132 " Parameters: " + parameters + correlationParameter + 133 (string.IsNullOrEmpty(stdIn) ? string.Empty : " StdIn: " + stdIn) + 134 " Timeout: " + timeOut; 135 136 TestContext.Out.WriteLine($"Starting command run. {inputMsg}"); 137 138 return RunAICLICommandViaDirectProcess(command, parameters + correlationParameter, stdIn, timeOut, throwOnTimeout); 139 } 140 141 /// <summary> 142 /// Run command. 143 /// </summary> 144 /// <param name="fileName">File name.</param> 145 /// <param name="args">Args.</param> 146 /// <param name="timeOut">Time out.</param> 147 /// <param name="throwOnFailure">If true, throw instead of returning false on a failure.</param> 148 /// <returns>True if exit code is 0.</returns> 149 public static bool RunCommand(string fileName, string args = "", int timeOut = 60000, bool throwOnFailure = false) 150 { 151 RunCommandResult result = RunCommandWithResult(fileName, args, timeOut); 152 153 if (result.ExitCode != 0) 154 { 155 TestContext.Out.WriteLine($"Command failed with: {result.ExitCode}"); 156 if (throwOnFailure) 157 { 158 throw new RunCommandException(fileName, args, result); 159 } 160 161 return false; 162 } 163 else 164 { 165 return true; 166 } 167 } 168 169 /// <summary> 170 /// Run command with result. 171 /// </summary> 172 /// <param name="fileName">File name.</param> 173 /// <param name="args">Args.</param> 174 /// <param name="timeOut">Optional timeout.</param> 175 /// <returns>Command result.</returns> 176 public static RunCommandResult RunCommandWithResult(string fileName, string args, int timeOut = 60000) 177 { 178 TestContext.Out.WriteLine($"Running command: {fileName} {args}"); 179 180 Process p = new Process(); 181 p.StartInfo = new ProcessStartInfo(fileName, args); 182 p.StartInfo.RedirectStandardOutput = true; 183 p.StartInfo.RedirectStandardError = true; 184 p.Start(); 185 186 RunCommandResult result = new (); 187 if (p.WaitForExit(timeOut)) 188 { 189 result.ExitCode = p.ExitCode; 190 result.StdOut = p.StandardOutput.ReadToEnd(); 191 result.StdErr = p.StandardError.ReadToEnd(); 192 193 if (TestSetup.Parameters.VerboseLogging) 194 { 195 TestContext.Out.WriteLine($"Command run finished. {fileName} {args} {timeOut}. Output: {result.StdOut} Error: {result.StdErr}"); 196 } 197 } 198 else 199 { 200 throw new TimeoutException($"Command run timed out. {fileName} {args} {timeOut}"); 201 } 202 203 return result; 204 } 205 206 /// <summary> 207 /// Get test file path. 208 /// </summary> 209 /// <param name="fileName">Test file name.</param> 210 /// <returns>Path of test file.</returns> 211 public static string GetTestFile(string fileName) 212 { 213 return Path.Combine(TestContext.CurrentContext.TestDirectory, fileName); 214 } 215 216 /// <summary> 217 /// Get test data file path. 218 /// </summary> 219 /// <param name="fileName">File name.</param> 220 /// <returns>Test file data path.</returns> 221 public static string GetTestDataFile(string fileName) 222 { 223 return GetTestFile(Path.Combine("TestData", fileName)); 224 } 225 226 /// <summary> 227 /// Get test work directory. Creates if not exists. 228 /// </summary> 229 /// <returns>The work directory.</returns> 230 public static string GetTestWorkDir() 231 { 232 string workDir = Path.Combine(TestContext.CurrentContext.TestDirectory, "WorkDirectory"); 233 Directory.CreateDirectory(workDir); 234 return workDir; 235 } 236 237 /// <summary> 238 /// Create random test directory. 239 /// </summary> 240 /// <returns>Path of new test directory.</returns> 241 public static string GetRandomTestDir() 242 { 243 string randDir = Path.Combine(GetTestWorkDir(), Path.GetRandomFileName()); 244 Directory.CreateDirectory(randDir); 245 return randDir; 246 } 247 248 /// <summary> 249 /// Creates new random file name. File is not created. 250 /// </summary> 251 /// <param name="extension">Extension of random file.</param> 252 /// <returns>Path of random file.</returns> 253 public static string GetRandomTestFile(string extension) 254 { 255 return Path.Combine(GetTestWorkDir(), Path.GetRandomFileName() + extension); 256 } 257 258 /// <summary> 259 /// Install msix package via PowerShell. 260 /// </summary> 261 /// <param name="file">Msix file.</param> 262 /// <returns>True if installed.</returns> 263 public static bool InstallMsix(string file) 264 { 265 return RunCommand("powershell", $"Add-AppxPackage \"{file}\"", throwOnFailure: true); 266 } 267 268 /// <summary> 269 /// Install and register msix package via appx manifest. 270 /// </summary> 271 /// <param name="packagePath">Path to package.</param> 272 /// <param name="forceShutdown">Force shutdown.</param> 273 /// <param name="throwOnFailure">Throw on failure.</param> 274 /// <returns>True if installed correctly.</returns> 275 public static bool InstallMsixRegister(string packagePath, bool forceShutdown = false, bool throwOnFailure = true) 276 { 277 string manifestFile = Path.Combine(packagePath, "AppxManifest.xml"); 278 279 var command = $"Add-AppxPackage -Register \"{manifestFile}\""; 280 if (forceShutdown) 281 { 282 command += " -ForceTargetApplicationShutdown"; 283 } 284 285 return RunCommand("powershell", command, throwOnFailure: throwOnFailure); 286 } 287 288 /// <summary> 289 /// Remove msix package. 290 /// </summary> 291 /// <param name="name">Package to remove.</param> 292 /// <param name="isProvisioned">Whether the package is provisioned.</param> 293 /// <returns>True if removed correctly.</returns> 294 public static bool RemoveMsix(string name, bool isProvisioned = false) 295 { 296 if (isProvisioned) 297 { 298 return RunCommand("powershell", $"Get-AppxProvisionedPackage -Online | Where-Object {{$_.PackageName -like \"*{name}*\"}} | Remove-AppxProvisionedPackage -Online -AllUsers") && 299 RunCommand("powershell", $"Get-AppxPackage \"{name}\" | Remove-AppxPackage -AllUsers"); 300 } 301 else 302 { 303 return RunCommand("powershell", $"Get-AppxPackage \"{name}\" | Remove-AppxPackage"); 304 } 305 } 306 307 /// <summary> 308 /// Gets the portable symlink directory. 309 /// </summary> 310 /// <param name="scope">Scope.</param> 311 /// <returns>The path of the symlinks.</returns> 312 public static string GetPortableSymlinkDirectory(Scope scope) 313 { 314 if (scope == Scope.User) 315 { 316 return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Links"); 317 } 318 else 319 { 320 return Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "WinGet", "Links"); 321 } 322 } 323 324 /// <summary> 325 /// Gets the portable package directory. 326 /// </summary> 327 /// <returns>The portable package directory.</returns> 328 public static string GetPortablePackagesDirectory() 329 { 330 return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "Microsoft", "WinGet", "Packages"); 331 } 332 333 /// <summary> 334 /// Gets the default download directory for the download command. 335 /// </summary> 336 /// <returns>The default download directory.</returns> 337 public static string GetDefaultDownloadDirectory() 338 { 339 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); 340 } 341 342 /// <summary> 343 /// Gets the checkpoints directory based on whether the command is invoked in desktop package or not. 344 /// </summary> 345 /// <returns>The default checkpoints directory.</returns> 346 public static string GetCheckpointsDirectory() 347 { 348 if (TestSetup.Parameters.PackagedContext) 349 { 350 return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), Constants.CheckpointDirectoryPackaged); 351 } 352 else 353 { 354 return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), Constants.CheckpointDirectoryUnpackaged); 355 } 356 } 357 358 /// <summary> 359 /// Verify portable package. 360 /// </summary> 361 /// <param name="installDir">Install dir.</param> 362 /// <param name="commandAlias">Command alias.</param> 363 /// <param name="filename">File name.</param> 364 /// <param name="productCode">Product code.</param> 365 /// <param name="shouldExist">Should exists.</param> 366 /// <param name="scope">Scope.</param> 367 /// <param name="installDirectoryAddedToPath">Install directory added to path instead of the symlink directory.</param> 368 public static void VerifyPortablePackage( 369 string installDir, 370 string commandAlias, 371 string filename, 372 string productCode, 373 bool shouldExist, 374 Scope scope = Scope.User, 375 bool installDirectoryAddedToPath = false) 376 { 377 // When portables are installed, if the exe path is inside a directory it will not be aliased 378 // if the exe path is at the root level, it will be aliased. Therefore, if either exist, the exe exists 379 string exePath = Path.Combine(installDir, filename); 380 string exeAliasedPath = Path.Combine(installDir, commandAlias); 381 bool exeExists = File.Exists(exePath) || File.Exists(exeAliasedPath); 382 383 string symlinkDirectory = GetPortableSymlinkDirectory(scope); 384 string symlinkPath = Path.Combine(symlinkDirectory, commandAlias); 385 bool symlinkExists = File.Exists(symlinkPath); 386 387 bool portableEntryExists; 388 RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; 389 string uninstallSubKey = Constants.UninstallSubKey; 390 using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(uninstallSubKey, true)) 391 { 392 RegistryKey portableEntry = uninstallRegistryKey.OpenSubKey(productCode, true); 393 portableEntryExists = portableEntry != null; 394 } 395 396 bool isAddedToPath; 397 string pathSubKey = scope == Scope.User ? Constants.PathSubKey_User : Constants.PathSubKey_Machine; 398 using (RegistryKey environmentRegistryKey = baseKey.OpenSubKey(pathSubKey, true)) 399 { 400 string pathName = "Path"; 401 var currentPathValue = (string)environmentRegistryKey.GetValue(pathName); 402 var portablePathValue = (installDirectoryAddedToPath ? installDir : symlinkDirectory) + ';'; 403 isAddedToPath = currentPathValue.Contains(portablePathValue); 404 } 405 406 // Always clean up as best effort. 407 RunAICLICommand("uninstall", $"--product-code {productCode} --force"); 408 409 Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}"); 410 Assert.AreEqual(shouldExist && !installDirectoryAddedToPath, symlinkExists, $"Expected portable symlink path: {symlinkPath}"); 411 Assert.AreEqual(shouldExist, portableEntryExists, $"Expected {productCode} subkey in path: {uninstallSubKey}"); 412 Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {(installDirectoryAddedToPath ? installDir : symlinkDirectory)}"); 413 } 414 415 /// <summary> 416 /// Copies log files to the path %TEMP%\E2ETestLogs. 417 /// </summary> 418 public static void PublishE2ETestLogs() 419 { 420 string tempPath = Path.GetTempPath(); 421 string localAppDataPath = Environment.GetEnvironmentVariable("LocalAppData"); 422 string testLogsPackagedSourcePath = Path.Combine(localAppDataPath, Constants.E2ETestLogsPathPackaged); 423 string testLogsUnpackagedSourcePath = Path.Combine(tempPath, Constants.E2ETestLogsPathUnpackaged); 424 string testLogsDestPath = Path.Combine(tempPath, "E2ETestLogs"); 425 string testLogsPackagedDestPath = Path.Combine(testLogsDestPath, "Packaged"); 426 string testLogsUnpackagedDestPath = Path.Combine(testLogsDestPath, "Unpackaged"); 427 428 if (Directory.Exists(testLogsPackagedSourcePath)) 429 { 430 CopyDirectory(testLogsPackagedSourcePath, testLogsPackagedDestPath); 431 } 432 433 if (Directory.Exists(testLogsUnpackagedSourcePath)) 434 { 435 CopyDirectory(testLogsUnpackagedSourcePath, testLogsUnpackagedDestPath); 436 } 437 } 438 439 /// <summary> 440 /// Gets the server certificate as a hex string. 441 /// </summary> 442 /// <returns>Hex string.</returns> 443 public static string GetTestServerCertificateHexString() 444 { 445 if (string.IsNullOrEmpty(TestSetup.Parameters.LocalServerCertPath)) 446 { 447 throw new Exception($"{Constants.LocalServerCertPathParameter} not set."); 448 } 449 450 if (!File.Exists(TestSetup.Parameters.LocalServerCertPath)) 451 { 452 throw new FileNotFoundException(TestSetup.Parameters.LocalServerCertPath); 453 } 454 455 return Convert.ToHexString(File.ReadAllBytes(TestSetup.Parameters.LocalServerCertPath)); 456 } 457 458 /// <summary> 459 /// Verify exe installer correctly. 460 /// </summary> 461 /// <param name="installDir">Install directory.</param> 462 /// <param name="expectedContent">Optional expected content.</param> 463 /// <returns>True if success.</returns> 464 public static bool VerifyTestExeInstalled(string installDir, string expectedContent = null) 465 { 466 bool verifyInstallSuccess = true; 467 468 if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) 469 { 470 TestContext.Out.WriteLine($"TestExeInstalled.exe not found at {installDir}"); 471 verifyInstallSuccess = false; 472 } 473 474 if (verifyInstallSuccess && !string.IsNullOrEmpty(expectedContent)) 475 { 476 string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeInstalledFileName)); 477 TestContext.Out.WriteLine($"TestExeInstalled.exe content: {content}"); 478 verifyInstallSuccess = content.Contains(expectedContent); 479 } 480 481 return verifyInstallSuccess; 482 } 483 484 /// <summary> 485 /// Verifies if the repair of the test executable was successful. 486 /// </summary> 487 /// <param name="installDir">The directory where the test executable is installed.</param> 488 /// <param name="expectedContent">The expected content in the test executable file. This is optional.</param> 489 /// <returns>Returns true if the repair was successful, false otherwise.</returns> 490 public static bool VerifyTestExeRepairSuccessful(string installDir, string expectedContent = null) 491 { 492 bool verifyRepairSuccess = true; 493 494 if (!File.Exists(Path.Combine(installDir, Constants.TestExeRepairCompletedFileName))) 495 { 496 TestContext.Out.WriteLine($"{Constants.TestExeRepairCompletedFileName} not found at {installDir}"); 497 verifyRepairSuccess = false; 498 } 499 500 if (verifyRepairSuccess && !string.IsNullOrEmpty(expectedContent)) 501 { 502 string content = File.ReadAllText(Path.Combine(installDir, Constants.TestExeRepairCompletedFileName)); 503 TestContext.Out.WriteLine($"TestExeRepairCompleted.txt content: {content}"); 504 verifyRepairSuccess = content.Contains(expectedContent); 505 } 506 507 return verifyRepairSuccess; 508 } 509 510 /// <summary> 511 /// Assert installer and manifest downloaded correctly and cleanup. 512 /// </summary> 513 /// <param name="downloadDir">Download directory.</param> 514 /// <param name="name">Package name.</param> 515 /// <param name="version">Package version.</param> 516 /// <param name="arch">Installer architecture.</param> 517 /// <param name="scope">Installer scope.</param> 518 /// <param name="installerType">Installer type.</param> 519 /// <param name="locale">Installer locale.</param> 520 /// <param name="isArchive">Boolean value indicating whether the installer is an archive.</param> 521 /// <param name="cleanup">Boolean value indicating whether to remove the installer file and directory.</param> 522 public static void AssertInstallerDownload( 523 string downloadDir, 524 string name, 525 string version, 526 Windows.System.ProcessorArchitecture arch, 527 Scope scope, 528 PackageInstallerType installerType, 529 string locale = null, 530 bool isArchive = false, 531 bool cleanup = true) 532 { 533 string expectedFileName = $"{name}_{version}"; 534 535 if (scope != Scope.Unknown) 536 { 537 expectedFileName += $"_{scope}"; 538 } 539 540 expectedFileName += $"_{arch}_{installerType}"; 541 542 if (!string.IsNullOrEmpty(locale)) 543 { 544 expectedFileName += $"_{locale}"; 545 } 546 547 string installerExtension; 548 if (isArchive) 549 { 550 installerExtension = ".zip"; 551 } 552 else 553 { 554 installerExtension = installerType switch 555 { 556 PackageInstallerType.Msi => ".msi", 557 PackageInstallerType.Msix => ".msix", 558 _ => ".exe" 559 }; 560 } 561 562 string installerDownloadPath = Path.Combine(downloadDir, expectedFileName + installerExtension); 563 string manifestDownloadPath = Path.Combine(downloadDir, expectedFileName + ".yaml"); 564 565 Assert.IsTrue(Directory.Exists(downloadDir), $"Download directory does not exist: {downloadDir}"); 566 Assert.IsTrue(File.Exists(installerDownloadPath), $"Installer file does not exist: {installerDownloadPath}"); 567 Assert.IsTrue(File.Exists(manifestDownloadPath), $"Manifest file does not exist: {manifestDownloadPath}"); 568 569 if (cleanup) 570 { 571 Directory.Delete(downloadDir, true); 572 } 573 } 574 575 /// <summary> 576 /// Best effort test exe cleanup. 577 /// </summary> 578 /// <param name="installDir">Install directory.</param> 579 public static void BestEffortTestExeCleanup(string installDir) 580 { 581 var uninstallerPath = Path.Combine(installDir, Constants.TestExeUninstallerFileName); 582 if (File.Exists(uninstallerPath)) 583 { 584 RunCommand(Path.Combine(installDir, Constants.TestExeUninstallerFileName)); 585 } 586 } 587 588 /// <summary> 589 /// Best effort test exe cleanup and install directory cleanup. 590 /// </summary> 591 /// <param name="installDir">Install directory.</param> 592 public static void CleanupTestExeAndDirectory(string installDir) 593 { 594 // Always try clean up and ignore clean up failure 595 BestEffortTestExeCleanup(installDir); 596 597 // Delete the install directory to reclaim disk space 598 if (Directory.Exists(installDir)) 599 { 600 Directory.Delete(installDir, true); 601 } 602 } 603 604 /// <summary> 605 /// Verify exe installer correctly and then uninstall it. 606 /// </summary> 607 /// <param name="installDir">Install directory.</param> 608 /// <param name="expectedContent">Optional expected content.</param> 609 /// <returns>True if success.</returns> 610 public static bool VerifyTestExeInstalledAndCleanup(string installDir, string expectedContent = null) 611 { 612 bool verifyInstallSuccess = VerifyTestExeInstalled(installDir, expectedContent); 613 614 // Always try clean up and ignore clean up failure 615 BestEffortTestExeCleanup(installDir); 616 617 return verifyInstallSuccess; 618 } 619 620 /// <summary> 621 /// Verify exe repair completed and cleanup. 622 /// </summary> 623 /// <param name="installDir">Install directory.</param> 624 /// <param name="expectedContent">Optional expected context.</param> 625 /// <returns>True if success.</returns> 626 public static bool VerifyTestExeRepairCompletedAndCleanup(string installDir, string expectedContent = null) 627 { 628 bool verifyRepairSuccess = VerifyTestExeRepairSuccessful(installDir, expectedContent); 629 CleanupTestExeAndDirectory(installDir); 630 631 return verifyRepairSuccess; 632 } 633 634 /// <summary> 635 /// Verify msi installed correctly. 636 /// </summary> 637 /// <param name="installDir">Installed directory.</param> 638 /// <returns>True if success.</returns> 639 public static bool VerifyTestMsiInstalledAndCleanup(string installDir) 640 { 641 string pathToCheck = Path.Combine(installDir, Constants.AppInstallerTestExeInstallerExe); 642 if (!File.Exists(pathToCheck)) 643 { 644 TestContext.Out.WriteLine($"File not found: {pathToCheck}"); 645 return false; 646 } 647 648 return RunCommand("msiexec.exe", $"/qn /x {Constants.MsiInstallerProductCode}"); 649 } 650 651 /// <summary> 652 /// Verify msix installed correctly. 653 /// </summary> 654 /// <param name="isProvisioned">Whether the package is provisioned.</param> 655 /// <returns>True if success.</returns> 656 public static bool VerifyTestMsixInstalledAndCleanup(bool isProvisioned = false) 657 { 658 var result = RunCommandWithResult("powershell", $"Get-AppxPackage {Constants.MsixInstallerName}"); 659 660 if (!result.StdOut.Contains(Constants.MsixInstallerName)) 661 { 662 return false; 663 } 664 665 if (isProvisioned) 666 { 667 result = RunCommandWithResult("powershell", $"Get-AppxProvisionedPackage -Online | Where-Object {{$_.PackageName -like \"*{Constants.MsixInstallerName}*\"}}"); 668 if (!result.StdOut.Contains(Constants.MsixInstallerName)) 669 { 670 return false; 671 } 672 } 673 674 return RemoveMsix(Constants.MsixInstallerName, isProvisioned); 675 } 676 677 /// <summary> 678 /// Verify test exe uninstalled. 679 /// </summary> 680 /// <param name="installDir">Installed directory.</param> 681 /// <returns>True if success.</returns> 682 public static bool VerifyTestExeUninstalled(string installDir) 683 { 684 return File.Exists(Path.Combine(installDir, Constants.TestExeUninstalledFileName)); 685 } 686 687 /// <summary> 688 /// Verify msi uninstalled. 689 /// </summary> 690 /// <param name="installDir">Install directory.</param> 691 /// <returns>True if success.</returns> 692 public static bool VerifyTestMsiUninstalled(string installDir) 693 { 694 return !File.Exists(Path.Combine(installDir, Constants.AppInstallerTestExeInstallerExe)); 695 } 696 697 /// <summary> 698 /// Verify msix uninstalled. 699 /// </summary> 700 /// <param name="isProvisioned">Whether the package is provisioned.</param> 701 /// <returns>True if success.</returns> 702 public static bool VerifyTestMsixUninstalled(bool isProvisioned = false) 703 { 704 bool isUninstalled = false; 705 var result = RunCommandWithResult("powershell", $"Get-AppxPackage {Constants.MsixInstallerName}"); 706 isUninstalled = string.IsNullOrWhiteSpace(result.StdOut); 707 708 if (isProvisioned) 709 { 710 result = RunCommandWithResult("powershell", $"Get-AppxProvisionedPackage -Online | Where-Object {{$_.PackageName -like \"*{Constants.MsixInstallerName}*\"}}"); 711 isUninstalled = isUninstalled && string.IsNullOrWhiteSpace(result.StdOut); 712 } 713 714 return isUninstalled; 715 } 716 717 /// <summary> 718 /// Modify uninstalled registry key. 719 /// </summary> 720 /// <param name="productCode">Product code.</param> 721 /// <param name="name">Name.</param> 722 /// <param name="value">Value.</param> 723 public static void ModifyPortableARPEntryValue(string productCode, string name, string value) 724 { 725 using (RegistryKey uninstallRegistryKey = Registry.CurrentUser.OpenSubKey(Constants.UninstallSubKey, true)) 726 { 727 RegistryKey entry = uninstallRegistryKey.OpenSubKey(productCode, true); 728 entry.SetValue(name, value); 729 } 730 } 731 732 /// <summary> 733 /// Set up test source. 734 /// </summary> 735 /// <param name="useGroupPolicyForTestSource">Use group policy.</param> 736 public static void SetupTestSource(bool useGroupPolicyForTestSource = false) 737 { 738 // Remove the test source so that its package is also removed. 739 RunAICLICommand("source remove", Constants.TestSourceName); 740 741 RunAICLICommand("source reset", "--force"); 742 RunAICLICommand("source remove", Constants.DefaultWingetSourceName); 743 RunAICLICommand("source remove", Constants.DefaultMSStoreSourceName); 744 745 // TODO: If/when cert pinning is implemented on the packaged index source, useGroupPolicyForTestSource should be set to default true 746 // to enable testing it by default. Until then, leaving this here... 747 if (useGroupPolicyForTestSource) 748 { 749 GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new GroupPolicyHelper.GroupPolicySource[] 750 { 751 new GroupPolicyHelper.GroupPolicySource 752 { 753 Name = Constants.TestSourceName, 754 Arg = Constants.TestSourceUrl, 755 Type = Constants.TestSourceType, 756 Data = Constants.TestSourceIdentifier, 757 Identifier = Constants.TestSourceIdentifier, 758 CertificatePinning = new GroupPolicyHelper.GroupPolicyCertificatePinning 759 { 760 Chains = new GroupPolicyHelper.GroupPolicyCertificatePinningChain[] 761 { 762 new GroupPolicyHelper.GroupPolicyCertificatePinningChain 763 { 764 Chain = new GroupPolicyHelper.GroupPolicyCertificatePinningDetails[] 765 { 766 new GroupPolicyHelper.GroupPolicyCertificatePinningDetails 767 { 768 Validation = new string[] { "publickey" }, 769 EmbeddedCertificate = GetTestServerCertificateHexString(), 770 }, 771 }, 772 }, 773 }, 774 }, 775 TrustLevel = new string[] { "None" }, 776 Explicit = false, 777 }, 778 }); 779 } 780 else 781 { 782 GroupPolicyHelper.EnableAdditionalSources.SetNotConfigured(); 783 RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl} --trust-level trusted"); 784 } 785 786 Thread.Sleep(2000); 787 } 788 789 /// <summary> 790 /// Tear down test source. 791 /// </summary> 792 public static void TearDownTestSource() 793 { 794 RunAICLICommand("source remove", Constants.TestSourceName); 795 RunAICLICommand("source reset", "--force"); 796 } 797 798 /// <summary> 799 /// Ensures that a module is in the desired state. 800 /// </summary> 801 /// <param name="moduleName">The module.</param> 802 /// <param name="present">Whether the module is present or not.</param> 803 /// <param name="repository">The repository to get the module from if needed.</param> 804 /// <param name="location">The location to install the module.</param> 805 public static void EnsureModuleState(string moduleName, bool present, string repository = null, TestCommon.TestModuleLocation location = TestModuleLocation.CurrentUser) 806 { 807 string wingetModulePath = TestCommon.GetExpectedModulePath(TestModuleLocation.WinGetModulePath); 808 string customPath = TestCommon.GetExpectedModulePath(TestModuleLocation.Custom); 809 810 ICollection<PSModuleInfo> e2eModule; 811 bool isPresent = false; 812 { 813 using var pwsh = new PowerShellHost(); 814 pwsh.AddModulePath($"{wingetModulePath};{customPath}"); 815 816 e2eModule = pwsh.PowerShell.AddCommand("Get-Module").AddParameter("Name", moduleName).AddParameter("ListAvailable").Invoke<PSModuleInfo>(); 817 isPresent = e2eModule.Any(); 818 } 819 820 TestContext.Out.WriteLine($"EnsureModuleState: {moduleName}[present:{present}] => isPresent:{isPresent}"); 821 822 if (isPresent) 823 { 824 // If the module was saved in a different location we can't Uninstall-Module. 825 foreach (var module in e2eModule) 826 { 827 var moduleBase = module.Path; 828 while (Path.GetFileName(moduleBase) != moduleName) 829 { 830 moduleBase = Path.GetDirectoryName(moduleBase); 831 } 832 833 if (!present) 834 { 835 TestContext.Out.WriteLine($"EnsureModuleState: Removing {moduleName} to match present=false"); 836 Directory.Delete(moduleBase, true); 837 } 838 else 839 { 840 // Must be present in the right location. 841 var expectedLocation = TestCommon.GetExpectedModulePath(location); 842 if (!moduleBase.StartsWith(expectedLocation)) 843 { 844 TestContext.Out.WriteLine($"EnsureModuleState: Removing {moduleName} as it is not in the correct location"); 845 Directory.Delete(moduleBase, true); 846 isPresent = false; 847 } 848 } 849 } 850 } 851 852 if (!isPresent && present) 853 { 854 if (location == TestModuleLocation.CurrentUser || 855 location == TestModuleLocation.AllUsers) 856 { 857 using var pwsh = new PowerShellHost(); 858 pwsh.AddModulePath($"{wingetModulePath};{customPath}"); 859 pwsh.PowerShell.AddCommand("Install-Module").AddParameter("Name", moduleName).AddParameter("Force"); 860 861 if (!string.IsNullOrEmpty(repository)) 862 { 863 pwsh.PowerShell.AddParameter("Repository", repository); 864 } 865 866 if (location == TestModuleLocation.CurrentUser) 867 { 868 pwsh.PowerShell.AddParameter("Scope", "CurrentUser"); 869 } 870 else if (location == TestModuleLocation.AllUsers) 871 { 872 pwsh.PowerShell.AddParameter("Scope", "AllUsers"); 873 } 874 875 TestContext.Out.WriteLine($"EnsureModuleState: Installing module {moduleName} to {location}"); 876 _ = pwsh.PowerShell.Invoke(); 877 } 878 else 879 { 880 string path = customPath; 881 if (location == TestModuleLocation.WinGetModulePath || 882 location == TestModuleLocation.Default) 883 { 884 path = wingetModulePath; 885 } 886 887 using var pwsh = new PowerShellHost(); 888 pwsh.AddModulePath($"{wingetModulePath};{customPath}"); 889 pwsh.PowerShell.AddCommand("Save-Module").AddParameter("Name", moduleName).AddParameter("Path", path).AddParameter("Force"); 890 891 if (!string.IsNullOrEmpty(repository)) 892 { 893 pwsh.PowerShell.AddParameter("Repository", repository); 894 } 895 896 TestContext.Out.WriteLine($"EnsureModuleState: Saving module {moduleName} to {path}"); 897 _ = pwsh.PowerShell.Invoke(); 898 } 899 } 900 } 901 902 /// <summary> 903 /// Creates an ARP entry from the given values. 904 /// </summary> 905 /// <param name="productCode">Product code of the entry.</param> 906 /// <param name="properties">The properties to set in the entry.</param> 907 /// <param name="scope">Scope of the entry.</param> 908 public static void CreateARPEntry( 909 string productCode, 910 object properties, 911 Scope scope = Scope.User) 912 { 913 RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; 914 using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(Constants.UninstallSubKey, true)) 915 { 916 RegistryKey entry = uninstallRegistryKey.CreateSubKey(productCode, true); 917 918 foreach (PropertyInfo property in properties.GetType().GetProperties()) 919 { 920 entry.SetValue(property.Name, property.GetValue(properties)); 921 } 922 } 923 } 924 925 /// <summary> 926 /// Removes an ARP entry. 927 /// </summary> 928 /// <param name="productCode">Product code of the entry.</param> 929 /// <param name="scope">Scope of the entry.</param> 930 public static void RemoveARPEntry( 931 string productCode, 932 Scope scope = Scope.User) 933 { 934 RegistryKey baseKey = scope == Scope.User ? Registry.CurrentUser : Registry.LocalMachine; 935 using (RegistryKey uninstallRegistryKey = baseKey.OpenSubKey(Constants.UninstallSubKey, true)) 936 { 937 uninstallRegistryKey.DeleteSubKey(productCode); 938 } 939 } 940 941 /// <summary> 942 /// Copies the contents of a given directory from a source path to a destination path. 943 /// </summary> 944 /// <param name="sourceDirName">Source directory name.</param> 945 /// <param name="destDirName">Destination directory name.</param> 946 public static void CopyDirectory(string sourceDirName, string destDirName) 947 { 948 DirectoryInfo dir = new DirectoryInfo(sourceDirName); 949 DirectoryInfo[] dirs = dir.GetDirectories(); 950 951 if (!Directory.Exists(destDirName)) 952 { 953 Directory.CreateDirectory(destDirName); 954 } 955 956 FileInfo[] files = dir.GetFiles(); 957 foreach (FileInfo file in files) 958 { 959 string temppath = Path.Combine(destDirName, file.Name); 960 file.CopyTo(temppath, false); 961 } 962 963 foreach (DirectoryInfo subdir in dirs) 964 { 965 string temppath = Path.Combine(destDirName, subdir.Name); 966 CopyDirectory(subdir.FullName, temppath); 967 } 968 } 969 970 /// <summary> 971 /// Gets the expected module path. 972 /// </summary> 973 /// <param name="location">Location.</param> 974 /// <returns>The expected path of the module.</returns> 975 public static string GetExpectedModulePath(TestModuleLocation location) 976 { 977 switch (location) 978 { 979 case TestModuleLocation.CurrentUser: 980 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"PowerShell\Modules"); 981 case TestModuleLocation.AllUsers: 982 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"PowerShell\Modules"); 983 case TestModuleLocation.WinGetModulePath: 984 case TestModuleLocation.Default: 985 return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Microsoft\WinGet\Configuration\Modules"); 986 case TestModuleLocation.Custom: 987 return Path.Combine(Path.GetTempPath(), "E2ECustomModules"); 988 default: 989 throw new ArgumentException(location.ToString()); 990 } 991 } 992 993 /// <summary> 994 /// Gets the instance identifier of the first configuration history item with name in its output line. 995 /// </summary> 996 /// <param name="name">The string to search for.</param> 997 /// <returns>The instance identifier of a configuration that matched the search, or an empty string if none did.</returns> 998 public static string GetConfigurationInstanceIdentifierFor(string name) 999 { 1000 var result = TestCommon.RunAICLICommand("configure list", string.Empty); 1001 Assert.AreEqual(0, result.ExitCode); 1002 1003 string[] lines = result.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); 1004 1005 foreach (string line in lines) 1006 { 1007 if (line.Contains(name)) 1008 { 1009 // Find the first GUID in the output 1010 int left = line.IndexOf('{'); 1011 int right = line.IndexOfAny(new char[] { '}', '…' }); 1012 Assert.AreNotEqual(-1, left); 1013 Assert.AreNotEqual(-1, right); 1014 Assert.LessOrEqual(right - left, 38); 1015 1016 return line.Substring(left, right - left); 1017 } 1018 } 1019 1020 return string.Empty; 1021 } 1022 1023 /// <summary> 1024 /// Copy the installer file to the ARP InstallSource directory. 1025 /// </summary> 1026 /// <param name="installerFilePath">Test installer to be copied.</param> 1027 /// <param name="productCode">Installer Product.</param> 1028 /// <param name="useWoW6432Node">is WoW6432Node to use.</param> 1029 /// <returns>Returns the installer source directory if the file operation is successful, otherwise returns an empty string.</returns> 1030 public static string CopyInstallerFileToARPInstallSourceDirectory(string installerFilePath, string productCode, bool useWoW6432Node = false) 1031 { 1032 if (string.IsNullOrEmpty(installerFilePath)) 1033 { 1034 new ArgumentNullException(nameof(installerFilePath)); 1035 } 1036 1037 if (!File.Exists(installerFilePath)) 1038 { 1039 new FileNotFoundException(installerFilePath); 1040 } 1041 1042 string outputDirectory = string.Empty; 1043 1044 // Define the registry paths for both x64 and x86 1045 string registryPath = useWoW6432Node 1046 ? $@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{productCode}" 1047 : $@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{productCode}"; 1048 1049 // Open the registry key where the uninstall information is stored 1050 using (RegistryKey key = Registry.LocalMachine.OpenSubKey(registryPath)) 1051 { 1052 if (key != null) 1053 { 1054 // Read the InstallSource value 1055 string arpInstallSourceDirectory = key.GetValue("InstallSource") as string; 1056 1057 if (!string.IsNullOrEmpty(arpInstallSourceDirectory)) 1058 { 1059 // Copy the MSI installer to the InstallSource directory 1060 string installerFileName = Path.GetFileName(installerFilePath); 1061 string installerDestinationPath = Path.Combine(arpInstallSourceDirectory, installerFileName); 1062 1063 if (!Directory.Exists(arpInstallSourceDirectory)) 1064 { 1065 Directory.CreateDirectory(arpInstallSourceDirectory); 1066 } 1067 1068 File.Copy(installerFilePath, installerDestinationPath, true); 1069 1070 outputDirectory = arpInstallSourceDirectory; 1071 } 1072 } 1073 } 1074 1075 return outputDirectory; 1076 } 1077 1078 /// <summary> 1079 /// Run winget command via direct process. 1080 /// </summary> 1081 /// <param name="command">Command to run.</param> 1082 /// <param name="parameters">Parameters.</param> 1083 /// <param name="stdIn">Optional std in.</param> 1084 /// <param name="timeOut">Optional timeout.</param> 1085 /// <param name="throwOnTimeout">Throw on timeout.</param> 1086 /// <returns>The result of the command.</returns> 1087 private static RunCommandResult RunAICLICommandViaDirectProcess(string command, string parameters, string stdIn, int timeOut, bool throwOnTimeout) 1088 { 1089 RunCommandResult result = new (); 1090 Process p = new Process(); 1091 p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters); 1092 p.StartInfo.UseShellExecute = false; 1093 1094 p.StartInfo.StandardOutputEncoding = Encoding.UTF8; 1095 p.StartInfo.RedirectStandardOutput = true; 1096 StringBuilder outputData = new (); 1097 p.OutputDataReceived += (sender, args) => 1098 { 1099 if (args.Data != null) 1100 { 1101 outputData.AppendLine(args.Data); 1102 } 1103 }; 1104 1105 p.StartInfo.StandardErrorEncoding = Encoding.UTF8; 1106 p.StartInfo.RedirectStandardError = true; 1107 StringBuilder errorData = new (); 1108 p.ErrorDataReceived += (sender, args) => 1109 { 1110 if (args.Data != null) 1111 { 1112 errorData.AppendLine(args.Data); 1113 } 1114 }; 1115 1116 if (!string.IsNullOrEmpty(stdIn)) 1117 { 1118 p.StartInfo.RedirectStandardInput = true; 1119 } 1120 1121 p.Start(); 1122 p.BeginOutputReadLine(); 1123 p.BeginErrorReadLine(); 1124 1125 if (!string.IsNullOrEmpty(stdIn)) 1126 { 1127 p.StandardInput.Write(stdIn); 1128 p.StandardInput.Close(); 1129 } 1130 1131 if (p.WaitForExit(timeOut)) 1132 { 1133 // According to documentation, this extra call will ensure that the redirected streams 1134 // have finished reading all of the data. 1135 p.WaitForExit(); 1136 1137 result.ExitCode = p.ExitCode; 1138 result.StdOut = outputData.ToString(); 1139 result.StdErr = errorData.ToString(); 1140 1141 TestContext.Out.WriteLine("Command run completed with exit code: " + result.ExitCode); 1142 1143 if (!string.IsNullOrEmpty(result.StdErr)) 1144 { 1145 TestContext.Error.WriteLine("Command run error. Error: " + result.StdErr); 1146 } 1147 1148 if (TestSetup.Parameters.VerboseLogging) 1149 { 1150 TestContext.Out.WriteLine("Command run output. Output:\n" + result.StdOut ?? "<null>"); 1151 } 1152 } 1153 else if (throwOnTimeout) 1154 { 1155 throw new TimeoutException($"Direct winget command run timed out: {command} {parameters}"); 1156 } 1157 1158 return result; 1159 } 1160 1161 /// <summary> 1162 /// Run command result. 1163 /// </summary> 1164 public struct RunCommandResult 1165 { 1166 /// <summary> 1167 /// Exit code. 1168 /// </summary> 1169 public int ExitCode; 1170 1171 /// <summary> 1172 /// StdOut. 1173 /// </summary> 1174 public string StdOut; 1175 1176 /// <summary> 1177 /// StdErr. 1178 /// </summary> 1179 public string StdErr; 1180 } 1181 } 1182 }