winget-cli

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

commit ca976dd96974db46996d4ea371a0bdb7b4f75c70
parent 21bb89e135a9a61c4256f3b6ad6e97ce1b44c9ee
Author: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com>
Date:   Thu, 15 Oct 2020 12:58:32 -0700

Integrate Local Test Server With All E2E Tests (#579)

* changed all command tests to utilize test server

* reverted test.runsettings

* added MSIX tests to azp

* add package signing to setup

* added working MSIX installer path to x86 E2E tests

* added E2E test template for list/update/uninstall and modified ExeInstaller to write to registry and take in Product ID parameter

* clean up

* cast DWORD to size_t type

* clean up names for E2E tests

* added search --exact tests for ID and name

* added README for E2E tests, feature enable tests, publish log artifacts for e2e tests

* edit paths for publish and features command tests

* testing localappdata path

* added displayName to yaml

* testing localappdata path

* use wingetdev path

* added working directory to command line task

* removed command line task

* added x64 publish task

* added base class and new log path

* removed msi build for x86

* modified source command with base class

* fixed mistyped character

* reverse x64 and x86 tasks

* revert to normal and add cmd task to check publish directory contents

* publish test logs before msix cleanup

* publish logs before final cleanup

* delete E2ETestDirectory

* changed x86 log path

* added cmd line task to identify directory contents

* fixed pipeline script

* used full path name

* cleanup and modified readme file

* reverted configuration
Diffstat:
Mazure-pipelines.yml | 49+++++++++++++++++++++++++++++--------------------
Asrc/AppInstallerCLIE2ETests/BaseCommand.cs | 31+++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Constants.cs | 7+++++++
Asrc/AppInstallerCLIE2ETests/FeaturesCommand.cs | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/HashCommand.cs | 4++--
Msrc/AppInstallerCLIE2ETests/InstallCommand.cs | 54+++++++++++++++---------------------------------------
Asrc/AppInstallerCLIE2ETests/ListCommand.cs | 29+++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/README.md | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/SearchCommand.cs | 175+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/AppInstallerCLIE2ETests/SetUpFixture.cs | 2++
Msrc/AppInstallerCLIE2ETests/ShowCommand.cs | 87+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/AppInstallerCLIE2ETests/SourceCommand.cs | 82++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 22++++++++++++++++------
Asrc/AppInstallerCLIE2ETests/TestData/Manifests/TestExampleInstaller.yaml | 19+++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/TestIndexSetup.cs | 20+++++++++++++-------
Asrc/AppInstallerCLIE2ETests/UninstallCommand.cs | 30++++++++++++++++++++++++++++++
Asrc/AppInstallerCLIE2ETests/UpdateCommand.cs | 24++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/ValidateCommand.cs | 10+++++-----
Msrc/AppInstallerTestExeInstaller/main.cpp | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
19 files changed, 713 insertions(+), 234 deletions(-)

diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -209,6 +209,17 @@ jobs: inputs: secureFile: 'HTTPSDevCert.pfx' + - task: MSBuild@1 + displayName: Build MSIX Test Installer File + inputs: + platform: 'x86' + solution: 'src/AppInstallerTestMsixInstaller/AppInstallerTestMsixInstaller.wapproj' + configuration: '$(buildConfiguration)' + msbuildArguments: '/p:AppxPackageOutput="$(Build.ArtifactStagingDirectory)\AppInstallerTestMsixInstaller.msix" + /p:AppxBundle=Never + /p:UapAppxPackageBuildMode=SideLoadOnly + /p:AppxPackageSigningEnabled=false' + - task: PowerShell@2 displayName: Install Root Certificate inputs: @@ -223,16 +234,6 @@ jobs: arguments: '-BuildRoot $(system.defaultWorkingDirectory)\src\x86\Release\LocalhostWebServer -StaticFileRoot $(Agent.TempDirectory)\TestLocalIndex -CertPath $(HTTPSDevCert.secureFilePath) -CertPassword microsoft' condition: succeededOrFailed() -# - task: VSTest@2 -# displayName: Run E2E Tests Unpackaged x64 -# inputs: -# testSelector: 'testAssemblies' -# testAssemblyVer2: 'src\x64\Release\AppInstallerCLIE2ETests\AppInstallerCLIE2ETests.dll' -# runSettingsFile: 'src\x64\Release\AppInstallerCLIE2ETests\Test.runsettings' -# overrideTestrunParameters: '-PackagedContext false -# -AICLIPath $(system.defaultWorkingDirectory)\src\x64\Release\AppInstallerCLI\AppInstallerCLI.exe' -# condition: succeededOrFailed() - - task: VSTest@2 displayName: Run E2E Tests Packaged x64 inputs: @@ -245,19 +246,18 @@ jobs: -LooseFileRegistration true -InvokeCommandInDesktopPackage true -StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex + -MsixTestInstallerPath $(Build.ArtifactStagingDirectory)\AppInstallerTestMsixInstaller.msix -ExeTestInstallerPath $(system.defaultWorkingDirectory)\src\x64\Release\AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe -PackageCertificatePath $(AppInstallerTest.secureFilePath)' condition: succeededOrFailed() -# - task: VSTest@2 -# displayName: Run E2E Tests Unpackaged x86 -# inputs: -# testSelector: 'testAssemblies' -# testAssemblyVer2: 'src\x86\Release\AppInstallerCLIE2ETests\AppInstallerCLIE2ETests.dll' -# runSettingsFile: 'src\x86\Release\AppInstallerCLIE2ETests\Test.runsettings' -# overrideTestrunParameters: '-PackagedContext false -# -AICLIPath $(system.defaultWorkingDirectory)\src\x86\Release\AppInstallerCLI\AppInstallerCLI.exe' -# condition: succeededOrFailed() + - task: PublishBuildArtifacts@1 + displayName: Publish E2E Tests Packaged x64 Log + inputs: + PathtoPublish: 'C:\Users\VssAdministrator\AppData\Local\E2ETestLogs' + ArtifactName: 'E2ETestPackagedx64Log' + publishLocation: 'Container' + condition: succeededOrFailed() - task: VSTest@2 displayName: Run E2E Tests Packaged x86 @@ -271,10 +271,19 @@ jobs: -LooseFileRegistration true -InvokeCommandInDesktopPackage true -StaticFileRootPath $(Agent.TempDirectory)\TestLocalIndex + -MsixTestInstallerPath $(Build.ArtifactStagingDirectory)\AppInstallerTestMsixInstaller.msix -ExeTestInstallerPath $(system.defaultWorkingDirectory)\src\x86\Release\AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe -PackageCertificatePath $(AppInstallerTest.secureFilePath)' condition: succeededOrFailed() - + + - task: PublishBuildArtifacts@1 + displayName: Publish E2E Tests Packaged x86 Log + inputs: + PathtoPublish: 'C:\Users\VssAdministrator\AppData\Local\Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir' + ArtifactName: 'E2ETestPackagedx86Log' + publishLocation: 'Container' + condition: succeededOrFailed() + - task: PublishBuildArtifacts@1 displayName: Publish CLI Binary inputs: diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + using System.Threading; + + public class BaseCommand + { + [OneTimeSetUp] + public void Setup() + { + ResetTestSource(); + } + + [OneTimeTearDown] + public void Teardown() + { + TestCommon.RunAICLICommand("source reset", "--force"); + } + + public void ResetTestSource() + { + TestCommon.RunAICLICommand("source reset", "--force"); + TestCommon.RunAICLICommand("source remove", Constants.DefaultSourceName); + TestCommon.RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); + Thread.Sleep(5000); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -20,6 +20,12 @@ namespace AppInstallerCLIE2ETests public const string AppInstallerTestCert = "AppInstallerTest.cer"; public const string AppInstallerTestCertThumbprint = "d03e7a688b388b1edde8476a627531c49db88017"; + // Test Sources + public const string DefaultSourceName = @"winget"; + public const string DefaultSourceUrl = @"https://winget.azureedge.net/cache"; + public const string TestSourceName = @"TestSource"; + public const string TestSourceUrl = @"https://localhost:5001/TestKit"; + // Todo: not needed if switch to use prod index for source tests public const string IndexPackageRootCert = "IndexPackageIntRoot.cer"; public const string IndexPackageRootCertThumbprint = "d17697cc206ed26e1a51f5bb96e9356d6d610b74"; @@ -37,6 +43,7 @@ namespace AppInstallerCLIE2ETests public const string SignTool = "signtool.exe"; public const string IndexCreationTool = "IndexCreationTool"; public const string WinGetUtil = "WinGetUtil"; + public const string E2ETestLogsPath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir"; public class ErrorCode { diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using System; + using System.IO; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using NUnit.Framework; + + public class FeaturesCommand + { + private const string SettingsJsonFilePath = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\settings.json"; + private const string LocalAppData = "LocalAppData"; + + [SetUp] + public void Setup() + { + InitializeAllFeatures(false); + } + + [TearDown] + public void TearDown() + { + InitializeAllFeatures(false); + } + + [Test] + public void DisplayFeatures() + { + var result = TestCommon.RunAICLICommand("features", ""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Command Sample")); + Assert.True(result.StdOut.Contains("Argument Sample")); + Assert.True(result.StdOut.Contains("Microsoft Store Support")); + Assert.False(result.StdOut.Contains("Enabled")); + } + + [Test] + public void EnableExperimentalFeatures() + { + ConfigureFeature("experimentalArg", true); + ConfigureFeature("experimentalCmd", true); + ConfigureFeature("experimentalMSStore", true); + var result = TestCommon.RunAICLICommand("features", ""); + Assert.True(result.StdOut.Contains("Enabled")); + } + + private void ConfigureFeature(string featureName, bool status) + { + string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); + JObject settingsJson = JObject.Parse(File.ReadAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath))); + JObject experimentalFeatures = (JObject)settingsJson["experimentalFeatures"]; + experimentalFeatures[featureName] = status; + + File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), settingsJson.ToString()); + } + + private void InitializeAllFeatures(bool status) + { + string localAppDataPath = Environment.GetEnvironmentVariable(LocalAppData); + + var settingsJson = new + { + experimentalFeatures = new + { + experimentalArg = status, + experimentalCmd = status, + experimentalMSStore = status + } + }; + + var serializedSettingsJson = JsonConvert.SerializeObject(settingsJson, Formatting.Indented); + File.WriteAllText(Path.Combine(localAppDataPath, SettingsJsonFilePath), serializedSettingsJson); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/HashCommand.cs b/src/AppInstallerCLIE2ETests/HashCommand.cs @@ -4,8 +4,9 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; + using NUnit.Framework.Internal; - public class HashCommand + public class HashCommand : BaseCommand { [Test] public void HashFile() @@ -27,7 +28,6 @@ namespace AppInstallerCLIE2ETests [Test] public void HashInvalidMSIX() { - // The input is not msix but -m is used var result = TestCommon.RunAICLICommand("hash", TestCommon.GetTestDataFile("AppInstallerTest.cer") + " -m"); Assert.AreEqual(Constants.ErrorCode.OPC_E_ZIP_MISSING_END_OF_CENTRAL_DIRECTORY, result.ExitCode); Assert.True(result.StdOut.Contains("9b4c49ad7e47afd97d2e666e93347745e1647c55f1a7ebba6d31b7dd5f69ee68")); diff --git a/src/AppInstallerCLIE2ETests/InstallCommand.cs b/src/AppInstallerCLIE2ETests/InstallCommand.cs @@ -6,37 +6,15 @@ namespace AppInstallerCLIE2ETests using NUnit.Framework; using System.IO; - public class InstallCommand + public class InstallCommand : BaseCommand { - // Todo: add unicode test cases after install tests are enabled. - private const string InstallTestSourceUrl = @"https://localhost:5001/TestKit"; - private const string InstallTestSourceName = @"InstallTestSource"; - private const string DefaultTestSourceUrl = @"https://winget.azureedge.net/cache"; - private const string DefaultTestSourceName = @"winget"; - private const string InstallTestExeInstalledFile = @"TestExeInstalled.txt"; private const string InstallTestMsiInstalledFile = @"AppInstallerTestMsiInstaller.msi"; private const string InstallTestMsiProductId = @"{A5D36CF1-1993-4F63-BFB4-3ACD910D36A1}"; private const string InstallTestMsixName = @"6c6338fe-41b7-46ca-8ba6-b5ad5312bb0e"; - [SetUp] - public void Setup() - { - TestCommon.RunAICLICommand("source remove", DefaultTestSourceName); - Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("source add", $"{InstallTestSourceName} {InstallTestSourceUrl}").ExitCode); - - } - - [TearDown] - public void TearDown() - { - TestCommon.RunAICLICommand("source remove", InstallTestSourceName); - TestCommon.RunAICLICommand("source add", $"{DefaultTestSourceName} {DefaultTestSourceUrl}"); - TestCommon.WaitForDeploymentFinish(); - } - [Test] - public void AppToInstallDoesNotExist() + public void InstallAppDoesNotExist() { var result = TestCommon.RunAICLICommand("install", "DoesNotExist"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); @@ -44,7 +22,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void MultipleAppsMatchQuery() + public void InstallWithMultipleAppsMatchingQuery() { var result = TestCommon.RunAICLICommand("install", "TestExeInstaller"); Assert.AreEqual(Constants.ErrorCode.ERROR_MULTIPLE_APPLICATIONS_FOUND, result.ExitCode); @@ -52,7 +30,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void InstallTestExe() + public void InstallExe() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); @@ -62,7 +40,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void InstallTestExeWithInsufficientMinOsVersion() + public void InstallExeWithInsufficientMinOsVersion() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"InapplicableOsVersion --silent -l {installDir}"); @@ -72,7 +50,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void ExeInstallWithHashMismatch() + public void InstallExeWithHashMismatch() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"TestExeSha256Mismatch --silent -l {installDir}"); @@ -93,7 +71,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void InstallTestBurn() + public void InstallBurn() { // Install test burn, manifest does not provide silent switch, we should be populating the default var installDir = TestCommon.GetRandomTestDir(); @@ -104,7 +82,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void InstallTestNullSoft() + public void InstallNullSoft() { // Install test Nullsoft, manifest does not provide silent switch, we should be populating the default var installDir = TestCommon.GetRandomTestDir(); @@ -115,7 +93,7 @@ namespace AppInstallerCLIE2ETests } //[Test] - public void InstallTestMSI() + public void InstallMSI() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"TestMsiInstaller --silent -l {installDir}"); @@ -124,10 +102,8 @@ namespace AppInstallerCLIE2ETests Assert.True(VerifyTestMsiInstalledAndCleanup(installDir)); } - - - //[Test] - public void InstallTestMSIX() + [Test] + public void InstallMSIX() { var result = TestCommon.RunAICLICommand("install", $"TestMsixInstaller"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); @@ -135,8 +111,8 @@ namespace AppInstallerCLIE2ETests Assert.True(VerifyTestMsixInstalledAndCleanup()); } - //[Test] - public void InstallTestMSIXWithSignature() + [Test] + public void InstallMSIXWithSignature() { var installDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("install", $"TestMsixWithSignatureHash --silent -l {installDir}"); @@ -145,8 +121,8 @@ namespace AppInstallerCLIE2ETests Assert.True(VerifyTestMsixInstalledAndCleanup()); } - //[Test] - public void InstallTestMSIXWithSignatureHashMismatch() + [Test] + public void InstallMSIXWithSignatureHashMismatch() { var result = TestCommon.RunAICLICommand("install", $"TestMsixSignatureHashMismatch"); Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALLER_HASH_MISMATCH, result.ExitCode); diff --git a/src/AppInstallerCLIE2ETests/ListCommand.cs b/src/AppInstallerCLIE2ETests/ListCommand.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + + public class ListCommand : BaseCommand + { + //[Test] + public void List() + { + var result = TestCommon.RunAICLICommand("list", ""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("PowerShell")); + Assert.True(result.StdOut.Contains("Microsoft.PowerShell")); + } + + //[Test] + public void ListAfterInstall() + { + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); + var result = TestCommon.RunAICLICommand("list", ""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/README.md b/src/AppInstallerCLIE2ETests/README.md @@ -0,0 +1,101 @@ +# How to Run End-To-End Tests for Windows Package Manager Client + + +## Step 1: Launch Localhost Web Server +In order to run any of the E2E tests, you will need to first launch the LocalhostWebServer executable. This program serves static test files from a given directory path through a HTTPS local loopback server in order to maintain a closed and controlled repository for resources used for testing purposes. + +### Parameters + +The executable has 3 mandatory parameters and 1 optional parameter: +|Parameter Name | Mandatory/Optional | Description | +|--|--|--| +| **StaticFileRoot** | Mandatory | Path to serve static root directory. If the directory path does not exist, a new directory will be created for you. | +| **CertPath** | Mandatory | Path to HTTPS Developer Certificate. A self signed developer certificate will need to be created in order to verify localhost https. | +| **CertPassword** | Mandatory | HTTPS Developer Certificate Password | +| **Port** | Optional | Port number [Default Port Number: 5001] | + +### How to create and trust an ASP.NET Core HTTPS Development Certificate +Windows Package Manager Client (WinGet.exe) requires new sources added to the WinGet repositories be securely accessed through HTTPS. Therefore, in order to verify the LocalhostWebServer, you will need to create a self-signed development certificate to verify the localhost address. + +- Open command prompt in administrator mode +- Run **dotnet dev-certs https --trust** in the command line +- Open up **certmgr** (search Manage User Certificates in Windows search bar) +- Locate the newly created localhost certificate in the Personal/Certificates folder with a friendly name of "ASP.NET Core HTTPS development certificate" +- Right click on the certificate --> All Tasks --> Export.. +- Click Yes to export the private key +- Export file using Personal Information Exchange (.pfx) file format +- Create and confirm password using SHA256 encryption (any password will work, just make sure to remember it for later) +- Save HTTPS development certificate and refer its certificate path and password when launching the Localhost Webserver +### How to run LocalhostWebServer.exe +The executable can most likely be found in this path: **\src\x86\Release\LocalhostWebServer** + +The command line call to run the executable needs to follow the format: + + LocalhostWebServer.exe StaticFileRoot=<Path to Serve Static Root Directory> CertPath=<Path to HTTPS Developer Certificate> CertPassword=<Certificate Password> <Port=Port Number> + +Therefore to run the executable in the command line, simply change into the directory that contains **LocalhostWebServer.exe** and run the executable with the corresponding parameter values. Here is an example: (Don't forget to modify the path to match your own local computer) + + cd C:\Users\MSFT\source\repos\winget-cli\src\AnyCPU\Debug\LocalhostWebServer + + LocalhostWebServer.exe StaticFileRoot=C:\Users\MSFT\AppData\Local\Temp\TestLocalIndex CertPath=C:\Users\MSFT\Temp\HTTPSDevCert.pfx CertPassword=password + + +## 2. Prepare Test.runsettings file + The E2E tests are built on the nunit testing framework and rely on this test.runsettings file located here: **D:\Src\WinGet\Client\src\AppInstallerCLIE2ETests\Test.runsettings**. These parameters are used by the tests at runtime and need to be configured before running any of the E2E tests. + After populating the parameters in the Test.runsettings file, make sure to configure the run settings to point to the file. This can be done by opening **Test Explorer - Settings - Configure Run Settings point to file: D:\Src\WinGet\Client\src\AppInstallerCLIE2ETests\Test.runsettings** + + +|Parameter| Description | +|--|--| +| PackagedContext | Indicates if the test should be run under packaged context | +| VerboseLogging | Turn on/off verbose logging in the test result | +| AICLIPath | The AICLI executable under test. If using loose file registration and using Invoke-Command when AppExecutionAlias is not available, this will be relative path to package root. | +| AICLIPackagePath | Used in packaged context. Path to the package containing executable under test. If LooseFileRegistration is true, this should be path to unpackaged root. | +| LooseFileRegistration | Bool to set if loose file registration should be used. | +| InvokeCommandInDesktopPackage | Bool to indicate using Invoke-CommandInDesktopPackage for test execution. This is used when AppExecutionAlias is not available, or disabled. | +| StaticFileRootPath | Path to the set of static test files that will be served as the source for testing purposes. This path should be identical to the one provided to the LocalHostWebServer| +| MsixTestInstallerPath | The MSIX (or APPX) Installer executable under test. | +| ExeTestInstallerPath |The Exe Installer executable under test. | +| PackageCertificatePath |Signing Certificate Path used to certify test index source package| + +#### Example of Test.runsettings format: + + <RunSettings> + <TestRunParameters> + <Parameter name="PackagedContext" value="true" /> + <Parameter name="VerboseLogging" value="false" /> + <Parameter name="AICLIPath" value="AppInst.exe" /> + <Parameter name="AICLIPackagePath" value="AppInstallerCLIPackage.appxbundle" /> + <Parameter name="LooseFileRegistration" value="false" /> + <Parameter name="InvokeCommandInDesktopPackage" value="false" /> + <Parameter name="StaticFileRootPath" value="\TestLocalIndex" /> + <Parameter name="MsixTestInstallerPath" value="MsixTestInstaller.msix" /> + <Parameter name="ExeTestInstallerPath" value="ExeTestInstaller.exe" /> + <Parameter name="PackageCertificatePath" value="certificate.pfx"/> + </TestRunParameters> + </RunSettings> + +#### Example of Test.runsettings with completed parameters: +Make sure to replace **MSFT** with your own user name. Modifying this example with the correct path to each test run parameter for your own local computer should be sufficient to successfully run the E2E tests once all steps are completed. + + <RunSettings> + <TestRunParameters> + <Parameter name="PackagedContext" value="false" /> + <Parameter name="VerboseLogging" value="false" /> + <Parameter name="AICLIPath" value="C:\Users\<user>\source\repos\winget-cli\src\x64\Debug\AppInstallerCLI\AppInstallerCLI.exe" /> + <Parameter name="AICLIPackagePath" value="AppInstallerCLIPackage.appxbundle" /> + <Parameter name="LooseFileRegistration" value="false" /> + <Parameter name="InvokeCommandInDesktopPackage" value="false" /> + <Parameter name="StaticFileRootPath" value="C:\Users\MSFT\AppData\Local\Temp\TestLocalIndex" /> + <Parameter name="MsixTestInstallerPath" value="C:\Users\MSFT\Temp\MsixTestInstaller.msix" /> + <Parameter name="ExeTestInstallerPath" value="C:\Users\MSFT\source\repos\winget-cli\src\x64\Debug\AppInstallerTestExeInstaller\AppInstallerTestExeInstaller.exe" /> + <Parameter name="PackageCertificatePath" value="C:\Users\MSFT\Temp\packageCertificate.pfx"/> + </TestRunParameters> + </RunSettings> + + +#### Log Files +After running the E2E Tests, the logs can be found in either the following two paths: + +- **%LOCALAPPDATA%\Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir** +- **%LOCALAPPDATA%\E2ETestLogs** diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -1,85 +1,92 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace AppInstallerCLIE2ETests -{ - using NUnit.Framework; - using System.Threading; - - public class SearchCommand - { - // Todo: use created test source when available - private const string SearchTestSourceUrl = @"https://winget-int.azureedge.net/cache"; - private const string SearchTestSourceName = @"SearchTestSource"; - - [SetUp] - public void Setup() - { - Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("source add", $"{SearchTestSourceName} {SearchTestSourceUrl}").ExitCode); - } - - [TearDown] - public void TearDown() - { - TestCommon.RunAICLICommand("source remove", SearchTestSourceName); - - TestCommon.WaitForDeploymentFinish(); - } - - [Test] - public void SearchWithoutArgs() - { - // Search without args list every app - var result = TestCommon.RunAICLICommand("search", ""); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.PowerToys")); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - } - - [Test] - public void SearchQuery() - { - // Search query - var result = TestCommon.RunAICLICommand("search", "VisualStudioCode"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - } - - [Test] - public void SearchWithID() - { - // Search through id found the app - var result = TestCommon.RunAICLICommand("search", "--id VisualStudioCode"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - } - - [Test] - public void SearchWithInvalidName() - { - // Search through name. No app found because name is "Visual Studio Code" - var result = TestCommon.RunAICLICommand("search", "--name VisualStudioCode"); - Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("No package found matching input criteria.")); - } - - [Test] - public void SearchReturnsMultiple() - { - // Search Microsoft should return multiple - var result = TestCommon.RunAICLICommand("search", "Microsoft"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.PowerToys")); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - } - - [Test] - public void SearchWithExactArg() - { - // Search 'powertoys' with exact arg should return none due to case sensitivity - var result = TestCommon.RunAICLICommand("search", "powertoys -e"); - Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); - Assert.True(result.StdOut.Contains("No package found matching input criteria.")); - } - } +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + + public class SearchCommand : BaseCommand + { + [Test] + public void SearchWithoutArgs() + { + var result = TestCommon.RunAICLICommand("search", ""); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestBurnInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchQuery() + { + var result = TestCommon.RunAICLICommand("search", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithName() + { + var result = TestCommon.RunAICLICommand("search", "--name testexampleinstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithID() + { + var result = TestCommon.RunAICLICommand("search", "--id appinstallertest.testexampleinstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithInvalidName() + { + var result = TestCommon.RunAICLICommand("search", "--name InvalidName"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); + } + + [Test] + public void SearchReturnsMultiple() + { + // Search Microsoft should return multiple + var result = TestCommon.RunAICLICommand("search", "AppInstallerTest"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestBurnInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithExactName() + { + var result = TestCommon.RunAICLICommand("search", "--exact TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithExactID() + { + var result = TestCommon.RunAICLICommand("search", "--exact AppInstallerTest.TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void SearchWithExactArgCaseSensitivity() + { + var result = TestCommon.RunAICLICommand("search", "--exact testexampleinstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); + } + } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -116,6 +116,8 @@ namespace AppInstallerCLIE2ETests TestCommon.RunCommand("certutil.exe", $"-delstore \"TRUSTEDPEOPLE\" {Constants.AppInstallerTestCertThumbprint}"); TestCommon.RunCommand("certutil.exe", $"-delstore \"ROOT\" {Constants.IndexPackageRootCertThumbprint}"); + TestCommon.PublishE2ETestLogs(); + if (TestCommon.PackagedContext) { TestCommon.RemoveMsix(Constants.AICLIPackageName); diff --git a/src/AppInstallerCLIE2ETests/ShowCommand.cs b/src/AppInstallerCLIE2ETests/ShowCommand.cs @@ -4,44 +4,26 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; - using System.Threading; - public class ShowCommand + public class ShowCommand : BaseCommand { - // Todo: use created test source when available - private const string ShowTestSourceUrl = @"https://winget-int.azureedge.net/cache"; - private const string ShowTestSourceName = @"ShowTestSource"; - - [SetUp] - public void Setup() - { - Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("source add", $"{ShowTestSourceName} {ShowTestSourceUrl}").ExitCode); - } - - [TearDown] - public void TearDown() - { - TestCommon.RunAICLICommand("source remove", ShowTestSourceName); - - TestCommon.WaitForDeploymentFinish(); - } - [Test] public void ShowWithNoArgs() { // Show with no arg lists every app and a warning message - var result = TestCommon.RunAICLICommand("show", $"-s {ShowTestSourceName}"); + var result = TestCommon.RunAICLICommand("show", ""); Assert.AreEqual(Constants.ErrorCode.ERROR_MULTIPLE_APPLICATIONS_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("Multiple packages found matching input criteria. Please refine the input.")); - Assert.True(result.StdOut.Contains("Microsoft.PowerToys")); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestBurnInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); } [Test] public void ShowWithNoMatches() { // Show with 0 search match shows a "please refine input" - var result = TestCommon.RunAICLICommand("show", $"DoesNotExist -s {ShowTestSourceName}"); + var result = TestCommon.RunAICLICommand("show", $"DoesNotExist"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("No package found matching input criteria.")); } @@ -50,29 +32,68 @@ namespace AppInstallerCLIE2ETests public void ShowWithSubstringMatch() { // Show with a substring match still returns 0 results - var result = TestCommon.RunAICLICommand("show", $"Microsoft -s {ShowTestSourceName}"); + var result = TestCommon.RunAICLICommand("show", $"AppInstallerTest"); Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); Assert.True(result.StdOut.Contains("No package found matching input criteria.")); } [Test] - public void ShowWithSingleMatch() + public void ShowWithNameMatch() { - // Show with 1 search match shows detailed manifest info - var result = TestCommon.RunAICLICommand("show", $"Microsoft.VisualStudioCode -s {ShowTestSourceName}"); + var result = TestCommon.RunAICLICommand("show", $"--name testexampleinstaller"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - Assert.True(result.StdOut.Contains("Visual Studio Code")); + Assert.True(result.StdOut.Contains("Found TestExampleInstaller [AppInstallerTest.TestExampleInstaller]")); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void ShowWithIDMatch() + { + var result = TestCommon.RunAICLICommand("show", $"--id appinstallertest.testexampleinstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Found TestExampleInstaller [AppInstallerTest.TestExampleInstaller]")); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); } [Test] public void ShowWithVersions() { // Show with --versions list the versions - var result = TestCommon.RunAICLICommand("show", $"Microsoft.VisualStudioCode --versions -s {ShowTestSourceName}"); + var result = TestCommon.RunAICLICommand("show", $"TestExampleInstaller --versions"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + Assert.True(result.StdOut.Contains("1.2.3.4")); + } + + [Test] + public void ShowWithExactName() + { + var result = TestCommon.RunAICLICommand("show", $"--exact TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Found TestExampleInstaller [AppInstallerTest.TestExampleInstaller]")); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void ShowWithExactID() + { + var result = TestCommon.RunAICLICommand("show", $"--exact AppInstallerTest.TestExampleInstaller"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Microsoft.VisualStudioCode")); - Assert.True(result.StdOut.Contains("1.41.1")); + Assert.True(result.StdOut.Contains("Found TestExampleInstaller [AppInstallerTest.TestExampleInstaller]")); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + + [Test] + public void ShowWithExactArgCaseSensitivity() + { + var result = TestCommon.RunAICLICommand("show", $"--exact testexampleinstaller"); + Assert.AreEqual(Constants.ErrorCode.ERROR_NO_APPLICATIONS_FOUND, result.ExitCode); + Assert.True(result.StdOut.Contains("No package found matching input criteria.")); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -5,45 +5,28 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; - public class SourceCommand + public class SourceCommand : BaseCommand { - // Todo: switch to use prod index when available - private const string SourceTestSourceUrl = @"https://winget-int.azureedge.net/cache"; - private const string SourceTestSourceName = @"SourceTestSource"; - - [SetUp] - public void Setup() - { - Assert.AreEqual(Constants.ErrorCode.S_OK, TestCommon.RunAICLICommand("source add", $"{SourceTestSourceName} {SourceTestSourceUrl}").ExitCode); - } - - [TearDown] - public void TearDown() - { - TestCommon.RunAICLICommand("source remove", SourceTestSourceName); - TestCommon.WaitForDeploymentFinish(); - } - [Test] - public void AddSource() + public void SourceAdd() { - var result = TestCommon.RunAICLICommand("source add", $"SourceTest {SourceTestSourceUrl}"); + var result = TestCommon.RunAICLICommand("source add", $"SourceTest {Constants.TestSourceUrl}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Done")); TestCommon.RunAICLICommand("source remove", $"-n SourceTest"); } [Test] - public void AddSourceWithDuplicateName() + public void SourceAddWithDuplicateName() { // Add source with duplicate name should fail - var result = TestCommon.RunAICLICommand("source add", $"{SourceTestSourceName} https://microsoft.com"); + var result = TestCommon.RunAICLICommand("source add", $"{Constants.TestSourceName} https://microsoft.com"); Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_ALREADY_EXISTS, result.ExitCode); Assert.True(result.StdOut.Contains("A source with the given name already exists and refers to a different location")); } [Test] - public void AddSourceWithInvalidURL() + public void SourceAddWithInvalidURL() { // Add source with invalid url should fail var result = TestCommon.RunAICLICommand("source add", "AnotherSource https://microsoft.com"); @@ -53,7 +36,7 @@ namespace AppInstallerCLIE2ETests [Test] - public void AddSourceWithHttpURL() + public void SourceAddWithHttpURL() { // Add source with an HTTP url should fail var result = TestCommon.RunAICLICommand("source add", "Insecure http://microsoft.com"); @@ -62,27 +45,27 @@ namespace AppInstallerCLIE2ETests } [Test] - public void ListWithNoArgs() + public void SourceListWithNoArgs() { // List with no args should list all available sources var result = TestCommon.RunAICLICommand("source list", ""); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("https://winget-int.azureedge.net/cache")); + Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); } [Test] - public void ListWithSourceName() + public void SourceListWithName() { - var result = TestCommon.RunAICLICommand("source list", $"-n {SourceTestSourceName}"); + var result = TestCommon.RunAICLICommand("source list", $"-n {Constants.TestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("SourceTestSource")); - Assert.True(result.StdOut.Contains("https://winget-int.azureedge.net/cache")); - Assert.True(result.StdOut.Contains("Microsoft.Winget.Source")); + Assert.True(result.StdOut.Contains("TestSource")); + Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + Assert.True(result.StdOut.Contains("Microsoft.PreIndexed.Package")); Assert.True(result.StdOut.Contains("Updated")); } [Test] - public void ListSourceNameMismatch() + public void SourceListNameMismatch() { var result = TestCommon.RunAICLICommand("source list", "-n UnknownName"); Assert.AreEqual(Constants.ErrorCode.ERROR_SOURCE_NAME_DOES_NOT_EXIST, result.ExitCode); @@ -92,7 +75,7 @@ namespace AppInstallerCLIE2ETests [Test] public void SourceUpdate() { - var result = TestCommon.RunAICLICommand("source update", $"-n {SourceTestSourceName}"); + var result = TestCommon.RunAICLICommand("source update", $"-n {Constants.TestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Done")); } @@ -106,6 +89,15 @@ namespace AppInstallerCLIE2ETests } [Test] + public void SourceRemoveValidName() + { + var result = TestCommon.RunAICLICommand("source remove", $"-n {Constants.TestSourceName}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Done")); + ResetTestSource(); + } + + [Test] public void SourceRemoveInvalidName() { var result = TestCommon.RunAICLICommand("source remove", "-n UnknownName"); @@ -114,11 +106,29 @@ namespace AppInstallerCLIE2ETests } [Test] - public void SourceRemoveValidName() + public void SourceReset() { - var result = TestCommon.RunAICLICommand("source remove", $"-n {SourceTestSourceName}"); + var result = TestCommon.RunAICLICommand("source reset", ""); + Assert.True(result.StdOut.Contains("The following sources will be reset if the --force option is given:")); + Assert.True(result.StdOut.Contains("TestSource")); + Assert.True(result.StdOut.Contains("https://localhost:5001/TestKit")); + } + + [Test] + public void SourceForceReset() + { + // Force Reset Sources + var result = TestCommon.RunAICLICommand("source reset", "--force"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Done")); + Assert.True(result.StdOut.Contains("Resetting all sources...Done")); + + //Verify sources have been reset + result = TestCommon.RunAICLICommand("source list", ""); + Assert.True(result.StdOut.Contains("winget")); + Assert.True(result.StdOut.Contains("https://winget.azureedge.net/cache")); + Assert.False(result.StdOut.Contains($"{Constants.TestSourceName}")); + Assert.False(result.StdOut.Contains($"{Constants.TestSourceUrl}")); + ResetTestSource(); } } } \ No newline at end of file diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -229,14 +229,24 @@ namespace AppInstallerCLIE2ETests return RunCommand("powershell", $"Get-AppxPackage \"{name}\" | Remove-AppxPackage"); } - public static void WaitForDeploymentFinish() + /// <summary> + /// Copies log files to the path %LOCALAPPDATA%\E2ETestLogs + /// </summary> + public static void PublishE2ETestLogs() { - if (PackagedContext) + string localAppDataPath = Environment.GetEnvironmentVariable("LocalAppData"); + string testLogsSourcePath = Path.Combine(localAppDataPath, Constants.E2ETestLogsPath); + string testLogsDestPath = Path.Combine(localAppDataPath, "E2ETestLogs"); + + if (Directory.Exists(testLogsDestPath)) + { + TestIndexSetup.DeleteDirectoryContents(new DirectoryInfo(testLogsDestPath)); + Directory.Delete(testLogsDestPath); + } + + if (Directory.Exists(testLogsSourcePath)) { - // Since we are doing a lot index add/remove, and some of the methods are fire and forget. - // Sometimes process start will fail because app is updating. - // Or index package is not completely added, removed. - Thread.Sleep(5000); + TestIndexSetup.CopyDirectory(testLogsSourcePath, testLogsDestPath); } } } diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExampleInstaller.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/TestExampleInstaller.yaml @@ -0,0 +1,19 @@ +Id: AppInstallerTest.TestExampleInstaller +Name: TestExampleInstaller +Version: 1.2.3.4 +Publisher: AppInstallerTest +License: Test +Installers: + - Arch: x86 + Url: https://localhost:5001/TestKit/AppInstallerTestExeInstaller/AppInstallerTestExeInstaller.exe + Sha256: <EXEHASH> + InstallerType: exe + Switches: + Custom: /execustom + SilentWithProgress: /exeswp + Silent: /exesilent + Interactive: /exeinteractive + Language: /exeenus + Log: /exelog <LOGPATH> + InstallLocation: /InstallDir <INSTALLPATH> +ManifestVersion: 0.1.0 diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -71,10 +71,8 @@ namespace AppInstallerCLIE2ETests // Package Test Source and Sign With Package Certificate string makeappxExecutable = Path.Combine(pathToSDK, "makeappx.exe"); - string signtoolExecutable = Path.Combine(pathToSDK, "signtool.exe"); - RunCommand(makeappxExecutable, $"pack /nv /v /o /d {packageDir} /p {indexPackageDestPath}"); - RunCommand(signtoolExecutable, $"sign /a /fd sha256 /f {TestCommon.PackageCertificatePath} {indexPackageDestPath}"); + SignFile(indexPackageDestPath); } catch (Exception e) { @@ -93,10 +91,8 @@ namespace AppInstallerCLIE2ETests File.Copy(TestCommon.ExeInstallerPath, exeInstallerFullName, true); TestCommon.ExeInstallerPath = exeInstallerFullName; - // Sign Installer File - string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; - string signtoolExecutable = Path.Combine(pathToSDK, "signtool.exe"); - RunCommand(signtoolExecutable, $"sign /a /fd sha256 /f {TestCommon.PackageCertificatePath} {exeInstallerFullName}"); + // Sign EXE Installer File + SignFile(TestCommon.ExeInstallerPath); } private static void CopyMsixInstallerToTestDirectory() @@ -110,6 +106,9 @@ namespace AppInstallerCLIE2ETests File.Copy(TestCommon.MsixInstallerPath, msixInstallerFullName, true); TestCommon.MsixInstallerPath = msixInstallerFullName; + + // Sign MSIX Installer File + SignFile(TestCommon.MsixInstallerPath); } private static void SetupLocalTestDirectory(string staticFileRootPath) @@ -124,6 +123,13 @@ namespace AppInstallerCLIE2ETests CopyDirectory(sourcePath, TestCommon.StaticFileRootPath); } + public static void SignFile(string filePath) + { + string pathToSDK = SDKDetector.Instance.LatestSDKBinPath; + string signtoolExecutable = Path.Combine(pathToSDK, "signtool.exe"); + RunCommand(signtoolExecutable, $"sign /a /fd sha256 /f {TestCommon.PackageCertificatePath} {filePath}"); + } + /// <summary> /// Deletes the contents of a given directory /// </summary> diff --git a/src/AppInstallerCLIE2ETests/UninstallCommand.cs b/src/AppInstallerCLIE2ETests/UninstallCommand.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + using System.IO; + + public class UninstallCommand : BaseCommand + { + private const string UninstallTestExeInstalledFile = @"TestExeUninstalled.txt"; + + //[Test] + public void UninstallTestExe() + { + // Example Uninstall Command Test + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); + var result = TestCommon.RunAICLICommand("uninstall", $"AppInstallerTest.TextExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully uninstalled")); + Assert.True(VerifyTestExeUninstalled(installDir)); + } + + private bool VerifyTestExeUninstalled(string uninstallDir) + { + return File.Exists(Path.Combine(uninstallDir, UninstallTestExeInstalledFile)); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/UpdateCommand.cs b/src/AppInstallerCLIE2ETests/UpdateCommand.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace AppInstallerCLIE2ETests +{ + using NUnit.Framework; + + public class UpdateCommand : BaseCommand + { + //[Test] + public void UpdateTest() + { + // Example Update Command Test + // TODO: Modify test once final behavior of Update Command is established + var installDir = TestCommon.GetRandomTestDir(); + TestCommon.RunAICLICommand("install", $"AppInstallerTest.OutdatedTestExeInstaller --silent -l {installDir}"); + var result = TestCommon.RunAICLICommand("update", "AppInstallerTest.TestExeInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExeInstaller updated")); + result = TestCommon.RunAICLICommand("list", ""); + Assert.True(result.StdOut.Contains("Version: 1.2.3.4")); + } + } +} diff --git a/src/AppInstallerCLIE2ETests/ValidateCommand.cs b/src/AppInstallerCLIE2ETests/ValidateCommand.cs @@ -5,10 +5,10 @@ namespace AppInstallerCLIE2ETests { using NUnit.Framework; - public class ValidateCommand + public class ValidateCommand : BaseCommand { [Test] - public void ValidManifest() + public void ValidateManifest() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestValidManifest.yaml")); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); @@ -16,7 +16,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void ValidManifestWithExtendedCharacter() + public void ValidateManifestWithExtendedCharacter() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TëstExeInstaller.yaml")); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); @@ -24,7 +24,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void InvalidManifest() + public void ValidateInvalidManifest() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\TestInvalidManifest.yaml")); Assert.AreEqual(Constants.ErrorCode.ERROR_MANIFEST_VALIDATION_FAILURE, result.ExitCode); @@ -32,7 +32,7 @@ namespace AppInstallerCLIE2ETests } [Test] - public void ManifestDoesNotExist() + public void ValidateManifestDoesNotExist() { var result = TestCommon.RunAICLICommand("validate", TestCommon.GetTestDataFile("Manifests\\DoesNotExist")); Assert.AreEqual(Constants.ErrorCode.ERROR_FILE_NOT_FOUND, result.ExitCode); diff --git a/src/AppInstallerTestExeInstaller/main.cpp b/src/AppInstallerTestExeInstaller/main.cpp @@ -1,16 +1,116 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#include <windows.h> +#include <winreg.h> +#include <winerror.h> #include <iostream> #include <fstream> #include <filesystem> #include <sstream> -// The installer simply prints all args to an output file +using namespace std::filesystem; + +std::wstring_view registrySubkey = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; +std::wstring_view defaultProductID = L"{A499DD5E-8DC5-4AD2-911A-BCD0263295E9}"; + +std::wstring GenerateUninstaller() { + path tempPath = temp_directory_path(); + path uninstallerPath = tempPath; + uninstallerPath /= "UninstallTestExe.bat"; + + std::cout << "Uninstaller located at path: " << uninstallerPath << '\n'; + + path uninstallerOutputTextFilePath = tempPath; + uninstallerOutputTextFilePath /= "TestExeUninstalled.txt"; + + std::ofstream uninstallerScript(uninstallerPath); + uninstallerScript << "@echo off\n"; + uninstallerScript << "ECHO. >" << uninstallerOutputTextFilePath << "\n"; + uninstallerScript << "ECHO AppInstallerTestExeInstaller.exe uninstalled successfully.\n"; + uninstallerScript.close(); + + return uninstallerPath.wstring(); +} + +void WriteToUninstallRegistry(const std::wstring& productID, const std::wstring& uninstallerPath) { + HKEY hkey; + LONG lReg; + + // String inputs to registry must be of wide char type + const wchar_t* displayName = L"AppInstallerTestExeInstaller\0"; + const wchar_t* publisher = L"Microsoft Corporation\0"; + const wchar_t* uninstallString = uninstallerPath.c_str(); + DWORD version = 1; + + std::wstring registryKey = (std::wstring)registrySubkey; + + if (!productID.empty()) + { + registryKey += productID; + std::wcout << "Product Code Overrided to: " << registryKey.c_str() << "\n"; + } + else + { + registryKey += defaultProductID; + std::wcout << "Default Product Code Used: " << registryKey.c_str() << "\n"; + } + + lReg = RegCreateKeyEx( + HKEY_LOCAL_MACHINE, + registryKey.c_str(), + 0, + NULL, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS | KEY_WOW64_64KEY, + NULL, + &hkey, + NULL); + + if (lReg == ERROR_SUCCESS) { + + std::cout << "Successfully opened registry key \n"; + + // Set Display Name Property Value + if (LONG res = RegSetValueEx(hkey, L"DisplayName", NULL, REG_SZ, (LPBYTE)displayName, (DWORD)(wcslen(displayName) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + std::cout << "Failed to write DisplayName value. Error Code: " << res << "\n"; + } + + // Set Publisher Property Value + if (LONG res = RegSetValueEx(hkey, L"Publisher", NULL, REG_SZ, (LPBYTE)publisher, (DWORD)(wcslen(publisher) + 1) * sizeof(wchar_t)) != ERROR_SUCCESS) + { + std::cout << "Failed to write Publisher value. Error Code: " << res << "\n"; + } + + // Set UninstallString Property Value + if (LONG res = RegSetValueEx(hkey, L"UninstallString", NULL, REG_EXPAND_SZ, (LPBYTE)uninstallString, (DWORD)wcslen(uninstallString + 1) * sizeof(wchar_t*)) != ERROR_SUCCESS) + { + std::cout << "Failed to write UninstallString value. Error Code: " << res << "\n"; + } + + // Set Version Property Value + if (LONG res = RegSetValueEx(hkey, L"Version", NULL, REG_DWORD, (LPBYTE)&version, sizeof(version)) != ERROR_SUCCESS) + { + std::cout << "Failed to write Version value. Error Code: " << res << "\n"; + } + + std::cout << "Write to registry key completed \n"; + } + else { + std::cout << "Key Creation Failed\n"; + } + + RegCloseKey(hkey); +} + +// The installer prints all args to an output file and writes to the Uninstall registry key int main(int argc, const char** argv) { - std::filesystem::path outFilePath = std::filesystem::temp_directory_path(); + path outFilePath = temp_directory_path(); + std::wstringstream productCodeStream; std::stringstream outContent; + std::wstring productCode; for (int i = 1; i < argc; i++) { @@ -22,6 +122,12 @@ int main(int argc, const char** argv) outFilePath = argv[i]; outContent << argv[i] << ' '; } + + // Supports custom product code ID + if (_stricmp(argv[i], "/ProductID") == 0 && ++i < argc) + { + productCodeStream << argv[i]; + } } outFilePath /= "TestExeInstalled.txt"; @@ -30,4 +136,17 @@ int main(int argc, const char** argv) file << outContent.str(); file.close(); + + if (!productCodeStream.str().empty()) + { + productCode = productCodeStream.str(); + } + + std::wstring uninstallerPath = GenerateUninstaller(); + + WriteToUninstallRegistry(productCode, uninstallerPath); + + return 0; } + +