winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }