From 5c64c86ca9c5272d1ac08c81fd78ac5f782daaaf Mon Sep 17 00:00:00 2001 From: Daniel Scott-Raynsford Date: Sun, 3 Jul 2016 17:19:15 +1200 Subject: [PATCH 01/49] * xGroup: Fix Verbose output in Get-MembersAsPrincipals function. --- .../MSFT_xGroupResource/MSFT_xGroupResource.psm1 | 11 +++++++++-- README.md | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 0f65f9c14..fc00adb5a 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -1518,7 +1518,7 @@ function Get-MembersAsPrincipals # The account is domain qualified - credential required to resolve it. elseif ($null -ne $Credential -or $null -ne $principalContext) { - Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $scope, $accountName) + Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $accountName, $scope) } else { @@ -1858,7 +1858,14 @@ function Get-PrincipalContext elseif ($null -ne $Credential) { # Create a PrincipalContext targeting $Scope using the network credentials that were passed in. - $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + if ($Credential.Domain) + { + $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + } + else + { + $principalContextName = $Credential.UserName + } $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope, $principalContextName, $Credential.Password ) # Cache the PrincipalContext for this scope for subsequent calls. diff --git a/README.md b/README.md index 58032226c..07eda624b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please check out common DSC Resources [contributing guidelines](https://github.c * **xEnvironment** configures and manages environment variables. * **xWindowsFeature** provides a mechanism to ensure that roles and features are added or removed on a target node. * **xScript** provides a mechanism to run Windows PowerShell script blocks on target nodes. -* **xGroupSet** configures multiple xGroups with common settings but different names. +* **xGroupSet** configures multiple xGroups with common settings but different names. * **xProcessSet** allows starting and stopping of a group of windows processes with no arguments. * **xServiceSet** allows starting, stopping and change in state or account type for a group of services. * **xWindowsFeatureSet** allows installation and uninstallation of a group of Windows features and their subfeatures. @@ -335,6 +335,9 @@ These parameters will be the same for each Windows optional feature in the set. ### Unreleased +* xGroup: Fix Verbose output in Get-MembersAsPrincipals function. + Fix bug when credential parameter passed does not contain local or domain context. + ### 3.12.0.0 * Removed localization for now so that resources can run on non-English systems. From d1f1ec3c5ab2c4be51734f70c4ed80acdeb984bd Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 5 Jul 2016 11:51:13 -0700 Subject: [PATCH 02/49] Moving xPackage examples and tests and merging with in-box. --- .../MSFT_xPackageResource.psm1 | 2 +- .../Tests/MSFT_xPackageResource.Tests.ps1 | 16 - ...ample_InstallExeCredsRegistry_xPackage.ps1 | 0 .../Sample_InstallExeCreds_xPackage.ps1 | 0 .../Sample_InstallMSIProductId_xPackage.ps1 | 0 .../Sample_InstallMSI_xPackage.ps1 | 0 .../MSFT_xPackageResource.TestHelper.psm1 | 1670 +++++++++++++++++ Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 377 ++++ 8 files changed, 2048 insertions(+), 17 deletions(-) delete mode 100644 DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 rename {DSCResources/MSFT_xPackageResource/Examples => Examples}/Sample_InstallExeCredsRegistry_xPackage.ps1 (100%) rename {DSCResources/MSFT_xPackageResource/Examples => Examples}/Sample_InstallExeCreds_xPackage.ps1 (100%) rename {DSCResources/MSFT_xPackageResource/Examples => Examples}/Sample_InstallMSIProductId_xPackage.ps1 (100%) rename {DSCResources/MSFT_xPackageResource/Examples => Examples}/Sample_InstallMSI_xPackage.ps1 (100%) create mode 100644 Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 create mode 100644 Tests/Unit/MSFT_xPackageResource.Tests.ps1 diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 3385953cc..1af9ea0ec 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -54,7 +54,7 @@ Function Trace-Message } } -$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" +$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xPackageResource" Function Throw-InvalidArgumentException { diff --git a/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 b/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 deleted file mode 100644 index 4b65c10b3..000000000 --- a/DSCResources/MSFT_xPackageResource/Tests/MSFT_xPackageResource.Tests.ps1 +++ /dev/null @@ -1,16 +0,0 @@ -#requires -Version 4.0 - -Remove-Module MSFT_xPackageResource -ErrorAction Ignore -$module = Import-Module $PSScriptRoot\..\MSFT_xPackageResource.psm1 -Force -PassThru -ErrorAction Stop - -Describe 'Get-MsiTools' { - It 'Uses Add-Type with a name that does not conflict with the original Package resource' { - InModuleScope MSFT_xPackageResource { - $hash = @{ Namespace = 'Mock not called' } - Mock Add-Type { $hash['Namespace'] = $Namespace } - $null = Get-MsiTools - - $hash['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' - } - } -} diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 b/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 rename to Examples/Sample_InstallExeCredsRegistry_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCreds_xPackage.ps1 b/Examples/Sample_InstallExeCreds_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallExeCreds_xPackage.ps1 rename to Examples/Sample_InstallExeCreds_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSIProductId_xPackage.ps1 b/Examples/Sample_InstallMSIProductId_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSIProductId_xPackage.ps1 rename to Examples/Sample_InstallMSIProductId_xPackage.ps1 diff --git a/DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSI_xPackage.ps1 b/Examples/Sample_InstallMSI_xPackage.ps1 similarity index 100% rename from DSCResources/MSFT_xPackageResource/Examples/Sample_InstallMSI_xPackage.ps1 rename to Examples/Sample_InstallMSI_xPackage.ps1 diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 new file mode 100644 index 000000000..19af22ec4 --- /dev/null +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -0,0 +1,1670 @@ +<############################################################################################# + + # File: DSC.Providers.Package.Helpers.psm1. + # + # Contains helper methods for test automation of Package DSC provider. + # + # Copyright (c)Microsoft Corp 2013. + # + + ############################################################################################> + + +<############################################################################################ +# Constants +############################################################################################> + +$global:end2EndFolder = "E2EScripts" +$global:end2EndScriptPath = "$global:homePath\$global:end2EndFolder" + +<############################################################################################ +# Common functions +# +##########################################################################################> + +<# +.Synopsis + Name: GetMsiProperties + Description: Retrieves the Properties of the given MSI package. + +.Parameters + msiFilePath: The path to the MSI package. + +.Returns + The Properties of the given MSI package in the form of HashTable. +#> +function GetMsiProperties +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $msiFilePath) + + # + + # Check if MSI file exists in the given path. + + # + + if (-not (Test-Path $msiFilePath)) + { + throw "Could not find MSI file path " + $msiFilePath + } + + # A hash table in which all the MSI properies are stored. + $msiProperties = @{} + + # + + # Create the COM object of type WindowsInstaller.Installer + + # + + $windowsInstaller = New-Object -com WindowsInstaller.Installer + + # + + # Load MSI Database. + + # + + $database = $windowsInstaller.GetType().InvokeMember( + "OpenDatabase", + "InvokeMethod", + $null, + $windowsInstaller, + @($msiFilePath, 0)) + + # + + # Open the Property view. + + # + + $query = "SELECT * FROM Property" + + $view = $database.GetType().InvokeMember( + "OpenView", + "InvokeMethod", + $null, + $database, + $query) + + $view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null) + + # + + # Fetch the Name and Value of each property from Property view. + + # + + $done = $false + + while (-not $done) + { + + $record = $view.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $view, $null) + + if ($null -eq $record) + { + $done = $true + } + else { + + $propertyName = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 1) + $propertyValue = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 2) + + $msiProperties[$propertyName] = $propertyValue + } + } + + # + + # Close the view. + + # + + $view.GetType().InvokeMember("Close", "InvokeMethod", $null, $view, $null) + + return $msiProperties +} + +<# +.Synopsis + Name: GetMsiProductId + Description: Retrieves the ProductID from the given MSI package. + +.Parameters + msiFilePath: The path to the MSI package. + +.Returns + The ProductID of the given MSI package. +#> +function GetMsiProductId +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $msiFilePath) + + # + + # Check if MSI file exists in the given path. + + # + + if (-not (Test-Path $msiFilePath)) + { + throw "Could not find MSI file path " + $msiFilePath + } + + $msiProperties = GetMsiProperties -msiFilePath $msiFilePath + + $msiProductId = $msiProperties.ProductCode + + if ($null -eq $msiProductId) + { + throw "Could not find the ProductID from MSI file " + $msiFilePath + } + + $msiProductId +} + +<# +.Synopsis + Name: IsMsiPackageInstalled + Description: Determines if the given MSI package is isntalled in the system or not. + +.Parameters + msiFilePath: The path to the MSI package. + +.Returns + $true in case the given MSI package is isntalled, otherwise $false. +#> +function IsMsiPackageInstalled +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $msiFilePath) + + # + + # Check if MSI file exists in the given path. + + # + + if (-not (Test-Path $msiFilePath)) + { + throw "Could not find MSI file path " + $msiFilePath + } + + $msiPackageInstalled = $false + + # + + # Get MSI ProductID + + # + + $msiProductId = GetMsiProductId -msiFilePath $msiFilePath + + # + + # Issue WMI query to determine if any product with given ProductId is installed. + + # + + $msiPackageInstalled = IsProductInstalled -productId $msiProductId + + return $msiPackageInstalled +} + +<# +.Synopsis + Name: IsProductInstalled + Description: Determines if the given product is installed using its ProductId. + +.Parameters + productId: The ProductId of the product. + +.Returns + $true in case the product is installed, otherwise $false. +#> +function IsProductInstalled +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $productId) + + $productInstalled = $false + + $installedProductInfo = Get-InstalledProductRegistryEntry -productId $productId + + if($null -ne $installedProductInfo) { + + $productInstalled = $true + } + + return $productInstalled +} + +<# +.Synopsis + Name: Get-InstalledProductRegistryEntry + Description: Retrieves the registry entry of the installed product. + +.Parameters + productId: The productID of the installed product. Should be a GUID. + +.Returns + The installed product's registry entry. +#> +function Get-InstalledProductRegistryEntry +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $productId) + + # Ensure the the provided ProductId is a Guid and surrounds it by curley brackets. + $productId = Format-ProductId -productId $productId + + $installedProductRegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0}" -f $productId + + $installedProductWoW64RegKey = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{0}" -f $productId + + $installedProductInfo = Get-Item -Path $installedProductRegKey -ErrorAction SilentlyContinue + + if ($null -eq $installedProductInfo) + { + $installedProductInfo = Get-Item -Path $installedProductWoW64RegKey -ErrorAction SilentlyContinue + } + + return $installedProductInfo +} + +<# +.Synopsis + Name: Get-InstalledProductProperties + Description: Retrieves the installed product's properties and values from system registry. + +.Parameters + productId: The productID of the installed product. Should be a GUID. + +.Returns + The installed product's properties and values. +#> +function Get-InstalledProductProperties +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $productId) + + $installedProductInfo = Get-InstalledProductRegistryEntry -productId $productId + + if ($null -eq $installedProductInfo) + { + throw "The Product is not installed. ProductId = {0}" -f $productId + } + + $size = $installedProductInfo.GetValue("EstimatedSize") + if ($size) + { + $size = $size / 1024 + } + + $name = $installedProductInfo.GetValue("DisplayName_Localized") + + if (-not $name) + { + $name = $installedProductInfo.GetValue("DisplayName") + } + + $publisher = $installedProductInfo.GetValue("Publisher_Localized") + + if (-not $publisher) + { + $publisher = $installedProductInfo.GetValue("Publisher") + } + + $version = $installedProductInfo.GetValue("DisplayVersion") + + $packageDescription = $installedProductInfo.GetValue("Comments") + + $installedOn = $installedProductInfo.GetValue("InstallDate") + + if ($installedOn) + { + try + { + $installedOn = "{0:d}" -f [DateTime]::ParseExact($installedOn, "yyyyMMdd", [System.Globalization.CultureInfo]::CurrentCulture).Date + } + catch + { + $installedOn = $null + } + } + + return @{ + Name = $name + InstalledOn = $installedOn + ProductId = Format-ProductId -productId $productId + Size = $size + Installed = $true + Version = $version + PackageDescription = $packageDescription + Publisher = $publisher + } +} + +<# +.Synopsis + Name: Format-ProductId + Description: Ensures that the productId is a GUID. Surrounds ProductId within curley brackets. + +.Parameters + productId: The productID of the installed product. Should be a GUID. + +.Returns + The formatted ProductId which is a GUID surrounded by curley brackets. +#> +function Format-ProductId +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $productId) + + return [Guid]::Parse($productId).ToString("B").ToUpper() +} + +<# +.Synopsis + Name: InstallMsiPackage + Description: Installs the given MSI package. + +.Parameters + msiFilePath: The path to the MSI package. +#> +function InstallMsiPackage +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $msiFilePath) + + # + + # Check if MSI file exists in the given path. + + # + + if (-not (Test-Path $msiFilePath)) + { + throw "Could not find MSI file path " + $msiFilePath + } + + # Execute msiexec to install an MSI package. + + # + + $msiProcess = Start-Process -Wait -PassThru msiexec.exe -ArgumentList "/i $msiFilePath", /passive, /quiet + + $msiProcess.ExitCode +} + +<# +.Synopsis + Name: UnInstallMsiPackage + Description: UnInstalls the given MSI package. + +.Parameters + msiFilePath: The path to the MSI package. +#> +function UnInstallMsiPackage +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $msiFilePath) + + # + + # Check if MSI file exists in the given path. + + # + + if (-not (Test-Path $msiFilePath)) + { + throw "Could not find MSI file path " + $msiFilePath + } + + # + + # Get MSI ProductID + + # + + $msiProductId = GetMsiProductId -msiFilePath $msiFilePath + + # + + # Execute msiexec to uninstall an MSI package given its ProductId. + + # + + $exitCode = UnInstallProduct -productId $msiProductId + + return $msiProcess.ExitCode +} + +<# +.Synopsis + Name: InstallExePackage + Description: Installs the given EXE package. + +.Parameters + exeSetupFilePath: The path to the exe setup package. + arguments: The arguments to pass to the exe installer. +#> +function InstallExePackage +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $exeSetupFilePath, + + [ValidateNotNullOrEmpty()] + [String]$arguments) + + # + + # Check if exe setup file exists in the given path. + + # + + if (-not (Test-Path $exeSetupFilePath)) + { + throw "Could not find exe setup file path " + $exeSetupFilePath + } + + # Execute the Exe setup. + + # + + $msiProcess = Start-Process -Wait -PassThru $exeSetupFilePath -ArgumentList $arguments + + $msiProcess.ExitCode +} + +<# +.Synopsis + Name: UnInstallProduct + Description: UnInstalls the product given its ProductId. MsiExec is used for uninstalling the product. + +.Parameters + productId: The ProductId of the product to be uninstalled. + +.Return + The exist code of msiexec process. +#> +function UnInstallProduct +{ + param + ( + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] $productId) + + $msiProcess = Start-Process -Wait -PassThru msiexec.exe -ArgumentList "/x$productId", /passive, /quiet + + return $msiProcess.ExitCode +} + +<# +.Synopsis + Name: InstallNetMon + Description: Installs the NetMon product. +#> +function InstallNetMon +{ + InstallExePackage -exeSetupFilePath $global:exeInstallerNetMon -arguments "/Q" +} + +<# +.Synopsis + Name: UnInstallNetMon + Description: UnInstalls the NetMon sample product from the machine. +#> +function UnInstallNetMon +{ + # NetMon installer installs two products, lets uninstall both of these products: + + UnInstallProduct -productId $global:productIdNetMon + + UnInstallProduct -productId $global:productIdNetMonParser +} + +<# +.Synopsis + Name: EmptyDirectory + Description: Removes all the contents of a directory. + +.Parameters + $directoryName: The name of a directory to be emptied. +#> +function EmptyDirectory([String]$directoryName) +{ + Remove-Item $directoryName -Recurse -Force -ErrorAction SilentlyContinue +} + +<# +.Synopsis + Name: ClearCacheDirectory + Description: Deletes the contents of the PAckage Provider's cache directory. +#> +function ClearCacheDirectory +{ + EmptyDirectory -directoryName $global:packageProviderCacheDirectory +} + +<# +.Synopsis + Name: EnsureIsEmptyOrNull + Description: Determines if the value of Ensure is empty, null or white space. +#> +function EnsureIsEmptyOrNull([String]$ensureValue) +{ + if ([String]::IsNullOrEmpty($ensureValue) -or [String]::IsNullOrWhiteSpace($ensureValue)) + { + return $true + } + + return $false +} + +<# +.Synopsis + Name: PathsAreEqual + Description: Determines if the two paths are equal or not. +#> +function PathsAreEqual([String]$firstPath, [String]$secondPath) +{ + [bool]$pathsAreEqual = $false; + + # if path is not a web URL. + if( (([uri]$firstPath).Scheme -eq "file")) + { + $fullFirstPath = [System.IO.Path]::GetFullPath($firstPath) + + $fullSecondPath = [System.IO.Path]::GetFullPath($secondPath) + + if (0 -eq [String]::Compare($fullFirstPath, $fullSecondPath, [StringComparison]::OrdinalIgnoreCase)) + { + + $pathsAreEqual = $true; + } + } + else # if path is a web URL. + { + if(([uri]$firstPath -eq [uri]$secondPath)) + { + $pathsAreEqual = $true; + } + } + + return $pathsAreEqual; +} + +<# +.Synopsis + Name: CreateDirectory + Description: Creates directory recursively. + +.Parameters + $directoryName: The name of a directory to be created recursively. +#> +function CreateDirectory([String]$directoryName) +{ + New-Item -Path $directoryName -type directory -Force -erroraction Silentlycontinue | Out-Null +} + +<# +.Synopsis + Name: PrepareWebDirectory + Description: Creates a Tools directory within IIS default site. So tests can use URL to download and install tools. +#> +function PrepareWebDirectory +{ + CreateDirectory($global:webDirectory) + + # copy mita in WebDirectory + CopyFileInWebDirectory -fileName $global:msiInstallerMita -webDirectoryName "$global:webDirectory" + + # copy NetMon in WebDirectory + CopyFileInWebDirectory -fileName $global:exeInstallerNetMon -webDirectoryName "$global:webDirectory" +} + +<# +.Synopsis + Name: CopyFileInWebDirectory + Description: Copies a file to the Tools directory within IIS default site. So tests can use URL to download and install tools. +#> +function CopyFileInWebDirectory([String]$fileName, [String]$webDirectoryName) +{ + Copy-Item -Path $fileName -Destination $webDirectoryName -Force | out-null +} + + +<# +.Synopsis + Name: GetMitaUrl + Description: Returns the web URL from where Mita setup can be downloaded. +#> +function GetMitaUrl +{ + $mitaFileName = split-path $global:msiInstallerMita -Leaf + + $toolsDirectoryName = split-Path $global:webDirectory -Leaf + + $mitaUrl = "http://$env:computerName/$toolsDirectoryName/$mitaFileName" + + return $mitaUrl +} + +<# +.Synopsis + Name: GetNetMonUrl + Description: Returns the web URL from where NetMon setup can be downloaded. +#> +function GetNetMonUrl +{ + $netMonFileName = split-path $global:exeInstallerNetMon -Leaf + + $toolsDirectoryName = split-Path $global:webDirectory -Leaf + + $netMonUrl = "http://$env:computerName/$toolsDirectoryName/$netMonFileName" + + return $netMonUrl +} + +<# +.Synopsis + Name: MachineRequiresReboot + Description: Returns true in case the machine requires reboot. Otherwise, false. +#> +function MachineRequiresReboot +{ + $rebootRequired = $false + + $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks + + $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore + + if(($featureData -and $featureData.RequiresReboot) -or $regData) + { + $rebootRequired = $true + } + + return $rebootRequired +} + +<# +.Synopsis + Name: ExecuteStartDscConfiguration + Description: Invokes the DSC engine to apply a configuration. + +.Parameters + path: The location where configuration mof files exists. + computerName: The name of computer where configuration is supposed to be applied. +#> +function ExecuteStartDscConfiguration +{ + param ( + [System.String] $path, + [System.String[]] $computerName) + + Write-Host $path + Write-Host $computerName + + $result = Start-DscConfiguration -Path $path -ComputerName $computerName -Verbose -Wait -Force +} + +<# +.Synopsis + Name: ExecuteGetDscConfiguration + Description: Invokes the DSC engine to get the configuration that was recently applied. + +.Parameters + cimSession: The CIM session to remote machine. + +.Output + The result of the Get-DSCConfiguration command. +#> +function ExecuteGetDscConfiguration +{ + param ( + [Microsoft.Management.Infrastructure.CimSession]$cimSession) + + $result = $null + + if(-not $cimSession) + { + $result = Get-DscConfiguration + } + else + { + $result = Get-DscConfiguration -CimSession $cimSession + } + + return $result +} + +<# +.Synopsis + Name: ExecuteTestDscConfiguration + Description: Invokes the DSC engine to get the configuration that was recently applied. + +.Parameters + cimSession: The CIM session to remote machine. + +.Output + The result of the Test-DSCConfiguration command. +#> +function ExecuteTestDscConfiguration +{ + param ( + [Microsoft.Management.Infrastructure.CimSession]$cimSession) + + $result = $null + + if(-not $cimSession) + { + $result = Test-DscConfiguration + } + else + { + $result = Test-DscConfiguration -CimSession $cimSession + } + + return $result +} + +<# +.Synopsis + Name: CreatePackageInstanceMof + Description: Creates the instance document for Package Provider E2E tests. + +.Parameters + configurationName: The configuration name. + resourceId: The resource id. + ensure: The value to be used for 'Ensure' parameter. +#> +function CreatePackageInstanceMof +{ + param + ( + [String]$configurationName, + [String]$resourceId, + [String]$path, + [String]$productId, + [String]$name, + [String]$arguments, + [String]$ensure, + + [String]$UserName, + [String]$Password + ) + + Log -message @" +Calling CreatePackageInstanceMof with parameters +configurationName=$configurationName +resourceId=$resourceId +path=$path +product=$productId +name=$name +arguments=$arguments +ensure=$ensure +UserName=$UserName +Password=$Password +"@ + + $scriptText = @' + +#Configuration script that uses Package resource + +if ("{8}" -ne [String]::Empty) +{{ + configuration {0} + {{ + + node ("localhost") + {{ + Package PACKAGE1 + {{ + Path = "{3}" + ProductId = "{4}" + Name = "{5}" + Arguments = "{6} ALLUSERS=1" + Ensure = "{7}" + PsDscRunAsCredential = New-Object System.Management.Automation.PSCredential -ArgumentList '{8}', (ConvertTo-SecureString -AsPlainText '{9}' -Force) + LogPath = "{1}\{2}.log" + }} + }} + }} +}} +else +{{ + configuration {0} + {{ + + node ("localhost") + {{ + Package PACKAGE1 + {{ + Path = "{3}" + ProductId = "{4}" + Name = "{5}" + Arguments = "{6}" + Ensure = "{7}" + LogPath = "{1}\{2}.log" + }} + }} + }} +}} + +$Global:AllNodes= +@{{ + AllNodes = @( + @{{ + NodeName = "localhost"; + RecurseValue = $true; + PSDscAllowPlainTextPassword = $true; + }}; + ); +}} + +{0} -output "{1}\{2}" -ConfigurationData $Global:AllNodes + +'@ + + # Create the DSC configuration script. + $script = $scriptText -f $configurationName, $global:end2EndScriptPath, $resourceId, $path, $productId, $name, $arguments, $ensure, $UserName, $Password + + # Save the script in *.ps1 file ( this helps in debugging ). + CreateDirectory -directoryName "$global:end2EndScriptPath\Config" + + $script > "$global:end2EndScriptPath\Config\$resourceId.ps1" + + # Create the instance document. + $scriptBlock = [ScriptBlock]::Create($script) + Invoke-Command -ScriptBlock $scriptBlock -Verbose + +} + +<############################################################################################ + # This section contains helper methods that will be used by Package Provider's DRTs. + ############################################################################################> + +$FolderCount = 0 + +<# + .SYNOPSIS + Clears the xPackage cache. +#> +function Clear-xPackageCache +{ + [CmdletBinding()] + param () + + $xPackageCacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\" + ` + "BuiltinProvCache\MSFT_xPackageResource" + + Remove-Item -Path $xPackageCacheLocation -ErrorAction 'SilentlyContinue' -Recurse +} + +<# + .SYNOPSIS + Tests if the package with the given name is installed. + + .PARAMETER + The name of the package to test for. +#> +function Test-PackageInstalled +{ + [OutputType([Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Name + ) + + $packageWithName = Get-CimInstance -ClassName 'Win32_Product' | Where-Object { $_.Name -ieq $Name } + + return $null -ne $packageWithName +} + +Function Get-InstalledById +{ + param($Id) + return Get-CimInstance Win32_Product | Where {$_.IdentifyingNumber.Equals($Id)} +} + +<# + .SYNOPSIS + Mimics a simple http/https server. +#> +function Serve-Binary +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $FileName + ) + + if (-not (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) + { + New-NetFirewallRule -DisplayName “UnitTestRule” -Direction Inbound -Program "$PSHome\powershell.exe" -Authentication NotRequired -Action Allow + } + + netsh advfirewall set allprofiles state off + + Start-Job -ArgumentList $FileName -ScriptBlock { + + # Create certificate + $cert = Get-ChildItem -Recurse Cert:\LocalMachine\My |Where-Object{$_.EnhancedKeyUsageList.FriendlyName -eq 'Server Authentication'} + + if ($cert.count -gt 1) + { + # just use the first one + $cert = $cert[0] + } + elseif ($cert.count -eq 0) + { + # create a self-signed one + $cert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME + } + $hash = $cert.Thumbprint + + # use net shell command to directly bind certificate to designated testing port + netsh http add sslcert ipport=0.0.0.0:1243 certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable + + # start listening endpoints + $httpListener = New-Object System.Net.HttpListener + $httpListener.Prefixes.Add([uri]"https://localhost:1243/") + $httpListener.Prefixes.Add([uri]"http://localhost:1242") + $httpListener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Negotiate + $httpListener.Start() + + # create a pipe to flag http/https client + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\dsctest1") + $pipe.Connect() + $pipe.Dispose() + + # prepare binary buffer for http/https response + $fileInfo = New-Object System.IO.FileInfo $args[0] + $numBytes = $fileInfo.Length; + $fileStream = New-Object System.IO.FileStream $args[0], 'Open' + $binaryReader = New-Object System.IO.BinaryReader $fileStream + [byte[]] $buf = $binaryReader.ReadBytes($numBytes) + $fileStream.Close() + + # send response + $response = ($httpListener.GetContext()).Response + $response.ContentType = "application/octet-stream" + $response.ContentLength64 = $buf.Length + $response.OutputStream.Write($buf, 0, $buf.Length) + $response.OutputStream.Flush() + + # wait for client to finish downloading + $pipe = New-Object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\dsctest2") + $pipe.WaitForConnection() + $pipe.Dispose() + + $response.Dispose() + $httpListener.Stop() + $httpListener.Close() + + # close pipe + + # use net shell command to clean up the certificate binding + netsh http delete sslcert ipport=0.0.0.0:1243 + } + + netsh advfirewall set allprofiles state on +} + +$share = $null +Function Share-ScriptFolder +{ + if($share) + { + return $share + } + + $shareName = "DSCUnitTestShare" + if(-not (Get-NetFirewallRule FPS-SMB-In-TCP).Enabled) + { + Enable-NetFirewallRule FPS-SMB-In-TCP + } + + if((Get-SmbShare -EA SilentlyContinue $shareName)) + { + Remove-SmbShare $shareName -Force + } + + $script:share = New-SmbShare -Name $shareName -Path $PSScriptRoot + return $share +} + +$setupExe = $null +Function Get-SetupExe +{ + if($setupExe) + { + return $setupExe + } + + $setupExe = "$PSScriptRoot\DummySetupProgram.exe" + rm $setupExe -EA SilentlyContinue #Kill any leftover from last unit test invocation + $sig = @' + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management; + using System.Text; + using System.Threading.Tasks; + using System.Management.Automation; + using System.Management.Automation.Runspaces; + using System.Runtime.InteropServices; + namespace Providers.Package.UnitTests.MySuite + { + class ExeTestClass + { + public static void Main(string[] args) + { + string cmdline = System.Environment.CommandLine; + Console.WriteLine("Cmdline was " + cmdline); + int endIndex = cmdline.IndexOf("\"", 1); + string self = cmdline.Substring(0, endIndex); + string other = cmdline.Substring(self.Length + 1); + string msiexecpath = System.IO.Path.Combine(System.Environment.SystemDirectory, "msiexec.exe"); + + self = self.Replace("\"", ""); + string packagePath = System.IO.Path.Combine(System.IO.Path.GetDirectoryName(self), "DSCSetupProject.msi"); + + string msiexecargs = String.Format("/i {0} {1}", packagePath, other); + System.Diagnostics.Process.Start(msiexecpath, msiexecargs).WaitForExit(); + } + } + } +'@ + Add-Type $sig -OutputAssembly $setupExe -OutputType ConsoleApplication + $script:setupExe = $setupExe + return $setupExe +} + +<# + .SYNOPSIS + Creates a new MSI package for testing. + + .PARAMETER DestinationPath + The path at which to create the test msi file. +#> +function New-TestMsi +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DestinationPath + ) + + #region msiContentInBase64 + $msiContentInBase64 = '0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgAEAP7/DAAGAAAAAAAAAAEAAAABAAAAAQA' + ` + 'AAAAAAAAAEAAAAgAAAAEAAAD+////AAAAAAAAAAD/////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '////////////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP3////+/////v///wYAAAD+////BAAAAP7////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '////////////////////////////////////////////////////////////9SAG8AbwB0ACAARQBuAHQAcgB' + ` + '5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAFAP//////////CQAAAIQQ' + ` + 'DAAAAAAAwAAAAAAAAEYAAAAAAAAAAAAAAADwRqG1qh/OAQMAAAAAEwAAAAAAAAUAUwB1AG0AbQBhAHIAeQBJA' + ` + 'G4AZgBvAHIAbQBhAHQAaQBvAG4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAIA////////////////AA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwCAAAAAAAAQEj/P+RD7EHkRaxEMUgAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAgETAAAABAAAAP////8A' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJAAAAOAcAAAAAAABASMpBMEOxOztCJkY3QhxCN' + ` + 'EZoRCZCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAACAQsAAAAKAAAA/////w' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACYAAAAwAAAAAAAAAEBIykEwQ7E/Ej8oRThCsUE' + ` + 'oSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAIBDAAAAP//////////' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJwAAABgAAAAAAAAAQEjKQflFzkaoQfhFKD8oR' + ` + 'ThCsUEoSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAAgD///////////////' + ` + '8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAKgAAAAAAAABASIxE8ERyRGhEN0gAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgACAP//////////////' + ` + '/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAAMAAAAAAAAAEBIDUM1QuZFckU8SAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAIADgAAAAIAAAD///' + ` + '//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgAAABIAAAAAAAAAQEgPQuRFeEUoSAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgD/////////////' + ` + '//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArAAAAEAAAAAAAAABASA9C5EV4RSg7MkSzR' + ` + 'DFC8UU2SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgACAQcAAAADAAAA//' + ` + '///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAEAAAAAAAAAEBIUkT2ReRDrzs7QiZ' + ` + 'GN0IcQjRGaEQmQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaAAIBBQAAAAEAAAD/' + ` + '////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALQAAAHIAAAAAAAAAQEhSRPZF5EOvPxI/K' + ` + 'EU4QrFBKEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYAAgH///////////' + ` + '////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvAAAAMAAAAAAAAABASBVBeETmQoxE8UH' + ` + 'sRaxEMUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAACAP//////////' + ` + '/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAEAAAAAAAAAEBIWUXyRGhFN0cAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAIBDwAAAP////' + ` + '//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMQAAACQAAAAAAAAAQEgbQipD9kU1RwA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgEQAAAADQAA' + ` + 'AP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyAAAADAAAAAAAAABASN5EakXkQShIA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAACAP////////' + ` + '///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMAAAAgAAAAAAAAAEBIfz9kQS9CNkg' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAIBEQAAAAgA' + ` + 'AAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAACAAAAAAAAAAQEg/O/JDOESxR' + ` + 'QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAgD///////' + ` + '////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1AAAAWAIAAAAAAABASD8/d0VsRGo' + ` + '+skQvSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAACAP//////' + ` + '/////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAYAwAAAAAAAEBIPz93RWxEa' + ` + 'jvkRSRIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAIBBgAAAB' + ` + 'IAAAD/////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAFAaAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////' + ` + '//////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////' + ` + '///////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////' + ` + '////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///' + ` + '////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//' + ` + '/////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//' + ` + '//////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/' + ` + '//////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP' + ` + '///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + '////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'D///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AP///////////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AA////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQA' + ` + 'AAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAD+////CgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEA' + ` + 'AAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhA' + ` + 'AAAIgAAACMAAAAkAAAAJQAAAP7////+/////v////7////+/////v////7////+////LgAAAP7////+/////v' + ` + '////7////+/////v////7///82AAAANwAAADgAAAA5AAAAOgAAADsAAAA8AAAAPQAAAD4AAAD+////QAAAAEE' + ` + 'AAABCAAAAQwAAAEQAAABFAAAARgAAAEcAAABIAAAASQAAAEoAAABLAAAA/v//////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '/////////////////////////////////////////////////////////////////////////////////////' + ` + '///////////////////7/AAAGAQIAAAAAAAAAAAAAAAAAAAAAAAEAAADghZ/y+U9oEKuRCAArJ7PZMAAAAAwC' + ` + 'AAAOAAAAAQAAAHgAAAACAAAAgAAAAAMAAACgAAAABAAAAMQAAAAFAAAA9AAAAAYAAAAIAQAABwAAAGwBAAAJA' + ` + 'AAAgAEAAAwAAACwAQAADQAAALwBAAAOAAAAyAEAAA8AAADQAQAAEgAAANgBAAATAAAABAIAAAIAAADkBAAAHg' + ` + 'AAABYAAABJbnN0YWxsYXRpb24gRGF0YWJhc2UAAAAeAAAAGwAAAEEgcGFja2FnZSBmb3IgdW5pdCB0ZXN0aW5' + ` + 'nAAAeAAAAKAAAAE1pY3Jvc29mdCBVbml0IFRlc3RpbmcgR3VpbGQgb2YgQW1lcmljYQAeAAAACgAAAEluc3Rh' + ` + 'bGxlcgAAAB4AAABcAAAAVGhpcyBpbnN0YWxsZXIgZGF0YWJhc2UgY29udGFpbnMgdGhlIGxvZ2ljIGFuZCBkY' + ` + 'XRhIHJlcXVpcmVkIHRvIGluc3RhbGwgRFNDVW5pdFRlc3RQYWNrYWdlLgAeAAAACwAAAEludGVsOzEwMzMAAB' + ` + '4AAAAnAAAAe0YxN0FGREExLUREMEItNDRFNi1CNDczLTlFQkUyREJEOUVBOX0AAEAAAAAAAOO0qh/OAUAAAAA' + ` + 'AAOO0qh/OAQMAAADIAAAAAwAAAAIAAAAeAAAAIwAAAFdpbmRvd3MgSW5zdGFsbGVyIFhNTCAoMy43LjEyMDQu' + ` + 'MCkAAAMAAAACAAAAAAAAAAYABgAGAAYABgAGAAYABgAGAAYACgAKACIAIgAiACkAKQApACoAKgAqACsAKwArA' + ` + 'CsAKwArADEAMQAxAD4APgA+AD4APgA+AD4APgBNAE0AUgBSAFIAUgBSAFIAUgBSAGAAYABgAGEAYQBhAGIAYg' + ` + 'BmAGYAZgBmAGYAZgByAHIAdgB2AHYAdgB2AHYAgACAAIAAgACAAIAAgAACAAUACwAMAA0ADgAPABAAEQASAAc' + ` + 'ACQAjACUAJwAjACUAJwAjACUAJwAlACsALQAwADMANgAxADoAPAALADAAMwA+AEAAQgBFAEcATgBQACcAMwBQ' + ` + 'AFIAVQBYAFoAXAAjACUAJwAjACUAJwALACUAZwBpAGsAbQBvAHEABwByAAEABwBQAHYAeAB6ADMAXACBAIMAh' + ` + 'QCJAIsACAAIABgAGAAYABgAGAAIABgAGAAIAAgACAAYABgACAAYABgACAAYABgAGAAIABgACAAIABgACAAYAA' + ` + 'gAGAAYAAgACAAYABgAGAAIAAgACAAIABgACAAIAAgACAAYABgACAAYABgACAAYABgACAAIAAgACAAYABgAGAA' + ` + 'YAAgACAAYABgACAAIAAgACAAIABgACAAYABgAGAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAgAEAAAAAAAAA' + ` + 'AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAAAAAAAAAAA' + ` + 'ABAACAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAA/P//fwAAAAAAAAAA/P//fwAAAAAAAAA' + ` + 'AAQAAgAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////fwAAAAAAAACAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAACA/////wAAAAAAAAAA/////wAAA' + ` + 'AAAAAAAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/38AgP9/AIAAAAAAAAAAAP//////fwCAAAA' + ` + 'AAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCAAAAAAAAAAAD/fwCA/////wAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAACAAAAAAP////8AAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxAAAANw' + ` + 'AAADEAAAAAADEAAAAAAD4AAAAAAAAAPgArAAAAAAArAAAAAAAAAFIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAArAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAGAAAABgAAAAAABgAAAAAABgAAAAAAAAAGAAYAAAAAAAYAAAAAAA' + ` + 'AABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAB' + ` + 'MAEwAfAB8AAAAAAAAAAAATAAAAAAAAABMAJQAAABMAJQAAABMAJQAAACUAEwAuABMAAAATABMAEwA8AB8ASQA' + ` + 'AABMAEwAfAAAAAAATABMAAAAAABMAEwBWAAAAWgBcABMAJQAAABMAJQAAAGQAJQAAAAAAHwBtAB8AcgAfABMA' + ` + 'ZABkABMAEwAAAHsAAABcAC4AHwAfAGQASQAAAAAAAAAAAB0AAAAAABYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAVACEAIAAeABw' + ` + 'AGgAXABsAGQAAAAAAJAAmACgAJAAmACgAJAAmACgANQAsAC8AMgA0ADgAOQA7AD0ARABKAEwAPwBBAEMARgBI' + ` + 'AE8AUQBfAF4AVABTAFcAWQBbAF0AJAAmACgAJAAmACgAZQBjAGgAagBsAG4AcABzAHUAdAB9AH4AfwB3AHkAf' + ` + 'ACIAIcAggCEAIYAigCMAAAAAAAAAAAAjQCOAI8AkACRAJIAkwCUAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IN4hd' + ` + 'yFPI+gj8iZAAAAAAAAAAAAAAAAAAAAAI0AjgCPAJUAAAAAAAAAAAAgg4SD6IMUhQAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACNAI8AkACRAJQAlgCXAAAAAAAAAAAAAAAAAAAAIIPog3iF3IXImZyY' + ` + 'AJkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmACZAJoABIAAAJsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJoAnACeAJwAngAAAJ0AnwCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAChAAAAogAAAAKAAYAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoQCYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI0AjgCPAJAAkQCUAJYAlwCjAKQApQCmAKcAqACpAKoAqwCsAK0AA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IN4hdyFyJmcmACZGYBkgLyCsIRAhg' + ` + 'iHKIqIk3CX1Jd5hQAAAAAAAAAAAAAAAAAAjQCOAI8AlQCjAKQApQCmAAAAAAAAAAAAAAAAAAAAAAAgg4SD6IM' + ` + 'UhRmAZIC8grCEAAAAAAAAAAAAAAAAAAAAAK4ArwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACBALAAsgC0ALYAuAC6AL0AvwC8ALEAswC1ALcAuQC7AL4AwAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmwACgMEAwgDDAJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALwAvAAAALsAuwAAAAAAAAABAACAAgAAgAAAAADEAMUAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGACIAKQAqACsAMQA+AE0AUgBgAGEAYgBmAHIAdgCAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAYABgAGAAYABgAGAAYABgAiACIAIgApACkAKQAqACoAK' + ` + 'gArACsAKwArACsAKwAxADEAMQA+AD4APgA+AD4APgA+AD4ATQBNAFIAUgBSAFIAUgBSAFIAUgBgAGAAYABhAG' + ` + 'EAYQBiAGIAZgBmAGYAZgBmAGYAcgByAHYAdgB2AHYAdgB2AIAAgACAAIAAgACAAIAAAYACgAOABIAFgAaAB4A' + ` + 'IgAmACoABgAKAA4ABgAKAA4ABgAKAA4ABgAKAA4AEgAWABoABgAKAA4ABgAKAA4AEgAWABoAHgAiAAYACgAGA' + ` + 'AoADgASABYAGgAeACIABgAKAA4ABgAKAA4ABgAKAAYACgAOABIAFgAaAAYACgAGAAoADgASABYAGgAGAAoADg' + ` + 'ASABYAGgAeAAgAFABAAEgAPABEADgANAAwACwAjACUAJwAjACUAJwAjACUAJwArAC0AMAAzACUANgAxADoAPA' + ` + 'A+AEAAQgALAEUARwAwADMATgBQAFIAUABVAFgAWgBcADMAJwAjACUAJwAjACUAJwAlAAsAZwBpAGsAbQBvAHE' + ` + 'AcgAHAHYAeAB6AAEABwBQAIEAgwCFAFwAMwCJAIsAIK0grQSNBJEEkf+dApUgnf+d/51Irf+dApVIrf+dApVI' + ` + 'rf+dApVIrSadSI0Chf+dSJ1IrUid/48mrSadQJ//nwKVAoVInQKFJq1IrUitSI3/jwSBSJ0UnQKVBIFIrf+dA' + ` + 'pVIrf+dApX/rf+PAqUEgUCf/50gnUidSK0Aj0itAoX/j/+fAJ9IjSatFL0Uvf+9BKH/nUiNAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAIABQACAAAAAAAAAAAABgACAAsAFQAFAAUAAQA' + ` + 'mAAoAAQATAAIACwAGAAMAAgAIAAIACQACAAgAAgBudGVnZXIgdG8gZGV0ZXJtaW5lIHNvcnQgb3JkZXIgZm9y' + ` + 'IHRhYmxlLkxhc3RTZXF1ZW5jZUZpbGUgc2VxdWVuY2UgbnVtYmVyIGZvciB0aGUgbGFzdCBmaWxlIGZvciB0a' + ` + 'GlzIG1lZGlhLkRpc2tQcm9tcHREaXNrIG5hbWU6IHRoZSB2aXNpYmxlIHRleHQgYWN0dWFsbHkgcHJpbnRlZC' + ` + 'BvbiB0aGUgZGlzay4gIFRoaXMgd2lsbCBiZSB1c2VkIHRvIHByb21wdCB0aGUgdXNlciB3aGVuIHRoaXMgZGl' + ` + 'zayBuZWVkcyB0byBiZSBpbnNlcnRlZC5DYWJpbmV0SWYgc29tZSBvciBhbGwgb2YgdGhlIGZpbGVzIHN0b3Jl' + ` + 'ZCBvbiB0aGUgbWVkaWEgYXJlIGNvbXByZXNzZWQgaW4gYSBjYWJpbmV0LCB0aGUgbmFtZSBvZiB0aGF0IGNhY' + ` + 'mluZXQuVm9sdW1lTGFiZWxUaGUgbGFiZWwgYXR0cmlidXRlZCB0byB0aGUgdm9sdW1lLlNvdXJjZVByb3Blcn' + ` + 'R5VGhlIHByb3BlcnR5IGRlZmluaW5nIHRoZSBsb2NhdGlvbiBvZiB0aGUgY2FiaW5ldCBmaWxlLk5hbWUgb2Y' + ` + 'gcHJvcGVydHksIHVwcGVyY2FzZSBpZiBzZXR0YWJsZSBieSBsYXVuY2hlciBvciBsb2FkZXIuU3RyaW5nIHZh' + ` + 'bHVlIGZvciBwcm9wZXJ0eS4gIE5ldmVyIG51bGwgb3IgZW1wdHkuUmVnaXN0cnlQcmltYXJ5IGtleSwgbm9uL' + ` + 'WxvY2FsaXplZCB0b2tlbi5Sb290VGhlIHByZWRlZmluZWQgcm9vdCBrZXkgZm9yIHRoZSByZWdpc3RyeSB2YW' + ` + 'x1ZSwgb25lIG9mIHJya0VudW0uS2V5UmVnUGF0aFRoZSBrZXkgZm9yIHRoZSByZWdpc3RyeSB2YWx1ZS5UaGU' + ` + 'gcmVnaXN0cnkgdmFsdWUgbmFtZS5UaGUgcmVnaXN0cnkgdmFsdWUuRm9yZWlnbiBrZXkgaW50byB0aGUgQ29t' + ` + 'cG9uZW50IHRhYmxlIHJlZmVyZW5jaW5nIGNvbXBvbmVudCB0aGF0IGNvbnRyb2xzIHRoZSBpbnN0YWxsaW5nI' + ` + 'G9mIHRoZSByZWdpc3RyeSB2YWx1ZS5VcGdyYWRlVXBncmFkZUNvZGVUaGUgVXBncmFkZUNvZGUgR1VJRCBiZW' + ` + 'xvbmdpbmcgdG8gdGhlIHByb2R1Y3RzIGluIHRoaXMgc2V0LlZlcnNpb25NaW5UaGUgbWluaW11bSBQcm9kdWN' + ` + '0VmVyc2lvbiBvZiB0aGUgcHJvZHVjdHMgaW4gdGhpcyBzZXQuICBUaGUgc2V0IG1heSBvciBtYXkgbm90IGlu' + ` + 'Y2x1ZGUgcHJvZHVjdHMgd2l0aCB0aGlzIHBhcnRpY3VsYXIgdmVyc2lvbi5WZXJzaW9uTWF4VGhlIG1heGltd' + ` + 'W0gUHJvZHVjdFZlcnNpb24gb2YgdGhlIHByb2R1Y3RzIGluIHRoaXMgc2V0LiAgVGhlIHNldCBtYXkgb3IgbW' + ` + 'F5IG5vdCBpbmNsdWRlIHByb2R1Y3RzIHdpdGggdGhpcyBwYXJ0aWN1bGFyIHZlcnNpb24uQSBjb21tYS1zZXB' + ` + 'hcmF0ZWQgbGlzdCBvZiBsYW5ndWFnZXMgZm9yIGVpdGhlciBwcm9kdWN0cyBpbiB0aGlzIHNldCBvciBwcm9k' + ` + 'dWN0cyBub3QgaW4gdGhpcyBzZXQuVGhlIGF0dHJpYnV0ZXMgb2YgdGhpcyBwcm9kdWN0IHNldC5SZW1vdmVUa' + ` + 'GUgbGlzdCBvZiBmZWF0dXJlcyB0byByZW1vdmUgd2hlbiB1bmluc3RhbGxpbmcgYSBwcm9kdWN0IGZyb20gdG' + ` + 'hpcyBzZXQuICBUaGUgZGVmYXVsdCBpcyAiQUxMIi5BY3Rpb25Qcm9wZXJ0eVRoZSBwcm9wZXJ0eSB0byBzZXQ' + ` + 'gd2hlbiBhIHByb2R1Y3QgaW4gdGhpcyBzZXQgaXMgZm91bmQuQ29zdEluaXRpYWxpemVGaWxlQ29zdENvc3RG' + ` + 'aW5hbGl6ZUluc3RhbGxWYWxpZGF0ZUluc3RhbGxJbml0aWFsaXplSW5zdGFsbEFkbWluUGFja2FnZUluc3Rhb' + ` + 'GxGaWxlc0luc3RhbGxGaW5hbGl6ZUV4ZWN1dGVBY3Rpb25QdWJsaXNoRmVhdHVyZXNQdWJsaXNoUHJvZHVjdF' + ` + 'Byb2R1Y3RDb21wb25lbnR7OTg5QjBFRDgtREVBRC01MjhELUI4RTMtN0NBRTQxODYyNEQ1fUlOU1RBTExGT0x' + ` + 'ERVJEdW1teUZsYWdWYWx1ZVByb2dyYW1GaWxlc0ZvbGRlcnE0cGZqNHo3fERTQ1NldHVwUHJvamVjdFRBUkdF' + ` + 'VERJUi5Tb3VyY2VEaXJQcm9kdWN0RmVhdHVyZURTQ1NldHVwUHJvamVjdEZpbmRSZWxhdGVkUHJvZHVjdHNMY' + ` + 'XVuY2hDb25kaXRpb25zVmFsaWRhdGVQcm9kdWN0SURNaWdyYXRlRmVhdHVyZVN0YXRlc1Byb2Nlc3NDb21wb2' + ` + '5lbnRzVW5wdWJsaXNoRmVhdHVyZXNSZW1vdmVSZWdpc3RyeVZhbHVlc1dyaXRlUmVnaXN0cnlWYWx1ZXNSZWd' + ` + 'pc3RlclVzZXJSZWdpc3RlclByb2R1Y3RSZW1vdmVFeGlzdGluZ1Byb2R1Y3RzTk9UIFdJWF9ET1dOR1JBREVf' + ` + 'REVURUNURURBIG5ld2VyIHZlcnNpb24gb2YgW1Byb2R1Y3ROYW1lXSBpcyBhbHJlYWR5IGluc3RhbGxlZC5BT' + ` + 'ExVU0VSUzFNYW51ZmFjdHVyZXJNaWNyb3NvZnQgVW5pdCBUZXN0aW5nIEd1aWxkIG9mIEFtZXJpY2FQcm9kdW' + ` + 'N0Q29kZXtERUFEQkVFRi04MEM2LTQxRTYtQTFCOS04QkRCOEEwNTAyN0Z9UHJvZHVjdExhbmd1YWdlMTAzM1B' + ` + 'yb2R1Y3ROYW1lRFNDVW5pdFRlc3RQYWNrYWdlUHJvZHVjdFZlcnNpb24xLjIuMy40ezgzQkMzNzkyLTgwQzYt' + ` + 'NDFFNi1BMUI5LThCREI4QTA1MDI3Rn1TZWN1cmVDdXN0b21Qcm9wZXJ0aWVzV0lYX0RPV05HUkFERV9ERVRFQ' + ` + '1RFRDtXSVhfVVBHUkFERV9ERVRFQ1RFRFdpeFBkYlBhdGhDOlxVc2Vyc1xiZWNhcnJcRG9jdW1lbnRzXFZpc3' + ` + 'VhbCBTdHVkaW8gMjAxMFxQcm9qZWN0c1xEU0NTZXR1cFByb2plY3RcRFNDU2V0dXBQcm9qZWN0XGJpblxEZWJ' + ` + '1Z1xEU0NTZXR1cFByb2plY3Qud2l4cGRiU29mdHdhcmVcRFNDVGVzdERlYnVnRW50cnlbfl1EVU1NWUZMQUc9' + ` + 'W0RVTU1ZRkxBR11bfl1XSVhfVVBHUkFERV9ERVRFQ1RFRFdJWF9ET1dOR1JBREVfREVURUNURURzZWQgdG8gZ' + ` + 'm9yY2UgYSBzcGVjaWZpYyBkaXNwbGF5IG9yZGVyaW5nLkxldmVsVGhlIGluc3RhbGwgbGV2ZWwgYXQgd2hpY2' + ` + 'ggcmVjb3JkIHdpbGwgYmUgaW5pdGlhbGx5IHNlbGVjdGVkLiBBbiBpbnN0YWxsIGxldmVsIG9mIDAgd2lsbCB' + ` + 'kaXNhYmxlIGFuIGl0ZW0gYW5kIHByZXZlbnQgaXRzIGRpc3BsYXkuVXBwZXJDYXNlVGhlIG5hbWUgb2YgdGhl' + ` + 'IERpcmVjdG9yeSB0aGF0IGNhbiBiZSBjb25maWd1cmVkIGJ5IHRoZSBVSS4gQSBub24tbnVsbCB2YWx1ZSB3a' + ` + 'WxsIGVuYWJsZSB0aGUgYnJvd3NlIGJ1dHRvbi4wOzE7Mjs0OzU7Njs4Ozk7MTA7MTY7MTc7MTg7MjA7MjE7Mj' + ` + 'I7MjQ7MjU7MjY7MzI7MzM7MzQ7MzY7Mzc7Mzg7NDg7NDk7NTA7NTI7NQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATmFtZVRhYmxlQ29sdW1uX1Zh' + ` + 'bGlkYXRpb25WYWx1ZU5Qcm9wZXJ0eUlkX1N1bW1hcnlJbmZvcm1hdGlvbkRlc2NyaXB0aW9uU2V0Q2F0ZWdvc' + ` + 'nlLZXlDb2x1bW5NYXhWYWx1ZU51bGxhYmxlS2V5VGFibGVNaW5WYWx1ZUlkZW50aWZpZXJOYW1lIG9mIHRhYm' + ` + 'xlTmFtZSBvZiBjb2x1bW5ZO05XaGV0aGVyIHRoZSBjb2x1bW4gaXMgbnVsbGFibGVZTWluaW11bSB2YWx1ZSB' + ` + 'hbGxvd2VkTWF4aW11bSB2YWx1ZSBhbGxvd2VkRm9yIGZvcmVpZ24ga2V5LCBOYW1lIG9mIHRhYmxlIHRvIHdo' + ` + 'aWNoIGRhdGEgbXVzdCBsaW5rQ29sdW1uIHRvIHdoaWNoIGZvcmVpZ24ga2V5IGNvbm5lY3RzVGV4dDtGb3JtY' + ` + 'XR0ZWQ7VGVtcGxhdGU7Q29uZGl0aW9uO0d1aWQ7UGF0aDtWZXJzaW9uO0xhbmd1YWdlO0lkZW50aWZpZXI7Qm' + ` + 'luYXJ5O1VwcGVyQ2FzZTtMb3dlckNhc2U7RmlsZW5hbWU7UGF0aHM7QW55UGF0aDtXaWxkQ2FyZEZpbGVuYW1' + ` + 'lO1JlZ1BhdGg7Q3VzdG9tU291cmNlO1Byb3BlcnR5O0NhYmluZXQ7U2hvcnRjdXQ7Rm9ybWF0dGVkU0RETFRl' + ` + 'eHQ7SW50ZWdlcjtEb3VibGVJbnRlZ2VyO1RpbWVEYXRlO0RlZmF1bHREaXJTdHJpbmcgY2F0ZWdvcnlUZXh0U' + ` + '2V0IG9mIHZhbHVlcyB0aGF0IGFyZSBwZXJtaXR0ZWREZXNjcmlwdGlvbiBvZiBjb2x1bW5BZG1pbkV4ZWN1dG' + ` + 'VTZXF1ZW5jZUFjdGlvbk5hbWUgb2YgYWN0aW9uIHRvIGludm9rZSwgZWl0aGVyIGluIHRoZSBlbmdpbmUgb3I' + ` + 'gdGhlIGhhbmRsZXIgRExMLkNvbmRpdGlvbk9wdGlvbmFsIGV4cHJlc3Npb24gd2hpY2ggc2tpcHMgdGhlIGFj' + ` + 'dGlvbiBpZiBldmFsdWF0ZXMgdG8gZXhwRmFsc2UuSWYgdGhlIGV4cHJlc3Npb24gc3ludGF4IGlzIGludmFsa' + ` + 'WQsIHRoZSBlbmdpbmUgd2lsbCB0ZXJtaW5hdGUsIHJldHVybmluZyBpZXNCYWRBY3Rpb25EYXRhLlNlcXVlbm' + ` + 'NlTnVtYmVyIHRoYXQgZGV0ZXJtaW5lcyB0aGUgc29ydCBvcmRlciBpbiB3aGljaCB0aGUgYWN0aW9ucyBhcmU' + ` + 'gdG8gYmUgZXhlY3V0ZWQuICBMZWF2ZSBibGFuayB0byBzdXBwcmVzcyBhY3Rpb24uQWRtaW5VSVNlcXVlbmNl' + ` + 'QWR2dEV4ZWN1dGVTZXF1ZW5jZUNvbXBvbmVudFByaW1hcnkga2V5IHVzZWQgdG8gaWRlbnRpZnkgYSBwYXJ0a' + ` + 'WN1bGFyIGNvbXBvbmVudCByZWNvcmQuQ29tcG9uZW50SWRHdWlkQSBzdHJpbmcgR1VJRCB1bmlxdWUgdG8gdG' + ` + 'hpcyBjb21wb25lbnQsIHZlcnNpb24sIGFuZCBsYW5ndWFnZS5EaXJlY3RvcnlfRGlyZWN0b3J5UmVxdWlyZWQ' + ` + 'ga2V5IG9mIGEgRGlyZWN0b3J5IHRhYmxlIHJlY29yZC4gVGhpcyBpcyBhY3R1YWxseSBhIHByb3BlcnR5IG5h' + ` + 'bWUgd2hvc2UgdmFsdWUgY29udGFpbnMgdGhlIGFjdHVhbCBwYXRoLCBzZXQgZWl0aGVyIGJ5IHRoZSBBcHBTZ' + ` + 'WFyY2ggYWN0aW9uIG9yIHdpdGggdGhlIGRlZmF1bHQgc2V0dGluZyBvYnRhaW5lZCBmcm9tIHRoZSBEaXJlY3' + ` + 'RvcnkgdGFibGUuQXR0cmlidXRlc1JlbW90ZSBleGVjdXRpb24gb3B0aW9uLCBvbmUgb2YgaXJzRW51bUEgY29' + ` + 'uZGl0aW9uYWwgc3RhdGVtZW50IHRoYXQgd2lsbCBkaXNhYmxlIHRoaXMgY29tcG9uZW50IGlmIHRoZSBzcGVj' + ` + 'aWZpZWQgY29uZGl0aW9uIGV2YWx1YXRlcyB0byB0aGUgJ1RydWUnIHN0YXRlLiBJZiBhIGNvbXBvbmVudCBpc' + ` + 'yBkaXNhYmxlZCwgaXQgd2lsbCBub3QgYmUgaW5zdGFsbGVkLCByZWdhcmRsZXNzIG9mIHRoZSAnQWN0aW9uJy' + ` + 'BzdGF0ZSBhc3NvY2lhdGVkIHdpdGggdGhlIGNvbXBvbmVudC5LZXlQYXRoRmlsZTtSZWdpc3RyeTtPREJDRGF' + ` + '0YVNvdXJjZUVpdGhlciB0aGUgcHJpbWFyeSBrZXkgaW50byB0aGUgRmlsZSB0YWJsZSwgUmVnaXN0cnkgdGFi' + ` + 'bGUsIG9yIE9EQkNEYXRhU291cmNlIHRhYmxlLiBUaGlzIGV4dHJhY3QgcGF0aCBpcyBzdG9yZWQgd2hlbiB0a' + ` + 'GUgY29tcG9uZW50IGlzIGluc3RhbGxlZCwgYW5kIGlzIHVzZWQgdG8gZGV0ZWN0IHRoZSBwcmVzZW5jZSBvZi' + ` + 'B0aGUgY29tcG9uZW50IGFuZCB0byByZXR1cm4gdGhlIHBhdGggdG8gaXQuVW5pcXVlIGlkZW50aWZpZXIgZm9' + ` + 'yIGRpcmVjdG9yeSBlbnRyeSwgcHJpbWFyeSBrZXkuIElmIGEgcHJvcGVydHkgYnkgdGhpcyBuYW1lIGlzIGRl' + ` + 'ZmluZWQsIGl0IGNvbnRhaW5zIHRoZSBmdWxsIHBhdGggdG8gdGhlIGRpcmVjdG9yeS5EaXJlY3RvcnlfUGFyZ' + ` + 'W50UmVmZXJlbmNlIHRvIHRoZSBlbnRyeSBpbiB0aGlzIHRhYmxlIHNwZWNpZnlpbmcgdGhlIGRlZmF1bHQgcG' + ` + 'FyZW50IGRpcmVjdG9yeS4gQSByZWNvcmQgcGFyZW50ZWQgdG8gaXRzZWxmIG9yIHdpdGggYSBOdWxsIHBhcmV' + ` + 'udCByZXByZXNlbnRzIGEgcm9vdCBvZiB0aGUgaW5zdGFsbCB0cmVlLkRlZmF1bHREaXJUaGUgZGVmYXVsdCBz' + ` + 'dWItcGF0aCB1bmRlciBwYXJlbnQncyBwYXRoLkZlYXR1cmVQcmltYXJ5IGtleSB1c2VkIHRvIGlkZW50aWZ5I' + ` + 'GEgcGFydGljdWxhciBmZWF0dXJlIHJlY29yZC5GZWF0dXJlX1BhcmVudE9wdGlvbmFsIGtleSBvZiBhIHBhcm' + ` + 'VudCByZWNvcmQgaW4gdGhlIHNhbWUgdGFibGUuIElmIHRoZSBwYXJlbnQgaXMgbm90IHNlbGVjdGVkLCB0aGV' + ` + 'uIHRoZSByZWNvcmQgd2lsbCBub3QgYmUgaW5zdGFsbGVkLiBOdWxsIGluZGljYXRlcyBhIHJvb3QgaXRlbS5U' + ` + 'aXRsZVNob3J0IHRleHQgaWRlbnRpZnlpbmcgYSB2aXNpYmxlIGZlYXR1cmUgaXRlbS5Mb25nZXIgZGVzY3Jpc' + ` + 'HRpdmUgdGV4dCBkZXNjcmliaW5nIGEgdmlzaWJsZSBmZWF0dXJlIGl0ZW0uRGlzcGxheU51bWVyaWMgc29ydC' + ` + 'BvcmRlciwgdXNlZCB0byBmb3JjZSBhIHNwZWNpZmljIGRpc3BsYXkgb3JkZXJpbmcuTGV2ZWxUaGUgaW5zdGF' + ` + 'sbCBsZXZlbCBhdCB3aGljaCByZWNvcmQgd2lsbCBiZSBpbml0aWFsbHkgc2VsZWN0ZWQuIEFuIGluc3RhbGwg' + ` + 'bGV2ZWwgb2YgMCB3aWxsIGRpc2FibGUgYW4gaXRlbSBhbmQgcHJldmVudCBpdHMgZGlzcGxheS5VcHBlckNhc' + ` + '2VUaGUgbmFtZSBvZiB0aGUgRGlyZWN0b3J5IHRoYXQgY2FuIGJlIGNvbmZpZ3VyZWQgYnkgdGhlIFVJLiBBIG' + ` + '5vbi1udWxsIHZhbHVlIHdpbGwgZW5hYmxlIHRoZSBicm93c2UgYnV0dG9uLjA7MTsyOzQ7NTs2Ozg7OTsxMDs' + ` + 'xNjsxNzsxODsyMDsyMTsyMjsyNDsyNTsyNjszMjszMzszNDszNjszNzszODs0ODs0OTs1MDs1Mjs1Mzs1NEZl' + ` + 'YXR1cmUgYXR0cmlidXRlc0ZlYXR1cmVDb21wb25lbnRzRmVhdHVyZV9Gb3JlaWduIGtleSBpbnRvIEZlYXR1c' + ` + 'mUgdGFibGUuQ29tcG9uZW50X0ZvcmVpZ24ga2V5IGludG8gQ29tcG9uZW50IHRhYmxlLkZpbGVQcmltYXJ5IG' + ` + 'tleSwgbm9uLWxvY2FsaXplZCB0b2tlbiwgbXVzdCBtYXRjaCBpZGVudGlmaWVyIGluIGNhYmluZXQuICBGb3I' + ` + 'gdW5jb21wcmVzc2VkIGZpbGVzLCB0aGlzIGZpZWxkIGlzIGlnbm9yZWQuRm9yZWlnbiBrZXkgcmVmZXJlbmNp' + ` + 'bmcgQ29tcG9uZW50IHRoYXQgY29udHJvbHMgdGhlIGZpbGUuRmlsZU5hbWVGaWxlbmFtZUZpbGUgbmFtZSB1c' + ` + '2VkIGZvciBpbnN0YWxsYXRpb24sIG1heSBiZSBsb2NhbGl6ZWQuICBUaGlzIG1heSBjb250YWluIGEgInNob3' + ` + 'J0IG5hbWV8bG9uZyBuYW1lIiBwYWlyLkZpbGVTaXplU2l6ZSBvZiBmaWxlIGluIGJ5dGVzIChsb25nIGludGV' + ` + 'nZXIpLlZlcnNpb25WZXJzaW9uIHN0cmluZyBmb3IgdmVyc2lvbmVkIGZpbGVzOyAgQmxhbmsgZm9yIHVudmVy' + ` + 'c2lvbmVkIGZpbGVzLkxhbmd1YWdlTGlzdCBvZiBkZWNpbWFsIGxhbmd1YWdlIElkcywgY29tbWEtc2VwYXJhd' + ` + 'GVkIGlmIG1vcmUgdGhhbiBvbmUuSW50ZWdlciBjb250YWluaW5nIGJpdCBmbGFncyByZXByZXNlbnRpbmcgZm' + ` + 'lsZSBhdHRyaWJ1dGVzICh3aXRoIHRoZSBkZWNpbWFsIHZhbHVlIG9mIGVhY2ggYml0IHBvc2l0aW9uIGluIHB' + ` + 'hcmVudGhlc2VzKVNlcXVlbmNlIHdpdGggcmVzcGVjdCB0byB0aGUgbWVkaWEgaW1hZ2VzOyBvcmRlciBtdXN0' + ` + 'IHRyYWNrIGNhYmluZXQgb3JkZXIuSW5zdGFsbEV4ZWN1dGVTZXF1ZW5jZUluc3RhbGxVSVNlcXVlbmNlTGF1b' + ` + 'mNoQ29uZGl0aW9uRXhwcmVzc2lvbiB3aGljaCBtdXN0IGV2YWx1YXRlIHRvIFRSVUUgaW4gb3JkZXIgZm9yIG' + ` + 'luc3RhbGwgdG8gY29tbWVuY2UuRm9ybWF0dGVkTG9jYWxpemFibGUgdGV4dCB0byBkaXNwbGF5IHdoZW4gY29' + ` + 'uZGl0aW9uIGZhaWxzIGFuZCBpbnN0YWxsIG11c3QgYWJvcnQuTWVkaWFEaXNrSWRQcmltYXJ5IGtleSwgaQgA' + ` + 'AgAIAAIACAACAAoAFgANAAEADgABAAMAAQAeAAEAAQAnABUAAQAVAAEANgABACQAAQD1AAEADwABAAQACQAgA' + ` + 'AEAFQABABQABwAGAAoAQgAFAAkAFQCfAAUACAAMAG8ABQAPAAcAEwAHAAkAEgA7AAEACwACAAQAAgA+AAEACg' + ` + 'AEAAkADADSAAEACgAIACcAAQDoAAEABwACABwAAQDjAAEAhgABABAAAgCmAAEACgADACkAAQAHABUAOQABAA4' + ` + 'AAgCUAAEABQACAC4AAQA6AAEABwACAD4AAQAFAAIAgQABAAkAAgBrAAEAUQABABIAAQARAAUACAACAB8AAQAK' + ` + 'AAYAIQABAAQAFABzAAEAOQABAAgAAgAIAAEAYwABAAgAAgAlAAEABwADAEEAAQAIAAYAPwABAHYAAQBKAAEAF' + ` + 'gAHABEABwAPAAUASAABAAkABABIAAEABQANAAYAAgA3AAEADAACADYAAQAKAAIAhAABAAcAAwBmAAEACwACAC' + ` + 'MAAQAGAAIACAAIADcAAQA+AAEAMAABAAgADwAhAAEABAACAD8AAQADAAIABwABAB8AAQAYAAEAEwABAG4AAQA' + ` + 'HAA8ACwADADsAAQAKAAIAfgABAAoAAgB+AAEAYAABACMAAQAGAAIAYAABAA4AAgA4AAEADgAFAAgABAAMAAUA' + ` + 'DwADABEAAwATAAEADAABAA8AAwANAAIADwACAA4AAgAQAAMAJgABAA0AAgAOAAIAEgACABgAAQAJAAIAAQABA' + ` + 'AkAAQAOAAIADwABABMAAgAQAAIAEQACABQAAgARAAEAEQABABQAAQATAAEADAABAA8AAQAWAAEAGgABADYAAQ' + ` + 'AIAAEAAQABAAwAAQAnAAEACwABACYAAQAPAAEABAABAAsAAQASAAEADgABAAcAAwAmAAMAFgABACsAAQAKAAE' + ` + 'AdgABABAAAQAKAAEAGwABABQAAQAWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + ` + 'AAAAAAAAAAAAAAAAAAA=' + #endregion + + $msiContentInBytes = [System.Convert]::FromBase64String($msiContentInBase64) + + Set-Content -Path $DestinationPath -Value $msiContentInBytes -Encoding 'Byte' | Out-Null +} + +Export-ModuleMember -Function ` + New-TestMsi, ` + Clear-xPackageCache, ` + Test-PackageInstalled diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 new file mode 100644 index 000000000..4800a1184 --- /dev/null +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -0,0 +1,377 @@ +$testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName 'xPSDesiredStateConfiguration' ` + -DSCResourceName 'MSFT_xPackageResource' ` + -TestType 'Unit' + +InModuleScope 'MSFT_xPackageResource' { + Describe 'MSFT_xPackageResource Unit Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force + + $testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + + New-Item -Path $testDirectoryPath -ItemType 'Directory' | Out-Null + + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $testDirectoryPath -ChildPath $script:msiName + + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + + New-TestMsi -DestinationPath $script:msiLocation | Out-Null + + Clear-xPackageCache | Out-Null + } + + BeforeEach { + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalled -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$PackageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalled -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } + } + + AfterAll { + Remove-Item -Path $testDirectoryPath -Recurse | Out-Null + } + + Context 'Test-TargetResource' { + It 'Should return correct value when package is absent' { + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $true + } + + It 'Should return correct value when package is present' -Pending { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([string]::Empty) + + Clear-xPackageCache + + if (-not (Test-PackageInstalled -Name $script:packageName)) + { + throw 'Failed to install the package' + } + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) + + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) + + $testTargetResourceResult | Should Be $false + } + } + + Context 'Set-TargetResource' { + It 'Should correctly install and remove a package' -Pending { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + Test-PackageInstalled -Name $script:packageName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + $getTargetResourceResult.Version | Should Be '1.2.3.4' + $getTargetResourceResult.InstalledOn | Should Be ("{0:d}" -f [DateTime]::Now.Date) + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $script:packageId + $getTargetResourceResult.Path | Should Be $script:msiLocation + + # Can't figure out how to set this within the MSI. + # $getTargetResourceResult.PackageDescription | Should Be 'A package for unit testing' + + [Math]::Round($getTargetResourceResult.Size, 2) | Should Be 0.03 + + Set-TargetResource -Ensure 'Absent' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + + Test-PackageInstalled -Name $script:packageName | Should Be $false + } + + It 'Should throw with incorrect product id' -Pending { + $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' + + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $wrongPackageId -Name ([String]::Empty) } | Should Throw + } + + It 'Should throw with incorrect name' -Pending { + $wrongPackageName = 'WrongPackageName' + + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId ([String]::Empty) -Name $wrongPackageName } | Should Throw + } + + It 'Should correctly install and remove a package from a URL' -Pending { #-Skip:(-not $script:shouldRun -or (-not (EnablePullServerTests)) -or (-not (RunOnServerSEDomainJoinedMachine)) -or (-not (IsWin8orAbove))) { + Invoke-Remotely { + $baseUrl = "http://localhost:1242/" + $msiUrl = "$baseUrl" + "package.msi" + Serve-Binary $MsiLocation + + # test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $PackageName -ProductId $PackageId } | Should Throw + + Set-TargetResource -Ensure 'Present' -Path $url -Name $PackageName -ProductId $PackageId -Verbose + if (-not (Is-NameInstalled $PackageName)) + { + throw "Failed to install the package" + } + + Set-TargetResource -Ensure 'Absent' -Path $url -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $false + + $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() + } + } + + It 'TestHttpsMsiInstallAndRemoval' -Pending { # -Skip:(-not $script:shouldRun -or (-not (EnablePullServerTests)) -or (-not (RunOnServerSEDomainJoinedMachine)) -or (-not (IsWin8orAbove))) { + Invoke-Remotely { + $baseUrl = "https://localhost:1243/" + $url = "$baseUrl" + "package.msi" + Serve-Binary $MsiLocation + + # test pipe connection as testing server readiness + $pipe = New-Object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\dsctest1") + $pipe.WaitForConnection() + $pipe.Dispose() + + $error = $null + try + { + Set-TargetResource -Ensure "Present" -Path $baseUrl -Name $PackageName -ProductId $PackageId -Verbose + } + catch { $error = $_} + + if (-not $error) + { + throw "Expected an error when trying to specify a non-msi file over HTTP" + } + + Set-TargetResource -Ensure "Present" -Path $url -Name $PackageName -ProductId $PackageId -Verbose + if (-not (Is-NameInstalled $PackageName)) + { + throw "Failed to install the package" + } + + Set-TargetResource -Ensure "Absent" -Path $url -Name $PackageName -ProductId $PackageId -Verbose + if (Is-NameInstalled $PackageName) + { + throw "Failed to uninstall the package" + } + + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\dsctest2") + $pipe.Connect() + $pipe.Dispose() + } + } + + It 'TestMSILoggingFunctionality' -Pending { + Invoke-Remotely { + param ($currentDir) + $logPath = "$currentDir\TestMsiLog.txt" + Set-TargetResource -Ensure "Present" -Path $MsiLocation -Name $PackageName -LogPath $logPath -Verbose -ProductId ([string]::Empty) + if(-not (Test-Path $logPath) -or -not (Get-Content $logPath)) + { + throw "Log was not properly written by the MSI - provider likely did not redirect" + } + } -ArgumentList $PSScriptRoot + } + } + + # TestCase TestLocalSetupExeInstall -tags @("DRT") { + # $exePath = Get-SetupExe + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is installed when it is not" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is installed when it is not" + # } + + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is installed when it is not when queried by ID" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is installed when it is not when queried by ID" + # } + + # $logPath = "$PSScriptRoot\TestLocalSetupExeInstall.log" + # Set-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Arguments "DUMMYFLAG=MYEXEVALUE" -LogPath $logPath -Verbose + + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is missing when it is not" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is missing when it is not" + # } + + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is missing when it is not when queried by ID" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is missing when it is not when queried by ID" + # } + + # $content = Get-Content $logPath + # if(-not $content -or -not $content.Contains("DUMMYFLAG=MYEXEVALUE")) + # { + # throw "Process output not appropriately captured - the expected data was not present" + # } + + # #Unit tests can be run on x86 Client SKU + # $item = Get-Item -EA Ignore HKLM:\SOFTWARE\DSCTest + # if(-not $item) + # { + # $item = Get-Item HKLM:\SOFTWARE\Wow6432Node\DSCTest + # } + + # $debugEntry = $item.GetValue("DebugEntry") + # if($debugEntry -ne "DUMMYFLAG=MYEXEVALUE") + # { + # throw "The registry key created by the package does not have the flag set appropriately. The provider likely did not pass the arguments correctly" + # } + # } + + # TestCase TestMSIOverUncPath -tags @("DRT") { + # $share = Share-ScriptFolder + # $shareName = $share.Name + # $sharePath = "\\localhost\$shareName" + # $uncMsiPath = Join-Path $sharePath $MsiName + + # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously belive package already exists when accessed over UNC" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously belive package already exists when accessed over UNC (Ensure=Absent case)" + # } + + # Set-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not (Is-NameInstalled $PackageName)) + # { + # throw "Failed to install the package" + # } + + # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously belive package is missing when accessed over UNC" + # } + + # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously belive package is missing when accessed over UNC (Ensure=Absent case)" + # } + # } + + Context 'Get-MsiTools' { + It 'Uses Add-Type with a name that does not conflict with the original Package resource' -Pending { + $hash = @{ Namespace = 'Mock not called' } + Mock Add-Type { $hash['Namespace'] = $Namespace } + $null = Get-MsiTools + + $hash['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' + } + } + + Context 'Get-RegistryValueIgnoreError' { + It 'Should get values from HKLM' -Pending { + $installValue = Get-RegistryValueIgnoreError 'LocalMachine' "SOFTWARE\Microsoft\Windows\CurrentVersion" "ProgramFilesDir" Registry64 + $installValue | should be $env:programfiles + } + + It 'Should get values from HKCU' -Pending { + $installValue = Get-RegistryValueIgnoreError 'CurrentUser' "Environment" "Temp" Registry64 + $installValue.length -gt 3 | should be $true + $installValue | should match $env:username + # comparing $installValue with $env:temp may fail if the username is longer than 8 characters + } + } + } +} From 9c519d6d76cdcc4021e6dbb0fec35ea7289ce03b Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 5 Jul 2016 14:57:20 -0700 Subject: [PATCH 03/49] Removing os specification and chocolately from appveyor.yml. --- appveyor.yml | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f6ce89caf..c9a007eb8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,28 +1,15 @@ #---------------------------------# # environment configuration # #---------------------------------# -os: WMF 5 version: 3.12.{build}.0 install: - git clone https://github.com/PowerShell/DscResource.Tests - - ps: Push-Location - - cd DscResource.Tests - - ps: Import-Module .\TestHelper.psm1 -force - - ps: Pop-Location - ps: | - Get-PackageProvider -Name nuget -ForceBootstrap -Force - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - (Get-date).AddHours(3).AddMinutes(-5) | Export-Clixml -Path ..\BuildTimeout.xml - $installed = $false - $retries = 1 - while($retries -lt 10 -and !$installed) - { - Write-Verbose -message "Attempting to install Pester, attempt # $retries" - $pesterModule = @(find-module 'Pester' -Repository PSGallery -ErrorAction SilentlyContinue) - Install-Module -InputObject $pesterModule -Force - $installed = ($LASTEXITCODE -eq 0) - $retries++ - } + Push-Location -Path .\DscResource.Tests + Import-Module -Name .\TestHelper.psm1 -Force + Pop-Location + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name Pester -Repository PSGallery -Force #---------------------------------# # build configuration # From 32270766add1a7a92e1b5f42087a2156a81486fb Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 5 Jul 2016 15:04:15 -0700 Subject: [PATCH 04/49] Updating README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 07eda624b..685d18ad5 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,8 @@ These parameters will be the same for each Windows optional feature in the set. * xGroup: Fix Verbose output in Get-MembersAsPrincipals function. Fix bug when credential parameter passed does not contain local or domain context. +* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* Updated appveyor.yml to use the default image. ### 3.12.0.0 From 5b035ba31079aa59dc934706ecc61bf7a6b2293e Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 5 Jul 2016 15:27:50 -0700 Subject: [PATCH 05/49] Removing unnecessary location changes. --- appveyor.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c9a007eb8..3be1cbe06 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,9 +5,7 @@ version: 3.12.{build}.0 install: - git clone https://github.com/PowerShell/DscResource.Tests - ps: | - Push-Location -Path .\DscResource.Tests - Import-Module -Name .\TestHelper.psm1 -Force - Pop-Location + Import-Module -Name .\DscResource.Tests\TestHelper.psm1 -Force Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force Install-Module -Name Pester -Repository PSGallery -Force From 84f63ed566e884ec8196e3532eed9a87a6ffce7c Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Wed, 6 Jul 2016 17:12:29 -0700 Subject: [PATCH 06/49] Updating xPackage tests. --- .../MSFT_xPackageResource.TestHelper.psm1 | 68 ++-- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 367 +++++++++--------- 2 files changed, 218 insertions(+), 217 deletions(-) diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 index 19af22ec4..bcf0decd7 100644 --- a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -970,7 +970,7 @@ Function Get-InstalledById .SYNOPSIS Mimics a simple http/https server. #> -function Serve-Binary +function New-MockFileServer { [CmdletBinding()] param @@ -978,65 +978,66 @@ function Serve-Binary [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] - $FileName + $FilePath ) - if (-not (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) + if ($null -eq (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) { - New-NetFirewallRule -DisplayName “UnitTestRule” -Direction Inbound -Program "$PSHome\powershell.exe" -Authentication NotRequired -Action Allow + New-NetFirewallRule -DisplayName 'UnitTestRule' -Direction 'Inbound' -Program "$PSHome\powershell.exe" -Authentication 'NotRequired' -Action 'Allow' } netsh advfirewall set allprofiles state off - Start-Job -ArgumentList $FileName -ScriptBlock { + Start-Job -ArgumentList @( $FilePath ) -ScriptBlock { # Create certificate - $cert = Get-ChildItem -Recurse Cert:\LocalMachine\My |Where-Object{$_.EnhancedKeyUsageList.FriendlyName -eq 'Server Authentication'} + $certificate = Get-ChildItem -Path 'Cert:\LocalMachine\My' -Recurse | Where-Object { $_.EnhancedKeyUsageList.FriendlyName -eq 'Server Authentication' } - if ($cert.count -gt 1) + if ($certificate.Count -gt 1) { - # just use the first one - $cert = $cert[0] + # Just use the first one + $certificate = $certificate[0] } - elseif ($cert.count -eq 0) + elseif ($certificate.count -eq 0) { - # create a self-signed one - $cert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -DnsName $env:COMPUTERNAME + # Create a self-signed one + $certificate = New-SelfSignedCertificate -CertStoreLocation 'Cert:\LocalMachine\My' -DnsName $env:computerName } - $hash = $cert.Thumbprint - # use net shell command to directly bind certificate to designated testing port + $hash = $certificate.Thumbprint + + # Use net shell command to directly bind certificate to designated testing port netsh http add sslcert ipport=0.0.0.0:1243 certhash=$hash appid='{833f13c2-319a-4799-9d1a-5b267a0c3593}' clientcertnegotiation=enable - # start listening endpoints - $httpListener = New-Object System.Net.HttpListener - $httpListener.Prefixes.Add([uri]"https://localhost:1243/") - $httpListener.Prefixes.Add([uri]"http://localhost:1242") + # Start listening endpoints + $httpListener = New-Object -TypeName 'System.Net.HttpListener' + $httpListener.Prefixes.Add([Uri]'https://localhost:1243/') + $httpListener.Prefixes.Add([Uri]'http://localhost:1242') $httpListener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Negotiate $httpListener.Start() - # create a pipe to flag http/https client - $pipe = New-Object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\dsctest1") + # Create a pipe to flag http/https client + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest1' ) $pipe.Connect() $pipe.Dispose() - # prepare binary buffer for http/https response - $fileInfo = New-Object System.IO.FileInfo $args[0] - $numBytes = $fileInfo.Length; - $fileStream = New-Object System.IO.FileStream $args[0], 'Open' - $binaryReader = New-Object System.IO.BinaryReader $fileStream - [byte[]] $buf = $binaryReader.ReadBytes($numBytes) + # Prepare binary buffer for http/https response + $fileInfo = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList @( $args[0] ) + $numBytes = $fileInfo.Length + $fileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $args[0], 'Open' ) + $binaryReader = New-Object -TypeName 'System.IO.BinaryReader' -ArgumentList @( $fileStream ) + [Byte[]] $buf = $binaryReader.ReadBytes($numBytes) $fileStream.Close() - # send response + # Send response $response = ($httpListener.GetContext()).Response - $response.ContentType = "application/octet-stream" + $response.ContentType = 'application/octet-stream' $response.ContentLength64 = $buf.Length $response.OutputStream.Write($buf, 0, $buf.Length) $response.OutputStream.Flush() - # wait for client to finish downloading - $pipe = New-Object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\dsctest2") + # Wait for client to finish downloading + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest2' ) $pipe.WaitForConnection() $pipe.Dispose() @@ -1044,9 +1045,9 @@ function Serve-Binary $httpListener.Stop() $httpListener.Close() - # close pipe + # Close pipe - # use net shell command to clean up the certificate binding + # Use net shell command to clean up the certificate binding netsh http delete sslcert ipport=0.0.0.0:1243 } @@ -1667,4 +1668,5 @@ function New-TestMsi Export-ModuleMember -Function ` New-TestMsi, ` Clear-xPackageCache, ` - Test-PackageInstalled + Test-PackageInstalled, ` + New-MockFileServer diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 4800a1184..8d6e92089 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -9,12 +9,12 @@ InModuleScope 'MSFT_xPackageResource' { Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force - $testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' - New-Item -Path $testDirectoryPath -ItemType 'Directory' | Out-Null + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null $script:msiName = 'DSCSetupProject.msi' - $script:msiLocation = Join-Path -Path $testDirectoryPath -ChildPath $script:msiName + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName $script:packageName = 'DSCUnitTestPackage' $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' @@ -40,7 +40,20 @@ InModuleScope 'MSFT_xPackageResource' { } AfterAll { - Remove-Item -Path $testDirectoryPath -Recurse | Out-Null + Remove-Item -Path $script:testDirectoryPath -Recurse | Out-Null + + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalled -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$PackageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalled -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } } Context 'Test-TargetResource' { @@ -78,7 +91,7 @@ InModuleScope 'MSFT_xPackageResource' { $testTargetResourceResult | Should Be $true } - It 'Should return correct value when package is present' -Pending { + It 'Should return correct value when package is present' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([string]::Empty) Clear-xPackageCache @@ -123,7 +136,7 @@ InModuleScope 'MSFT_xPackageResource' { } Context 'Set-TargetResource' { - It 'Should correctly install and remove a package' -Pending { + It 'Should correctly install and remove a package' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) Test-PackageInstalled -Name $script:packageName | Should Be $true @@ -146,231 +159,217 @@ InModuleScope 'MSFT_xPackageResource' { Test-PackageInstalled -Name $script:packageName | Should Be $false } - It 'Should throw with incorrect product id' -Pending { + It 'Should throw with incorrect product id' { $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $wrongPackageId -Name ([String]::Empty) } | Should Throw } - It 'Should throw with incorrect name' -Pending { + It 'Should throw with incorrect name' { $wrongPackageName = 'WrongPackageName' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId ([String]::Empty) -Name $wrongPackageName } | Should Throw } - It 'Should correctly install and remove a package from a URL' -Pending { #-Skip:(-not $script:shouldRun -or (-not (EnablePullServerTests)) -or (-not (RunOnServerSEDomainJoinedMachine)) -or (-not (IsWin8orAbove))) { - Invoke-Remotely { - $baseUrl = "http://localhost:1242/" - $msiUrl = "$baseUrl" + "package.msi" - Serve-Binary $MsiLocation + It 'Should correctly install and remove a package from a HTTP URL' { + $baseUrl = 'http://localhost:1242/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation - # test pipe connection as testing server readiness - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.WaitForConnection() - $pipe.Dispose() + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() - { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $PackageName -ProductId $PackageId } | Should Throw + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw - Set-TargetResource -Ensure 'Present' -Path $url -Name $PackageName -ProductId $PackageId -Verbose - if (-not (Is-NameInstalled $PackageName)) - { - throw "Failed to install the package" - } + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $true - Set-TargetResource -Ensure 'Absent' -Path $url -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $false + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $false - $pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.Connect() - $pipe.Dispose() - } + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() } - It 'TestHttpsMsiInstallAndRemoval' -Pending { # -Skip:(-not $script:shouldRun -or (-not (EnablePullServerTests)) -or (-not (RunOnServerSEDomainJoinedMachine)) -or (-not (IsWin8orAbove))) { - Invoke-Remotely { - $baseUrl = "https://localhost:1243/" - $url = "$baseUrl" + "package.msi" - Serve-Binary $MsiLocation - - # test pipe connection as testing server readiness - $pipe = New-Object System.IO.Pipes.NamedPipeServerStream("\\.\pipe\dsctest1") - $pipe.WaitForConnection() - $pipe.Dispose() - - $error = $null - try - { - Set-TargetResource -Ensure "Present" -Path $baseUrl -Name $PackageName -ProductId $PackageId -Verbose - } - catch { $error = $_} - - if (-not $error) - { - throw "Expected an error when trying to specify a non-msi file over HTTP" - } - - Set-TargetResource -Ensure "Present" -Path $url -Name $PackageName -ProductId $PackageId -Verbose - if (-not (Is-NameInstalled $PackageName)) - { - throw "Failed to install the package" - } - - Set-TargetResource -Ensure "Absent" -Path $url -Name $PackageName -ProductId $PackageId -Verbose - if (Is-NameInstalled $PackageName) - { - throw "Failed to uninstall the package" - } - - $pipe = New-Object System.IO.Pipes.NamedPipeClientStream("\\.\pipe\dsctest2") - $pipe.Connect() - $pipe.Dispose() - } + It 'Should correctly install and remove a package from a HTTPS URL' -Pending { + $baseUrl = 'https://localhost:1243/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation + + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() + + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw + + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $true + + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $false + + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() } - It 'TestMSILoggingFunctionality' -Pending { - Invoke-Remotely { - param ($currentDir) - $logPath = "$currentDir\TestMsiLog.txt" - Set-TargetResource -Ensure "Present" -Path $MsiLocation -Name $PackageName -LogPath $logPath -Verbose -ProductId ([string]::Empty) - if(-not (Test-Path $logPath) -or -not (Get-Content $logPath)) - { - throw "Log was not properly written by the MSI - provider likely did not redirect" - } - } -ArgumentList $PSScriptRoot + It 'Should write to the specified log path' { + $logPath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestMsiLog.txt' + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -Name $script:packageName -LogPath $logPath -ProductId ([string]::Empty) + + Test-Path -Path $logPath | Should Be $true + Get-Content -Path $logPath | Should Not Be $null } } - - # TestCase TestLocalSetupExeInstall -tags @("DRT") { - # $exePath = Get-SetupExe - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is installed when it is not" - # } + + Context 'Test-TargetResource' { + It 'TestLocalSetupExeInstall' -Pending { + # $exePath = Get-SetupExe + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is installed when it is not" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is installed when it is not" - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is installed when it is not" + # } - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is installed when it is not when queried by ID" - # } + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is installed when it is not when queried by ID" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is installed when it is not when queried by ID" - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is installed when it is not when queried by ID" + # } - # $logPath = "$PSScriptRoot\TestLocalSetupExeInstall.log" - # Set-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Arguments "DUMMYFLAG=MYEXEVALUE" -LogPath $logPath -Verbose + # $logPath = "$PSScriptRoot\TestLocalSetupExeInstall.log" + # Set-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Arguments "DUMMYFLAG=MYEXEVALUE" -LogPath $logPath -Verbose - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is missing when it is not" - # } + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is missing when it is not" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is missing when it is not" - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is missing when it is not" + # } - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is missing when it is not when queried by ID" - # } + # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose + # if(-not $res) + # { + # throw "Erroneously believe EXE is missing when it is not when queried by ID" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is missing when it is not when queried by ID" - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose + # if($res) + # { + # throw "Erroneously believe EXE is missing when it is not when queried by ID" + # } - # $content = Get-Content $logPath - # if(-not $content -or -not $content.Contains("DUMMYFLAG=MYEXEVALUE")) - # { - # throw "Process output not appropriately captured - the expected data was not present" - # } + # $content = Get-Content $logPath + # if(-not $content -or -not $content.Contains("DUMMYFLAG=MYEXEVALUE")) + # { + # throw "Process output not appropriately captured - the expected data was not present" + # } - # #Unit tests can be run on x86 Client SKU - # $item = Get-Item -EA Ignore HKLM:\SOFTWARE\DSCTest - # if(-not $item) - # { - # $item = Get-Item HKLM:\SOFTWARE\Wow6432Node\DSCTest - # } + # #Unit tests can be run on x86 Client SKU + # $item = Get-Item -EA Ignore HKLM:\SOFTWARE\DSCTest + # if(-not $item) + # { + # $item = Get-Item HKLM:\SOFTWARE\Wow6432Node\DSCTest + # } - # $debugEntry = $item.GetValue("DebugEntry") - # if($debugEntry -ne "DUMMYFLAG=MYEXEVALUE") - # { - # throw "The registry key created by the package does not have the flag set appropriately. The provider likely did not pass the arguments correctly" - # } - # } + # $debugEntry = $item.GetValue("DebugEntry") + # if($debugEntry -ne "DUMMYFLAG=MYEXEVALUE") + # { + # throw "The registry key created by the package does not have the flag set appropriately. The provider likely did not pass the arguments correctly" + # } + } - # TestCase TestMSIOverUncPath -tags @("DRT") { - # $share = Share-ScriptFolder - # $shareName = $share.Name - # $sharePath = "\\localhost\$shareName" - # $uncMsiPath = Join-Path $sharePath $MsiName + It 'TestMSIOverUncPath' -Pending { + # $share = Share-ScriptFolder + # $shareName = $share.Name + # $sharePath = "\\localhost\$shareName" + # $uncMsiPath = Join-Path $sharePath $MsiName - # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously belive package already exists when accessed over UNC" - # } + # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously belive package already exists when accessed over UNC" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously belive package already exists when accessed over UNC (Ensure=Absent case)" - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously belive package already exists when accessed over UNC (Ensure=Absent case)" + # } - # Set-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not (Is-NameInstalled $PackageName)) - # { - # throw "Failed to install the package" - # } + # Set-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not (Is-NameInstalled $PackageName)) + # { + # throw "Failed to install the package" + # } - # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously belive package is missing when accessed over UNC" - # } + # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose + # if(-not $res) + # { + # throw "Erroneously belive package is missing when accessed over UNC" + # } - # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously belive package is missing when accessed over UNC (Ensure=Absent case)" - # } - # } + # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose + # if($res) + # { + # throw "Erroneously belive package is missing when accessed over UNC (Ensure=Absent case)" + # } + } + } Context 'Get-MsiTools' { - It 'Uses Add-Type with a name that does not conflict with the original Package resource' -Pending { - $hash = @{ Namespace = 'Mock not called' } - Mock Add-Type { $hash['Namespace'] = $Namespace } - $null = Get-MsiTools + It 'Should add MSI tools in the Microsoft.Windows.DesiredStateConfiguration.xPackageResource namespace' { + $addTypeResult = @{ Namespace = 'Mock not called' } + Mock Add-Type { $addTypeResult['Namespace'] = $Namespace } + + Get-MsiTools | Out-Null - $hash['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' + $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' } } Context 'Get-RegistryValueIgnoreError' { - It 'Should get values from HKLM' -Pending { - $installValue = Get-RegistryValueIgnoreError 'LocalMachine' "SOFTWARE\Microsoft\Windows\CurrentVersion" "ProgramFilesDir" Registry64 - $installValue | should be $env:programfiles + It 'Should retrieve the correct value from the HKLM registry' { + $registryValue = Get-RegistryValueIgnoreError ` + -RegistryHive 'LocalMachine' ` + -Key 'SOFTWARE\Microsoft\Windows\CurrentVersion' ` + -Value 'ProgramFilesDir' ` + -RegistryView 'Registry64' + + $registryValue | Should Be $env:programFiles } - It 'Should get values from HKCU' -Pending { - $installValue = Get-RegistryValueIgnoreError 'CurrentUser' "Environment" "Temp" Registry64 - $installValue.length -gt 3 | should be $true - $installValue | should match $env:username - # comparing $installValue with $env:temp may fail if the username is longer than 8 characters + It 'Should retrieve the correct value from the HKCU registry' { + $registryValue = Get-RegistryValueIgnoreError ` + -RegistryHive 'CurrentUser' ` + -Key 'Environment' ` + -Value 'Temp' ` + -RegistryView 'Registry64' + + # Comparing $installValue with $env:temp may fail if the username is longer than 8 characters + $registryValue.Length -gt 3 | Should Be $true + $registryValue | Should Match $env:username } } } From 911c72c806ad7f8202f32c8e4abd9a6927667979 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 15:45:22 -0700 Subject: [PATCH 07/49] Merging Package and updating tests. --- DSCResources/CommonResourceHelper.psm1 | 4 +- .../MSFT_xPackageResource.psm1 | 1623 +++++++---------- Tests/CommonTestHelper.psm1 | 50 +- .../MSFT_xPackageResource.TestHelper.psm1 | 1013 +--------- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 199 +- 5 files changed, 827 insertions(+), 2062 deletions(-) diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 544b9127b..39dc0b203 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -86,5 +86,5 @@ function New-InvalidOperationException Export-ModuleMember -Function ` Test-IsNanoServer, ` - Throw-InvalidArgumentException, ` - Throw-TerminatingError + New-InvalidArgumentException, ` + New-InvalidOperationException diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 1af9ea0ec..97629385a 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -1,562 +1,465 @@ data LocalizedData { - # culture="en-US" + # culture='en-US' # TODO: Support WhatIf - ConvertFrom-StringData @' -InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid -InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP -InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file -NeedsMoreInfo=Either Name or ProductId is required -InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported -CouldNotOpenLog=The specified LogPath ({0}) could not be opened -CouldNotStartProcess=The process {0} could not be started -UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct -PathDoesNotExist=The given Path ({0}) could not be found -CouldNotOpenDestFile=Could not open the file {0} for writing -CouldNotGetHttpStream=Could not get the {0} stream for file {1} -ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1} -PackageConfigurationComplete=Package configuration finished -PackageConfigurationStarting=Package configuration starting -InstalledPackage=Installed package -UninstalledPackage=Uninstalled package -NoChangeRequired=Package found in desired state, no action required -RemoveExistingLogFile=Remove existing log file -CreateLogFile=Create log file -MountSharePath=Mount share to get media -DownloadHTTPFile=Download the media over HTTP or HTTPS -StartingProcessMessage=Starting process {0} with arguments {1} -RemoveDownloadedFile=Remove the downloaded file -PackageInstalled=Package has been installed -PackageUninstalled=Package has been uninstalled -MachineRequiresReboot=The machine requires a reboot -PackageDoesNotAppearInstalled=The package {0} is not installed -PackageAppearsInstalled=The package {0} is already installed -PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details -CheckingFileHash=Checking file '{0}' for expected {2} hash value of {1} -InvalidFileHash=File '{0}' does not match expected {2} hash value of {1}. -CheckingFileSignature=Checking file '{0}' for valid digital signature. -FileHasValidSignature=File '{0}' contains a valid digital signature. Signer Thumbprint: {1}, Subject: {2} -InvalidFileSignature=File '{0}' does not have a valid Authenticode signature. Status: {1} -WrongSignerSubject=File '{0}' was not signed by expected signer subject '{1}' -WrongSignerThumbprint=File '{0}' was not signed by expected signer certificate thumbprint '{1}' -CreatingRegistryValue=Creating package registry value of {0}. -RemovingRegistryValue=Removing package registry value of {0}. + ConvertFrom-StringData -StringData @' +InvalidIdentifyingNumber = The specified IdentifyingNumber ({0}) is not a valid Guid +InvalidPath = The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP +InvalidNameOrId = The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file +NeedsMoreInfo = Either Name or ProductId is required +InvalidBinaryType = The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported +CouldNotOpenLog = The specified LogPath ({0}) could not be opened +CouldNotStartProcess = The process {0} could not be started +UnexpectedReturnCode = The return code {0} was not expected. Configuration is likely not correct +PathDoesNotExist = The given Path ({0}) could not be found +CouldNotOpenDestFile = Could not open the file {0} for writing +CouldNotGetHttpStream = Could not get the {0} stream for file {1} +ErrorCopyingDataToFile = Encountered error while writing the contents of {0} to {1} +PackageConfigurationComplete = Package configuration finished +PackageConfigurationStarting = Package configuration starting +InstalledPackage = Installed package +UninstalledPackage = Uninstalled package +NoChangeRequired = Package found in desired state, no action required +RemoveExistingLogFile = Remove existing log file +CreateLogFile = Create log file +MountSharePath = Mount share to get media +DownloadHTTPFile = Download the media over HTTP or HTTPS +StartingProcessMessage = Starting process {0} with arguments {1} +RemoveDownloadedFile = Remove the downloaded file +PackageInstalled = Package has been installed +PackageUninstalled = Package has been uninstalled +MachineRequiresReboot = The machine requires a reboot +PackageDoesNotAppearInstalled = The package {0} is not installed +PackageAppearsInstalled = The package {0} is installed +PostValidationError = Package from {0} was installed, but the specified ProductId and/or Name does not match package details +CheckingFileHash = Checking file '{0}' for expected {2} hash value of {1} +InvalidFileHash = File '{0}' does not match expected {2} hash value of {1}. +CheckingFileSignature = Checking file '{0}' for valid digital signature. +FileHasValidSignature = File '{0}' contains a valid digital signature. Signer Thumbprint: {1}, Subject: {2} +InvalidFileSignature = File '{0}' does not have a valid Authenticode signature. Status: {1} +WrongSignerSubject = File '{0}' was not signed by expected signer subject '{1}' +WrongSignerThumbprint = File '{0}' was not signed by expected signer certificate thumbprint '{1}' +CreatingRegistryValue = Creating package registry value of {0}. +RemovingRegistryValue = Removing package registry value of {0}. +ValidateStandardArgumentsPathwasPath = Validate-StandardArguments, Path was {0} +TheurischemewasuriScheme = The uri scheme was {0} +ThepathextensionwaspathExt = The path extension was {0} +ParsingProductIdasanidentifyingNumber = Parsing {0} as an identifyingNumber +ParsedProductIdasidentifyingNumber = Parsed {0} as {1} +EnsureisEnsure = Ensure is {0} +productisproduct = product {0} found +productasbooleanis = product as boolean is {0} +Creatingcachelocation = Creating cache location +NeedtodownloadfilefromschemedestinationwillbedestName = Need to download file from {0}, destination will be {1} +Creatingthedestinationcachefile = Creating the destination cache file +Creatingtheschemestream = Creating the {0} stream +Settingdefaultcredential = Setting default credential +Settingauthenticationlevel = Setting authentication level +Ignoringbadcertificates = Ignoring bad certificates +Gettingtheschemeresponsestream = Getting the {0} response stream +ErrorOutString = Error: {0} +Copyingtheschemestreambytestothediskcache = Copying the {0} stream bytes to the disk cache +Redirectingpackagepathtocachefilelocation = Redirecting package path to cache file location +ThebinaryisanEXE = The binary is an EXE +Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process +StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1} '@ } -$Debug = $true -Function Trace-Message -{ - param([string] $Message) - if($Debug) - { - Write-Verbose $Message - } -} - -$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xPackageResource" - -Function Throw-InvalidArgumentException -{ - param( - [string] $Message, - [string] $ParamName - ) - - $exception = new-object System.ArgumentException $Message,$ParamName - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null - throw $errorRecord -} - -Function Throw-InvalidNameOrIdException -{ - param( - [string] $Message - ) +# Commented-out until more languages are supported +# Import-LocalizedData -BindingVariable 'LocalizedData' -FileName 'MSFT_xPackageResource.strings.psd1' - $exception = new-object System.ArgumentException $Message - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null - throw $errorRecord -} +Import-Module -Name "$PSScriptRoot\..\CommonResourceHelper.psm1" -Force -Function Throw-TerminatingError -{ - param( - [string] $Message, - [System.Management.Automation.ErrorRecord] $ErrorRecord - ) +$script:packageCacheLocation = "$env:programData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_xPackageResource" +$script:msiTools = $null - $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception - $errorRecord = New-Object System.Management.Automation.ErrorRecord ($exception.ToString()),"MachineStateIncorrect","InvalidOperation",$null - throw $errorRecord -} +<# + .SYNOPSIS + Asserts that the path extension is valid. -Function Set-RegistryValue + .PARAMETER Path + The path to validate the extension of. +#> +function Assert-PathExtensionValid { + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Key, - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $Value, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Data + [String] + $Path ) - try - { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) - $subKey = $baseKey.OpenSubKey($Key, $true) ## Opens the subkey with write access - if($subKey -eq $null) - { - $subKey = $baseKey.CreateSubKey($Key) - } - $subKey.SetValue($Value, $Data) - } - catch + $pathExtension = [System.IO.Path]::GetExtension($Path) + Write-Verbose -Message ($LocalizedData.ThePathExtensionWasPathExt -f $pathExtension) + + $validPathExtensions = @( '.msi', '.exe' ) + + if ($validPathExtensions -notcontains $pathExtension.ToLower()) { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Set-RegistryValue: $exceptionText" + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidBinaryType -f $Path) } } -Function Remove-RegistryValue +<# + .SYNOPSIS + Retrieves the product ID as an identifying number. + + .PARAMETER ProductId + The product id to retrieve as an identifying number. +#> +function Convert-ProductIdToIdentifyingNumber { + [OutputType([String])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $Key, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Value + [String] + $ProductId ) try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) - $subKey = $baseKey.OpenSubKey($Key, $true) ## Opens the subkey with write access - $subKey.DeleteValue($Value) + Write-Verbose -Message ($LocalizedData.ParsingProductIdAsAnIdentifyingNumber -f $ProductId) + $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + + Write-Verbose -Message ($LocalizedData.ParsedProductIdAsIdentifyingNumber -f $ProductId, $identifyingNumber) + return $identifyingNumber } catch { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Remove-RegistryValue: $exceptionText" + New-InvalidArgumentException -ArgumentName 'ProductId' -Messsage ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) } } -Function Get-RegistryValueIgnoreError +<# + .SYNOPSIS + Converts the given path to a URI. + Throws an exception if the path's scheme as a URI is not valid. + + .PARAMETER Path + The path to retrieve as a URI. +#> +function Convert-PathToUri { + [OutputType([Uri])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] - [System.String] - $Key, - - [parameter(Mandatory = $true)] - [System.String] - $Value, - - [parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryView] - $RegistryView + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path ) try { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) - $subKey = $baseKey.OpenSubKey($Key) - if($subKey -ne $null) - { - return $subKey.GetValue($Value) - } + $uri = [Uri] $Path } catch { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText" + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path) } - return $null -} -Function Validate-StandardArguments -{ - param( - $Path, - $ProductId, - $Name - ) + $validUriSchemes = @( 'file', 'http', 'https' ) - Trace-Message "Validate-StandardArguments, Path was $Path" - $uri = $null - try + if ($validUriSchemes -notcontains $uri.Scheme) { - $uri = [uri] $Path - } - catch - { - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" + Write-Verbose -Message ($Localized.TheUriSchemeWasUriScheme -f $uri.Scheme) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidPath -f $Path) } - if(-not @("file", "http", "https") -contains $uri.Scheme) - { - Trace-Message "The uri scheme was $uri.Scheme" - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" - } + return $uri +} - $pathExt = [System.IO.Path]::GetExtension($Path) - Trace-Message "The path extension was $pathExt" - if(-not @(".msi",".exe") -contains $pathExt.ToLower()) - { - Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path" - } +<# + .SYNOPSIS + Retrieves the product entry for the package with the given name and/or identifying number. - $identifyingNumber = $null - if(-not $Name -and -not $ProductId) - { - #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version - Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId" - } - elseif($ProductId) - { - try - { - Trace-Message "Parsing $ProductId as an identifyingNumber" - $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() - Trace-Message "Parsed $ProductId as $identifyingNumber" - } - catch - { - Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId - } - } - - return $uri, $identifyingNumber -} + .PARAMETER Name + The name of the product entry to retrieve. -Function Get-ProductEntry + .PARAMETER IdentifyingNumber + The identifying number of the product entry to retrieve. +#> +function Get-ProductEntry { + [CmdletBinding()] param ( - [string] $Name, - [string] $IdentifyingNumber, - [string] $InstalledCheckRegHive = 'LocalMachine', - [string] $InstalledCheckRegKey, - [string] $InstalledCheckRegValueName, - [string] $InstalledCheckRegValueData - ) + [String] + $Name, - $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" - $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + [String] + $IdentifyingNumber + ) - if($IdentifyingNumber) - { - $keyLocation = "$uninstallKey\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue - if(-not $item) - { - $keyLocation = "$uninstallKeyWow64\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue - } + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' - return $item - } + $productEntry = $null - foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) + if (-not [String]::IsNullOrEmpty($IdentifyingNumber)) { - if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKey -ChildPath $IdentifyingNumber + + $productEntry = Get-Item -Path $productEntryKeyLocation -ErrorAction 'SilentlyContinue' + + if ($null -eq $productEntry) { - return $item + $productEntryKeyLocation = Join-Path -Path $uninstallRegistryKeyWow64 -ChildPath $IdentifyingNumber + $productEntry = Get-Item $productEntryKeyLocation -ErrorAction 'SilentlyContinue' } } - - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + else { - $installValue = $null - - #if 64bit OS, check 64bit registry view first - if ((Get-WmiObject -Class Win32_OperatingSystem -ea 0).OSArchitecture -eq '64-bit') - { - $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 - } - - if($installValue -eq $null) - { - $installValue = Get-RegistryValueIgnoreError $InstalledCheckRegHive "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 - } - - if($installValue) + foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' )) { - if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName')) { - return @{ - Installed = $true - } + $productEntry = $registryKeyEntry + break } } } - return $null + return $productEntry } function Test-TargetResource { - [OutputType([System.Boolean])] + [OutputType([Boolean])] + [CmdletBinding()] param ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, - - [string] $Arguments, - - [pscredential] $Credential, - - [System.UInt32[]] $ReturnCode, - - [string] $LogPath, - - [pscredential] $RunAsCredential, + [String] + $ProductId, - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', + [String] + $Arguments, - [string] $InstalledCheckRegKey, + [PSCredential] + $Credential, - [string] $InstalledCheckRegValueName, + [Int[]] + $ReturnCode, - [string] $InstalledCheckRegValueData, + [String] + $LogPath + ) - [boolean] $CreateCheckRegValue, + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path - [string] $FileHash, + if (-not [String]::IsNullOrEmpty($ProductId)) + { + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId + } - [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] - [string] $HashAlgorithm, + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber - [string] $SignerSubject, - [string] $SignerThumbprint, + Write-Verbose -Message ($LocalizedData.EnsureIsEnsure -f $Ensure) - [string] $ServerCertificateValidationCallback - ) - - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - Trace-Message "Ensure is $Ensure" - if($product) + if ($null -eq $productEntry) { - Trace-Message "product found" + Write-Verbose -Message ($LocalizedData.ProductIsProduct -f $productEntry) } else { - Trace-Message "product installation cannot be determined" + Write-Verbose -Message 'Product installation cannot be determined' } - Trace-Message ("product as boolean is {0}" -f [boolean]$product) - $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent") - # install registry test overrides the product id test and there is no true product information - # when doing a lookup via registry key - if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + Write-Verbose -Message ($LocalizedData.ProductAsBooleanIs -f [Boolean]$productEntry) + + if ($null -ne $productEntry) { - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $displayName) } else - { - if ($product -ne $null) + { + $displayName = $null + + if (-not [String]::IsNullOrEmpty($Name)) { - $name = Get-LocalizableRegKeyValue $product "DisplayName" - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) + $displayName = $Name } else { - $displayName = $null - if($Name) - { - $displayName = $Name - } - else - { - $displayName = $ProductId - } - - Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) + $displayName = $ProductId } - + + Write-Verbose -Message ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) } - return $res + return ($null -ne $productEntry -and $Ensure -eq 'Present') -or ($null -eq $productEntry -and $Ensure -eq 'Absent') } -function Get-LocalizableRegKeyValue +<# + .SYNOPSIS + Retrieves a localized registry key value. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER ValueName + The name of the value to retrieve. +#> +function Get-LocalizedRegistryKeyValue { + [CmdletBinding()] param( - [object] $RegKey, - [string] $ValueName + [Object] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ValueName ) - $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) - if(-not $res) + $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName) + + if ($null -eq $localizedRegistryKeyValue) { - $res = $RegKey.GetValue($ValueName) + $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName) } - return $res + return $localizedRegistryKeyValue } function Get-TargetResource { - [OutputType([System.Collections.Hashtable])] + [OutputType([Hashtable])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, - - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', - - [string] $InstalledCheckRegKey, - - [string] $InstalledCheckRegValueName, - - [string] $InstalledCheckRegValueData, - - [boolean] $CreateCheckRegValue + [String] + $ProductId ) - #If the user gave the ProductId then we derive $identifyingNumber - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - - $localMsi = $uri.IsFile -and -not $uri.IsUnc - - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path - if(-not $product) + if (-not [String]::IsNullOrEmpty($ProductId)) { - return @{ - Ensure = "Absent" - Name = $Name - ProductId = $identifyingNumber - Installed = $false - InstalledCheckRegHive = $InstalledCheckRegHive - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData - } + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + if ($null -eq $productEntry) { return @{ - Ensure = "Present" + Ensure = 'Absent' Name = $Name ProductId = $identifyingNumber - Installed = $true - InstalledCheckRegHive = $InstalledCheckRegHive - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData + Installed = $false } } - #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE) - #If the user gave a ProductId just pass it through, otherwise fill it from the product - if(-not $identifyingNumber) + <# + Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE). + If the user gave a product ID just pass it through, otherwise get it from the product. + #> + if ($null -eq $identifyingNumber) { - $identifyingNumber = Split-Path -Leaf $product.Name + $identifyingNumber = Split-Path -Path $productEntry.Name -Leaf } - $date = $product.GetValue("InstallDate") - if($date) + $installDate = $productEntry.GetValue('InstallDate') + + if ($null -ne $installDate) { try { - $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date + $installDate = '{0:d}' -f [DateTime]::ParseExact($installDate, 'yyyyMMdd',[System.Globalization.CultureInfo]::CurrentCulture).Date } catch { - $date = $null + $installDate = $null } } - $publisher = Get-LocalizableRegKeyValue $product "Publisher" - $size = $product.GetValue("EstimatedSize") - if($size) + $publisher = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'Publisher' + + $estimatedSize = $productEntry.GetValue('EstimatedSize') + + if ($null -ne $estimatedSize) { - $size = $size/1024 + $estimatedSize = $estimatedSize / 1024 } - $version = $product.GetValue("DisplayVersion") - $description = $product.GetValue("Comments") - $name = Get-LocalizableRegKeyValue $product "DisplayName" + $displayVersion = $productEntry.GetValue('DisplayVersion') + + $comments = $productEntry.GetValue('Comments') + + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + return @{ - Ensure = "Present" - Name = $name + Ensure = 'Present' + Name = $displayName Path = $Path - InstalledOn = $date + InstalledOn = $installDate ProductId = $identifyingNumber - Size = $size + Size = $estimatedSize Installed = $true - Version = $version - PackageDescription = $description + Version = $displayVersion + PackageDescription = $comments Publisher = $publisher } } -Function Get-MsiTools +<# + .SYNOPSIS + Retrieves the MSI tools type. +#> +function Get-MsiTools { - if($script:MsiTools) + [OutputType([System.Type])] + [CmdletBinding()] + param () + + if ($null -ne $script:msiTools) { - return $script:MsiTools + return $script:msiTools } - $sig = @' + $msiToolsCodeDefinition = @' [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] - private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); + private static extern UInt32 MsiOpenPackageExW(string szPackagePath, int dwOptions, out IntPtr hProduct); [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] private static extern uint MsiCloseHandle(IntPtr hAny); @@ -569,7 +472,7 @@ Function Get-MsiTools IntPtr MsiHandle = IntPtr.Zero; try { - var res = MsiOpenPackageW(msi, out MsiHandle); + var res = MsiOpenPackageExW(msi, 1, out MsiHandle); if (res != 0) { return null; @@ -598,354 +501,446 @@ Function Get-MsiTools return GetPackageProperty(msi, "ProductName"); } '@ - $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.xPackageResource ` - -Name MsiTools -Using System.Text -MemberDefinition $sig - return $script:MsiTools + + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + $script:msiTools = ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + $script:msiTools = Add-Type ` + -Namespace 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' ` + -Name 'MsiTools' ` + -Using 'System.Text' ` + -MemberDefinition $msiToolsCodeDefinition ` + -PassThru + } + + return $script:msiTools } +<# + .SYNOPSIS + Retrieves the name of a product from an msi. -Function Get-MsiProductEntry + .PARAMETER Path + The path to the msi to retrieve the name from. +#> +function Get-MsiProductName { + [OutputType([String])] + [CmdletBinding()] param ( - [string] $Path + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path ) - if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) - { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) - } + $msiTools = Get-MsiTools - $tools = Get-MsiTools + $productName = $msiTools::GetProductName($Path) - $pn = $tools::GetProductName($Path) + return $productName +} - $pc = $tools::GetProductCode($Path) +<# + .SYNOPSIS + Retrieves the code of a product from an msi. - return $pn,$pc -} + .PARAMETER Path + The path to the msi to retrieve the code from. +#> +function Get-MsiProductCode +{ + [OutputType([String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Path + ) + + $msiTools = Get-MsiTools + + $productCode = $msiTools::GetProductCode($Path) + return $productCode +} function Set-TargetResource { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $Name, + [String] + $Name, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $Path, + [String] + $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [string] $ProductId, - - [string] $Arguments, - - [pscredential] $Credential, - - [System.UInt32[]] $ReturnCode, - - [string] $LogPath, - - [pscredential] $RunAsCredential, + [String] + $ProductId, - [ValidateSet('LocalMachine','CurrentUser')] - [string] $InstalledCheckRegHive = 'LocalMachine', + [String] + $Arguments, - [string] $InstalledCheckRegKey, + [PSCredential] + $Credential, - [string] $InstalledCheckRegValueName, + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [ValidateNotNullOrEmpty()] + [Int[]] + $ReturnCode = @( 0, 1641, 3010 ), - [string] $InstalledCheckRegValueData, + [String] + $LogPath, - [boolean] $CreateCheckRegValue, + [Boolean] + $CreateCheckRegValue, + + [String] + $FileHash, - [string] $FileHash, + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [String] + $HashAlgorithm, - [ValidateSet('SHA1','SHA256','SHA384','SHA512','MD5','RIPEMD160')] - [string] $HashAlgorithm, + [String] + $SignerSubject, - [string] $SignerSubject, - [string] $SignerThumbprint, + [String] + $SignerThumbprint, - [string] $ServerCertificateValidationCallback + [String] + $ServerCertificateValidationCallback ) - $ErrorActionPreference = "Stop" + $ErrorActionPreference = 'Stop' - if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` - -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` - -InstalledCheckRegValueData $InstalledCheckRegValueData)) + if (Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId) { return } - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + Assert-PathExtensionValid -Path $Path + $uri = Convert-PathToUri -Path $Path - #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded - #but the named package wasn't present on the system afterward so we can give a better message - $OrigPath = $Path - - Write-Verbose $LocalizedData.PackageConfigurationStarting - if(-not $ReturnCode) + if (-not [String]::IsNullOrEmpty($ProductId)) { - $ReturnCode = @(0) + $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + <# + Path gets overwritten in the download code path. Retain the user's original Path in case + the install succeeded but the named package wasn't present on the system afterward so we + can give a better error message. + #> + $originalPath = $Path + + Write-Verbose -Message $LocalizedData.PackageConfigurationStarting + $logStream = $null - $psdrive = $null + $psDrive = $null $downloadedFileName = $null + try { $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() - if($LogPath) + if (-not [String]::IsNullOrEmpty($LogPath)) { try { - if($fileExtension -eq ".msi") + if ($fileExtension -eq '.msi') { - #We want to pre-verify the path exists and is writable ahead of time - #even in the MSI case, as detecting WHY the MSI log doesn't exist would - #be rather problematic for the user - if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null)) + <# + We want to pre-verify the log path exists and is writable ahead of time + even in the MSI case, as detecting WHY the MSI log path doesn't exist would + be rather problematic for the user. + #> + if ((Test-Path -Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile, $null, $null)) { - rm $LogPath + Remove-Item -Path $LogPath } - if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + if ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { - New-Item -Type File $LogPath | Out-Null + New-Item -Path $LogPath -Type 'File' | Out-Null } } - elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + elseif ($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) { - $logStream = new-object "System.IO.StreamWriter" $LogPath,$false + $logStream = New-Object -TypeName 'System.IO.StreamWriter' -ArgumentList @( $LogPath, $false ) } } catch { - Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ + New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenLog -f $LogPath) -ErrorRecord $_ } } - #Download or mount file as necessary - if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) + # Download or mount file as necessary + if (-not ($fileExtension -eq '.msi' -and $Ensure -eq 'Absent')) { - if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) + if ($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) { - $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)} - if($Credential) + $psDriveArgs = @{ + Name = [Guid]::NewGuid() + PSProvider = 'FileSystem' + Root = Split-Path -Path $uri.LocalPath + } + + # If we pass a null for Credential, a dialog will pop up. + if ($null -ne $Credential) { - #We need to optionally include these and then splat the hash otherwise - #we pass a null for Credential which causes the cmdlet to pop a dialog up - $psdriveArgs["Credential"] = $Credential + $psDriveArgs['Credential'] = $Credential } - $psdrive = New-PSDrive @psdriveArgs - $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary? + $psDrive = New-PSDrive @psDriveArgs + $Path = Join-Path -Path $psDrive.Root -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) } - elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) + elseif (@( 'http', 'https' ) -contains $uri.Scheme -and $Ensure -eq 'Present' -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) { - $scheme = $uri.Scheme + $uriScheme = $uri.Scheme $outStream = $null $responseStream = $null try { - Trace-Message "Creating cache location" + Write-Verbose -Message ($LocalizedData.CreatingCacheLocation) - if(-not (Test-Path -PathType Container $CacheLocation)) + if (-not (Test-Path -Path $script:packageCacheLocation -PathType 'Container')) { - mkdir $CacheLocation | Out-Null + New-Item -Path $script:packageCacheLocation -ItemType 'Directory' | Out-Null } - $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) + $destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Split-Path -Path $uri.LocalPath -Leaf) - Trace-Message "Need to download file from $scheme, destination will be $destName" + Write-Verbose -Message ($LocalizedData.NeedtodownloadfilefromschemedestinationwillbedestName -f $uriScheme, $destinationPath) try { - Trace-Message "Creating the destination cache file" - $outStream = New-Object System.IO.FileStream $destName, "Create" + Write-Verbose -Message ($LocalizedData.CreatingTheDestinationCacheFile) + $outStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $destinationPath, 'Create' ) } catch { - #Should never happen since we own the cache directory - Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ + # Should never happen since we own the cache directory + New-InvalidOperationException -Message ($LocalizedData.CouldNotOpenDestFile -f $destinationPath) -ErrorRecord $_ } try { - Trace-Message "Creating the $scheme stream" - $request = [System.Net.WebRequest]::Create($uri) - Trace-Message "Setting default credential" - $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials - if ($scheme -eq "http") + Write-Verbose -Message ($LocalizedData.CreatingTheSchemeStream -f $uriScheme) + $webRequest = [System.Net.WebRequest]::Create($uri) + + Write-Verbose -Message ($LocalizedData.SettingDefaultCredential) + $webRequest.Credentials = [System.Net.CredentialCache]::DefaultCredentials + + if ($uriScheme -eq 'http') { - Trace-Message "Setting authentication level" - # default value is MutualAuthRequested, which applies to https scheme - $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + # Default value is MutualAuthRequested, which applies to the https scheme + Write-Verbose -Message ($LocalizedData.SettingAuthenticationLevel) + $webRequest.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None } - if ($scheme -eq "https" -and -not [string]::IsNullOrEmpty($ServerCertificateValidationCallback)) + elseif ($uriScheme -eq 'https' -and -not [String]::IsNullOrEmpty($ServerCertificateValidationCallback)) { - Trace-Message "Assigning user-specified certificate verification callback" - $scriptBlock = [scriptblock]::Create($ServerCertificateValidationCallback) - $request.ServerCertificateValidationCallBack = $scriptBlock + Write-Verbose -Message 'Assigning user-specified certificate verification callback' + $serverCertificateValidationScriptBlock = [ScriptBlock]::Create($ServerCertificateValidationCallback) + $webRequest.ServerCertificateValidationCallBack = $serverCertificateValidationScriptBlock } - Trace-Message "Getting the $scheme response stream" - $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() + + Write-Verbose -Message ($LocalizedData.Gettingtheschemeresponsestream -f $uriScheme) + $responseStream = (([System.Net.HttpWebRequest]$webRequest).GetResponse()).GetResponseStream() } catch { - Trace-Message ("Error: " + ($_ | Out-String)) - Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ + Write-Verbose -Message ($LocalizedData.ErrorOutString -f ($_ | Out-String)) + New-InvalidOperationException -Message ($LocalizedData.CouldNotGetHttpStream -f $uriScheme, $Path) -ErrorRecord $_ } try { - Trace-Message "Copying the $scheme stream bytes to the disk cache" + Write-Verbose -Message ($LocalizedData.CopyingTheSchemeStreamBytesToTheDiskCache -f $uriScheme) $responseStream.CopyTo($outStream) $responseStream.Flush() $outStream.Flush() } catch { - Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ + New-InvalidOperationException -Message ($LocalizedData.ErrorCopyingDataToFile -f $Path, $destinationPath) -ErrorRecord $_ } } finally { - if($outStream) + if ($null -ne $outStream) { $outStream.Close() } - if($responseStream) + if ($null -ne $responseStream) { $responseStream.Close() } } - Trace-Message "Redirecting package path to cache file location" - $Path = $downloadedFileName = $destName + + Write-Verbose -Message ($LocalizedData.RedirectingPackagePathToCacheFileLocation) + $Path = $destinationPath + $downloadedFileName = $destinationPath } - } - if (-not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi")) - { - #At this point the Path ought to be valid unless it's an MSI uninstall case - if(-not (Test-Path -PathType Leaf $Path)) + # At this point the Path ought to be valid unless it's a MSI uninstall case + if (-not (Test-Path -Path $Path -PathType 'Leaf')) { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + New-InvalidOperationException -Message ($LocalizedData.PathDoesNotExist -f $Path) } - ValidateFile -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint + Assert-FileValid -Path $Path -HashAlgorithm $HashAlgorithm -FileHash $FileHash -SignerSubject $SignerSubject -SignerThumbprint $SignerThumbprint } - $startInfo = New-Object System.Diagnostics.ProcessStartInfo - $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea - $process = New-Object System.Diagnostics.Process + $startInfo = New-Object -TypeName 'System.Diagnostics.ProcessStartInfo' + + # Necessary for I/O redirection and just generally a good idea + $startInfo.UseShellExecute = $false + + $process = New-Object -TypeName 'System.Diagnostics.Process' $process.StartInfo = $startInfo - $errLogPath = $LogPath + ".err" #Concept only, will never touch disk - if($fileExtension -eq ".msi") + + # Concept only, will never touch disk + $errorLogPath = $LogPath + ".err" + + if ($fileExtension -eq '.msi') { - $startInfo.FileName = "$env:windir\system32\msiexec.exe" - if($Ensure -eq "Present") + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + if ($Ensure -eq 'Present') { - # check if Msi package contains the ProductName and Code specified + # Check if the MSI package specifies the ProductName and Code + $productName = Get-MsiProductName -Path $Path + $productCode = Get-MsiProductCode -Path $Path - $pName,$pCode = Get-MsiProductEntry -Path $Path + if ((-not [String]::IsNullOrEmpty($Name)) -and ($productName -ne $Name)) + { + New-InvalidArgumentException -ArgumentName 'Name' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) + } - if ( - ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` - -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) - ) + if ((-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $productCode)) { - Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidNameOrId -f $Name, $identifyingNumber, $productName, $productCode) } $startInfo.Arguments = '/i "{0}"' -f $Path } else { - $product = Get-ProductEntry $Name $identifyingNumber - $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID - $startInfo.Arguments = ("/x{0}" -f $id) + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + # We may have used the Name earlier, now we need the actual ID + $id = Split-Path -Path $productEntry.Name -Leaf + $startInfo.Arguments = '/x{0}' -f $id } - if($LogPath) + if ($LogPath) { $startInfo.Arguments += ' /log "{0}"' -f $LogPath } $startInfo.Arguments += " /quiet" - if($Arguments) + if ($Arguments) { - $startInfo.Arguments += " " + $Arguments + $startInfo.Arguments += "$Arguments" } } - else #EXE + else { - Trace-Message "The binary is an EXE" - $startInfo.FileName = $Path - $startInfo.Arguments = $Arguments - if($LogPath) + # EXE + Write-Verbose -Message $LocalizedData.TheBinaryIsAnExe + + if ($Ensure -eq 'Present') + { + $startInfo.FileName = $Path + $startInfo.Arguments = $Arguments + + if ($LogPath) + { + Write-Verbose -Message ($LocalizedData.UserHasRequestedLoggingNeedToAttachEventHandlersToTheProcess) + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardOutput = $true + + Register-ObjectEvent -InputObject $process -EventName 'OutputDataReceived' -SourceIdentifier $LogPath + Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -SourceIdentifier $errorLogPath + } + } + else { - Trace-Message "User has requested logging, need to attach event handlers to the process" - $startInfo.RedirectStandardError = $true - $startInfo.RedirectStandardOutput = $true - Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath - Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath + # Absent case + $startInfo.FileName = "$env:winDir\system32\msiexec.exe" + + $id = Split-Path -Path $productEntry.Name -Leaf + $startInfo.Arguments = ('/x{0} /quiet' -f $id) + + # Never let msiexec restart automatically. DSC should handle reboot requests. + $startInfo.Arguments += ' /norestart' + + if ($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + if ($Arguments) + { + $startInfo.Arguments += "$Arguments" + } } } - Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) + Write-Verbose -Message ($LocalizedData.StartingwithstartInfoFileNamestartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) - if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) + if ($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) { try { $exitCode = 0 - if($PSBoundParameters.ContainsKey("RunAsCredential")) + $process.Start() | Out-Null + + # Identical to $fileExtension -eq '.exe' -and $logPath + if ($logStream) { - CallPInvoke - [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` - $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` - $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) + $process.BeginOutputReadLine() + $process.BeginErrorReadLine() } - else - { - $process.Start() | Out-Null + + $process.WaitForExit() - if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath - { - $process.BeginOutputReadLine(); - $process.BeginErrorReadLine(); - } - - $process.WaitForExit() - - if($process) - { - $exitCode = $process.ExitCode - } + if ($process) + { + $exitCode = $process.ExitCode } } catch { - Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ + New-InvalidOperationException -Message ($LocalizedData.CouldNotStartProcess -f $Path) -ErrorRecord $_ } - if($logStream) + if ($logStream) { #We have to re-mux these since they appear to us as different streams #The underlying Win32 APIs prevent this problem, as would constructing a script @@ -967,431 +962,156 @@ function Set-TargetResource if(-not ($ReturnCode -contains $exitCode)) { - Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + New-InvalidOperationException ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) } } } finally { - if($psdrive) + if ($psDrive) { - Remove-PSDrive -Force $psdrive + Remove-PSDrive -Name $psDrive -Force } - if($logStream) + if ($logStream) { $logStream.Dispose() } } - if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) + if ($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) { - #This is deliberately not in the Finally block. We want to leave the downloaded file on disk - #in the error case as a debugging aid for the user - rm $downloadedFileName + <# + This is deliberately not in the finally block because we want to leave the downloaded + file on disk if an error occurred as a debugging aid for the user. + #> + Remove-Item -Path $downloadedFileName } - $operationString = $LocalizedData.PackageUninstalled - if($Ensure -eq "Present") + $operationMessageString = $LocalizedData.PackageUninstalled + if ($Ensure -eq 'Present') { - $operationString = $LocalizedData.PackageInstalled + $operationMessageString = $LocalizedData.PackageInstalled } - if($CreateCheckRegValue -eq $true) - { - $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName - if($Ensure -eq 'Present') - { - Write-Verbose ($LocalizedData.CreatingRegistryValue -f $registryValueString) - Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData - } - else - { - Write-Verbose ($LocalizedData.RemovingRegistryValue -f $registryValueString) - Remove-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName - } - } + <# + Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is + missing on some client SKUs (worked on both Server and Client Skus in Windows 10). + #> + + $serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -ErrorAction 'Ignore' + $registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore' - # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs - $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks - $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore - if(($featureData -and $featureData.RequiresReboot) -or $regData) + if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641) { Write-Verbose $LocalizedData.MachineRequiresReboot $global:DSCMachineStatus = 1 } - if($Ensure -eq "Present") + if ($Ensure -eq 'Present') { - $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegHive $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - if(-not $productEntry) + $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + + if (-not $productEntry) { - Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath) + New-InvalidOperationException -Message ($LocalizedData.PostValidationError -f $originalPath) } } - Write-Verbose $operationString - Write-Verbose $LocalizedData.PackageConfigurationComplete + Write-Verbose -Message $operationMessageString + Write-Verbose -Message $LocalizedData.PackageConfigurationComplete } -function CallPInvoke -{ -$script:ProgramSource = @" -using System; -using System.Collections.Generic; -using System.Text; -using System.Security; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Security.Principal; -using System.ComponentModel; -using System.IO; - -namespace Source -{ - [SuppressUnmanagedCodeSecurity] - public static class NativeMethods - { - //The following structs and enums are used by the various Win32 API's that are used in the code below +<# + .SYNOPSIS + Asserts that the file at the given path is valid. - [StructLayout(LayoutKind.Sequential)] - public struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public Int32 dwProcessID; - public Int32 dwThreadID; - } - - [Flags] - public enum LogonType - { - LOGON32_LOGON_INTERACTIVE = 2, - LOGON32_LOGON_NETWORK = 3, - LOGON32_LOGON_BATCH = 4, - LOGON32_LOGON_SERVICE = 5, - LOGON32_LOGON_UNLOCK = 7, - LOGON32_LOGON_NETWORK_CLEARTEXT = 8, - LOGON32_LOGON_NEW_CREDENTIALS = 9 - } - - [Flags] - public enum LogonProvider - { - LOGON32_PROVIDER_DEFAULT = 0, - LOGON32_PROVIDER_WINNT35, - LOGON32_PROVIDER_WINNT40, - LOGON32_PROVIDER_WINNT50 - } - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public Int32 Length; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } + .PARAMETER Path + The path to the file to check. - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } + .PARAMETER FileHash + The hash that should match the hash of the file. - public enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } + .PARAMETER HashAlgorithm + The algorithm to use to retrieve the file hash. - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct TokPriv1Luid - { - public int Count; - public long Luid; - public int Attr; - } + .PARAMETER SignerThumbprint + The certificate thumbprint that should match the file's signer certificate. - public const int GENERIC_ALL_ACCESS = 0x10000000; - public const int CREATE_NO_WINDOW = 0x08000000; - internal const int SE_PRIVILEGE_ENABLED = 0x00000002; - internal const int TOKEN_QUERY = 0x00000008; - internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; - internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; - - [DllImport("kernel32.dll", - EntryPoint = "CloseHandle", SetLastError = true, - CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern bool CloseHandle(IntPtr handle); - - [DllImport("advapi32.dll", - EntryPoint = "CreateProcessAsUser", SetLastError = true, - CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] - public static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandle, - Int32 dwCreationFlags, - IntPtr lpEnvrionment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - ref PROCESS_INFORMATION lpProcessInformation - ); - - [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] - public static extern bool DuplicateTokenEx( - IntPtr hExistingToken, - Int32 dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - Int32 ImpersonationLevel, - Int32 dwTokenType, - ref IntPtr phNewToken - ); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern Boolean LogonUser( - String lpszUserName, - String lpszDomain, - String lpszPassword, - LogonType dwLogonType, - LogonProvider dwLogonProvider, - out IntPtr phToken - ); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool AdjustTokenPrivileges( - IntPtr htok, - bool disall, - ref TokPriv1Luid newst, - int len, - IntPtr prev, - IntPtr relen - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GetCurrentProcess(); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool OpenProcessToken( - IntPtr h, - int acc, - ref IntPtr phtok - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern int WaitForSingleObject( - IntPtr h, - int milliseconds - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern bool GetExitCodeProcess( - IntPtr h, - out int exitcode - ); - - [DllImport("advapi32.dll", SetLastError = true)] - internal static extern bool LookupPrivilegeValue( - string host, - string name, - ref long pluid - ); - - public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) - { - var hToken = IntPtr.Zero; - var hDupedToken = IntPtr.Zero; - TokPriv1Luid tp; - var pi = new PROCESS_INFORMATION(); - var sa = new SECURITY_ATTRIBUTES(); - sa.Length = Marshal.SizeOf(sa); - Boolean bResult = false; - try - { - bResult = LogonUser( - strName, - strDomain, - strPassword, - LogonType.LOGON32_LOGON_BATCH, - LogonProvider.LOGON32_PROVIDER_DEFAULT, - out hToken - ); - if (!bResult) - { - throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); - } - IntPtr hproc = GetCurrentProcess(); - IntPtr htok = IntPtr.Zero; - bResult = OpenProcessToken( - hproc, - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - ref htok - ); - if(!bResult) - { - throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); - } - tp.Count = 1; - tp.Luid = 0; - tp.Attr = SE_PRIVILEGE_ENABLED; - bResult = LookupPrivilegeValue( - null, - SE_INCRASE_QUOTA, - ref tp.Luid - ); - if(!bResult) - { - throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); - } - bResult = AdjustTokenPrivileges( - htok, - false, - ref tp, - 0, - IntPtr.Zero, - IntPtr.Zero - ); - if(!bResult) - { - throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); - } - - bResult = DuplicateTokenEx( - hToken, - GENERIC_ALL_ACCESS, - ref sa, - (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, - (int)TOKEN_TYPE.TokenPrimary, - ref hDupedToken - ); - if(!bResult) - { - throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); - } - var si = new STARTUPINFO(); - si.cb = Marshal.SizeOf(si); - si.lpDesktop = ""; - bResult = CreateProcessAsUser( - hDupedToken, - null, - strCommand, - ref sa, - ref sa, - false, - 0, - IntPtr.Zero, - null, - ref si, - ref pi - ); - if(!bResult) - { - throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); - } - - int status = WaitForSingleObject(pi.hProcess, -1); - if(status == -1) - { - throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); - } + .PARAMETER SignerSubject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileValid +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, - bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); - if(!bResult) - { - throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); - } - } - finally - { - if (pi.hThread != IntPtr.Zero) - { - CloseHandle(pi.hThread); - } - if (pi.hProcess != IntPtr.Zero) - { - CloseHandle(pi.hProcess); - } - if (hDupedToken != IntPtr.Zero) - { - CloseHandle(hDupedToken); - } - } - } - } -} + [String] + $FileHash, -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} + [String] + $HashAlgorithm, -function ValidateFile -{ - [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + [String] + $SignerThumbprint, - [string] $FileHash, - [string] $HashAlgorithm, - [string] $SignerThumbprint, - [string] $SignerSubject + [String] + $SignerSubject ) - if ($FileHash) + if (-not [String]::IsNullOrEmpty($FileHash)) { - ValidateFileHash -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm + Assert-FileHashValid -Path $Path -Hash $FileHash -Algorithm $HashAlgorithm } - if ($SignerThumbprint -or $SignerSubject) + if (-not [String]::IsNullOrEmpty($SignerThumbprint) -or -not [String]::IsNullOrEmpty($SignerSubject)) { - ValidateFileSignature -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject + Assert-FileSignatureValid -Path $Path -Thumbprint $SignerThumbprint -Subject $SignerSubject } } -function ValidateFileHash +<# + .SYNOPSIS + Asserts that the hash of the file at the given path matches the given hash. + + .PARAMETER Path + The path to the file to check the hash of. + + .PARAMETER Hash + The hash to check against. + + .PARAMETER Algorithm + The algorithm to use to retrieve the file's hash. +#> +function Assert-FileHashValid { [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, [Parameter(Mandatory)] - [string] $Hash, + [String] + $Hash, - [string] $Algorithm + [String] + $Algorithm = 'SHA256' ) - if ([string]::IsNullOrEmpty($Algorithm)) { $Algorithm = 'SHA256' } + if ([String]::IsNullOrEmpty($Algorithm)) + { + $Algorithm = 'SHA256' + } - Trace-Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) + Write-Verbose -Message ($LocalizedData.CheckingFileHash -f $Path, $Hash, $Algorithm) - $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction Stop + $fileHash = Get-FileHash -LiteralPath $Path -Algorithm $Algorithm -ErrorAction 'Stop' if ($fileHash.Hash -ne $Hash) { @@ -1399,21 +1119,38 @@ function ValidateFileHash } } -function ValidateFileSignature +<# + .SYNOPSIS + Asserts that the signature of the file at the given path is valid. + + .PARAMETER Path + The path to the file to check the signature of + + .PARAMETER Thumbprint + The certificate thumbprint that should match the file's signer certificate. + + .PARAMETER Subject + The certificate subject that should match the file's signer certificate. +#> +function Assert-FileSignatureValid { [CmdletBinding()] - param ( - [Parameter(Mandatory)] - [string] $Path, + param + ( + [Parameter(Mandatory = $true)] + [String] + $Path, - [string] $Thumbprint, + [String] + $Thumbprint, - [string] $Subject + [String] + $Subject ) - Trace-Message ($LocalizedData.CheckingFileSignature -f $Path) + Write-Verbose -Message ($LocalizedData.CheckingFileSignature -f $Path) - $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction Stop + $signature = Get-AuthenticodeSignature -LiteralPath $Path -ErrorAction 'Stop' if ($signature.Status -ne [System.Management.Automation.SignatureStatus]::Valid) { @@ -1421,18 +1158,18 @@ function ValidateFileSignature } else { - Trace-Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) + Write-Verbose -Message ($LocalizedData.FileHasValidSignature -f $Path, $signature.SignerCertificate.Thumbprint, $signature.SignerCertificate.Subject) } - if ($Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) + if ($null -ne $Subject -and ($signature.SignerCertificate.Subject -notlike $Subject)) { throw ($LocalizedData.WrongSignerSubject -f $Path, $Subject) } - if ($Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) + if ($null -ne $Thumbprint -and ($signature.SignerCertificate.Thumbprint -ne $Thumbprint)) { throw ($LocalizedData.WrongSignerThumbprint -f $Path, $Thumbprint) } } -Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource +Export-ModuleMember -Function *-TargetResource diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 14423abfc..f344784e0 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -555,10 +555,58 @@ function Test-IsFileLocked } } +<# + .SYNOPSIS + Initializes a DSC Resource unit test. +#> +function Initialize-DscResourceUnitTest +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DscResourceModuleName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $DscResourceName, + + [Parameter(Mandatory = $true)] + [ValidateSet('Unit', 'Integration')] + [String] + $TestType + ) + + if ((-not (Test-Path -Path "$PSScriptRoot\..\DSCResource.Tests")) -or (-not (Test-Path -Path "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1"))) + { + Push-Location "$PSScriptRoot\.." + git clone https://github.com/PowerShell/DscResource.Tests.git --quiet + Pop-Location + } + else + { + Push-Location "$PSScriptRoot\..\DSCResource.Tests" + git pull origin master --quiet + Pop-Location + } + + Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" -Force + + Initialize-TestEnvironment ` + -DSCModuleName $DscResourceModuleName ` + -DSCResourceName $DscResourceName ` + -TestType $TestType ` + | Out-Null +} + Export-ModuleMember -Function ` Test-GetTargetResourceResult, ` New-User, ` Remove-User, ` Test-User, ` Wait-ScriptBlockReturnTrue, ` - Test-IsFileLocked + Test-IsFileLocked, ` + Initialize-DscResourceUnitTest diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 index bcf0decd7..93662a161 100644 --- a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -1,927 +1,4 @@ -<############################################################################################# - - # File: DSC.Providers.Package.Helpers.psm1. - # - # Contains helper methods for test automation of Package DSC provider. - # - # Copyright (c)Microsoft Corp 2013. - # - - ############################################################################################> - - -<############################################################################################ -# Constants -############################################################################################> - -$global:end2EndFolder = "E2EScripts" -$global:end2EndScriptPath = "$global:homePath\$global:end2EndFolder" - -<############################################################################################ -# Common functions -# -##########################################################################################> - -<# -.Synopsis - Name: GetMsiProperties - Description: Retrieves the Properties of the given MSI package. - -.Parameters - msiFilePath: The path to the MSI package. - -.Returns - The Properties of the given MSI package in the form of HashTable. -#> -function GetMsiProperties -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $msiFilePath) - - # - - # Check if MSI file exists in the given path. - - # - - if (-not (Test-Path $msiFilePath)) - { - throw "Could not find MSI file path " + $msiFilePath - } - - # A hash table in which all the MSI properies are stored. - $msiProperties = @{} - - # - - # Create the COM object of type WindowsInstaller.Installer - - # - - $windowsInstaller = New-Object -com WindowsInstaller.Installer - - # - - # Load MSI Database. - - # - - $database = $windowsInstaller.GetType().InvokeMember( - "OpenDatabase", - "InvokeMethod", - $null, - $windowsInstaller, - @($msiFilePath, 0)) - - # - - # Open the Property view. - - # - - $query = "SELECT * FROM Property" - - $view = $database.GetType().InvokeMember( - "OpenView", - "InvokeMethod", - $null, - $database, - $query) - - $view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null) - - # - - # Fetch the Name and Value of each property from Property view. - - # - - $done = $false - - while (-not $done) - { - - $record = $view.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $view, $null) - - if ($null -eq $record) - { - $done = $true - } - else { - - $propertyName = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 1) - $propertyValue = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 2) - - $msiProperties[$propertyName] = $propertyValue - } - } - - # - - # Close the view. - - # - - $view.GetType().InvokeMember("Close", "InvokeMethod", $null, $view, $null) - - return $msiProperties -} - -<# -.Synopsis - Name: GetMsiProductId - Description: Retrieves the ProductID from the given MSI package. - -.Parameters - msiFilePath: The path to the MSI package. - -.Returns - The ProductID of the given MSI package. -#> -function GetMsiProductId -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $msiFilePath) - - # - - # Check if MSI file exists in the given path. - - # - - if (-not (Test-Path $msiFilePath)) - { - throw "Could not find MSI file path " + $msiFilePath - } - - $msiProperties = GetMsiProperties -msiFilePath $msiFilePath - - $msiProductId = $msiProperties.ProductCode - - if ($null -eq $msiProductId) - { - throw "Could not find the ProductID from MSI file " + $msiFilePath - } - - $msiProductId -} - -<# -.Synopsis - Name: IsMsiPackageInstalled - Description: Determines if the given MSI package is isntalled in the system or not. - -.Parameters - msiFilePath: The path to the MSI package. - -.Returns - $true in case the given MSI package is isntalled, otherwise $false. -#> -function IsMsiPackageInstalled -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $msiFilePath) - - # - - # Check if MSI file exists in the given path. - - # - - if (-not (Test-Path $msiFilePath)) - { - throw "Could not find MSI file path " + $msiFilePath - } - - $msiPackageInstalled = $false - - # - - # Get MSI ProductID - - # - - $msiProductId = GetMsiProductId -msiFilePath $msiFilePath - - # - - # Issue WMI query to determine if any product with given ProductId is installed. - - # - - $msiPackageInstalled = IsProductInstalled -productId $msiProductId - - return $msiPackageInstalled -} - -<# -.Synopsis - Name: IsProductInstalled - Description: Determines if the given product is installed using its ProductId. - -.Parameters - productId: The ProductId of the product. - -.Returns - $true in case the product is installed, otherwise $false. -#> -function IsProductInstalled -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $productId) - - $productInstalled = $false - - $installedProductInfo = Get-InstalledProductRegistryEntry -productId $productId - - if($null -ne $installedProductInfo) { - - $productInstalled = $true - } - - return $productInstalled -} - -<# -.Synopsis - Name: Get-InstalledProductRegistryEntry - Description: Retrieves the registry entry of the installed product. - -.Parameters - productId: The productID of the installed product. Should be a GUID. - -.Returns - The installed product's registry entry. -#> -function Get-InstalledProductRegistryEntry -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $productId) - - # Ensure the the provided ProductId is a Guid and surrounds it by curley brackets. - $productId = Format-ProductId -productId $productId - - $installedProductRegKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0}" -f $productId - - $installedProductWoW64RegKey = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{0}" -f $productId - - $installedProductInfo = Get-Item -Path $installedProductRegKey -ErrorAction SilentlyContinue - - if ($null -eq $installedProductInfo) - { - $installedProductInfo = Get-Item -Path $installedProductWoW64RegKey -ErrorAction SilentlyContinue - } - - return $installedProductInfo -} - -<# -.Synopsis - Name: Get-InstalledProductProperties - Description: Retrieves the installed product's properties and values from system registry. - -.Parameters - productId: The productID of the installed product. Should be a GUID. - -.Returns - The installed product's properties and values. -#> -function Get-InstalledProductProperties -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $productId) - - $installedProductInfo = Get-InstalledProductRegistryEntry -productId $productId - - if ($null -eq $installedProductInfo) - { - throw "The Product is not installed. ProductId = {0}" -f $productId - } - - $size = $installedProductInfo.GetValue("EstimatedSize") - if ($size) - { - $size = $size / 1024 - } - - $name = $installedProductInfo.GetValue("DisplayName_Localized") - - if (-not $name) - { - $name = $installedProductInfo.GetValue("DisplayName") - } - - $publisher = $installedProductInfo.GetValue("Publisher_Localized") - - if (-not $publisher) - { - $publisher = $installedProductInfo.GetValue("Publisher") - } - - $version = $installedProductInfo.GetValue("DisplayVersion") - - $packageDescription = $installedProductInfo.GetValue("Comments") - - $installedOn = $installedProductInfo.GetValue("InstallDate") - - if ($installedOn) - { - try - { - $installedOn = "{0:d}" -f [DateTime]::ParseExact($installedOn, "yyyyMMdd", [System.Globalization.CultureInfo]::CurrentCulture).Date - } - catch - { - $installedOn = $null - } - } - - return @{ - Name = $name - InstalledOn = $installedOn - ProductId = Format-ProductId -productId $productId - Size = $size - Installed = $true - Version = $version - PackageDescription = $packageDescription - Publisher = $publisher - } -} - -<# -.Synopsis - Name: Format-ProductId - Description: Ensures that the productId is a GUID. Surrounds ProductId within curley brackets. - -.Parameters - productId: The productID of the installed product. Should be a GUID. - -.Returns - The formatted ProductId which is a GUID surrounded by curley brackets. -#> -function Format-ProductId -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $productId) - - return [Guid]::Parse($productId).ToString("B").ToUpper() -} - -<# -.Synopsis - Name: InstallMsiPackage - Description: Installs the given MSI package. - -.Parameters - msiFilePath: The path to the MSI package. -#> -function InstallMsiPackage -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $msiFilePath) - - # - - # Check if MSI file exists in the given path. - - # - - if (-not (Test-Path $msiFilePath)) - { - throw "Could not find MSI file path " + $msiFilePath - } - - # Execute msiexec to install an MSI package. - - # - - $msiProcess = Start-Process -Wait -PassThru msiexec.exe -ArgumentList "/i $msiFilePath", /passive, /quiet - - $msiProcess.ExitCode -} - -<# -.Synopsis - Name: UnInstallMsiPackage - Description: UnInstalls the given MSI package. - -.Parameters - msiFilePath: The path to the MSI package. -#> -function UnInstallMsiPackage -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $msiFilePath) - - # - - # Check if MSI file exists in the given path. - - # - - if (-not (Test-Path $msiFilePath)) - { - throw "Could not find MSI file path " + $msiFilePath - } - - # - - # Get MSI ProductID - - # - - $msiProductId = GetMsiProductId -msiFilePath $msiFilePath - - # - - # Execute msiexec to uninstall an MSI package given its ProductId. - - # - - $exitCode = UnInstallProduct -productId $msiProductId - - return $msiProcess.ExitCode -} - -<# -.Synopsis - Name: InstallExePackage - Description: Installs the given EXE package. - -.Parameters - exeSetupFilePath: The path to the exe setup package. - arguments: The arguments to pass to the exe installer. -#> -function InstallExePackage -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $exeSetupFilePath, - - [ValidateNotNullOrEmpty()] - [String]$arguments) - - # - - # Check if exe setup file exists in the given path. - - # - - if (-not (Test-Path $exeSetupFilePath)) - { - throw "Could not find exe setup file path " + $exeSetupFilePath - } - - # Execute the Exe setup. - - # - - $msiProcess = Start-Process -Wait -PassThru $exeSetupFilePath -ArgumentList $arguments - - $msiProcess.ExitCode -} - -<# -.Synopsis - Name: UnInstallProduct - Description: UnInstalls the product given its ProductId. MsiExec is used for uninstalling the product. - -.Parameters - productId: The ProductId of the product to be uninstalled. - -.Return - The exist code of msiexec process. -#> -function UnInstallProduct -{ - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] $productId) - - $msiProcess = Start-Process -Wait -PassThru msiexec.exe -ArgumentList "/x$productId", /passive, /quiet - - return $msiProcess.ExitCode -} - -<# -.Synopsis - Name: InstallNetMon - Description: Installs the NetMon product. -#> -function InstallNetMon -{ - InstallExePackage -exeSetupFilePath $global:exeInstallerNetMon -arguments "/Q" -} - -<# -.Synopsis - Name: UnInstallNetMon - Description: UnInstalls the NetMon sample product from the machine. -#> -function UnInstallNetMon -{ - # NetMon installer installs two products, lets uninstall both of these products: - - UnInstallProduct -productId $global:productIdNetMon - - UnInstallProduct -productId $global:productIdNetMonParser -} - -<# -.Synopsis - Name: EmptyDirectory - Description: Removes all the contents of a directory. - -.Parameters - $directoryName: The name of a directory to be emptied. -#> -function EmptyDirectory([String]$directoryName) -{ - Remove-Item $directoryName -Recurse -Force -ErrorAction SilentlyContinue -} - -<# -.Synopsis - Name: ClearCacheDirectory - Description: Deletes the contents of the PAckage Provider's cache directory. -#> -function ClearCacheDirectory -{ - EmptyDirectory -directoryName $global:packageProviderCacheDirectory -} - -<# -.Synopsis - Name: EnsureIsEmptyOrNull - Description: Determines if the value of Ensure is empty, null or white space. -#> -function EnsureIsEmptyOrNull([String]$ensureValue) -{ - if ([String]::IsNullOrEmpty($ensureValue) -or [String]::IsNullOrWhiteSpace($ensureValue)) - { - return $true - } - - return $false -} - -<# -.Synopsis - Name: PathsAreEqual - Description: Determines if the two paths are equal or not. -#> -function PathsAreEqual([String]$firstPath, [String]$secondPath) -{ - [bool]$pathsAreEqual = $false; - - # if path is not a web URL. - if( (([uri]$firstPath).Scheme -eq "file")) - { - $fullFirstPath = [System.IO.Path]::GetFullPath($firstPath) - - $fullSecondPath = [System.IO.Path]::GetFullPath($secondPath) - - if (0 -eq [String]::Compare($fullFirstPath, $fullSecondPath, [StringComparison]::OrdinalIgnoreCase)) - { - - $pathsAreEqual = $true; - } - } - else # if path is a web URL. - { - if(([uri]$firstPath -eq [uri]$secondPath)) - { - $pathsAreEqual = $true; - } - } - - return $pathsAreEqual; -} - -<# -.Synopsis - Name: CreateDirectory - Description: Creates directory recursively. - -.Parameters - $directoryName: The name of a directory to be created recursively. -#> -function CreateDirectory([String]$directoryName) -{ - New-Item -Path $directoryName -type directory -Force -erroraction Silentlycontinue | Out-Null -} - -<# -.Synopsis - Name: PrepareWebDirectory - Description: Creates a Tools directory within IIS default site. So tests can use URL to download and install tools. -#> -function PrepareWebDirectory -{ - CreateDirectory($global:webDirectory) - - # copy mita in WebDirectory - CopyFileInWebDirectory -fileName $global:msiInstallerMita -webDirectoryName "$global:webDirectory" - - # copy NetMon in WebDirectory - CopyFileInWebDirectory -fileName $global:exeInstallerNetMon -webDirectoryName "$global:webDirectory" -} - -<# -.Synopsis - Name: CopyFileInWebDirectory - Description: Copies a file to the Tools directory within IIS default site. So tests can use URL to download and install tools. -#> -function CopyFileInWebDirectory([String]$fileName, [String]$webDirectoryName) -{ - Copy-Item -Path $fileName -Destination $webDirectoryName -Force | out-null -} - - -<# -.Synopsis - Name: GetMitaUrl - Description: Returns the web URL from where Mita setup can be downloaded. -#> -function GetMitaUrl -{ - $mitaFileName = split-path $global:msiInstallerMita -Leaf - - $toolsDirectoryName = split-Path $global:webDirectory -Leaf - - $mitaUrl = "http://$env:computerName/$toolsDirectoryName/$mitaFileName" - - return $mitaUrl -} - -<# -.Synopsis - Name: GetNetMonUrl - Description: Returns the web URL from where NetMon setup can be downloaded. -#> -function GetNetMonUrl -{ - $netMonFileName = split-path $global:exeInstallerNetMon -Leaf - - $toolsDirectoryName = split-Path $global:webDirectory -Leaf - - $netMonUrl = "http://$env:computerName/$toolsDirectoryName/$netMonFileName" - - return $netMonUrl -} - -<# -.Synopsis - Name: MachineRequiresReboot - Description: Returns true in case the machine requires reboot. Otherwise, false. -#> -function MachineRequiresReboot -{ - $rebootRequired = $false - - $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks - - $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore - - if(($featureData -and $featureData.RequiresReboot) -or $regData) - { - $rebootRequired = $true - } - - return $rebootRequired -} - -<# -.Synopsis - Name: ExecuteStartDscConfiguration - Description: Invokes the DSC engine to apply a configuration. - -.Parameters - path: The location where configuration mof files exists. - computerName: The name of computer where configuration is supposed to be applied. -#> -function ExecuteStartDscConfiguration -{ - param ( - [System.String] $path, - [System.String[]] $computerName) - - Write-Host $path - Write-Host $computerName - - $result = Start-DscConfiguration -Path $path -ComputerName $computerName -Verbose -Wait -Force -} - -<# -.Synopsis - Name: ExecuteGetDscConfiguration - Description: Invokes the DSC engine to get the configuration that was recently applied. - -.Parameters - cimSession: The CIM session to remote machine. - -.Output - The result of the Get-DSCConfiguration command. -#> -function ExecuteGetDscConfiguration -{ - param ( - [Microsoft.Management.Infrastructure.CimSession]$cimSession) - - $result = $null - - if(-not $cimSession) - { - $result = Get-DscConfiguration - } - else - { - $result = Get-DscConfiguration -CimSession $cimSession - } - - return $result -} - -<# -.Synopsis - Name: ExecuteTestDscConfiguration - Description: Invokes the DSC engine to get the configuration that was recently applied. - -.Parameters - cimSession: The CIM session to remote machine. - -.Output - The result of the Test-DSCConfiguration command. -#> -function ExecuteTestDscConfiguration -{ - param ( - [Microsoft.Management.Infrastructure.CimSession]$cimSession) - - $result = $null - - if(-not $cimSession) - { - $result = Test-DscConfiguration - } - else - { - $result = Test-DscConfiguration -CimSession $cimSession - } - - return $result -} - -<# -.Synopsis - Name: CreatePackageInstanceMof - Description: Creates the instance document for Package Provider E2E tests. - -.Parameters - configurationName: The configuration name. - resourceId: The resource id. - ensure: The value to be used for 'Ensure' parameter. -#> -function CreatePackageInstanceMof -{ - param - ( - [String]$configurationName, - [String]$resourceId, - [String]$path, - [String]$productId, - [String]$name, - [String]$arguments, - [String]$ensure, - - [String]$UserName, - [String]$Password - ) - - Log -message @" -Calling CreatePackageInstanceMof with parameters -configurationName=$configurationName -resourceId=$resourceId -path=$path -product=$productId -name=$name -arguments=$arguments -ensure=$ensure -UserName=$UserName -Password=$Password -"@ - - $scriptText = @' - -#Configuration script that uses Package resource - -if ("{8}" -ne [String]::Empty) -{{ - configuration {0} - {{ - - node ("localhost") - {{ - Package PACKAGE1 - {{ - Path = "{3}" - ProductId = "{4}" - Name = "{5}" - Arguments = "{6} ALLUSERS=1" - Ensure = "{7}" - PsDscRunAsCredential = New-Object System.Management.Automation.PSCredential -ArgumentList '{8}', (ConvertTo-SecureString -AsPlainText '{9}' -Force) - LogPath = "{1}\{2}.log" - }} - }} - }} -}} -else -{{ - configuration {0} - {{ - - node ("localhost") - {{ - Package PACKAGE1 - {{ - Path = "{3}" - ProductId = "{4}" - Name = "{5}" - Arguments = "{6}" - Ensure = "{7}" - LogPath = "{1}\{2}.log" - }} - }} - }} -}} - -$Global:AllNodes= -@{{ - AllNodes = @( - @{{ - NodeName = "localhost"; - RecurseValue = $true; - PSDscAllowPlainTextPassword = $true; - }}; - ); -}} - -{0} -output "{1}\{2}" -ConfigurationData $Global:AllNodes - -'@ - - # Create the DSC configuration script. - $script = $scriptText -f $configurationName, $global:end2EndScriptPath, $resourceId, $path, $productId, $name, $arguments, $ensure, $UserName, $Password - - # Save the script in *.ps1 file ( this helps in debugging ). - CreateDirectory -directoryName "$global:end2EndScriptPath\Config" - - $script > "$global:end2EndScriptPath\Config\$resourceId.ps1" - - # Create the instance document. - $scriptBlock = [ScriptBlock]::Create($script) - Invoke-Command -ScriptBlock $scriptBlock -Verbose - -} - -<############################################################################################ - # This section contains helper methods that will be used by Package Provider's DRTs. - ############################################################################################> - -$FolderCount = 0 - -<# +<# .SYNOPSIS Clears the xPackage cache. #> @@ -960,15 +37,16 @@ function Test-PackageInstalled return $null -ne $packageWithName } -Function Get-InstalledById -{ - param($Id) - return Get-CimInstance Win32_Product | Where {$_.IdentifyingNumber.Equals($Id)} -} - <# .SYNOPSIS - Mimics a simple http/https server. + Mimics a simple http or https file server. + + .PARAMETER FilePath + The path to the file to add on the mock file server. + + .PARAMETER Https + Indicates that the new file server should use https. + Otherwise the new file server will use http. #> function New-MockFileServer { @@ -978,7 +56,10 @@ function New-MockFileServer [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] - $FilePath + $FilePath, + + [Switch] + $Https ) if ($null -eq (Get-NetFirewallRule -DisplayName 'UnitTestRule' -ErrorAction 'SilentlyContinue')) @@ -1011,8 +92,16 @@ function New-MockFileServer # Start listening endpoints $httpListener = New-Object -TypeName 'System.Net.HttpListener' - $httpListener.Prefixes.Add([Uri]'https://localhost:1243/') - $httpListener.Prefixes.Add([Uri]'http://localhost:1242') + + if ($Https) + { + $httpListener.Prefixes.Add([Uri]'https://localhost:1243') + } + else + { + $httpListener.Prefixes.Add([Uri]'http://localhost:1242') + } + $httpListener.AuthenticationSchemes = [System.Net.AuthenticationSchemes]::Negotiate $httpListener.Start() @@ -1054,40 +143,30 @@ function New-MockFileServer netsh advfirewall set allprofiles state on } -$share = $null -Function Share-ScriptFolder +<# + .SYNOPSIS + Creates a new test executable. + + .PARAMETER DestinationPath + The path at which to create the test executable. +#> +function New-TestExecutable { - if($share) - { - return $share - } - - $shareName = "DSCUnitTestShare" - if(-not (Get-NetFirewallRule FPS-SMB-In-TCP).Enabled) - { - Enable-NetFirewallRule FPS-SMB-In-TCP - } + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String]$DestinationPath + ) - if((Get-SmbShare -EA SilentlyContinue $shareName)) + if (Test-Path -Path $DestinationPath) { - Remove-SmbShare $shareName -Force + Write-Verbose -Message "Removing old executable at $DestinationPath..." + Remove-Item -Path $DestinationPath -Force } - - $script:share = New-SmbShare -Name $shareName -Path $PSScriptRoot - return $share -} -$setupExe = $null -Function Get-SetupExe -{ - if($setupExe) - { - return $setupExe - } - - $setupExe = "$PSScriptRoot\DummySetupProgram.exe" - rm $setupExe -EA SilentlyContinue #Kill any leftover from last unit test invocation - $sig = @' + $testExecutableCode = @' using System; using System.Collections.Generic; using System.Linq; @@ -1119,9 +198,8 @@ Function Get-SetupExe } } '@ - Add-Type $sig -OutputAssembly $setupExe -OutputType ConsoleApplication - $script:setupExe = $setupExe - return $setupExe + + Add-Type -TypeDefinition $testExecutableCode -OutputAssembly $DestinationPath -OutputType 'ConsoleApplication' } <# @@ -1669,4 +747,5 @@ Export-ModuleMember -Function ` New-TestMsi, ` Clear-xPackageCache, ` Test-PackageInstalled, ` - New-MockFileServer + New-MockFileServer, ` + New-TestExecutable diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 8d6e92089..601413148 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -1,16 +1,29 @@ -$testEnvironment = Initialize-TestEnvironment ` - -DSCModuleName 'xPSDesiredStateConfiguration' ` - -DSCResourceName 'MSFT_xPackageResource' ` - -TestType 'Unit' +Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force -InModuleScope 'MSFT_xPackageResource' { - Describe 'MSFT_xPackageResource Unit Tests' { +$script:dscResourceModuleName = 'xPSDesiredStateConfiguration' +$script:dscResourceName = 'MSFT_xPackageResource' +$script:testType = 'Unit' + +Initialize-DscResourceUnitTest ` + -DscResourceModuleName $script:dscResourceModuleName ` + -DscResourceName $script:dscResourceName ` + -TestType $script:testType ` +| Out-Null + +InModuleScope "$script:dscResourceName" { + Describe "$script:dscResourceName $script:testType Tests" { BeforeAll { - Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force + $script:skipHttpsTest = $true + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null $script:msiName = 'DSCSetupProject.msi' @@ -21,6 +34,9 @@ InModuleScope 'MSFT_xPackageResource' { New-TestMsi -DestinationPath $script:msiLocation | Out-Null + $script:testExecutablePath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestExecutable.exe' + New-TestExecutable -DestinationPath $script:testExecutablePath | Out-Null + Clear-xPackageCache | Out-Null } @@ -40,7 +56,10 @@ InModuleScope 'MSFT_xPackageResource' { } AfterAll { - Remove-Item -Path $script:testDirectoryPath -Recurse | Out-Null + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } Clear-xPackageCache | Out-Null @@ -92,7 +111,7 @@ InModuleScope 'MSFT_xPackageResource' { } It 'Should return correct value when package is present' { - Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([string]::Empty) + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) Clear-xPackageCache @@ -194,10 +213,10 @@ InModuleScope 'MSFT_xPackageResource' { $pipe.Dispose() } - It 'Should correctly install and remove a package from a HTTPS URL' -Pending { + It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { $baseUrl = 'https://localhost:1243/' $msiUrl = "$baseUrl" + "package.msi" - New-MockFileServer -FilePath $script:msiLocation + New-MockFileServer -FilePath $script:msiLocation -Https # Test pipe connection as testing server readiness $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) @@ -219,6 +238,12 @@ InModuleScope 'MSFT_xPackageResource' { It 'Should write to the specified log path' { $logPath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestMsiLog.txt' + + if (Test-Path -Path $logPath) + { + Remove-Item -Path $logPath -Force + } + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -Name $script:packageName -LogPath $logPath -ProductId ([string]::Empty) Test-Path -Path $logPath | Should Be $true @@ -226,150 +251,26 @@ InModuleScope 'MSFT_xPackageResource' { } } - Context 'Test-TargetResource' { - It 'TestLocalSetupExeInstall' -Pending { - # $exePath = Get-SetupExe - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is installed when it is not" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is installed when it is not" - # } - - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is installed when it is not when queried by ID" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is installed when it is not when queried by ID" - # } - - # $logPath = "$PSScriptRoot\TestLocalSetupExeInstall.log" - # Set-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Arguments "DUMMYFLAG=MYEXEVALUE" -LogPath $logPath -Verbose - - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is missing when it is not" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is missing when it is not" - # } - - # $res = Test-TargetResource -Ensure "Present" -Path $exePath -ProductId $PackageId -Verbose - # if(-not $res) - # { - # throw "Erroneously believe EXE is missing when it is not when queried by ID" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $exePath -ProductId $PackageId -Verbose - # if($res) - # { - # throw "Erroneously believe EXE is missing when it is not when queried by ID" - # } - - # $content = Get-Content $logPath - # if(-not $content -or -not $content.Contains("DUMMYFLAG=MYEXEVALUE")) - # { - # throw "Process output not appropriately captured - the expected data was not present" - # } - - # #Unit tests can be run on x86 Client SKU - # $item = Get-Item -EA Ignore HKLM:\SOFTWARE\DSCTest - # if(-not $item) - # { - # $item = Get-Item HKLM:\SOFTWARE\Wow6432Node\DSCTest - # } - - # $debugEntry = $item.GetValue("DebugEntry") - # if($debugEntry -ne "DUMMYFLAG=MYEXEVALUE") - # { - # throw "The registry key created by the package does not have the flag set appropriately. The provider likely did not pass the arguments correctly" - # } - } - - It 'TestMSIOverUncPath' -Pending { - # $share = Share-ScriptFolder - # $shareName = $share.Name - # $sharePath = "\\localhost\$shareName" - # $uncMsiPath = Join-Path $sharePath $MsiName - - # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously belive package already exists when accessed over UNC" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously belive package already exists when accessed over UNC (Ensure=Absent case)" - # } - - # Set-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not (Is-NameInstalled $PackageName)) - # { - # throw "Failed to install the package" - # } - - # $res = Test-TargetResource -Ensure "Present" -Path $uncMsiPath -Name $PackageName -Verbose - # if(-not $res) - # { - # throw "Erroneously belive package is missing when accessed over UNC" - # } - - # $res = Test-TargetResource -Ensure "Absent" -Path $uncMsiPath -Name $PackageName -Verbose - # if($res) - # { - # throw "Erroneously belive package is missing when accessed over UNC (Ensure=Absent case)" - # } - } - } - Context 'Get-MsiTools' { It 'Should add MSI tools in the Microsoft.Windows.DesiredStateConfiguration.xPackageResource namespace' { $addTypeResult = @{ Namespace = 'Mock not called' } - Mock Add-Type { $addTypeResult['Namespace'] = $Namespace } - - Get-MsiTools | Out-Null - - $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' - } - } + Mock -CommandName 'Add-Type' -MockWith { $addTypeResult['Namespace'] = $Namespace } - Context 'Get-RegistryValueIgnoreError' { - It 'Should retrieve the correct value from the HKLM registry' { - $registryValue = Get-RegistryValueIgnoreError ` - -RegistryHive 'LocalMachine' ` - -Key 'SOFTWARE\Microsoft\Windows\CurrentVersion' ` - -Value 'ProgramFilesDir' ` - -RegistryView 'Registry64' + $msiTools = Get-MsiTools + + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + Assert-MockCalled -CommandName 'Add-Type' -Times 0 - $registryValue | Should Be $env:programFiles - } + $msiTools | Should Be ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + Assert-MockCalled -CommandName 'Add-Type' -Times 1 - It 'Should retrieve the correct value from the HKCU registry' { - $registryValue = Get-RegistryValueIgnoreError ` - -RegistryHive 'CurrentUser' ` - -Key 'Environment' ` - -Value 'Temp' ` - -RegistryView 'Registry64' - - # Comparing $installValue with $env:temp may fail if the username is longer than 8 characters - $registryValue.Length -gt 3 | Should Be $true - $registryValue | Should Match $env:username + $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' + $msiTools | Should Be $null + } } } } From bee8f3970a2184c3f96e9c1ecfa21344de8030bc Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 15:48:16 -0700 Subject: [PATCH 08/49] Updating README. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 685d18ad5..9cd428e06 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ These parameters will be the same for each Windows optional feature in the set. Fix bug when credential parameter passed does not contain local or domain context. * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. * Updated appveyor.yml to use the default image. +* Merged xPackage with in-box Package resource and added tests. ### 3.12.0.0 From 044c3e49a45a753e7488561b2ec0e9d0307c40b1 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 16:26:54 -0700 Subject: [PATCH 09/49] Updating mof to match resource schema. --- .../MSFT_xPackageResource.psm1 | 25 ++++++++++++++---- .../MSFT_xPackageResource.schema.mof | Bin 1237 -> 1814 bytes 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 97629385a..403806857 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -261,11 +261,29 @@ function Test-TargetResource [PSCredential] $Credential, + # Return codes 1641 and 3010 indicate success when a restart is requested per installation + [ValidateNotNullOrEmpty()] [Int[]] - $ReturnCode, + $ReturnCode = @( 0, 1641, 3010 ), + + [String] + $LogPath, [String] - $LogPath + $FileHash, + + [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', 'RIPEMD160')] + [String] + $HashAlgorithm, + + [String] + $SignerSubject, + + [String] + $SignerThumbprint, + + [String] + $ServerCertificateValidationCallback ) Assert-PathExtensionValid -Path $Path @@ -608,9 +626,6 @@ function Set-TargetResource [String] $LogPath, - - [Boolean] - $CreateCheckRegValue, [String] $FileHash, diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 57b72c3e4ad9bc647626016b372e0be8d8623038..464e70e6d0ab4ff0fdc80f5d02891036081a16e5 100644 GIT binary patch literal 1814 zcmchYTW`}q5QWb(692*S6G#ziQ;HBgg_Md4LaV0YrK(Vz7-LABQaeCF{p&5?tYgJn z+g1C5B42hhXD(;Xj&H6{HPl=a<@`P=)&5L%w~*Y2DT`{(+CwjDIHfu*<36)NxKO zjOf0w5fjLyO2^x018{cs!R=b)`4hj7w`^%)xz?GKnPYri-C{OH0&u())Ym=*%` zXXnF|QRP=h`u+6d?US8V^bV|X(vEAN0%K4E=9``WkcnigzIOG(+u6}eJ@_fxcP>++Nh*9wbx+E=Q~ M`fB~OuSj+M1G}ClE&u=k literal 1237 zcmbVMQE!_t5Ps)ZSUj~7s*-f6rsau}M2S|`DCznVMU^o(@M=tUw!5`e|ND$dfG%mL zjR&yqyE}h(_Z{9k5>%;MK*dXW;`h&bXFdNkm?8(6ipMXsfD`}Mg0g#>fq%Lo9;*UK zyqezJ?d?lYr9lSd6S%Dw%DFt3I+9Sx-(y)t4Q^*4QT45W}TDACM(zgEA^pSZ})UF7~R-8omg$ zTZL=?7!*Uk)pk-Y`n8Wq1XnrWA>?=q0qJ!&ZpUfGpZ2}$+_)1&Cs$II0;nWSvbKPK zYS7ZiP#y2mt_@QzU`ACvw?CD$nuUEo@a<W7A3xNGVSiu`@px$8%lTpwkNX!r zzg5 Date: Mon, 11 Jul 2016 16:35:57 -0700 Subject: [PATCH 10/49] Updating xPackage schema in README. --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9cd428e06..869180720 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,6 @@ For a complete list of properties, please use Get-DscResource ### xPackage -For a complete list, please use Get-DscResource. - * **Ensure**: Ensures that the package is **Present** or **Absent**. * **Name**: The name of the package. * **Path**: The source path of the package. @@ -115,17 +113,20 @@ For a complete list, please use Get-DscResource. * **Credential**: PSCredential needed to access Path. * **ReturnCode**: An array of return codes that are returned after a successful installation. * **LogPath**: The destination path of the log. +* **FileHash**: The hash that should match the hash of the package file. +* **HashAlgorithm**: The algorithm to use to get the hash of the package file. + - Supported values: SHA1, SHA256, SHA384, SHA512, MD5, RIPEMD160 +* **SignerSubject**: The certificate subject that should match that of the package file's signing certificate. +* **SignerThumbprint**: The certificate thumbprint that should match that of the package file's signing certificate. +* **ServerCertificateValidationCallback**: A callback function to validate the server certificate. + +Read-Only Properties: * **PackageDescription**: A text description of the package being installed. * **Publisher**: Publisher's name. * **InstalledOn**: Date of installation. * **Size**: Size of the installation. * **Version**: Version of the package. * **Installed**: Is the package installed? -* **RunAsCredential**: Credentials to use when installing the package. -* **InstalledCheckRegKey**: Registry key to open to check for package installation status. -* **InstalledCheckRegValueName**: Registry value name to check for package installation status. -* **InstalledCheckRegValueData**: Value to compare against the retrieved value to check for package installation. -* **CreateCheckRegValue**: Creates the InstallCheckRegValueName registry value/data after successful package installation. ### xGroup From ea13bc5e384bafa5acb8dbeeaa84482f84be33e9 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 16:48:25 -0700 Subject: [PATCH 11/49] Reverting ReturnCode back to a UInt32 to match schema. --- DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 403806857..4759ee095 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -263,7 +263,7 @@ function Test-TargetResource # Return codes 1641 and 3010 indicate success when a restart is requested per installation [ValidateNotNullOrEmpty()] - [Int[]] + [UInt32[]] $ReturnCode = @( 0, 1641, 3010 ), [String] @@ -621,7 +621,7 @@ function Set-TargetResource # Return codes 1641 and 3010 indicate success when a restart is requested per installation [ValidateNotNullOrEmpty()] - [Int[]] + [UInt32[]] $ReturnCode = @( 0, 1641, 3010 ), [String] From 371691a45e61eb7ca4d037ccc7f9c1e7b02ad7ba Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 16:51:50 -0700 Subject: [PATCH 12/49] Converting mof from Unicode to ASCII. --- .../MSFT_xPackageResource.schema.mof | Bin 1814 -> 906 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 464e70e6d0ab4ff0fdc80f5d02891036081a16e5..89ac4017dd37e593e0c6c47043fcf54b3f7b9835 100644 GIT binary patch literal 906 zcmbVLO;6)65WQFGf3SQ4DMD@8A_S+BwxU|HL=ECnRmh2l)ZjQ}Z0{~0{yWYGX$s+j zq9mhv^TsbTFDqX%t#1Lf$khQI9u1BL^w6850LA6=AC|!Z{U5UAk)=Qn*QDP|3I++H z`OV!2umqi3On{t`>tJzr!8ClN`n}hFkC4?r6b8IoCM^VP`$|Iu4a!iD&`DfIZxyOL zu{F^Kg-XdxX^U`P_WTXcO}o^FBO zEQ^72;0vViu5#osTou@rg(*nySer38Mx$E<}#;-#5l>(tsG?RIgVOJIsL{kpzm zE+kwsT~GhB1)|Hzkb2a;M&pwzI6nPV1>@n!1;KRep36lz3#P-90o_^T2i#^ig`$Z} zbLYrS)+IXSA!5L%w~*Y2DT`{(+CwjDIHfu*<36)NxKO zjOf0w5fjLyO2^x018{cs!R=b)`4hj7w`^%)xz?GKnPYri-C{OH0&u())Ym=*%` zXXnF|QRP=h`u+6d?US8V^bV|X(vEAN0%K4E=9``WkcnigzIOG(+u6}eJ@_fxcP>++Nh*9wbx+E=Q~ M`fB~OuSj+M1G}ClE&u=k From 6ec5df8b7e23797c132a77b39a242e4a8b048354 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 17:08:56 -0700 Subject: [PATCH 13/49] Replacing error throw with Should block in tests. --- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 601413148..b76d6fa8b 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -45,7 +45,7 @@ InModuleScope "$script:dscResourceName" { if (Test-PackageInstalled -Name $script:packageName) { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$PackageId", '/passive') -Wait | Out-Null + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null Start-Sleep -Seconds 1 | Out-Null } @@ -65,7 +65,7 @@ InModuleScope "$script:dscResourceName" { if (Test-PackageInstalled -Name $script:packageName) { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$PackageId", '/passive') -Wait | Out-Null + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null Start-Sleep -Seconds 1 | Out-Null } @@ -115,10 +115,7 @@ InModuleScope "$script:dscResourceName" { Clear-xPackageCache - if (-not (Test-PackageInstalled -Name $script:packageName)) - { - throw 'Failed to install the package' - } + Test-PackageInstalled -Name $script:packageName | Should Be $true $testTargetResourceResult = Test-TargetResource ` -Ensure 'Present' ` From eb00366a06770076329dc989cce88bcd2bc095cf Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 11 Jul 2016 17:15:42 -0700 Subject: [PATCH 14/49] Fixing variables that Pester doesn't like. --- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index b76d6fa8b..bc1710272 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -10,8 +10,8 @@ Initialize-DscResourceUnitTest ` -TestType $script:testType ` | Out-Null -InModuleScope "$script:dscResourceName" { - Describe "$script:dscResourceName $script:testType Tests" { +InModuleScope 'MSFT_xPackageResource' { + Describe 'MSFT_xPackageResource Unit Tests" { BeforeAll { Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force From 5fe36abf588dca26edd0b0da8300891e884b3a91 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 09:17:16 -0700 Subject: [PATCH 15/49] Fixing messed up quote. --- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index bc1710272..de5d6f5b3 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -11,7 +11,7 @@ Initialize-DscResourceUnitTest ` | Out-Null InModuleScope 'MSFT_xPackageResource' { - Describe 'MSFT_xPackageResource Unit Tests" { + Describe 'MSFT_xPackageResource Unit Tests' { BeforeAll { Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force From 4dfabfe508e85b19741d822b180ff3adefb021a3 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 11:26:56 -0700 Subject: [PATCH 16/49] Adding test environment cleanup and fixing BatchSize warning. --- .../MSFT_xPackageResource.psm1 | 4 +- Tests/CommonTestHelper.psm1 | 59 ++- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 386 +++++++++--------- 3 files changed, 248 insertions(+), 201 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 4759ee095..50cfe8496 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -925,7 +925,7 @@ function Set-TargetResource } } - Write-Verbose -Message ($LocalizedData.StartingwithstartInfoFileNamestartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) + Write-Verbose -Message ($LocalizedData.StartingWithStartInfoFileNameStartInfoArguments -f $startInfo.FileName, $startInfo.Arguments) if ($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) { @@ -1014,7 +1014,7 @@ function Set-TargetResource missing on some client SKUs (worked on both Server and Client Skus in Windows 10). #> - $serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -ErrorAction 'Ignore' + $serverFeatureData = Invoke-CimMethod -Name 'GetServerFeature' -Namespace 'root\microsoft\windows\servermanager' -Class 'MSFT_ServerManagerTasks' -Arguments @{ BatchSize = 256 } -ErrorAction 'Ignore' $registryData = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction 'Ignore' if (($serverFeatureData -and $serverFeatureData.RequiresReboot) -or $registryData -or $exitcode -eq 3010 -or $exitcode -eq 1641) diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index f344784e0..0e987b0d8 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -557,10 +557,20 @@ function Test-IsFileLocked <# .SYNOPSIS - Initializes a DSC Resource unit test. + Enters a DSC Resource test environment. + + .PARAMETER DscResourceModuleName + The name of the module that contains the DSC Resource to test. + + .PARAMETER DscResourceName + The name of the DSC resource to test. + + .PARAMETER TestType + Specifies whether the test environment will run a Unit test or an Integration test. #> -function Initialize-DscResourceUnitTest +function Enter-DscResourceTestEnvironment { + [OutputType([PSObject])] [CmdletBinding()] param ( @@ -588,18 +598,48 @@ function Initialize-DscResourceUnitTest } else { - Push-Location "$PSScriptRoot\..\DSCResource.Tests" - git pull origin master --quiet - Pop-Location + $gitInstalled = $null -ne (Get-Command -Name 'git' -ErrorAction 'SilentlyContinue') + + if ($gitInstalled) + { + Push-Location "$PSScriptRoot\..\DSCResource.Tests" + git pull origin master --quiet + Pop-Location + } + else + { + Write-Verbose -Message "Git not installed. Leaving current DSCResource.Tests as is." + } } Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" -Force - Initialize-TestEnvironment ` + return Initialize-TestEnvironment ` -DSCModuleName $DscResourceModuleName ` -DSCResourceName $DscResourceName ` - -TestType $TestType ` - | Out-Null + -TestType $TestType +} + +<# + .SYNOPSIS + Exits the specified DSC Resource test environment. + + .PARAMETER TestEnvironment + The test environment to exit. +#> +function Exit-DscResourceTestEnvironment +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSObject]$TestEnvironment + ) + + Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" + + Restore-TestEnvironment -TestEnvironment $TestEnvironment } Export-ModuleMember -Function ` @@ -609,4 +649,5 @@ Export-ModuleMember -Function ` Test-User, ` Wait-ScriptBlockReturnTrue, ` Test-IsFileLocked, ` - Initialize-DscResourceUnitTest + Enter-DscResourceTestEnvironment, ` + Exit-DscResourceTestEnvironment diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index de5d6f5b3..460d35aea 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -4,271 +4,277 @@ $script:dscResourceModuleName = 'xPSDesiredStateConfiguration' $script:dscResourceName = 'MSFT_xPackageResource' $script:testType = 'Unit' -Initialize-DscResourceUnitTest ` +$script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName $script:dscResourceModuleName ` -DscResourceName $script:dscResourceName ` - -TestType $script:testType ` -| Out-Null + -TestType $script:testType -InModuleScope 'MSFT_xPackageResource' { - Describe 'MSFT_xPackageResource Unit Tests' { - BeforeAll { - Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force +try +{ + InModuleScope 'MSFT_xPackageResource' { + Describe 'MSFT_xPackageResource Unit Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force - $script:skipHttpsTest = $true + $script:skipHttpsTest = $true - $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' - if (Test-Path -Path $script:testDirectoryPath) - { - Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null - } + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } - New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null - $script:msiName = 'DSCSetupProject.msi' - $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName - $script:packageName = 'DSCUnitTestPackage' - $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' - New-TestMsi -DestinationPath $script:msiLocation | Out-Null + New-TestMsi -DestinationPath $script:msiLocation | Out-Null - $script:testExecutablePath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestExecutable.exe' - New-TestExecutable -DestinationPath $script:testExecutablePath | Out-Null + $script:testExecutablePath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestExecutable.exe' + New-TestExecutable -DestinationPath $script:testExecutablePath | Out-Null - Clear-xPackageCache | Out-Null - } + Clear-xPackageCache | Out-Null + } - BeforeEach { - Clear-xPackageCache | Out-Null + BeforeEach { + Clear-xPackageCache | Out-Null - if (Test-PackageInstalled -Name $script:packageName) - { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null - Start-Sleep -Seconds 1 | Out-Null - } + if (Test-PackageInstalled -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } - if (Test-PackageInstalled -Name $script:packageName) - { - throw 'Test output will not be valid - package could not be removed.' + if (Test-PackageInstalled -Name $script:packageName) + { + throw 'Package could not be removed.' + } } - } - AfterAll { - if (Test-Path -Path $script:testDirectoryPath) - { - Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null - } + AfterAll { + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } - Clear-xPackageCache | Out-Null + Clear-xPackageCache | Out-Null - if (Test-PackageInstalled -Name $script:packageName) - { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null - Start-Sleep -Seconds 1 | Out-Null - } + if (Test-PackageInstalled -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } - if (Test-PackageInstalled -Name $script:packageName) - { - throw 'Test output will not be valid - package could not be removed.' + if (Test-PackageInstalled -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } } - } - Context 'Test-TargetResource' { - It 'Should return correct value when package is absent' { - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Present' ` - -Path $script:msiLocation ` - -ProductId $script:packageId ` - -Name ([String]::Empty) + Context 'Test-TargetResource' { + It 'Should return correct value when package is absent' { + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) - $testTargetResourceResult | Should Be $false + $testTargetResourceResult | Should Be $false - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Present' ` - -Path $script:msiLocation ` - -Name $script:packageName ` - -ProductId ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) - $testTargetResourceResult | Should Be $false + $testTargetResourceResult | Should Be $false - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Absent' ` - -Path $script:msiLocation ` - -ProductId $script:packageId ` - -Name ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) - $testTargetResourceResult | Should Be $true + $testTargetResourceResult | Should Be $true - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Absent' ` - -Path $script:msiLocation ` - -Name $script:packageName ` - -ProductId ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) - $testTargetResourceResult | Should Be $true - } + $testTargetResourceResult | Should Be $true + } - It 'Should return correct value when package is present' { - Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + It 'Should return correct value when package is present' { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - Clear-xPackageCache + Clear-xPackageCache - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalled -Name $script:packageName | Should Be $true - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Present' ` - -Path $script:msiLocation ` - -ProductId $script:packageId ` - -Name ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) - $testTargetResourceResult | Should Be $true + $testTargetResourceResult | Should Be $true - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Present' ` - -Path $script:msiLocation ` - -Name $script:packageName ` - -ProductId ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Present' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) - $testTargetResourceResult | Should Be $true + $testTargetResourceResult | Should Be $true - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Absent' ` - -Path $script:msiLocation ` - -ProductId $script:packageId ` - -Name ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -ProductId $script:packageId ` + -Name ([String]::Empty) - $testTargetResourceResult | Should Be $false + $testTargetResourceResult | Should Be $false - $testTargetResourceResult = Test-TargetResource ` - -Ensure 'Absent' ` - -Path $script:msiLocation ` - -Name $script:packageName ` - -ProductId ([String]::Empty) + $testTargetResourceResult = Test-TargetResource ` + -Ensure 'Absent' ` + -Path $script:msiLocation ` + -Name $script:packageName ` + -ProductId ([String]::Empty) - $testTargetResourceResult | Should Be $false + $testTargetResourceResult | Should Be $false + } } - } - Context 'Set-TargetResource' { - It 'Should correctly install and remove a package' { - Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + Context 'Set-TargetResource' { + It 'Should correctly install and remove a package' { + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalled -Name $script:packageName | Should Be $true - $getTargetResourceResult = Get-TargetResource -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + $getTargetResourceResult = Get-TargetResource -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - $getTargetResourceResult.Version | Should Be '1.2.3.4' - $getTargetResourceResult.InstalledOn | Should Be ("{0:d}" -f [DateTime]::Now.Date) - $getTargetResourceResult.Installed | Should Be $true - $getTargetResourceResult.ProductId | Should Be $script:packageId - $getTargetResourceResult.Path | Should Be $script:msiLocation + $getTargetResourceResult.Version | Should Be '1.2.3.4' + $getTargetResourceResult.InstalledOn | Should Be ("{0:d}" -f [DateTime]::Now.Date) + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $script:packageId + $getTargetResourceResult.Path | Should Be $script:msiLocation - # Can't figure out how to set this within the MSI. - # $getTargetResourceResult.PackageDescription | Should Be 'A package for unit testing' + # Can't figure out how to set this within the MSI. + # $getTargetResourceResult.PackageDescription | Should Be 'A package for unit testing' - [Math]::Round($getTargetResourceResult.Size, 2) | Should Be 0.03 + [Math]::Round($getTargetResourceResult.Size, 2) | Should Be 0.03 - Set-TargetResource -Ensure 'Absent' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) + Set-TargetResource -Ensure 'Absent' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - Test-PackageInstalled -Name $script:packageName | Should Be $false - } + Test-PackageInstalled -Name $script:packageName | Should Be $false + } - It 'Should throw with incorrect product id' { - $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' + It 'Should throw with incorrect product id' { + $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' - { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $wrongPackageId -Name ([String]::Empty) } | Should Throw - } + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $wrongPackageId -Name ([String]::Empty) } | Should Throw + } - It 'Should throw with incorrect name' { - $wrongPackageName = 'WrongPackageName' + It 'Should throw with incorrect name' { + $wrongPackageName = 'WrongPackageName' - { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId ([String]::Empty) -Name $wrongPackageName } | Should Throw - } + { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId ([String]::Empty) -Name $wrongPackageName } | Should Throw + } - It 'Should correctly install and remove a package from a HTTP URL' { - $baseUrl = 'http://localhost:1242/' - $msiUrl = "$baseUrl" + "package.msi" - New-MockFileServer -FilePath $script:msiLocation + It 'Should correctly install and remove a package from a HTTP URL' { + $baseUrl = 'http://localhost:1242/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation - # Test pipe connection as testing server readiness - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.WaitForConnection() - $pipe.Dispose() + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() - { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw - Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $true + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $true - Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $false + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $false - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.Connect() - $pipe.Dispose() - } + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() + } - It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { - $baseUrl = 'https://localhost:1243/' - $msiUrl = "$baseUrl" + "package.msi" - New-MockFileServer -FilePath $script:msiLocation -Https + It 'Should correctly install and remove a package from a HTTPS URL' -Skip:$script:skipHttpsTest { + $baseUrl = 'https://localhost:1243/' + $msiUrl = "$baseUrl" + "package.msi" + New-MockFileServer -FilePath $script:msiLocation -Https - # Test pipe connection as testing server readiness - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) - $pipe.WaitForConnection() - $pipe.Dispose() + # Test pipe connection as testing server readiness + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeServerStream' -ArgumentList @( '\\.\pipe\dsctest1' ) + $pipe.WaitForConnection() + $pipe.Dispose() - { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw + { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw - Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $true + Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $true - Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $false + Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId + Test-PackageInstalled -Name $script:packageName | Should Be $false - $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) - $pipe.Connect() - $pipe.Dispose() - } + $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) + $pipe.Connect() + $pipe.Dispose() + } - It 'Should write to the specified log path' { - $logPath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestMsiLog.txt' + It 'Should write to the specified log path' { + $logPath = Join-Path -Path $script:testDirectoryPath -ChildPath 'TestMsiLog.txt' - if (Test-Path -Path $logPath) - { - Remove-Item -Path $logPath -Force - } + if (Test-Path -Path $logPath) + { + Remove-Item -Path $logPath -Force + } - Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -Name $script:packageName -LogPath $logPath -ProductId ([string]::Empty) + Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -Name $script:packageName -LogPath $logPath -ProductId ([string]::Empty) - Test-Path -Path $logPath | Should Be $true - Get-Content -Path $logPath | Should Not Be $null + Test-Path -Path $logPath | Should Be $true + Get-Content -Path $logPath | Should Not Be $null + } } - } - Context 'Get-MsiTools' { - It 'Should add MSI tools in the Microsoft.Windows.DesiredStateConfiguration.xPackageResource namespace' { - $addTypeResult = @{ Namespace = 'Mock not called' } - Mock -CommandName 'Add-Type' -MockWith { $addTypeResult['Namespace'] = $Namespace } + Context 'Get-MsiTools' { + It 'Should add MSI tools in the Microsoft.Windows.DesiredStateConfiguration.xPackageResource namespace' { + $addTypeResult = @{ Namespace = 'Mock not called' } + Mock -CommandName 'Add-Type' -MockWith { $addTypeResult['Namespace'] = $Namespace } - $msiTools = Get-MsiTools + $msiTools = Get-MsiTools - if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) - { - Assert-MockCalled -CommandName 'Add-Type' -Times 0 - - $msiTools | Should Be ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type - } - else - { - Assert-MockCalled -CommandName 'Add-Type' -Times 1 - - $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' - $msiTools | Should Be $null + if (([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type) + { + Assert-MockCalled -CommandName 'Add-Type' -Times 0 + + $msiTools | Should Be ([System.Management.Automation.PSTypeName]'Microsoft.Windows.DesiredStateConfiguration.xPackageResource.MsiTools').Type + } + else + { + Assert-MockCalled -CommandName 'Add-Type' -Times 1 + + $addTypeResult['Namespace'] | Should Be 'Microsoft.Windows.DesiredStateConfiguration.xPackageResource' + $msiTools | Should Be $null + } } } } } } +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} From 40b5732c1c05b60504abcd4615b5140cd3547006 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 12:25:08 -0700 Subject: [PATCH 17/49] Removing extra xPackage test and suppressing Get-CimInstance error. --- Tests/MSFT_xPackageResource.tests.ps1 | 49 ------------------- .../MSFT_xPackageResource.TestHelper.psm1 | 2 +- 2 files changed, 1 insertion(+), 50 deletions(-) delete mode 100644 Tests/MSFT_xPackageResource.tests.ps1 diff --git a/Tests/MSFT_xPackageResource.tests.ps1 b/Tests/MSFT_xPackageResource.tests.ps1 deleted file mode 100644 index 25b0df112..000000000 --- a/Tests/MSFT_xPackageResource.tests.ps1 +++ /dev/null @@ -1,49 +0,0 @@ -<# -.summary - Test suite for MSFT_xPackageResource.psm1 -#> -[CmdletBinding()] -param() - -Import-Module $PSScriptRoot\..\DSCResources\MSFT_xPackageResource\MSFT_xPackageResource.psm1 - -$ErrorActionPreference = 'stop' -Set-StrictMode -Version latest - - -function Suite.BeforeAll { - # Remove any leftovers from previous test runs - Suite.AfterAll - -} - -function Suite.AfterAll { - Remove-Module MSFT_xPackageResource -} - -function Suite.BeforeEach { -} - -try -{ - InModuleScope MSFT_xPackageResource { - Describe 'Get-RegistryValueIgnoreError' { - - It 'Should get values from HKLM' { - $installValue = Get-RegistryValueIgnoreError 'LocalMachine' "SOFTWARE\Microsoft\Windows\CurrentVersion" "ProgramFilesDir" Registry64 - $installValue | should be $env:programfiles - } - It 'Should get values from HKCU' { - $installValue = Get-RegistryValueIgnoreError 'CurrentUser' "Environment" "Temp" Registry64 - $installValue.length -gt 3 | should be $true - $installValue | should match $env:username - # comparing $installValue with $env:temp may fail if the username is longer than 8 characters - } - } - } -} -finally -{ - Suite.AfterAll -} - diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 index 93662a161..293b61bae 100644 --- a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -32,7 +32,7 @@ function Test-PackageInstalled $Name ) - $packageWithName = Get-CimInstance -ClassName 'Win32_Product' | Where-Object { $_.Name -ieq $Name } + $packageWithName = Get-CimInstance -ClassName 'Win32_Product' -ErrorAction 'SilentlyContinue' | Where-Object { $_.Name -ieq $Name } return $null -ne $packageWithName } From c2c208fd1e0b744240c244ef8b7ce4eb5273c60a Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 14:43:21 -0700 Subject: [PATCH 18/49] Removing use of Win32_Prodcut in attempt to reduce test time. --- .../MSFT_xPackageResource.psm1 | 3 +- .../MSFT_xPackageResource.TestHelper.psm1 | 60 ++++++++++++++++--- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 22 +++---- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 50cfe8496..a69c214f8 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -346,7 +346,8 @@ function Test-TargetResource function Get-LocalizedRegistryKeyValue { [CmdletBinding()] - param( + param + ( [Object] $RegistryKey, diff --git a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 index 293b61bae..6f87fbd09 100644 --- a/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xPackageResource.TestHelper.psm1 @@ -17,24 +17,34 @@ function Clear-xPackageCache .SYNOPSIS Tests if the package with the given name is installed. - .PARAMETER + .PARAMETER Name The name of the package to test for. #> -function Test-PackageInstalled +function Test-PackageInstalledByName { [OutputType([Boolean])] [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] [String] $Name ) - $packageWithName = Get-CimInstance -ClassName 'Win32_Product' -ErrorAction 'SilentlyContinue' | Where-Object { $_.Name -ieq $Name } + $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' + $uninstallRegistryKeyWow64 = 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + + $productEntry = $null + + foreach ($registryKeyEntry in (Get-ChildItem -Path @( $uninstallRegistryKey, $uninstallRegistryKeyWow64) -ErrorAction 'Ignore' )) + { + if ($Name -eq (Get-LocalizedRegistryKeyValue -RegistryKey $registryKeyEntry -ValueName 'DisplayName')) + { + $productEntry = $registryKeyEntry + break + } + } - return $null -ne $packageWithName + return ($null -ne $productEntry) } <# @@ -743,9 +753,43 @@ function New-TestMsi Set-Content -Path $DestinationPath -Value $msiContentInBytes -Encoding 'Byte' | Out-Null } +<# + .SYNOPSIS + Retrieves a localized registry key value. + + .PARAMETER RegistryKey + The registry key to retrieve the value from. + + .PARAMETER ValueName + The name of the value to retrieve. +#> +function Get-LocalizedRegistryKeyValue +{ + [CmdletBinding()] + param + ( + [Object] + $RegistryKey, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $ValueName + ) + + $localizedRegistryKeyValue = $RegistryKey.GetValue('{0}_Localized' -f $ValueName) + + if ($null -eq $localizedRegistryKeyValue) + { + $localizedRegistryKeyValue = $RegistryKey.GetValue($ValueName) + } + + return $localizedRegistryKeyValue +} + Export-ModuleMember -Function ` New-TestMsi, ` Clear-xPackageCache, ` - Test-PackageInstalled, ` New-MockFileServer, ` - New-TestExecutable + New-TestExecutable, ` + Test-PackageInstalledByName diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 460d35aea..43517d6d5 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -44,13 +44,13 @@ try BeforeEach { Clear-xPackageCache | Out-Null - if (Test-PackageInstalled -Name $script:packageName) + if (Test-PackageInstalledByName -Name $script:packageName) { Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null Start-Sleep -Seconds 1 | Out-Null } - if (Test-PackageInstalled -Name $script:packageName) + if (Test-PackageInstalledByName -Name $script:packageName) { throw 'Package could not be removed.' } @@ -64,13 +64,13 @@ try Clear-xPackageCache | Out-Null - if (Test-PackageInstalled -Name $script:packageName) + if (Test-PackageInstalledByName -Name $script:packageName) { Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null Start-Sleep -Seconds 1 | Out-Null } - if (Test-PackageInstalled -Name $script:packageName) + if (Test-PackageInstalledByName -Name $script:packageName) { throw 'Test output will not be valid - package could not be removed.' } @@ -116,7 +116,7 @@ try Clear-xPackageCache - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalledByName -Name $script:packageName | Should Be $true $testTargetResourceResult = Test-TargetResource ` -Ensure 'Present' ` @@ -156,7 +156,7 @@ try It 'Should correctly install and remove a package' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalledByName -Name $script:packageName | Should Be $true $getTargetResourceResult = Get-TargetResource -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) @@ -173,7 +173,7 @@ try Set-TargetResource -Ensure 'Absent' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) - Test-PackageInstalled -Name $script:packageName | Should Be $false + Test-PackageInstalledByName -Name $script:packageName | Should Be $false } It 'Should throw with incorrect product id' { @@ -201,10 +201,10 @@ try { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalledByName -Name $script:packageName | Should Be $true Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $false + Test-PackageInstalledByName -Name $script:packageName | Should Be $false $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) $pipe.Connect() @@ -224,10 +224,10 @@ try { Set-TargetResource -Ensure 'Present' -Path $baseUrl -Name $script:packageName -ProductId $script:packageId } | Should Throw Set-TargetResource -Ensure 'Present' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $true + Test-PackageInstalledByName -Name $script:packageName | Should Be $true Set-TargetResource -Ensure 'Absent' -Path $msiUrl -Name $script:packageName -ProductId $script:packageId - Test-PackageInstalled -Name $script:packageName | Should Be $false + Test-PackageInstalledByName -Name $script:packageName | Should Be $false $pipe = New-Object -TypeName 'System.IO.Pipes.NamedPipeClientStream' -ArgumentList @( '\\.\pipe\dsctest2' ) $pipe.Connect() From f3434068da7305fbd9f268e9a4b393953fe82e39 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 16:15:53 -0700 Subject: [PATCH 19/49] Fixed a logic bug for MembersToInclude/Exclude. --- .../MSFT_xGroupResource/MSFT_xGroupResource.psm1 | 15 ++++----------- README.md | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index fc00adb5a..8cc106701 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -501,7 +501,7 @@ function Set-TargetResourceOnFullSKU } } - if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Length -eq 0) + if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Length -gt 0) { if (Remove-GroupMembers -Group $group -MembersAsPrincipals $membersToExcludeAsPrincipals) { @@ -509,7 +509,7 @@ function Set-TargetResourceOnFullSKU } } - if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Length -eq 0) + if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Length -gt 0) { if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersToIncludeAsPrincipals) { @@ -1518,7 +1518,7 @@ function Get-MembersAsPrincipals # The account is domain qualified - credential required to resolve it. elseif ($null -ne $Credential -or $null -ne $principalContext) { - Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $accountName, $scope) + Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $scope, $accountName) } else { @@ -1858,14 +1858,7 @@ function Get-PrincipalContext elseif ($null -ne $Credential) { # Create a PrincipalContext targeting $Scope using the network credentials that were passed in. - if ($Credential.Domain) - { - $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" - } - else - { - $principalContextName = $Credential.UserName - } + $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope, $principalContextName, $Credential.Password ) # Cache the PrincipalContext for this scope for subsequent calls. diff --git a/README.md b/README.md index 685d18ad5..195a24e16 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ These parameters will be the same for each Windows optional feature in the set. Fix bug when credential parameter passed does not contain local or domain context. * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. * Updated appveyor.yml to use the default image. +* xGroup: Fixed logic bug in MembersToInclude and MembersToExclude ### 3.12.0.0 From 976075f9b99b269f66965faf7ecb63d046c90e89 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 16:33:24 -0700 Subject: [PATCH 20/49] Adding a few more Set-TargetResource tests. --- Tests/Unit/MSFT_xGroupResource.Tests.ps1 | 113 +++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 index 3c5335701..3d4e029bb 100644 --- a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 @@ -98,6 +98,119 @@ InModuleScope 'MSFT_xGroupResource' { } Context 'Set-TargetResource' { + It 'Should create a group with 2 users using Members' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -Members @( $testUserName1, $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 2 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + + It 'Should create a group with 2 users using MembersToInclude' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -MembersToInclude @( $testUserName1, $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 2 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + + It 'Should remove a member from a group with MembersToExclude' { + $testUserName1 = 'LocalTestUser1' + $testUserName2 = 'LocalTestUser2' + + $testDescription = 'Some Description' + $testUserPassword = 'StrongOne7.' + + $testGroupName = 'LocalTestGroup' + + $secureTestPassword = ConvertTo-SecureString $testUserPassword -AsPlainText -Force + $testCredential1 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName1, $secureTestPassword ) + $testCredential2 = New-Object -TypeName 'PSCredential' -ArgumentList @( $testUserName2, $secureTestPassword ) + + try + { + New-User -Credential $testCredential1 -Description $testDescription + New-User -Credential $testCredential2 -Description $testDescription + + New-Group -GroupName $testGroupName -Description $testDescription -MemberUserNames @( $testUserName1, $testUserName2 ) + + $setTargetResourceResult = Set-TargetResource $testGroupName -Ensure 'Present' -MembersToExclude @( $testUserName2 ) -Description $testDescription + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Description'] | Should Be $testDescription + $getTargetResourceResult['Members'].Count | Should Be 1 + } + finally + { + Remove-User -UserName $testUserName1 + Remove-User -UserName $testUserName2 + Remove-Group -GroupName $testGroupName + } + } + It 'Should not remove an existing group when Ensure is Present' { $testUserName1 = 'LocalTestUser1' $testUserName2 = 'LocalTestUser2' From a095cd6dd744726b9e98e7554e435173a693b711 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 16:41:30 -0700 Subject: [PATCH 21/49] Replacing some other changes that got overwritten. --- .../MSFT_xGroupResource/MSFT_xGroupResource.psm1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 8cc106701..9bf3410dd 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -1518,7 +1518,7 @@ function Get-MembersAsPrincipals # The account is domain qualified - credential required to resolve it. elseif ($null -ne $Credential -or $null -ne $principalContext) { - Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $scope, $accountName) + Write-Verbose -Message ($LocalizedData.ResolvingDomainAccount -f $accountName, $scope) } else { @@ -1858,7 +1858,14 @@ function Get-PrincipalContext elseif ($null -ne $Credential) { # Create a PrincipalContext targeting $Scope using the network credentials that were passed in. - $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + if ($Credential.Domain) + { + $principalContextName = "$($Credential.Domain)\$($Credential.UserName)" + } + else + { + $principalContextName = $Credential.UserName + } $principalContext = New-Object -TypeName 'System.DirectoryServices.AccountManagement.PrincipalContext' -ArgumentList @( [System.DirectoryServices.AccountManagement.ContextType]::Domain, $Scope, $principalContextName, $Credential.Password ) # Cache the PrincipalContext for this scope for subsequent calls. @@ -2257,4 +2264,4 @@ function Assert-GroupNameValid } } -Export-ModuleMember -Function *-TargetResource +Export-ModuleMember -Function *-TargetResource \ No newline at end of file From adc3d6e7bf60420002728394c26753b3abce617d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 12 Jul 2016 16:42:58 -0700 Subject: [PATCH 22/49] Adding back newline at end of file. --- DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 9bf3410dd..65bca09da 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -2264,4 +2264,4 @@ function Assert-GroupNameValid } } -Export-ModuleMember -Function *-TargetResource \ No newline at end of file +Export-ModuleMember -Function *-TargetResource From 05710161d6b59d53dd4a9d1611af73dc4925be49 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Wed, 13 Jul 2016 16:07:30 -0700 Subject: [PATCH 23/49] Starting to merge Process. --- DSCResources/CommonResourceHelper.psm1 | 52 ++ .../MSFT_xProcessResource.psm1 | 754 ++++++------------ .../MSFT_xProcessResource.TestHelper.psm1 | 9 +- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 463 +++++++++++ 4 files changed, 780 insertions(+), 498 deletions(-) create mode 100644 Tests/Unit/MSFT_xProcessResource.Tests.ps1 diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 544b9127b..dcb8c3ac6 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -84,6 +84,58 @@ function New-InvalidOperationException throw $errorRecordToThrow } +<# +# The goal of this function is to get domain and username from PSCredential +# without calling GetNetworkCredential() method. +# Call to GetNetworkCredential() expose password as a plain text in memory. +#> +function Get-DomainAndUserName([PSCredential]$Credential) +{ + # + # Supported formats: DOMAIN\username, username@domain + # + $wrongFormat = $false + if ($Credential.UserName.Contains('\')) + { + $segments = $Credential.UserName.Split('\') + if ($segments.Length -gt 2) + { + # i.e. domain\user\foo + $wrongFormat = $true + } else { + $Domain = $segments[0] + $UserName = $segments[1] + } + } + elseif ($Credential.UserName.Contains('@')) + { + $segments = $Credential.UserName.Split('@') + if ($segments.Length -gt 2) + { + # i.e. user@domain@foo + $wrongFormat = $true + } else { + $UserName = $segments[0] + $Domain = $segments[1] + } + } + else + { + # support for default domain (localhost) + return @( $env:COMPUTERNAME, $Credential.UserName ) + } + + if ($wrongFormat) + { + $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName + Write-Verbose $message + $exception = New-Object System.ArgumentException $message + throw New-Object System.Management.Automation.ErrorRecord $exception, "InvalidUserName", InvalidArgument, $null + } + + return @( $Domain, $UserName ) +} + Export-ModuleMember -Function ` Test-IsNanoServer, ` Throw-InvalidArgumentException, ` diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 64db5a61a..a54c35d3f 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -2,38 +2,54 @@ data LocalizedData { # culture="en-US" ConvertFrom-StringData @' -FileNotFound=File not found in the environment path. -AbsolutePathOrFileName=Absolute path or file name expected. -InvalidArgument=Invalid argument: '{0}' with value: '{1}'. -InvalidArgumentAndMessage={0} {1} -ProcessStarted=Process matching path '{0}' started -ProcessesStopped=Proceses matching path '{0}' with Ids '({1})' stopped. -ProcessAlreadyStarted=Process matching path '{0}' found running and no action required. -ProcessAlreadyStopped=Process matching path '{0}' not found running and no action required. -ErrorStopping=Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. -ErrorStarting=Failure starting process matching path '{0}'. Message: {1}. -StartingProcessWhatif=Start-Process -ProcessNotFound=Process matching path '{0}' not found -PathShouldBeAbsolute="The path should be absolute" -PathShouldExist="The path should exist" -ParameterShouldNotBeSpecified="Parameter {0} should not be specified." -FailureWaitingForProcessesToStart="Failed to wait for processes to start" -FailureWaitingForProcessesToStop="Failed to wait for processes to stop" +FileNotFound = File not found in the environment path. +AbsolutePathOrFileName = Absolute path or file name expected. +InvalidArgument = Invalid argument: '{0}' with value: '{1}'. +InvalidArgumentAndMessage = {0} {1} +ProcessStarted = Process matching path '{0}' started +ProcessesStopped = Proceses matching path '{0}' with Ids '({1})' stopped. +ProcessAlreadyStarted = Process matching path '{0}' found running and no action required. +ProcessAlreadyStopped = Process matching path '{0}' not found running and no action required. +ErrorStopping = Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. +ErrorStarting = Failure starting process matching path '{0}'. Message: {1}. +StartingProcessWhatif = Start-Process +ProcessNotFound = Process matching path '{0}' not found +PathShouldBeAbsolute = The path should be absolute +PathShouldExist = The path should exist +ParameterShouldNotBeSpecified = Parameter {0} should not be specified. +FailureWaitingForProcessesToStart = Failed to wait for processes to start +FailureWaitingForProcessesToStop = Failed to wait for processes to stop +ErrorParametersNotSupportedWithCredential = Can't specify StandardOutputPath, StandardInputPath or WorkingDirectory when trying to run a process under a user context. +VerboseInProcessHandle = In process handle {0} +ErrorRunAsCredentialParameterNotSupported = The PsDscRunAsCredential parameter is not supported by the Process resource. To start the process with user '{0}', add the Credential parameter. +ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredential parameter is not supported by the Process resource, and cannot be used with the Credential parameter. To start the process with user '{0}', use only the Credential parameter, not the PsDscRunAsCredential parameter. '@ } -# Commented-out until more languages are supported -# Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1 +# Commented out until more languages are supported +# Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 -function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[string[]]$newArgumentNames) +function Convert-ArgumentNames { + [CmdletBinding()] + param + ( + $functionBoundParameters, + + [String[]] + $argumentNames, + + [String[]] + $newArgumentNames + ) + $returnValue=@{} for($i=0;$i -lt $argumentNames.Count;$i++) { $argumentName=$argumentNames[$i] if($newArgumentNames -eq $null) - { + { $newArgumentName=$argumentName } else @@ -50,29 +66,39 @@ function ExtractArguments($functionBoundParameters,[string[]]$argumentNames,[str return $returnValue } +function IsRunFromLocalSystemUser() +{ + (New-Object Security.Principal.WindowsPrincipal ( [Security.Principal.WindowsIdentity]::GetCurrent())).Identity.IsSystem +} + function Get-TargetResource { - [OutputType([System.Collections.Hashtable])] + [OutputType([Hashtable])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential ) - - $Path=(ResolvePath $Path) + + $Path = Expand-Path -Path $Path $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") + + $getArguments = @{ + + } + $processes = @(GetWin32_Process @getArguments) if($processes.Count -eq 0) @@ -103,7 +129,6 @@ function Get-TargetResource } } - function Set-TargetResource { [CmdletBinding(SupportsShouldProcess=$true)] @@ -154,7 +179,7 @@ function Set-TargetResource $processIds=$processes.ProcessId $err=Stop-Process -Id $processIds -force 2>&1 - + if($err -eq $null) { Write-Log ($LocalizedData.ProcessesStopped -f $Path,($processIds -join ",")) @@ -196,39 +221,38 @@ function Set-TargetResource if($PSCmdlet.ShouldProcess($Path,$LocalizedData.StartingProcessWhatif)) { - if($PSBoundParameters.ContainsKey("Credential")) + # + # Start-Process calls .net Process.Start() + # If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + # CreateProcessWithLogonW cannot be called as LocalSystem user. + # Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003) + # + # In this case we call another api. + # + if($PSBoundParameters.ContainsKey("Credential") -and (IsRunFromLocalSystemUser)) { - $argumentError = $false - try + if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) { - if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) - { - $argumentError = $true - $errorMessage = "Can't specify StandardOutptPath, StandardInputPath or WorkingDirectory when trying to run a process under a user context" - throw $errorMessage - } - else - { - CallPInvoke - [Source.NativeMethods]::CreateProcessAsUser(("$Path "+$Arguments), $Credential.GetNetworkCredential().Domain, $Credential.GetNetworkCredential().UserName, $Credential.GetNetworkCredential().Password) - } + $exception = New-Object System.ArgumentException $LocalizedData.ErrorParametersNotSupportedWithCredential + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null } - catch + else { - $exception = New-Object System.ArgumentException $_; - if($argumentError) + $Domain, $UserName = Get-DomainAndUserName $Credential + try { - $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"Invalid combination of arguments", $errorCategory, $null + # + # Internally we use win32 api LogonUser() with dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + # It grants process ability for second-hop. + # + Import-DscNativeMethods + [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $Domain, $UserName, $Credential.Password, $false, [ref] $null ) } - else + catch { - $errorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, "Win32Exception", $errorCategory, $null + throw New-Object System.Management.Automation.ErrorRecord $_.Exception, "Win32Exception", OperationStopped, $null } - $err = $errorRecord } - } else { @@ -262,7 +286,6 @@ function Set-TargetResource function Test-TargetResource { - [OutputType([System.Boolean])] param ( [parameter(Mandatory = $true)] @@ -296,6 +319,23 @@ function Test-TargetResource $WorkingDirectory ) + if($PsDscContext.RunAsUser) + { + if($PSBoundParameters.ContainsKey("Credential")) + { + $exception = New-Object System.ArgumentException ($LocalizedData.ErrorCredentialParameterNotSupportedWithRunAsCredential -f $PsDscContext.RunAsUser) + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidArgument", InvalidArgument, $null + } + else + { + $exception = New-Object System.ArgumentException ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null + } + + Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) + throw $err + } + $Path=ResolvePath $Path $PSBoundParameters["Path"] = $Path $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") @@ -312,30 +352,35 @@ function Test-TargetResource } } -function GetWin32ProcessOwner +<# + .SYNOPSIS + Retrieves the owner of a Win32_Process. + + .PARAMETER Process + The Win32_Process to retrieve the owner of. + + .NOTES + If the process was killed by the time this function is called, this function will throw a + WMIMethodException with the message "Not found". +#> +function Get-Win32ProcessOwner { + [OutputType([String])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNull()] - $process + $Process ) - # if the process was killed by the time this is called, GetOwner - # will throw a WMIMethodException "Not found" - try - { - $owner = $process.GetOwner() - } - catch - { - } - - if($owner.Domain -ne $null) + $owner = Invoke-CimMethod -InputObject $Process -MethodName 'GetOwner' -ErrorAction 'SilentlyContinue' + + if ($null -ne $owner.Domain) { - return $owner.Domain + "\" + $owner.User + return $owner.Domain + '\' + $owner.User } - else + else { return $owner.User } @@ -368,140 +413,150 @@ function WaitForProcessCount $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") $value = @(GetWin32_Process @getArguments).Count -eq $waitCount } while(!$value -and ([DateTime]::Now - $start).TotalMilliseconds -lt 2000) - + return $value } -function GetWin32_Process +<# + If there are many processes it is faster to perform a Get-WmiObject in order to get + Win32_Process objects for all processes. + #> +<# + If there are less processes than the threshold, building a Win32_Process for each matching result of get-process is faster + #> +function Get-Win32Process { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - - [parameter(Mandatory = $true)] + + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - $useWmiObjectCount=8 + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $UseGetCimInstanceThreshold = 8 ) + $processName = [IO.Path]::GetFileNameWithoutExtension($Path) + $getProcessResult = @( Get-Process -Name $processName -ErrorAction 'SilentlyContinue' ) - $fileName = [io.path]::GetFileNameWithoutExtension($Path) - - $gpsProcesses = @(get-process -Name $fileName -ErrorAction SilentlyContinue) + $processes = @() - if($gpsProcesses.Count -ge $useWmiObjectCount) + if ($getProcessResult.Count -ge $UseGetWmiObjectThreshold) { - # if there are many processes it is faster to perform a Get-WmiObject - # in order to get Win32_Process objects for all processes - Write-Verbose "When gpsprocess.count is greater than usewmiobjectcount" - $Path=WQLEscape $Path - $filter = "ExecutablePath = '$Path'" - $processes = Get-WmiObject Win32_Process -Filter $filter + + $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path + $wqlFilter = "ExecutablePath = '$escapedPathForWqlFilter'" + + $processes = Get-CimInstance -ClassName 'Win32_Process' -Filter $wqlFilter } else { - # if there are few processes, building a Win32_Process for - # each matching result of get-process is faster - $processes = foreach($gpsProcess in $gpsProcesses) + foreach ($process in $getProcessResult) { - if(!($gpsProcess.Path -ieq $Path)) - { - continue - } - - try + if ($process.Path -ieq $Path) { - Write-Verbose "in process handle, $($gpsProcess.Id)" - [wmi]"Win32_Process.Handle='$($gpsProcess.Id)'" - } - catch - { - #ignore if could not retrieve process + Write-Verbose -Message ($LocalizedData.VerboseInProcessHandle -f $process.Id) + $processes += Get-CimInstance -ClassName 'Win32_Process' -Filter "ProcessId = $($process.Id)" -ErrorAction 'SilentlyContinue' } } } - if($PSBoundParameters.ContainsKey('Credential')) + if ($PSBoundParameters.ContainsKey('Credential')) { - # Since there are credentials we need to call the GetOwner method in each process to search for matches - $processes = $processes | where { (GetWin32ProcessOwner $_) -eq $Credential.UserName } + $Domain, $UserName = Get-DomainAndUserName $Credential + $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$Domain\$UserName" } + } + + if ($null -eq $Arguments) + { + $Arguments = [String]::Empty } - if($Arguments -eq $null) {$Arguments = ""} - $processes = $processes | where { (GetProcessArgumentsFromCommandLine $_.CommandLine) -eq $Arguments } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput $_.CommandLine) -eq $Arguments } return $processes } <# -.Synopsis - Strips the Arguments part of a commandLine. In "c:\temp\a.exe X Y Z" the Arguments part is "X Y Z". + .SYNOPSIS + Retrieves the 'arguments' part of command line input. + + .PARAMETER CommandLineInput + The command line input to retrieve the arguments from. + + .EXAMPLE + Get-ArgumentsFromCommandLineInput -CommandLineInput 'C:\temp\a.exe X Y Z' + Returns 'X Y Z'. #> -function GetProcessArgumentsFromCommandLine +function Get-ArgumentsFromCommandLineInput { + [OutputType([String])] + [CmdletBinding()] param ( - [System.String] - $commandLine + [String] + $CommandLineInput ) - if($commandLine -eq $null) + if ([String]::IsNullOrWhitespace($CommandLineInput)) { - return "" + return [String]::Empty } + + $CommandLineInput = $CommandLineInput.Trim() - $commandLine=$commandLine.Trim() - - if($commandLine.Length -eq 0) + if ($CommandLineInput.StartsWith('"')) { - return "" - } - - if($commandLine[0] -eq '"') - { - $charToLookfor=[char]'"' + $endOfCommandChar = [Char]'"' } else { - $charToLookfor=[char]' ' + $endOfCommandChar = [Char]' ' } - $endofCommand=$commandLine.IndexOf($charToLookfor ,1) - if($endofCommand -eq -1) + $endofCommandIndex = $CommandLineInput.IndexOf($endOfCommandChar, 1) + if ($endofCommandIndex -eq -1) { - return "" + return [String]::Empty } - return $commandLine.Substring($endofCommand+1).Trim() + return $CommandLineInput.Substring($endofCommandIndex + 1).Trim() } <# -.Synopsis - Escapes a string to be used in a WQL filter as the one passed to get-wmiobject + .SYNOPSIS + Converts a string to an escaped string to be used in a WQL filter such as the one passed in + the Filter parameter of Get-WmiObject. + + .PARAMETER FilterString + The string to convert. #> -function WQLEscape +function ConvertTo-EscapedStringForWqlFilter { + [OutputType([String])] + [CmdletBinding()] param ( - - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] - $query + [String] + $FilterString ) - return $query.Replace("\","\\").Replace('"','\"').Replace("'","\'") + return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") } function ThrowInvalidArgumentError @@ -509,7 +564,7 @@ function ThrowInvalidArgumentError [CmdletBinding()] param ( - + [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] @@ -527,72 +582,82 @@ function ThrowInvalidArgumentError throw $errorRecord } -function ResolvePath +<# + .SYNOPSIS + Expands a shortened path into a full, rooted path. + + .PARAMETER Path + The shortened path to expand. +#> +function Expand-Path { + [OutputType([String])] [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path ) $Path = [Environment]::ExpandEnvironmentVariables($Path) - if(IsRootedPath $Path) + if (Test-IsRootedPath -Path $Path) { - if(!(Test-Path $Path -PathType Leaf)) + if (-not (Test-Path -Path $Path -PathType 'Leaf')) { - ThrowInvalidArgumentError "CannotFindRootedPath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } return $Path } - if([string]::IsNullOrEmpty($env:Path)) + if ([String]::IsNullOrEmpty($env:Path)) { - ThrowInvalidArgumentError "EmptyEnvironmentPath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } - # This will block relative paths. The statement is only true id $Path contains a plain file name. - # Checking a relative path against segments of the $env:Path does not make sense - if((Split-Path $Path -Leaf) -ne $Path) + <# + This will block relative paths. The statement is only true when $Path contains a plain file name. + Checking a relative path against segments of $env:Path does not make sense. + #> + if ((Split-Path -Path $Path -Leaf) -ne $Path) { - ThrowInvalidArgumentError "NotAbsolutePathOrFileName" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.AbsolutePathOrFileName) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.AbsolutePathOrFileName) } - foreach($rawSegment in $env:Path.Split(";")) + foreach ($rawEnvPathSegment in $env:Path.Split(';')) { - $segment = [Environment]::ExpandEnvironmentVariables($rawSegment) - - # if an exception causes $segmentedRooted not to be set, we will consider it $false - $segmentRooted = $false + $envPathSegment = [Environment]::ExpandEnvironmentVariables($rawEnvPathSegment) + + # If an exception causes $envPathSegmentRooted not to be set, we will consider it $false + $envPathSegmentRooted = $false + + <# + If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have + invalid characters, so the segment has no invalid characters and will not throw as well. + #> try { - # If the whole path passed through [IO.Path]::IsPathRooted with no exceptions, it does not have - # invalid characters, so segment has no invalid characters and will not throw as well - $segmentRooted=[IO.Path]::IsPathRooted($segment) + $envPathSegmentRooted = [IO.Path]::IsPathRooted($envPathSegment) } catch {} - - if(!$segmentRooted) + + if ($envPathSegmentRooted) { - continue - } - - $candidate = join-path $segment $Path - - if(Test-Path $candidate -PathType Leaf) - { - return $candidate + $fullPathCandidate = Join-Path -Path $envPathSegment -ChildPath $Path + + if (Test-Path -Path $fullPathCandidate -PathType 'Leaf') + { + return $fullPathCandidate + } } } - ThrowInvalidArgumentError "CannotFindRelativePath" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $LocalizedData.FileNotFound) + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } - function AssertAbsolutePath { [CmdletBinding()] @@ -610,16 +675,16 @@ function AssertAbsolutePath Process { - if(!$ParentBoundParameters.ContainsKey($ParameterName)) + if(!$ParentBoundParameters.ContainsKey($ParameterName)) { return } $path=$ParentBoundParameters[$ParameterName] - + if(!(IsRootedPath $Path)) { - ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), + ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldBeAbsolute) } @@ -630,7 +695,7 @@ function AssertAbsolutePath if(!(Test-Path $Path)) { - ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), + ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), $LocalizedData.PathShouldExist) } } @@ -650,20 +715,29 @@ function AssertParameterIsNotSpecified Process { - if($ParentBoundParameters.ContainsKey($ParameterName)) + if($ParentBoundParameters.ContainsKey($ParameterName)) { ThrowInvalidArgumentError "ParameterShouldNotBeSpecified" ($LocalizedData.ParameterShouldNotBeSpecified -f $ParameterName) } } } -function IsRootedPath +<# + .SYNOPSIS + Tests is the given path is rooted. + + .PARAMETER Path + The path to test. +#> +function Test-IsRootedPath { + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path ) @@ -673,319 +747,9 @@ function IsRootedPath } catch { - # if the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on - ThrowInvalidArgumentError "CannotGetIsPathRooted" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f "Path",$Path), $_.Exception.Message) + # If the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on + New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $_.Exception.Message) } } -function Write-Log -{ - [CmdletBinding(SupportsShouldProcess=$true)] - param - ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Message - ) - - if ($PSCmdlet.ShouldProcess($Message, $null, $null)) - { - Write-Verbose $Message - } -} - -function CallPInvoke -{ -$script:ProgramSource = @" -using System; -using System.Collections.Generic; -using System.Text; -using System.Security; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Security.Principal; -using System.ComponentModel; -using System.IO; - -namespace Source -{ - [SuppressUnmanagedCodeSecurity] - public static class NativeMethods - { - //The following structs and enums are used by the various Win32 API's that are used in the code below - - [StructLayout(LayoutKind.Sequential)] - public struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public Int32 dwProcessID; - public Int32 dwThreadID; - } - - [Flags] - public enum LogonType - { - LOGON32_LOGON_INTERACTIVE = 2, - LOGON32_LOGON_NETWORK = 3, - LOGON32_LOGON_BATCH = 4, - LOGON32_LOGON_SERVICE = 5, - LOGON32_LOGON_UNLOCK = 7, - LOGON32_LOGON_NETWORK_CLEARTEXT = 8, - LOGON32_LOGON_NEW_CREDENTIALS = 9 - } - - [Flags] - public enum LogonProvider - { - LOGON32_PROVIDER_DEFAULT = 0, - LOGON32_PROVIDER_WINNT35, - LOGON32_PROVIDER_WINNT40, - LOGON32_PROVIDER_WINNT50 - } - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public Int32 Length; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } - - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - public enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct TokPriv1Luid - { - public int Count; - public long Luid; - public int Attr; - } - - public const int GENERIC_ALL_ACCESS = 0x10000000; - public const int CREATE_NO_WINDOW = 0x08000000; - internal const int SE_PRIVILEGE_ENABLED = 0x00000002; - internal const int TOKEN_QUERY = 0x00000008; - internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; - internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; - - [DllImport("kernel32.dll", - EntryPoint = "CloseHandle", SetLastError = true, - CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern bool CloseHandle(IntPtr handle); - - [DllImport("advapi32.dll", - EntryPoint = "CreateProcessAsUser", SetLastError = true, - CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] - public static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandle, - Int32 dwCreationFlags, - IntPtr lpEnvrionment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - ref PROCESS_INFORMATION lpProcessInformation - ); - - [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] - public static extern bool DuplicateTokenEx( - IntPtr hExistingToken, - Int32 dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - Int32 ImpersonationLevel, - Int32 dwTokenType, - ref IntPtr phNewToken - ); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern Boolean LogonUser( - String lpszUserName, - String lpszDomain, - String lpszPassword, - LogonType dwLogonType, - LogonProvider dwLogonProvider, - out IntPtr phToken - ); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool AdjustTokenPrivileges( - IntPtr htok, - bool disall, - ref TokPriv1Luid newst, - int len, - IntPtr prev, - IntPtr relen - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GetCurrentProcess(); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool OpenProcessToken( - IntPtr h, - int acc, - ref IntPtr phtok - ); - - [DllImport("advapi32.dll", SetLastError = true)] - internal static extern bool LookupPrivilegeValue( - string host, - string name, - ref long pluid - ); - - public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword) - { - var hToken = IntPtr.Zero; - var hDupedToken = IntPtr.Zero; - TokPriv1Luid tp; - var pi = new PROCESS_INFORMATION(); - var sa = new SECURITY_ATTRIBUTES(); - sa.Length = Marshal.SizeOf(sa); - Boolean bResult = false; - try - { - bResult = LogonUser( - strName, - strDomain, - strPassword, - LogonType.LOGON32_LOGON_BATCH, - LogonProvider.LOGON32_PROVIDER_DEFAULT, - out hToken - ); - if (!bResult) - { - throw new Win32Exception("The user could not be logged on. Ensure that the user has an existing profile on the machine and that correct credentials are provided. Logon error #" + Marshal.GetLastWin32Error().ToString()); - } - IntPtr hproc = GetCurrentProcess(); - IntPtr htok = IntPtr.Zero; - bResult = OpenProcessToken( - hproc, - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - ref htok - ); - if(!bResult) - { - throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); - } - tp.Count = 1; - tp.Luid = 0; - tp.Attr = SE_PRIVILEGE_ENABLED; - bResult = LookupPrivilegeValue( - null, - SE_INCRASE_QUOTA, - ref tp.Luid - ); - if(!bResult) - { - throw new Win32Exception("Error in looking up privilege of the process. This should not happen if DSC is running as LocalSystem Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); - } - bResult = AdjustTokenPrivileges( - htok, - false, - ref tp, - 0, - IntPtr.Zero, - IntPtr.Zero - ); - if(!bResult) - { - throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); - } - - bResult = DuplicateTokenEx( - hToken, - GENERIC_ALL_ACCESS, - ref sa, - (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, - (int)TOKEN_TYPE.TokenPrimary, - ref hDupedToken - ); - if(!bResult) - { - throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); - } - var si = new STARTUPINFO(); - si.cb = Marshal.SizeOf(si); - si.lpDesktop = ""; - bResult = CreateProcessAsUser( - hDupedToken, - null, - strCommand, - ref sa, - ref sa, - false, - 0, - IntPtr.Zero, - null, - ref si, - ref pi - ); - if(!bResult) - { - throw new Win32Exception("The process could not be created. Create process as user error #" + Marshal.GetLastWin32Error().ToString()); - } - } - finally - { - if (pi.hThread != IntPtr.Zero) - { - CloseHandle(pi.hThread); - } - if (pi.hProcess != IntPtr.Zero) - { - CloseHandle(pi.hProcess); - } - if (hDupedToken != IntPtr.Zero) - { - CloseHandle(hDupedToken); - } - } - } - } -} - -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} - -Export-ModuleMember -function Get-TargetResource, Set-TargetResource, Test-TargetResource - +Export-ModuleMember -Function *-TargetResource diff --git a/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 b/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 index eb1c7a0af..5ac5a225e 100644 --- a/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 +++ b/Tests/Unit/MSFT_xProcessResource.TestHelper.psm1 @@ -2,7 +2,10 @@ Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" <# .SYNOPSIS - Stops all instances of a process using the process name. + Stops all instances of the process with the given name. + + .PARAMETER ProcessName + The name of the process to stop. #> function Stop-ProcessByName { @@ -13,8 +16,8 @@ function Stop-ProcessByName $ProcessName ) - Stop-Process -Name $ProcessName -Force -ErrorAction SilentlyContinue - Wait-ScriptBlockReturnTrue -ScriptBlock {$null -eq (Get-Process -Name $ProcessName -ErrorAction SilentlyContinue)} -TimeoutSeconds 15 + Stop-Process -Name $ProcessName -ErrorAction 'SilentlyContinue' -Force + Wait-ScriptBlockReturnTrue -ScriptBlock { return $null -eq (Get-Process -Name $ProcessName -ErrorAction 'SilentlyContinue') } -TimeoutSeconds 15 } Export-ModuleMember -Function Stop-ProcessByName diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 new file mode 100644 index 000000000..a39cd28a1 --- /dev/null +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -0,0 +1,463 @@ +Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xProcessResource' ` + -TestType 'Unit' + +try +{ + InModuleScope 'MSFT_xProcessResource' { + Describe 'MSFT_xProcessResource Unit Tests' { + BeforeAll { + Import-Module -Name "$PSScriptRoot\MSFT_xProcessResource.TestHelper.psm1" -Force + + $script:cmdProcessShortName = 'ProcessTest' + $script:cmdProcessFullName = 'ProcessTest.exe' + $script:cmdProcessFullPath = "$env:winDir\system32\ProcessTest.exe" + Copy-Item -Path "$env:winDir\system32\cmd.exe" -Destination $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + } + + AfterAll { + Stop-ProcessByName $script:cmdProcessShortName + + if (Test-Path -Path $script:cmdProcessFullPath) + { + Remove-Item -Path $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + } + } + + BeforeEach { + Stop-ProcessByName -ProcessName $script:cmdProcessShortName + } + + Context 'Get-TargetResource' { + It 'Should return the correct properties' -Pending { + $processArguments = 'TestGetProperties' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceProperties $getTargetResourceProperties + + $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Path.IndexOf("ProcessTest.exe",[Stringcomparison]::OrdinalIgnoreCase) -le 0 | Should Be $false + $getTargetResourceResult.NonPagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.HandleCount -le 0 | Should Be $false + $getTargetResourceResult.ProcessId -le 0 | Should Be $false + $getTargetResourceResult.Count | Should Be 8 + } + } + + Context 'Set-TargetResource' { + It 'Should start and stop a process with no arguments' -Pending { + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $true + + Set-TargetResource -Path $script:cmdProcessFullPath -Ensure 'Absent' -Arguments '' + + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false + } + + It 'Should have correct output with WhatIf is specified' -Pending { + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $script:cmdProcessFullPath + TestWhatif $script $script:cmdProcessFullPath + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $script:cmdProcessFullPath + TestWhatif $script $script:cmdProcessFullPath + } + + It 'TestWhatifStop' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments '') + { + throw "before set, there should be no process" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments '' + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $exePath + TestWhatif $script $exePath + + $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $exePath + TestWhatif $script $exePath + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure "Absent" -Arguments '' + } + } + + <# + .Synopsis + tests input, output and error streams are hooked up as well as the working directory + #> + It 'TestStreamsAndWorkingDirectory' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } + + $errorPath="$PWD\TestStreamsError.txt" + $outputPath="$PWD\TestStreamsOutput.txt" + $inputPath="$PWD\TestStreamsInput.txt" + + Remove-Item $errorPath -Force -ErrorAction SilentlyContinue + Remove-Item $outputPath -Force -ErrorAction SilentlyContinue + + "ECHO Testing ProcessTest.exe ` + dir volumeSyntaxError:\ ` + set /p waitforinput=Press [y/n]?: " | out-file $inputPath -Encoding ascii + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -WorkingDirectory $processTestPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments "" + + if(!(TryForAWhile ([scriptblock]::create("(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments '').Ensure -eq 'Absent'")))) + { + throw "process did not terminate" + } + + # Race condition exists for retrieving contents of the process error stream file + Start-Sleep -Seconds 2 + + $errorFile=get-content $errorPath -Raw + $outputFile=get-content $outputPath -Raw + + if((Get-Culture).Name -ieq 'en-us') + { + Assert ($errorFile.Contains("The filename, directory name, or volume label syntax is incorrect.")) "no stdErr string in file" + Assert ($outputFile.Contains("Press [y/n]?:")) "no stdOut string in file" + Assert ($outputFile.ToLower().Contains($processTestPath.ToLower())) "working directory: $processTestPath, not in output file" + } + else + { + Assert ($errorFile.Length -gt 0) "no stdErr string in file" + Assert ($outputFile.Length -gt 0) "no stdOut string in file" + } + } + } + + It 'TestCannotWritePropertiesWithAbsent' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Absent";Arguments=""} + $null=$args.Add($writeProperty,"anything") + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set when using Absent" -f $writeProperty) + } + } + } + + It 'TestCannotUseInvalidPath' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + $relativePath = "..\ExistingFile.txt" + "something" > $relativePath + + $nonExistingPath = "$processTestPath\IDoNotExist.Really.I.Do.Not" + del $nonExistingPath -ErrorAction SilentlyContinue + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $null=$args.Add($writeProperty,$relativePath) + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set to relative path" -f $writeProperty) + } + + + foreach($writeProperty in "StandardInputPath","WorkingDirectory") + { + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $args[$writeProperty] = $nonExistingPath + $thrown = $false + try + { + MSFT_ProcessResource\Set-TargetResource @args + } + catch + { + $thrown = $true + } + Assert $thrown ("{0} cannot be set to nonexisting path" -f $writeProperty) + } + + foreach($writeProperty in "StandardOutputPath","StandardErrorPath") + { + # Paths that need not exist so no exception should be thrown + $args=@{Path=$exePath;Ensure="Present";Arguments=""} + $null=$args.Add($writeProperty,$nonExistingPath) + MSFT_ProcessResource\Set-TargetResource @args + get-process ProcessTest | stop-process + del $nonExistingPath -ErrorAction SilentlyContinue + } + } + } + + It 'TestGetWmiObject' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "" + MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "abc" + $r=@(GetWin32_Process $exePath -useWmiObjectCount 0) + AssertEquals $r.Count 1 "get-wmiobject with filter" + $a=@(GetWin32_Process $exePath -useWmiObjectCount 5) + AssertEquals $a.Count 1 "through get-process" + MSFT_ProcessResource\Set-TargetResource $exePath -Ensure Absent -Arguments "" + } + } + } + + Context 'Test-TargetResource' { + It 'TestTestWithDirectoryArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + $exists = MSFT_ProcessResource\Test-TargetResource $exePath -WorkingDirectory "something" -StandardOutputPath "something" ` + -StandardErrorPath "something" -StandardInputPath "something" -Arguments "" + Assert !$exists "there should be no process" + } + } + } + + + + + <# + SPLIT into 3 + + .Synopsis + Tests a process with an argument. + .DESCRIPTION + - Starts a process with an argument + - Ensures Test with "" as Arguments is false + - Ensures Test with the wrong arguments is false + - Ensures Test with no Arguments key is true + - Ensures Test with the right Arguments key is true + - Ensures Get with no arguments key is 1 Present + - Ensures Get with "" as Arguments is 1 Absent + + - Starts process with no arguments + - Ensures Get with no arguments key is 2 + - Ensures Get with null arguments is 1 Present with no Arguments + - Ensures Get with argument is 1 Present with the Arguments + - Set All process to be absent + - Ensure none are left + #> + It 'TestGetSetProcessResourceWithArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set, cannot find process with no arguments" + } + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") + { + throw "after set, cannot find process with arguments that were not the ones we set" + } + + if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) + { + throw "after set, there should be a process if we specify arguments" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') + { + throw "there should be no process without arguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with no argument" + } + + if($processes[0].Arguments.length -ne 0) + { + throw "there should no arguments in the process" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" + } + + if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') + { + throw "the argument should be TestGetSetProcessResourceWithArguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" + + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set absent, there should be no process" + } + } + } + + Context 'WQLEscape' { + It 'TestWQLEscape' -Pending { + WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + } + } + + Context 'Get-ProcessArgumentsFromCommandLine' { + It 'TestGetProcessArgumentsFromCommandLine' -Pending { + $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) + foreach($testCase in $testCases) + { + $test=$testCase[0] + $expected=$testCase[1] + $actual = GetProcessArgumentsFromCommandLine $test + + $actual | Should Be $expected + } + } + } + + + + + + + + # + # Tests for Get-DomainAndUserName function. + # + Context 'Get-DomainAndUserName' { + It 'TestDomainUserNameParseAt' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "user@domain", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } + } + + It 'TestDomainUserNameParseSlash' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } + } + + It 'TestDomainUserNameParseImplicitDomain' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "localuser", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" + Assert ($UserName -eq "localuser") "wrong user $UserName" + } + } + + Context 'TestDomainUserNameParseSlashFail.Context' { + + BeforeEach { + Invoke-Remotely { + $script:originalErrorActionPreference = $ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + } + } + + AfterEach { + Invoke-Remotely { + $global:ErrorActionPreference = $script:originalErrorActionPreference + } + } + + It 'TestDomainUserNameParseSlashFail' { + Invoke-Remotely { + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user\foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" + } + Assert ($exceptionThrown) "no exception thrown" + } + } + + It 'TestDomainUserNameParseAtFail' { + Invoke-Remotely { + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain@user@foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" + } + Assert ($exceptionThrown) "no exception thrown" + } + } + } + } + } + } +} +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} From ede6107d45c1057092d69ed1f93ce7637d7f1cfd Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Wed, 13 Jul 2016 16:19:00 -0700 Subject: [PATCH 24/49] Updating HQRM plan progress. --- HighQualityResourceModulePlan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HighQualityResourceModulePlan.md b/HighQualityResourceModulePlan.md index 9c320a892..634bb12b4 100644 --- a/HighQualityResourceModulePlan.md +++ b/HighQualityResourceModulePlan.md @@ -39,8 +39,8 @@ The PSDesiredStateConfiguration High Quality Resource Module will consist of the - [ ] [2. Merge In-Box & Open-Source Resources](#merge-in-box-and-open-source-resources) - [x] Archive - [x] Group - - [ ] Package (In Progress) - - [ ] Process + - [x] Package + - [ ] Process (In Progress) - [ ] Registry - [x] Service - [ ] WindowsOptionalFeature From 06313963ad861ad2d1c44e23b75fa5a4f916be03 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Thu, 21 Jul 2016 15:47:34 -0700 Subject: [PATCH 25/49] Updating xProcess and tests. --- .../MSFT_xProcessResource.psm1 | 645 ++++++++++-------- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 404 ++++++----- 2 files changed, 602 insertions(+), 447 deletions(-) diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index a54c35d3f..49b3676e9 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -29,46 +29,99 @@ ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredenti # Commented out until more languages are supported # Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 -function Convert-ArgumentNames +Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1" + +<# + .SYNOPSIS + Tests if the current user is from the local system. +#> +function Test-IsRunFromLocalSystemUser { + [OutputType([Boolean])] [CmdletBinding()] - param - ( - $functionBoundParameters, + param () - [String[]] - $argumentNames, - [String[]] - $newArgumentNames + $currentUser = (New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList @( [Security.Principal.WindowsIdentity]::GetCurrent() )) + + return $currenUser.Identity.IsSystem +} + +<# + .SYNOPSIS + Splits a credential into a username and domain wihtout calling GetNetworkCredential. + Calls to GetNetworkCredential expose the password as plain text in memory. + + .PARAMETER Credential + The credential to pull the username and domain out of. + + .NOTES + Supported formats: DOMAIN\username, username@domain +#> +function Split-Credential +{ + [OutputType([Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [PSCredential] + $Credential ) - $returnValue=@{} - for($i=0;$i -lt $argumentNames.Count;$i++) - { - $argumentName=$argumentNames[$i] + $wrongFormat = $false - if($newArgumentNames -eq $null) - { - $newArgumentName=$argumentName - } + if ($Credential.UserName.Contains('\')) + { + $credentialSegments = $Credential.UserName.Split('\') + + if ($credentialSegments.Length -gt 2) + { + # i.e. domain\user\foo + $wrongFormat = $true + } else { - $newArgumentName=$newArgumentNames[$i] + $domain = $credentialSegments[0] + $userName = $credentialSegments[1] } + } + elseif ($Credential.UserName.Contains('@')) + { + $credentialSegments = $Credential.UserName.Split('@') - if($functionBoundParameters.ContainsKey($argumentName)) + if ($credentialSegments.Length -gt 2) { - $null=$returnValue.Add($newArgumentName,$functionBoundParameters[$argumentName]) + # i.e. user@domain@foo + $wrongFormat = $true + } + else + { + $UserName = $credentialSegments[0] + $Domain = $credentialSegments[1] } } + else + { + # support for default domain (localhost) + $domain = $env:computerName + $userName = $Credential.UserName + } - return $returnValue -} + if ($wrongFormat) + { + $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName + + Write-Verbose -Message $message -function IsRunFromLocalSystemUser() -{ - (New-Object Security.Principal.WindowsPrincipal ( [Security.Principal.WindowsIdentity]::GetCurrent())).Identity.IsSystem + New-InvalidArgumentException -ArgumentName 'Credential' -Message $message + } + + return @{ + Domain = $domain + UserName = $userName + } } function Get-TargetResource @@ -93,262 +146,323 @@ function Get-TargetResource ) $Path = Expand-Path -Path $Path - $PSBoundParameters["Path"] = $Path - $getArguments = @{ + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential } - $processes = @(GetWin32_Process @getArguments) + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) - if($processes.Count -eq 0) + if ($win32Processes.Count -eq 0) { return @{ - Path=$Path - Arguments=$Arguments - Ensure='Absent' + Path = $Path + Arguments = $Arguments + Ensure ='Absent' } } - foreach($process in $processes) + foreach ($win32Process in $win32Processes) { - # in case the process was killed between GetWin32_Process and this point, we should - # ignore errors which will generate empty entries in the return - $gpsProcess = (get-process -id $process.ProcessId -ErrorAction Ignore) + $getProcessResult = Get-Process -ID $win32Process.ProcessId -ErrorAction 'Ignore' - @{ - Path=$process.Path - Arguments=(GetProcessArgumentsFromCommandLine $process.CommandLine) - PagedMemorySize=$gpsProcess.PagedMemorySize64 - NonPagedMemorySize=$gpsProcess.NonpagedSystemMemorySize64 - VirtualMemorySize=$gpsProcess.VirtualMemorySize64 - HandleCount=$gpsProcess.HandleCount - Ensure='Present' - ProcessId=$process.ProcessId + return @{ + Path = $win32Process.Path + Arguments = (Get-ArgumentsFromCommandLineInput -CommandLineInput $win32Process.CommandLine) + PagedMemorySize = $getProcessResult.PagedMemorySize64 + NonPagedMemorySize = $getProcessResult.NonpagedSystemMemorySize64 + VirtualMemorySize = $getProcessResult.VirtualMemorySize64 + HandleCount = $getProcessResult.HandleCount + Ensure = 'Present' + ProcessId = $win32Process.ProcessId } } } function Set-TargetResource { - [CmdletBinding(SupportsShouldProcess=$true)] + [CmdletBinding(SupportsShouldProcess = $true)] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - [System.String] - [ValidateSet("Present", "Absent")] - $Ensure="Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [System.String] + [String] $StandardOutputPath, - [System.String] + [String] $StandardErrorPath, - [System.String] + [String] $StandardInputPath, - [System.String] + [String] $WorkingDirectory ) - $Path=ResolvePath $Path - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + if ($null -ne $PsDscContext.RunAsUser) + { + New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) + } + + $Path = Expand-Path -Path $Path + + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential + } - if($Ensure -eq 'Absent') + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) + + if ($Ensure -eq 'Absent') { - "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory" | AssertParameterIsNotSpecified $PSBoundParameters + Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - if ($processes.Count -gt 0) + if ($win32Processes.Count -gt 0) { - $processIds=$processes.ProcessId + $processIds = $win32Processes.ProcessId - $err=Stop-Process -Id $processIds -force 2>&1 + $stopProcessError = Stop-Process -Id $processIds -Force 2>&1 - if($err -eq $null) + if ($null -eq $stopProcessError) { - Write-Log ($LocalizedData.ProcessesStopped -f $Path,($processIds -join ",")) + Write-Verbose -Message ($LocalizedData.ProcessesStopped -f $Path, ($processIds -join ',')) } else { - Write-Log ($LocalizedData.ErrorStopping -f $Path,($processIds -join ","),($err | out-string)) - throw $err + Write-Verbose -Message ($LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), ($stopProcessError | Out-String)) + throw $stopProcessError } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work - if (!(WaitForProcessCount @getArguments -waitCount 0)) + if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 0)) { - $message = $LocalizedData.ErrorStopping -f $Path,($processIds -join ","),$LocalizedData.FailureWaitingForProcessesToStop - Write-Log $message - ThrowInvalidArgumentError "FailureWaitingForProcessesToStop" $message + $message = $LocalizedData.ErrorStopping -f $Path, ($processIds -join ','), $LocalizedData.FailureWaitingForProcessesToStop + + Write-Verbose -Message $message + + New-InvalidOperationException -Message $message } } else { - Write-Log ($LocalizedData.ProcessAlreadyStopped -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessAlreadyStopped -f $Path) } } else { - "StandardInputPath","WorkingDirectory" | AssertAbsolutePath $PSBoundParameters -Exist - "StandardOutputPath","StandardErrorPath" | AssertAbsolutePath $PSBoundParameters + $shouldBeRootedPathArguments = @( 'StandardInputPath', 'WorkingDirectory', 'StandardOutputPath', 'StandardErrorPath' ) - if ($processes.Count -eq 0) + foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) { - $startArguments = ExtractArguments $PSBoundParameters ` - ("Path", "Arguments", "Credential", "StandardOutputPath", "StandardErrorPath", "StandardInputPath", "WorkingDirectory") ` - ("FilePath", "ArgumentList", "Credential", "RedirectStandardOutput", "RedirectStandardError", "RedirectStandardInput", "WorkingDirectory") + if ($null -ne $PSBoundParameters[$shouldBeRootedPathArgument]) + { + Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] + } + } - if([string]::IsNullOrEmpty($Arguments)) + $shouldExistPathArguments = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($shouldExistPathArgument in $shouldExistPathArguments) + { + if ($null -ne $PSBoundParameters[$shouldExistPathArgument]) { - $null=$startArguments.Remove("ArgumentList") + Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] + } + } + + if ($win32Processes.Count -eq 0) + { + $startProcessArguments = @{ + FilePath = $Path } - if($PSCmdlet.ShouldProcess($Path,$LocalizedData.StartingProcessWhatif)) + $startProcessOptionalArgumentMap = @{ + Credential = 'Credential' + RedirectStandardOutput = 'StandardOutputPath' + RedirectStandardError = 'StandardErrorPath' + RedirectStandardInput = 'StandardInputPath' + WorkingDirectory = 'WorkingDirectory' + } + + foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - # - # Start-Process calls .net Process.Start() - # If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx - # CreateProcessWithLogonW cannot be called as LocalSystem user. - # Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003) - # - # In this case we call another api. - # - if($PSBoundParameters.ContainsKey("Credential") -and (IsRunFromLocalSystemUser)) + if ($null -ne $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]) { - if($PSBoundParameters.ContainsKey("StandardOutputPath") -or $PSBoundParameters.ContainsKey("StandardInputPath") -or $PSBoundParameters.ContainsKey("WorkingDirectory")) + $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]] + } + } + + if (-not [String]::IsNullOrEmpty($Arguments)) + { + $startProcessArguments['ArgumentList'] = $Arguments + } + + if ($PSCmdlet.ShouldProcess($Path, $LocalizedData.StartingProcessWhatif)) + { + <# + Start-Process calls .net Process.Start() + If -Credential is present Process.Start() uses win32 api CreateProcessWithLogonW http://msdn.microsoft.com/en-us/library/0w4h05yb(v=vs.110).aspx + CreateProcessWithLogonW cannot be called as LocalSystem user. + Details http://msdn.microsoft.com/en-us/library/windows/desktop/ms682431(v=vs.85).aspx (section Remarks/Windows XP with SP2 and Windows Server 2003) + + In this case we call another api. + #> + if ($PSBoundParameters.ContainsKey('Credential') -and (Test-IsRunFromLocalSystemUser)) + { + if ($PSBoundParameters.ContainsKey('StandardOutputPath')) { - $exception = New-Object System.ArgumentException $LocalizedData.ErrorParametersNotSupportedWithCredential - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null + New-InvalidArgumentException -ArgumentName 'StandardOutputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential } - else + + if ($PSBoundParameters.ContainsKey('StandardInputPath')) { - $Domain, $UserName = Get-DomainAndUserName $Credential - try - { - # - # Internally we use win32 api LogonUser() with dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. - # It grants process ability for second-hop. - # - Import-DscNativeMethods - [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $Domain, $UserName, $Credential.Password, $false, [ref] $null ) - } - catch - { - throw New-Object System.Management.Automation.ErrorRecord $_.Exception, "Win32Exception", OperationStopped, $null - } + New-InvalidArgumentException -ArgumentName 'StandardInputPath' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + if ($PSBoundParameters.ContainsKey('WorkingDirectory')) + { + New-InvalidArgumentException -ArgumentName 'WorkingDirectory' -Message $LocalizedData.ErrorParametersNotSupportedWithCredential + } + + $splitCredentialResult = Split-Credential $Credential + try + { + <# + Internally we use win32 api LogonUser() with dwLogonType == LOGON32_LOGON_NETWORK_CLEARTEXT. + It grants process ability for second-hop. + #> + Import-DscNativeMethods + + [PSDesiredStateConfiguration.NativeMethods]::CreateProcessAsUser( "$Path $Arguments", $domain, $userName, $Credential.Password, $false, [Ref]$null ) + } + catch + { + throw (New-Object -TypeName 'System.Management.Automation.ErrorRecord' -ArgumentList @( $_.Exception, 'Win32Exception', 'OperationStopped', $null )) } } else { - $err=Start-Process @startArguments 2>&1 + $startProcessError = Start-Process @startProcessArguments 2>&1 } - if($err -eq $null) + + if ($null -eq $startProcessError) { - Write-Log ($LocalizedData.ProcessStarted -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessStarted -f $Path) } else { - Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) - throw $err + Write-Verbose -Message ($LocalizedData.ErrorStarting -f $Path, ($startProcessError | Out-String)) + throw $startProcessError } # Before returning from Set-TargetResource we have to ensure a subsequent Test-TargetResource is going to work - if (!(WaitForProcessCount @getArguments -waitCount 1)) + if (-not (Wait-ProcessCount -ProcessSettings $getWin32ProcessArguments -ProcessCount 1)) { - $message = $LocalizedData.ErrorStarting -f $Path,$LocalizedData.FailureWaitingForProcessesToStart - Write-Log $message - ThrowInvalidArgumentError "FailureWaitingForProcessesToStart" $message + $message = $LocalizedData.ErrorStarting -f $Path, $LocalizedData.FailureWaitingForProcessesToStart + + Write-Verbose -Message $message + + New-InvalidOperationException -Message $message } } } else { - Write-Log ($LocalizedData.ProcessAlreadyStarted -f $Path) + Write-Verbose -Message ($LocalizedData.ProcessAlreadyStarted -f $Path) } } } function Test-TargetResource { + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.String] + [String] $Path, - [parameter(Mandatory = $true)] + [Parameter(Mandatory = $true)] [AllowEmptyString()] - [System.String] + [String] $Arguments, [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] + [PSCredential] $Credential, - [System.String] - [ValidateSet("Present", "Absent")] - $Ensure="Present", + [ValidateSet('Present', 'Absent')] + [String] + $Ensure = 'Present', - [System.String] + [String] $StandardOutputPath, - [System.String] + [String] $StandardErrorPath, - [System.String] + [String] $StandardInputPath, - [System.String] + [String] $WorkingDirectory ) - if($PsDscContext.RunAsUser) - { - if($PSBoundParameters.ContainsKey("Credential")) - { - $exception = New-Object System.ArgumentException ($LocalizedData.ErrorCredentialParameterNotSupportedWithRunAsCredential -f $PsDscContext.RunAsUser) - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidArgument", InvalidArgument, $null - } - else - { - $exception = New-Object System.ArgumentException ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) - $err = New-Object System.Management.Automation.ErrorRecord $exception, "InvalidCombinationOfArguments", InvalidArgument, $null - } - - Write-Log ($LocalizedData.ErrorStarting -f $Path,($err | Out-String)) - throw $err + if ($null -ne $PsDscContext.RunAsUser) + { + New-InvalidArgumentException -ArgumentName 'PsDscRunAsCredential' -Message ($LocalizedData.ErrorRunAsCredentialParameterNotSupported -f $PsDscContext.RunAsUser) } - $Path=ResolvePath $Path - $PSBoundParameters["Path"] = $Path - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $processes = @(GetWin32_Process @getArguments) + $Path = Expand-Path -Path $Path + + $getWin32ProcessArguments = @{ + Path = $Path + Arguments = $Arguments + } + + if ($null -ne $Credential) + { + $getWin32ProcessArguments['Credential'] = $Credential + } + $win32Processes = @( Get-Win32Process @getWin32ProcessArguments ) - if($Ensure -eq 'Absent') + if ($Ensure -eq 'Absent') { - return ($processes.Count -eq 0) + return ($win32Processes.Count -eq 0) } else { - return ($processes.Count -gt 0) + return ($win32Processes.Count -gt 0) } } @@ -386,50 +500,68 @@ function Get-Win32ProcessOwner } } -function WaitForProcessCount +<# + .SYNOPSIS + Waits for the given number of processes with the given settings to be running. + + .PARAMETER ProcessSettings + The settings of the running process(s) to get the count of. + + .PARAMETER ProcessCount + The number of processes running to wait for. +#> +function Wait-ProcessCount { - [CmdletBinding(SupportsShouldProcess=$true)] + [OutputType([Boolean])] + [CmdletBinding()] param ( - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Path, - - [System.String] - $Arguments, - + [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] - $Credential, + [Hashtable] + $ProcessSettings, - [parameter(Mandatory=$true)] - $waitCount + [Parameter(Mandatory = $true)] + [ValidateRange(0, [Int]::MaxValue)] + [Int] + $ProcessCount ) - $start = [DateTime]::Now + $startTime = [DateTime]::Now + do { - $getArguments = ExtractArguments $PSBoundParameters ("Path","Arguments","Credential") - $value = @(GetWin32_Process @getArguments).Count -eq $waitCount - } while(!$value -and ([DateTime]::Now - $start).TotalMilliseconds -lt 2000) + $actualProcessCount = @( Get-Win32Process @ProcessSettings ).Count + } while ($actualProcessCount -ne $ProcessCount -and ([DateTime]::Now - $startTime).TotalMilliseconds -lt 2000) - return $value + return $actualProcessCount -eq $ProcessCount } <# - If there are many processes it is faster to perform a Get-WmiObject in order to get - Win32_Process objects for all processes. - #> -<# - If there are less processes than the threshold, building a Win32_Process for each matching result of get-process is faster - #> + .SYNOPSIS + Retrieves any Win32_Process objects that match the given path, arguments, and credential. + + .PARAMETER Path + The path that should match the retrieved process. + + .PARAMETER Arguments + The arguments that should match the retrieved process. + + .PARAMETER Credential + The credential whose user name should match the owner of the process. + + .PARAMETER UseGetCimInstanceThreshold + If the number of processes returned by the Get-Process method is greater than or equal to + this value, this function will retrieve all processes at the executable path. This will + help the function execute faster. Otherwise, this function will retrieve each Win32_Process + objects with the product ids returned from Get-Process. +#> function Get-Win32Process { + [OutputType([Object[]])] [CmdletBinding(SupportsShouldProcess = $true)] param ( - [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String] @@ -453,7 +585,7 @@ function Get-Win32Process $processes = @() - if ($getProcessResult.Count -ge $UseGetWmiObjectThreshold) + if ($getProcessResult.Count -ge $UseGetCimInstanceThreshold) { $escapedPathForWqlFilter = ConvertTo-EscapedStringForWqlFilter -FilterString $Path @@ -475,9 +607,9 @@ function Get-Win32Process if ($PSBoundParameters.ContainsKey('Credential')) { - $Domain, $UserName = Get-DomainAndUserName $Credential + $splitCredentialResult = Split-Credenital -Credential $Credential - $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$Domain\$UserName" } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-Win32ProcessOwner -Process $_) -eq "$($splitCredentialResult.Domain)\$($splitCredentialResult.UserName)" } } if ($null -eq $Arguments) @@ -485,7 +617,7 @@ function Get-Win32Process $Arguments = [String]::Empty } - $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput $_.CommandLine) -eq $Arguments } + $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput -CommandLineInput ($_.CommandLine)) -eq $Arguments } return $processes } @@ -559,29 +691,6 @@ function ConvertTo-EscapedStringForWqlFilter return $FilterString.Replace("\","\\").Replace('"','\"').Replace("'","\'") } -function ThrowInvalidArgumentError -{ - [CmdletBinding()] - param - ( - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $errorId, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $errorMessage - ) - - $errorCategory=[System.Management.Automation.ErrorCategory]::InvalidArgument - $exception = New-Object System.ArgumentException $errorMessage; - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception, $errorId, $errorCategory, $null - throw $errorRecord -} - <# .SYNOPSIS Expands a shortened path into a full, rooted path. @@ -603,7 +712,7 @@ function Expand-Path $Path = [Environment]::ExpandEnvironmentVariables($Path) - if (Test-IsRootedPath -Path $Path) + if ([IO.Path]::IsPathRooted($Path)) { if (-not (Test-Path -Path $Path -PathType 'Leaf')) { @@ -612,6 +721,10 @@ function Expand-Path return $Path } + else + { + New-InvalidArgumentException -ArgumentName 'Path' + } if ([String]::IsNullOrEmpty($env:Path)) { @@ -658,97 +771,97 @@ function Expand-Path New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $LocalizedData.FileNotFound) } -function AssertAbsolutePath +<# + .SYNOPSIS + Throws an error if the given path argument is not rooted. + + .PARAMETER PathArgumentName + The name of the path argument that should be rooted. + + .PARAMETER PathArgument + The path arguments that should be rooted. +#> +function Assert-PathArgumentRooted { [CmdletBinding()] param ( - $ParentBoundParameters, - - [System.String] - [Parameter (ValueFromPipeline=$true)] - $ParameterName, - - [switch] - $Exist + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [Hashtable] + $PathArguments ) - - Process + + foreach ($pathArgumentName in $PathArguments.Keys) { - if(!$ParentBoundParameters.ContainsKey($ParameterName)) - { - return - } - - $path=$ParentBoundParameters[$ParameterName] - - if(!(IsRootedPath $Path)) - { - ThrowInvalidArgumentError "PathShouldBeAbsolute" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), - $LocalizedData.PathShouldBeAbsolute) - } - - if(!$Exist.IsPresent) - { - return - } - - if(!(Test-Path $Path)) + if (-not ([IO.Path]::IsPathRooted($PathArguments[$pathArgumentName]))) { - ThrowInvalidArgumentError "PathShouldExist" ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $ParameterName,$Path), - $LocalizedData.PathShouldExist) + New-InvalidArgumentException -ArgumentName $pathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $pathArgumentName, $PathArguments[$pathArgumentName]), $LocalizedData.PathShouldBeAbsolute) } } } -function AssertParameterIsNotSpecified +<# + .SYNOPSIS + Throws an error if the given path argument does not exist. + + .PARAMETER PathArgumentName + The name of the path argument that should exist. + + .PARAMETER PathArgument + The path argument that should exist. +#> +function Assert-PathArgumentExists { [CmdletBinding()] param ( - $ParentBoundParameters, + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgumentName, - [System.String] - [Parameter (ValueFromPipeline=$true)] - $ParameterName + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument ) - Process + if (-not (Test-Path -Path $PathArgument)) { - if($ParentBoundParameters.ContainsKey($ParameterName)) - { - ThrowInvalidArgumentError "ParameterShouldNotBeSpecified" ($LocalizedData.ParameterShouldNotBeSpecified -f $ParameterName) - } + New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldExist) } } <# .SYNOPSIS - Tests is the given path is rooted. + Throws an exception if the given hashtable contains the given key(s). - .PARAMETER Path - The path to test. + .PARAMETER Hashtable + The hashtable to check the keys of. + + .PARAMETER Key + The key(s) that should not be in the hashtable. #> -function Test-IsRootedPath +function Assert-HashtableDoesNotContainKey { - [OutputType([Boolean])] [CmdletBinding()] param ( + [Hashtable] + $Hashtable, + [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [String] - $Path + [String[]] + $Key ) - try - { - return [IO.Path]::IsPathRooted($Path) - } - catch + foreach ($keyName in $Key) { - # If the Path has invalid characters like >, <, etc, we cannot determine if it is rooted so we do not go on - New-InvalidArgumentException -ArgumentName 'Path' -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f 'Path', $Path), $_.Exception.Message) + if ($Hashtable.ContainsKey($keyName)) + { + New-InvalidArgumentException -ArgumentName $keyName -Message ($LocalizedData.ParameterShouldNotBeSpecified -f $keyName) + } } } diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index a39cd28a1..e41bb0711 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -19,7 +19,7 @@ try } AfterAll { - Stop-ProcessByName $script:cmdProcessShortName + Stop-ProcessByName -ProcessName $script:cmdProcessShortName if (Test-Path -Path $script:cmdProcessFullPath) { @@ -32,7 +32,21 @@ try } Context 'Get-TargetResource' { - It 'Should return the correct properties' -Pending { + It 'Should return the correct properties for a process that is absent' { + $processArguments = 'TestGetProperties' + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'Arguments', 'Ensure', 'Path' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.Path -icontains $script:cmdProcessFullPath | Should Be $true + $getTargetResourceResult.Count | Should Be 3 + } + + It 'Should return the correct properties for a process that is present' { $processArguments = 'TestGetProperties' Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments @@ -40,7 +54,7 @@ try $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) - Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceProperties $getTargetResourceProperties + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false $getTargetResourceResult.Arguments | Should Be $processArguments @@ -52,10 +66,45 @@ try $getTargetResourceResult.ProcessId -le 0 | Should Be $false $getTargetResourceResult.Count | Should Be 8 } + + It 'Should return correct Ensure value based on Arguments parameter with multiple processes' { + $actualArguments = 'TestProcessResourceWithArguments' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Absent' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments) + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotOrginalArguments') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Absent' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '') + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + $processes[0].Arguments.Length | Should Be 0 + + $processes = @( Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments) + + $processes.Count | Should Be 1 + $processes[0].Ensure | Should Be 'Present' + $processes[0].Arguments | Should Be $actualArguments + } } Context 'Set-TargetResource' { - It 'Should start and stop a process with no arguments' -Pending { + It 'Should start and stop a process with no arguments' { Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' @@ -238,220 +287,213 @@ try } Context 'Test-TargetResource' { - It 'TestTestWithDirectoryArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - $exists = MSFT_ProcessResource\Test-TargetResource $exePath -WorkingDirectory "something" -StandardOutputPath "something" ` - -StandardErrorPath "something" -StandardInputPath "something" -Arguments "" - Assert !$exists "there should be no process" - } - } - } + It 'Should return correct value based on Arguments' { + $actualArguments = 'TestProcessResourceWithArguments' - - + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments - <# - SPLIT into 3 - - .Synopsis - Tests a process with an argument. - .DESCRIPTION - - Starts a process with an argument - - Ensures Test with "" as Arguments is false - - Ensures Test with the wrong arguments is false - - Ensures Test with no Arguments key is true - - Ensures Test with the right Arguments key is true - - Ensures Get with no arguments key is 1 Present - - Ensures Get with "" as Arguments is 1 Absent - - - Starts process with no arguments - - Ensures Get with no arguments key is 2 - - Ensures Get with null arguments is 1 Present with no Arguments - - Ensures Get with argument is 1 Present with the Arguments - - Set All process to be absent - - Ensure none are left - #> - It 'TestGetSetProcessResourceWithArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "before set, there should be no process" - } + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotTheOriginalArguments' | Should Be $false - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set, cannot find process with no arguments" + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments | Should Be $true } - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") - { - throw "after set, cannot find process with arguments that were not the ones we set" + + It 'Should return false for absent process with directory arguments' { + $testTargetResourceResult = Test-TargetResource ` + -Path $script:cmdProcessFullPath ` + -WorkingDirectory 'something' ` + -StandardOutputPath 'something' ` + -StandardErrorPath 'something' ` + -StandardInputPath 'something' ` + -Arguments '' + + $testTargetResourceResult | Should Be $false } + } + + <# + SPLIT into 3 + + .Synopsis + Tests a process with an argument. + .DESCRIPTION + - Starts a process with an argument + - Ensures Test with "" as Arguments is false + - Ensures Test with the wrong arguments is false + - Ensures Test with no Arguments key is true + - Ensures Test with the right Arguments key is true + - Ensures Get with no arguments key is 1 Present + - Ensures Get with "" as Arguments is 1 Absent + + - Starts process with no arguments + - Ensures Get with no arguments key is 2 + - Ensures Get with null arguments is 1 Present with no Arguments + - Ensures Get with argument is 1 Present with the Arguments + - Set All process to be absent + - Ensure none are left + #> + It 'TestGetSetProcessResourceWithArguments' -Pending { + Invoke-Remotely { + $exePath = $script:cmdProcessFullPath - if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) - { - throw "after set, there should be a process if we specify arguments" - } + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "before set, there should be no process" + } - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') - { - throw "there should be no process without arguments" - } + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set, cannot find process with no arguments" + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") + { + throw "after set, cannot find process with arguments that were not the ones we set" + } - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with no argument" - } + if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) + { + throw "after set, there should be a process if we specify arguments" + } - if($processes[0].Arguments.length -ne 0) - { - throw "there should no arguments in the process" - } + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') + { + throw "there should be no process without arguments" + } + + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with no argument" + } + + if($processes[0].Arguments.length -ne 0) + { + throw "there should no arguments in the process" + } + + $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" - } + if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') + { + throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" + } - if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') - { - throw "the argument should be TestGetSetProcessResourceWithArguments" - } + if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') + { + throw "the argument should be TestGetSetProcessResourceWithArguments" + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" + MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set absent, there should be no process" + if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + { + throw "after set absent, there should be no process" + } } } - } - Context 'WQLEscape' { - It 'TestWQLEscape' -Pending { - WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + Context 'WQLEscape' { + It 'TestWQLEscape' -Pending { + WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + } } - } - Context 'Get-ProcessArgumentsFromCommandLine' { - It 'TestGetProcessArgumentsFromCommandLine' -Pending { - $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) - foreach($testCase in $testCases) - { - $test=$testCase[0] - $expected=$testCase[1] - $actual = GetProcessArgumentsFromCommandLine $test - - $actual | Should Be $expected + Context 'Get-ProcessArgumentsFromCommandLine' { + It 'TestGetProcessArgumentsFromCommandLine' -Pending { + $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) + foreach($testCase in $testCases) + { + $test=$testCase[0] + $expected=$testCase[1] + $actual = GetProcessArgumentsFromCommandLine $test + + $actual | Should Be $expected + } } } - } - - - - - - - # - # Tests for Get-DomainAndUserName function. - # - Context 'Get-DomainAndUserName' { - It 'TestDomainUserNameParseAt' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "user@domain", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseSlash' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseImplicitDomain' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "localuser", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" - Assert ($UserName -eq "localuser") "wrong user $UserName" + Context 'Get-DomainAndUserName' { + It 'TestDomainUserNameParseAt' -Pending { + Invoke-Remotely { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "user@domain", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" + } } - } - Context 'TestDomainUserNameParseSlashFail.Context' { - - BeforeEach { + It 'TestDomainUserNameParseSlash' -Pending { Invoke-Remotely { - $script:originalErrorActionPreference = $ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq "domain") "wrong domain $Domain" + Assert ($UserName -eq "user") "wrong user $UserName" } } - - AfterEach { + + It 'TestDomainUserNameParseImplicitDomain' -Pending { Invoke-Remotely { - $global:ErrorActionPreference = $script:originalErrorActionPreference + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "localuser", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" + Assert ($UserName -eq "localuser") "wrong user $UserName" } } - - It 'TestDomainUserNameParseSlashFail' { - Invoke-Remotely { - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user\foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + + It 'TestDomainUserNameParseSlashFail' -Pending { + $originalErrorActionPreference = $global:ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain\user\foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" } + Assert ($exceptionThrown) "no exception thrown" + + $global:ErrorActionPreference = $script:originalErrorActionPreference } - It 'TestDomainUserNameParseAtFail' { - Invoke-Remotely { - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain@user@foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" - } + It 'TestDomainUserNameParseAtFail' -Pending { + $originalErrorActionPreference = $global:ErrorActionPreference + $global:ErrorActionPreference = 'Stop' + + try + { + $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( + "domain@user@foo", + ("dummy" | ConvertTo-SecureString -asPlainText -Force) + ) ) + } + catch + { + $exceptionThrown = $true + Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" } + Assert ($exceptionThrown) "no exception thrown" + + $global:ErrorActionPreference = $script:originalErrorActionPreference } } } From 554f6630953e8235f715320c95c423027545771c Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Thu, 21 Jul 2016 16:20:01 -0700 Subject: [PATCH 26/49] Replacing group.Clear() and adding empty group test. --- .../MSFT_xGroupResource.psm1 | 49 +++++++++++++++++-- Tests/Unit/MSFT_xGroupResource.Tests.ps1 | 27 ++++++++-- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 65bca09da..14da111e0 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -363,10 +363,18 @@ function Set-TargetResourceOnFullSKU if ($Ensure -eq 'Present') { + $actualMembersAsPrincipals = $null + if ($groupOriginallyExists) { $disposables.Add($group) | Out-Null $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation) + + $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + -Group $group ` + -PrincipalContexts $principalContexts ` + -Disposables $disposables ` + -Credential $Credential } else { @@ -435,12 +443,45 @@ function Set-TargetResourceOnFullSKU if ($membersAsPrincipals.Length -gt 0) { - $group.Members.Clear() + if ($null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Length -gt 0) + { + $membersToAdd = @() + $membersToRemove = @() + + foreach ($membersAsPrincipal in $membersAsPrincipals) + { + if ($actualMembersAsPrincipals -notcontains $membersAsPrincipal) + { + $membersToAdd += $membersAsPrincipal + } + } - # Set the members of the group - if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersAsPrincipals) + foreach ($actualMembersAsPrincipal in $actualMembersAsPrincipals) + { + if ($membersAsPrincipals -notcontains $actualMembersAsPrincipal) + { + $membersToRemove += $actualMembersAsPrincipal + } + } + + # Set the members of the group + if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersToAdd) + { + $saveChanges = $true + } + + if (Remove-GroupMembers -Group $group -MembersAsPrincipals $membersToRemove) + { + $saveChanges = $true + } + } + else { - $saveChanges = $true + # Set the members of the group + if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersAsPrincipals) + { + $saveChanges = $true + } } } else diff --git a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 index 3d4e029bb..d4c9b5d1d 100644 --- a/Tests/Unit/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xGroupResource.Tests.ps1 @@ -98,6 +98,27 @@ InModuleScope 'MSFT_xGroupResource' { } Context 'Set-TargetResource' { + It 'Should create an empty group' { + $testGroupName = 'LocalTestGroup' + + try + { + $setTargetResourceResult = Set-TargetResource -GroupName $testGroupName -Ensure 'Present' + + Test-GroupExists -GroupName $testGroupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName + + $getTargetResourceResult['GroupName'] | Should Be $testGroupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Members'].Count | Should Be 0 + } + finally + { + Remove-Group -GroupName $testGroupName + } + } + It 'Should create a group with 2 users using Members' { $testUserName1 = 'LocalTestUser1' $testUserName2 = 'LocalTestUser2' @@ -120,7 +141,7 @@ InModuleScope 'MSFT_xGroupResource' { Test-GroupExists -GroupName $testGroupName | Should Be $true - $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName $getTargetResourceResult['GroupName'] | Should Be $testGroupName $getTargetResourceResult['Ensure'] | Should Be 'Present' @@ -157,7 +178,7 @@ InModuleScope 'MSFT_xGroupResource' { Test-GroupExists -GroupName $testGroupName | Should Be $true - $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName $getTargetResourceResult['GroupName'] | Should Be $testGroupName $getTargetResourceResult['Ensure'] | Should Be 'Present' @@ -196,7 +217,7 @@ InModuleScope 'MSFT_xGroupResource' { Test-GroupExists -GroupName $testGroupName | Should Be $true - $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName -Credential $domainCredential + $getTargetResourceResult = Get-TargetResource -GroupName $testGroupName $getTargetResourceResult['GroupName'] | Should Be $testGroupName $getTargetResourceResult['Ensure'] | Should Be 'Present' From c68bbd3cc50dae211edca250fe9d2c7d55a3c9ff Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 13:27:10 -0700 Subject: [PATCH 27/49] Fixing bugs and adding integration tests. --- .../MSFT_xGroupResource.psm1 | 57 +++++--- README.md | 9 +- .../Integration/MSFT_xGroupResource.Tests.ps1 | 122 ++++++++++++++++++ 3 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 Tests/Integration/MSFT_xGroupResource.Tests.ps1 diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index 14da111e0..dedc08290 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -441,9 +441,9 @@ function Set-TargetResourceOnFullSKU -Disposables $disposables ` -Credential $Credential - if ($membersAsPrincipals.Length -gt 0) + if ($membersAsPrincipals.Count -gt 0) { - if ($null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Length -gt 0) + if ($null -ne $actualMembersAsPrincipals -and $actualMembersAsPrincipals.Count -gt 0) { $membersToAdd = @() $membersToRemove = @() @@ -536,13 +536,13 @@ function Set-TargetResourceOnFullSKU } } - if ($membersToIncludeAsPrincipals.Length -eq 0 -and $membersToExcludeAsPrincipals.Length -eq 0) + if ($membersToIncludeAsPrincipals.Count -eq 0 -and $membersToExcludeAsPrincipals.Count -eq 0) { New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' -Message ($LocalizedData.IncludeAndExcludeAreEmpty) } } - if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Length -gt 0) + if ($null -ne $membersToExcludeAsPrincipals -and $membersToExcludeAsPrincipals.Count -gt 0) { if (Remove-GroupMembers -Group $group -MembersAsPrincipals $membersToExcludeAsPrincipals) { @@ -550,7 +550,7 @@ function Set-TargetResourceOnFullSKU } } - if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Length -gt 0) + if ($null -ne $membersToIncludeAsPrincipals -and $membersToIncludeAsPrincipals.Count -gt 0) { if (Add-GroupMembers -Group $group -MembersAsPrincipals $membersToIncludeAsPrincipals) { @@ -679,6 +679,11 @@ function Test-TargetResourceOnFullSKU Assert-GroupNameValid -GroupName $GroupName + if ($PSBoundParameters.ContainsKey('Members')) + { + $Members = @( $Members ) + } + $principalContexts = @{} $disposables = New-Object -TypeName 'System.Collections.ArrayList' @@ -692,6 +697,15 @@ function Test-TargetResourceOnFullSKU return ($Ensure -eq 'Absent') } + if ($null -ne $group.Members) + { + $actualGroupMembers = @($group.Members) + } + else + { + $actualGroupMembers = $null + } + $disposables.Add($group) | Out-Null Write-Verbose -Message ($LocalizedData.GroupExists -f $GroupName) @@ -727,7 +741,7 @@ function Test-TargetResourceOnFullSKU if ($Members.Count -eq 0) { - return ($group.Members.Count -eq 0) + return ($null -eq $actualGroupMembers -or $actualGroupMembers.Count -eq 0) } else { @@ -735,15 +749,15 @@ function Test-TargetResourceOnFullSKU $Members = @( Remove-DuplicateMembers -Members $Members ) # Resolve the names to actual principal objects. - $expectedMembersAsPrincipals = ConvertTo-Principals ` + $expectedMembersAsPrincipals = @( ConvertTo-Principals ` -MemberNames $Members ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) - if ($expectedMembersAsPrincipals.Length -ne $group.Members.Count) + if (($null -eq $expectedMembersAsPrincipals -xor $null -eq $actualGroupMembers) -or (($null -ne $expectedMembersAsPrincipals -and $null -ne $actualGroupMembers) -and $expectedMembersAsPrincipals.Count -ne $actualGroupMembers.Count)) { - Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembersAsPrincipals.Length, $group.Members.Count) + Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembersAsPrincipals.Count, $actualGroupMembers.Count) return $false } @@ -764,7 +778,7 @@ function Test-TargetResourceOnFullSKU } } } - else + elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) { $actualMembersAsPrincipals = Get-MembersAsPrincipals ` -Group $group ` @@ -1012,7 +1026,7 @@ function Set-TargetResourceOnNanoServer # Remove duplicate names as strings. $Members = @( Remove-DuplicateMembers -Members $Members ) - if ($Members.Length -gt 0) + if ($Members.Count -gt 0) { # Get current members $groupMembers = Get-MembersOnNanoServer -Group $group @@ -1054,7 +1068,7 @@ function Set-TargetResourceOnNanoServer } } - if ($MembersToInclude.Length -eq 0 -and $MembersToExclude.Length -eq 0) + if ($MembersToInclude.Count -eq 0 -and $MembersToExclude.Count -eq 0) { New-InvalidArgumentException -ArgumentName 'MembersToInclude and MembersToExclude' -Message ($LocalizedData.IncludeAndExcludeAreEmpty) } @@ -1221,8 +1235,6 @@ function Test-TargetResourceOnNanoServer if ($PSBoundParameters.ContainsKey('Members')) { - Write-Verbose 'Testing Members...' - if ($PSBoundParameters.ContainsKey('MembersToInclude')) { New-InvalidArgumentException -ArgumentName 'MembersToInclude' -Message ($LocalizedData.MembersAndIncludeExcludeConflict -f 'Members', 'MembersToInclude') @@ -1239,9 +1251,9 @@ function Test-TargetResourceOnNanoServer # Get current members $groupMembers = Get-MembersOnNanoServer -Group $group - if ($expectedMembers.Length -ne $groupMembers.Length) + if ($expectedMembers.Count -ne $groupMembers.Count) { - Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembers.Length, $groupMembers.Length) + Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembers.Count, $groupMembers.Count) return $false } @@ -2259,7 +2271,16 @@ function Get-Group -Disposables $Disposables ` -Scope $env:computerName - return [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $GroupName) + try + { + $group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($principalContext, $GroupName) + } + catch + { + $group = $null + } + + return $group } <# diff --git a/README.md b/README.md index 947073307..0d8731c25 100644 --- a/README.md +++ b/README.md @@ -336,12 +336,15 @@ These parameters will be the same for each Windows optional feature in the set. ### Unreleased -* xGroup: Fix Verbose output in Get-MembersAsPrincipals function. - Fix bug when credential parameter passed does not contain local or domain context. * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. * Updated appveyor.yml to use the default image. * Merged xPackage with in-box Package resource and added tests. -* xGroup: Fixed logic bug in MembersToInclude and MembersToExclude +* xGroup: + * Fixed Verbose output in Get-MembersAsPrincipals function. + * Fixed bug when credential parameter passed does not contain local or domain context. + * Fixed logic bug in MembersToInclude and MembersToExclude. + * Fixed bug when trying to include the built-in Administrator in Members. + * Fixed bug where Test-TargetResource would check for members when none specified. ### 3.12.0.0 diff --git a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 new file mode 100644 index 000000000..12b50a54f --- /dev/null +++ b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 @@ -0,0 +1,122 @@ +Import-Module "$PSScriptRoot\..\..\DSCResource.Tests\TestHelper.psm1" -Force + +Initialize-TestEnvironment ` + -DSCModuleName 'xPSDesiredStateConfiguration' ` + -DSCResourceName 'MSFT_xGroupResource' ` + -TestType Integration ` + | Out-Null + +InModuleScope 'MSFT_xGroupResource' { + Describe 'xGroup Integration Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xGroupResource.TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\..\DSCResources\CommonResourceHelper.psm1" -Force + } + + It 'Should create an empty group' { + $configurationName = 'CreateEmptyGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + + $resourceName = 'EmptyGroup' + $groupName = 'Empty' + + try + { + Configuration $configurationName + { + param () + + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + + xGroup $resourceName + { + Ensure = 'Present' + GroupName = $groupName + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null + } + else + { + Test-GroupExists -GroupName $groupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $groupName + + $getTargetResourceResult['GroupName'] | Should Be $groupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Members'].Count | Should Be 0 + } + } + finally + { + Remove-Group -GroupName $groupName + + if (Test-Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + + It 'Should create an Administrators group with the built-in Administrator' { + $configurationName = 'CreateAdministratorsGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + + $resourceName = 'AdministratorsGroup' + $groupName = 'Administrators' + $builtInAdministratorUsername = 'Administrator' + + try + { + Configuration $configurationName + { + param () + + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + + xGroup $resourceName + { + GroupName = $groupName + MembersToInclude = $builtInAdministratorUsername + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null + } + else + { + Test-GroupExists -GroupName $groupName | Should Be $true + + $getTargetResourceResult = Get-TargetResource -GroupName $groupName + + $getTargetResourceResult['GroupName'] | Should Be $groupName + $getTargetResourceResult['Ensure'] | Should Be 'Present' + $getTargetResourceResult['Members'].Count -gt 0 | Should Be $true + } + } + finally + { + if (Test-Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } + } +} \ No newline at end of file From 49fc7e0a58684973fb9f86efb9041b174a537dd8 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 13:30:12 -0700 Subject: [PATCH 28/49] Removing extra code. --- DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index dedc08290..a7bf418ef 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -679,11 +679,6 @@ function Test-TargetResourceOnFullSKU Assert-GroupNameValid -GroupName $GroupName - if ($PSBoundParameters.ContainsKey('Members')) - { - $Members = @( $Members ) - } - $principalContexts = @{} $disposables = New-Object -TypeName 'System.Collections.ArrayList' From 55608492466584387aff667a554e48e99330273e Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 13:32:08 -0700 Subject: [PATCH 29/49] Adding newline at end of file. --- Tests/Integration/MSFT_xGroupResource.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 index 12b50a54f..69e4293a9 100644 --- a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 @@ -119,4 +119,4 @@ InModuleScope 'MSFT_xGroupResource' { } } } -} \ No newline at end of file +} From f69243d34344cf74d883f0ab2d38156566ad550d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 16:35:17 -0700 Subject: [PATCH 30/49] Switching test environment to Unit to fix module location for now. --- Tests/Integration/MSFT_xGroupSet.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Integration/MSFT_xGroupSet.Tests.ps1 b/Tests/Integration/MSFT_xGroupSet.Tests.ps1 index 2356813f1..afcd8b149 100644 --- a/Tests/Integration/MSFT_xGroupSet.Tests.ps1 +++ b/Tests/Integration/MSFT_xGroupSet.Tests.ps1 @@ -4,7 +4,7 @@ param () $TestEnvironment = Initialize-TestEnvironment ` -DSCModuleName 'xPSDesiredStateConfiguration' ` -DSCResourceName 'MSFT_xGroupSet' ` - -TestType Integration + -TestType Unit Describe "xGroupSet Integration Tests" { BeforeAll { From 4e1065b0eed43c99f386717aa799696656fcf834 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 16:42:42 -0700 Subject: [PATCH 31/49] Switched wrong test. --- Tests/Integration/MSFT_xGroupResource.Tests.ps1 | 2 +- Tests/Integration/MSFT_xGroupSet.Tests.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 index 69e4293a9..0c0fdc8b2 100644 --- a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 @@ -3,7 +3,7 @@ Initialize-TestEnvironment ` -DSCModuleName 'xPSDesiredStateConfiguration' ` -DSCResourceName 'MSFT_xGroupResource' ` - -TestType Integration ` + -TestType Unit ` | Out-Null InModuleScope 'MSFT_xGroupResource' { diff --git a/Tests/Integration/MSFT_xGroupSet.Tests.ps1 b/Tests/Integration/MSFT_xGroupSet.Tests.ps1 index afcd8b149..2356813f1 100644 --- a/Tests/Integration/MSFT_xGroupSet.Tests.ps1 +++ b/Tests/Integration/MSFT_xGroupSet.Tests.ps1 @@ -4,7 +4,7 @@ param () $TestEnvironment = Initialize-TestEnvironment ` -DSCModuleName 'xPSDesiredStateConfiguration' ` -DSCResourceName 'MSFT_xGroupSet' ` - -TestType Unit + -TestType Integration Describe "xGroupSet Integration Tests" { BeforeAll { From 5f3c1715e24dde7c454d4fb53ad0b2c6349938a3 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 22 Jul 2016 16:45:12 -0700 Subject: [PATCH 32/49] Removing InModuleScope and switching test environment back to Integration. --- .../Integration/MSFT_xGroupResource.Tests.ps1 | 156 ++++++++---------- 1 file changed, 71 insertions(+), 85 deletions(-) diff --git a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 index 0c0fdc8b2..eadd44846 100644 --- a/Tests/Integration/MSFT_xGroupResource.Tests.ps1 +++ b/Tests/Integration/MSFT_xGroupResource.Tests.ps1 @@ -3,119 +3,105 @@ Initialize-TestEnvironment ` -DSCModuleName 'xPSDesiredStateConfiguration' ` -DSCResourceName 'MSFT_xGroupResource' ` - -TestType Unit ` + -TestType Integration ` | Out-Null -InModuleScope 'MSFT_xGroupResource' { - Describe 'xGroup Integration Tests' { - BeforeAll { - Import-Module "$PSScriptRoot\..\Unit\MSFT_xGroupResource.TestHelper.psm1" -Force - Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force - Import-Module "$PSScriptRoot\..\..\DSCResources\CommonResourceHelper.psm1" -Force - } - - It 'Should create an empty group' { - $configurationName = 'CreateEmptyGroup' - $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName - - $resourceName = 'EmptyGroup' - $groupName = 'Empty' - - try - { - Configuration $configurationName - { - param () +Describe 'xGroup Integration Tests' { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xGroupResource.TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\..\DSCResources\CommonResourceHelper.psm1" -Force + } - Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + It 'Should create an empty group' { + $configurationName = 'CreateEmptyGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName - xGroup $resourceName - { - Ensure = 'Present' - GroupName = $groupName - } - } + $resourceName = 'EmptyGroup' + $groupName = 'Empty' - & $configurationName -OutputPath $configurationPath + try + { + Configuration $configurationName + { + param () - Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' - if (Test-IsNanoServer) + xGroup $resourceName { - $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' - $localGroup | Should Not Be $null + Ensure = 'Present' + GroupName = $groupName } - else - { - Test-GroupExists -GroupName $groupName | Should Be $true + } - $getTargetResourceResult = Get-TargetResource -GroupName $groupName + & $configurationName -OutputPath $configurationPath - $getTargetResourceResult['GroupName'] | Should Be $groupName - $getTargetResourceResult['Ensure'] | Should Be 'Present' - $getTargetResourceResult['Members'].Count | Should Be 0 - } + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null } - finally + else { - Remove-Group -GroupName $groupName - - if (Test-Path $configurationPath) - { - Remove-Item -Path $configurationPath -Recurse -Force - } + Test-GroupExists -GroupName $groupName | Should Be $true } } + finally + { + Remove-Group -GroupName $groupName - It 'Should create an Administrators group with the built-in Administrator' { - $configurationName = 'CreateAdministratorsGroup' - $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName - - $resourceName = 'AdministratorsGroup' - $groupName = 'Administrators' - $builtInAdministratorUsername = 'Administrator' - - try + if (Test-Path $configurationPath) { - Configuration $configurationName - { - param () + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } - Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' + It 'Should create an Administrators group with the built-in Administrator' { + $configurationName = 'CreateAdministratorsGroup' + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName - xGroup $resourceName - { - GroupName = $groupName - MembersToInclude = $builtInAdministratorUsername - } - } + $resourceName = 'AdministratorsGroup' + $groupName = 'Administrators' + $builtInAdministratorUsername = 'Administrator' - & $configurationName -OutputPath $configurationPath + try + { + Configuration $configurationName + { + param () - Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + Import-DSCResource -ModuleName 'xPSDesiredStateConfiguration' - if (Test-IsNanoServer) + xGroup $resourceName { - $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' - $localGroup | Should Not Be $null + GroupName = $groupName + MembersToInclude = $builtInAdministratorUsername } - else - { - Test-GroupExists -GroupName $groupName | Should Be $true + } - $getTargetResourceResult = Get-TargetResource -GroupName $groupName + & $configurationName -OutputPath $configurationPath - $getTargetResourceResult['GroupName'] | Should Be $groupName - $getTargetResourceResult['Ensure'] | Should Be 'Present' - $getTargetResourceResult['Members'].Count -gt 0 | Should Be $true - } + Start-DscConfiguration -Path $configurationPath -Wait -Verbose -Force + + if (Test-IsNanoServer) + { + $localGroup = Get-LocalGroup -Name $groupName -ErrorAction 'SilentlyContinue' + $localGroup | Should Not Be $null } - finally + else { - if (Test-Path $configurationPath) - { - Remove-Item -Path $configurationPath -Recurse -Force - } + Test-GroupExists -GroupName $groupName | Should Be $true + } + } + finally + { + if (Test-Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force } } } From 90f64302dd736689125a57366f36bb636c66bea2 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 25 Jul 2016 10:17:24 -0700 Subject: [PATCH 33/49] Adding more bug fixes and updating README. --- .../MSFT_xGroupResource.psm1 | 26 ++++++++++++------- README.md | 3 +++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 index a7bf418ef..f9884d7d1 100644 --- a/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 +++ b/DSCResources/MSFT_xGroupResource/MSFT_xGroupResource.psm1 @@ -370,11 +370,11 @@ function Set-TargetResourceOnFullSKU $disposables.Add($group) | Out-Null $whatIfShouldProcess = $pscmdlet.ShouldProcess(($LocalizedData.GroupWithName -f $GroupName), $LocalizedData.SetOperation) - $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` -Group $group ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) } else { @@ -750,17 +750,17 @@ function Test-TargetResourceOnFullSKU -Disposables $disposables ` -Credential $Credential ) - if (($null -eq $expectedMembersAsPrincipals -xor $null -eq $actualGroupMembers) -or (($null -ne $expectedMembersAsPrincipals -and $null -ne $actualGroupMembers) -and $expectedMembersAsPrincipals.Count -ne $actualGroupMembers.Count)) + if ($expectedMembersAsPrincipals.Count -ne $actualGroupMembers.Count) { Write-Verbose -Message ($LocalizedData.MembersNumberMismatch -f 'Members', $expectedMembersAsPrincipals.Count, $actualGroupMembers.Count) return $false } - $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` -Group $group ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) # Compare the two member lists. foreach ($expectedMemberAsPrincipal in $expectedMembersAsPrincipals) @@ -775,11 +775,11 @@ function Test-TargetResourceOnFullSKU } elseif ($PSBoundParameters.ContainsKey('MembersToInclude') -or $PSBoundParameters.ContainsKey('MembersToExclude')) { - $actualMembersAsPrincipals = Get-MembersAsPrincipals ` + $actualMembersAsPrincipals = @( Get-MembersAsPrincipals ` -Group $group ` -PrincipalContexts $principalContexts ` -Disposables $disposables ` - -Credential $Credential + -Credential $Credential ) if ($PSBoundParameters.ContainsKey('MembersToInclude')) { @@ -1426,7 +1426,7 @@ function Get-MembersOnFullSKU $members = New-Object -TypeName 'System.Collections.ArrayList' - $membersAsPrincipals = Get-MembersAsPrincipals -Group $Group -PrincipalContexts $PrincipalContexts -Disposables $Disposables -Credential $Credential + $membersAsPrincipals = @( Get-MembersAsPrincipals -Group $Group -PrincipalContexts $PrincipalContexts -Disposables $Disposables -Credential $Credential ) foreach ($membersAsPrincipal in $membersAsPrincipals) { @@ -2300,7 +2300,10 @@ function Assert-GroupNameValid if ($GroupName.IndexOfAny($invalidCharacters) -ne -1) { - ThrowInvalidArgumentError -ErrorId 'GroupNameHasInvalidCharacter' -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [String]::Join(' ', $invalidCharacters)) + New-InvalidArgumentException ` + -ArgumentName 'GroupNameHasInvalidCharacter' ` + -Message ($LocalizedData.InvalidGroupName ` + -f $GroupName, [String]::Join(' ', $invalidCharacters)) } $nameContainsOnlyWhitspaceOrDots = $true @@ -2317,7 +2320,10 @@ function Assert-GroupNameValid if ($nameContainsOnlyWhitspaceOrDots) { - ThrowInvalidArgumentError -ErrorId 'GroupNameHasOnlyWhiteSpacesAndDots' -ErrorMessage ($LocalizedData.InvalidGroupName -f $GroupName, [String]::Join(' ', $invalidCharacters)) + New-InvalidArgumentException ` + -ErrorId 'GroupNameHasOnlyWhiteSpacesAndDots' ` + -Message ($LocalizedData.InvalidGroupName ` + -f $GroupName, [String]::Join(' ', $invalidCharacters)) } } diff --git a/README.md b/README.md index 0d8731c25..3734da8e1 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,9 @@ These parameters will be the same for each Windows optional feature in the set. * Fixed logic bug in MembersToInclude and MembersToExclude. * Fixed bug when trying to include the built-in Administrator in Members. * Fixed bug where Test-TargetResource would check for members when none specified. + * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bugs in Assert-GroupNameValid to throw correct exception. ### 3.12.0.0 From 81172924e9c7dbb0830714e576a6256f8734b26d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:07:25 -0700 Subject: [PATCH 34/49] Updating xProcess resource and tests. --- .../MSFT_xProcessResource.psm1 | 37 +- Tests/CommonTestHelper.psm1 | 85 +++ Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 591 +++++++++--------- 3 files changed, 397 insertions(+), 316 deletions(-) diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 49b3676e9..11403b890 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -13,6 +13,7 @@ ProcessAlreadyStopped = Process matching path '{0}' not found running and no act ErrorStopping = Failure stopping processes matching path '{0}' with IDs '({1})'. Message: {2}. ErrorStarting = Failure starting process matching path '{0}'. Message: {1}. StartingProcessWhatif = Start-Process +StoppingProcessWhatIf = Stop-Process ProcessNotFound = Process matching path '{0}' not found PathShouldBeAbsolute = The path should be absolute PathShouldExist = The path should exist @@ -244,7 +245,7 @@ function Set-TargetResource { Assert-HashtableDoesNotContainKey -Hashtable $PSBoundParameters -Key @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - if ($win32Processes.Count -gt 0) + if ($win32Processes.Count -gt 0 -and $PSCmdlet.ShouldProcess($Path, $LocalizedData.StoppingProcessWhatif)) { $processIds = $win32Processes.ProcessId @@ -281,7 +282,7 @@ function Set-TargetResource foreach ($shouldBeRootedPathArgument in $shouldBeRootedPathArguments) { - if ($null -ne $PSBoundParameters[$shouldBeRootedPathArgument]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldBeRootedPathArgument])) { Assert-PathArgumentRooted -PathArgumentName $shouldBeRootedPathArgument -PathArgument $PSBoundParameters[$shouldBeRootedPathArgument] } @@ -291,7 +292,7 @@ function Set-TargetResource foreach ($shouldExistPathArgument in $shouldExistPathArguments) { - if ($null -ne $PSBoundParameters[$shouldExistPathArgument]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$shouldExistPathArgument])) { Assert-PathArgumentExists -PathArgumentName $shouldExistPathArgument -PathArgument $PSBoundParameters[$shouldExistPathArgument] } @@ -313,7 +314,7 @@ function Set-TargetResource foreach ($startProcessOptionalArgumentName in $startProcessOptionalArgumentMap.Keys) { - if ($null -ne $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]]) + if (-not [String]::IsNullOrEmpty($PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]])) { $startProcessArguments[$startProcessOptionalArgumentName] = $PSBoundParameters[$startProcessOptionalArgumentMap[$startProcessOptionalArgumentName]] } @@ -617,9 +618,17 @@ function Get-Win32Process $Arguments = [String]::Empty } - $processes = Where-Object -InputObject $processes -FilterScript { (Get-ArgumentsFromCommandLineInput -CommandLineInput ($_.CommandLine)) -eq $Arguments } + $processesWithMatchingArguments = @() - return $processes + foreach ($process in $processes) + { + if ((Get-ArgumentsFromCommandLineInput -CommandLineInput ($process.CommandLine)) -eq $Arguments) + { + $processesWithMatchingArguments += $process + } + } + + return $processesWithMatchingArguments } <# @@ -788,16 +797,18 @@ function Assert-PathArgumentRooted ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [Hashtable] - $PathArguments + [String] + $PathArgumentName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $PathArgument ) - foreach ($pathArgumentName in $PathArguments.Keys) + if (-not ([IO.Path]::IsPathRooted($PathArgument))) { - if (-not ([IO.Path]::IsPathRooted($PathArguments[$pathArgumentName]))) - { - New-InvalidArgumentException -ArgumentName $pathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $pathArgumentName, $PathArguments[$pathArgumentName]), $LocalizedData.PathShouldBeAbsolute) - } + New-InvalidArgumentException -ArgumentName $PathArgumentName -Message ($LocalizedData.InvalidArgumentAndMessage -f ($LocalizedData.InvalidArgument -f $PathArgumentName, $PathArgument), $LocalizedData.PathShouldBeAbsolute) } } diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 0e987b0d8..9813bd523 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -555,6 +555,90 @@ function Test-IsFileLocked } } +<# + .SYNOPSIS + Tests that calling the Set-TargetResource cmdlet with the WhatIf parameter specified produces output that contains all the given expected output. + If empty or null expected output is specified, this cmdlet will check that there was no output from Set-TargetResource with WhatIf specified. + Uses Pester. + + .PARAMETER Parameters + The parameters to pass to Set-TargetResource. + These parameters do not need to contain that WhatIf parameter, but if they do, + this function will run Set-TargetResource with WhatIf = $true no matter what is in the Parameters Hashtable. + + .PARAMETER ExpectedOutput + The output expected to be in the output from running WhatIf with the Set-TargetResource cmdlet. + If this parameter is empty or null, this cmdlet will check that there was no output from Set-TargetResource with WhatIf specified. +#> +function Test-SetTargetResourceWithWhatIf +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [Hashtable] + $Parameters, + + [String[]] + $ExpectedOutput + ) + + $transcriptPath = Join-Path -Path (Get-Location) -ChildPath 'WhatIfTestTranscript.txt' + if (Test-Path -Path $transcriptPath) + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} -TimeoutSeconds 10 + Remove-Item -Path $transcriptPath -Force + } + + $Parameters['WhatIf'] = $true + + try + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} + + Start-Transcript -Path $transcriptPath + Set-TargetResource @Parameters + Stop-Transcript + + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} + + $transcriptContent = Get-Content -Path $transcriptPath -Raw + $transcriptContent | Should Not Be $null + + $regexString = '\*+[^\*]*\*+' + + # Removing transcript diagnostic logging at top and bottom of file + $selectedString = Select-String -InputObject $transcriptContent -Pattern $regexString -AllMatches + + foreach ($match in $selectedString.Matches) + { + $transcriptContent = $transcriptContent.Replace($match.Captures, '') + } + + $transcriptContent = $transcriptContent.Replace("`r`n", "").Replace("`n", "") + + if ($null -eq $ExpectedOutput -or $ExpectedOutput.Count -eq 0) + { + [String]::IsNullOrEmpty($transcriptContent) | Should Be $true + } + else + { + foreach ($expectedOutputPiece in $ExpectedOutput) + { + $transcriptContent.Contains($expectedOutputPiece) | Should Be $true + } + } + } + finally + { + if (Test-Path -Path $transcriptPath) + { + Wait-ScriptBlockReturnTrue -ScriptBlock {-not (Test-IsFileLocked -Path $transcriptPath)} -TimeoutSeconds 10 + Remove-Item -Path $transcriptPath -Force + } + } +} + <# .SYNOPSIS Enters a DSC Resource test environment. @@ -649,5 +733,6 @@ Export-ModuleMember -Function ` Test-User, ` Wait-ScriptBlockReturnTrue, ` Test-IsFileLocked, ` + Test-SetTargetResourceWithWhatIf, ` Enter-DscResourceTestEnvironment, ` Exit-DscResourceTestEnvironment diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index e41bb0711..d3346eb0f 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -1,4 +1,4 @@ -Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" +Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" -Force $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName 'xPSDesiredStateConfiguration' ` @@ -16,6 +16,17 @@ try $script:cmdProcessFullName = 'ProcessTest.exe' $script:cmdProcessFullPath = "$env:winDir\system32\ProcessTest.exe" Copy-Item -Path "$env:winDir\system32\cmd.exe" -Destination $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force + + $script:processTestFolder = Join-Path -Path (Get-Location) -ChildPath 'ProcessTestFolder' + + if (Test-Path -Path $script:processTestFolder) + { + Remove-Item -Path $script:processTestFolder -Recurse -Force + } + + New-Item -Path $script:processTestFolder -ItemType 'Directory' | Out-Null + + Push-Location -Path $script:processTestFolder } AfterAll { @@ -25,6 +36,13 @@ try { Remove-Item -Path $script:cmdProcessFullPath -ErrorAction 'SilentlyContinue' -Force } + + Pop-Location + + if (Test-Path -Path $script:processTestFolder) + { + Remove-Item -Path $script:processTestFolder -Recurse -Force + } } BeforeEach { @@ -32,7 +50,7 @@ try } Context 'Get-TargetResource' { - It 'Should return the correct properties for a process that is absent' { + It 'Should return the correct properties for a process that is absent with Arguments' { $processArguments = 'TestGetProperties' $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments @@ -46,7 +64,21 @@ try $getTargetResourceResult.Count | Should Be 3 } - It 'Should return the correct properties for a process that is present' { + It 'Should return the correct properties for a process that is absent without Arguments' { + $processArguments = '' + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'Arguments', 'Ensure', 'Path' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Absent' + $getTargetResourceResult.Path -icontains $script:cmdProcessFullPath | Should Be $true + $getTargetResourceResult.Count | Should Be 3 + } + + It 'Should return the correct properties for a process that is present with Arguments' { $processArguments = 'TestGetProperties' Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments @@ -67,6 +99,27 @@ try $getTargetResourceResult.Count | Should Be 8 } + It 'Should return the correct properties for a process that is present without Arguments' { + $processArguments = '' + + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + + $getTargetResourceResult = Get-TargetResource -Path $script:cmdProcessFullPath -Arguments $processArguments + $getTargetResourceProperties = @( 'VirtualMemorySize', 'Arguments', 'Ensure', 'PagedMemorySize', 'Path', 'NonPagedMemorySize', 'HandleCount', 'ProcessId' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceProperties + + $getTargetResourceResult.VirtualMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Arguments | Should Be $processArguments + $getTargetResourceResult.Ensure | Should Be 'Present' + $getTargetResourceResult.PagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.Path.IndexOf("ProcessTest.exe",[Stringcomparison]::OrdinalIgnoreCase) -le 0 | Should Be $false + $getTargetResourceResult.NonPagedMemorySize -le 0 | Should Be $false + $getTargetResourceResult.HandleCount -le 0 | Should Be $false + $getTargetResourceResult.ProcessId -le 0 | Should Be $false + $getTargetResourceResult.Count | Should Be 8 + } + It 'Should return correct Ensure value based on Arguments parameter with multiple processes' { $actualArguments = 'TestProcessResourceWithArguments' @@ -116,172 +169,194 @@ try Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false } - It 'Should have correct output with WhatIf is specified' -Pending { - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $script:cmdProcessFullPath - TestWhatif $script $script:cmdProcessFullPath + It 'Should have correct output for absent process with WhatIf specified and default Ensure' { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Arguments = '' + } + + $expectedWhatIfOutput = @( $LocalizedData.StartingProcessWhatif, $script:cmdProcessFullPath ) + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput $expectedWhatIfOutput + + if ($setTargetResourceParameters.ContainsKey('WhatIf')) + { + $setTargetResourceParameters.Remove('WhatIf') + } + + $testTargetResourceResult = Test-TargetResource @setTargetResourceParameters + $testTargetResourceResult | Should Be $false + } + + It 'Should have no output for absent process with WhatIf specified and Ensure Absent' { + $setTargetResourceParameters = @{ + Ensure = 'Absent' + Path = $script:cmdProcessFullPath + Arguments = '' + } - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $script:cmdProcessFullPath - TestWhatif $script $script:cmdProcessFullPath + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput '' } - It 'TestWhatifStop' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath + It 'Should have correct output for existing process with WhatIf specified and Ensure Absent' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments '') - { - throw "before set, there should be no process" - } + $setTargetResourceParameters = @{ + Ensure = 'Absent' + Path = $script:cmdProcessFullPath + Arguments = '' + } - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments '' + $expectedWhatIfOutput = @( $LocalizedData.StoppingProcessWhatif, $script:cmdProcessFullPath ) - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Ensure Absent -Whatif -Arguments ''" -f $exePath - TestWhatif $script $exePath + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput $expectedWhatIfOutput - $script = "MSFT_ProcessResource\Set-TargetResource -Path {0} -Whatif -Arguments ''" -f $exePath - TestWhatif $script $exePath - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure "Absent" -Arguments '' + if ($setTargetResourceParameters.ContainsKey('WhatIf')) + { + $setTargetResourceParameters.Remove('WhatIf') } + + $testTargetResourceResult = Test-TargetResource @setTargetResourceParameters + $testTargetResourceResult | Should Be $false } - <# - .Synopsis - tests input, output and error streams are hooked up as well as the working directory - #> - It 'TestStreamsAndWorkingDirectory' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") + It 'Should have no output for existing process with WhatIf specified and default Ensure' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Arguments = '' + } + + Test-SetTargetResourceWithWhatIf -Parameters $setTargetResourceParameters -ExpectedOutput '' + } + + It 'Should provide correct error output to the specified error and output streams when using invalid input from the specified input stream' { + $errorPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsError.txt' + $outputPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsOutput.txt' + $inputPath = Join-Path -Path (Get-Location) -ChildPath 'TestStreamsInput.txt' + + $workingDirectoryPath = Join-Path -Path (Get-Location) -ChildPath 'TestWorkingDirectory' + + foreach ($path in @( $errorPath, $outputPath, $inputPath, $workingDirectoryPath )) + { + if (Test-Path -Path $path) { - throw "before set, there should be no process" + Remove-Item -Path $path -Recurse -Force } - - $errorPath="$PWD\TestStreamsError.txt" - $outputPath="$PWD\TestStreamsOutput.txt" - $inputPath="$PWD\TestStreamsInput.txt" + } + + New-Item -Path $workingDirectoryPath -ItemType 'Directory' | Out-Null - Remove-Item $errorPath -Force -ErrorAction SilentlyContinue - Remove-Item $outputPath -Force -ErrorAction SilentlyContinue + $inputFileText = "ECHO Testing ProcessTest.exe ` + dir volumeSyntaxError:\ ` + set /p waitforinput=Press [y/n]?: " - "ECHO Testing ProcessTest.exe ` - dir volumeSyntaxError:\ ` - set /p waitforinput=Press [y/n]?: " | out-file $inputPath -Encoding ascii + Out-File -FilePath $inputPath -InputObject $inputFileText -Encoding 'ASCII' - MSFT_ProcessResource\Set-TargetResource -Path $exePath -WorkingDirectory $processTestPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments "" + Set-TargetResource -Path $script:cmdProcessFullPath -WorkingDirectory $workingDirectoryPath -StandardOutputPath $outputPath -StandardErrorPath $errorPath -StandardInputPath $inputPath -Arguments '' - if(!(TryForAWhile ([scriptblock]::create("(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments '').Ensure -eq 'Absent'")))) - { - throw "process did not terminate" - } + Wait-ScriptBlockReturnTrue -ScriptBlock { (Get-TargetResource -Path $script:cmdProcessFullPath -Arguments '').Ensure -ieq 'Absent' } -TimeoutSeconds 10 - # Race condition exists for retrieving contents of the process error stream file - Start-Sleep -Seconds 2 + Wait-ScriptBlockReturnTrue -ScriptBlock { Test-IsFileLocked -Path $errorPath } -TimeoutSeconds 2 - $errorFile=get-content $errorPath -Raw - $outputFile=get-content $outputPath -Raw + $errorFileContent = Get-Content -Path $errorPath -Raw + $errorFileContent | Should Not Be $null - if((Get-Culture).Name -ieq 'en-us') - { - Assert ($errorFile.Contains("The filename, directory name, or volume label syntax is incorrect.")) "no stdErr string in file" - Assert ($outputFile.Contains("Press [y/n]?:")) "no stdOut string in file" - Assert ($outputFile.ToLower().Contains($processTestPath.ToLower())) "working directory: $processTestPath, not in output file" - } - else - { - Assert ($errorFile.Length -gt 0) "no stdErr string in file" - Assert ($outputFile.Length -gt 0) "no stdOut string in file" - } + Wait-ScriptBlockReturnTrue -ScriptBlock { Test-IsFileLocked -Path $outputPath } -TimeoutSeconds 2 + + $outputFileContent = Get-Content -Path $outputPath -Raw + $outputFileContent | Should Not Be $null + + if ((Get-Culture).Name -ieq 'en-us') + { + $errorFileContent.Contains('The filename, directory name, or volume label syntax is incorrect.') | Should Be $true + $outputFileContent.Contains('Press [y/n]?:') | Should Be $true + $outputFileContent.ToLower().Contains($workingDirectoryPath.ToLower()) | Should Be $true + } + else + { + $errorFileContent.Length -gt 0 | Should Be $true + $outputFileContent.Length -gt 0 | Should Be $true } } - It 'TestCannotWritePropertiesWithAbsent' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Absent";Arguments=""} - $null=$args.Add($writeProperty,"anything") - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set when using Absent" -f $writeProperty) - } - } - } + It 'Should throw when trying to specify streams or working directory with Ensure Absent' { + $invalidPropertiesWithAbsent = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - It 'TestCannotUseInvalidPath' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - $relativePath = "..\ExistingFile.txt" - "something" > $relativePath + foreach ($invalidPropertyWithAbsent in $invalidPropertiesWithAbsent) + { + $setTargetResourceArguments = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Absent' + Arguments = '' + $invalidPropertyWithAbsent = 'Something' + } + + { Set-TargetResource @setTargetResourceArguments } | Should Throw ($LocalizedData.ParameterShouldNotBeSpecified -f $invalidPropertyWithAbsent) + } + } - $nonExistingPath = "$processTestPath\IDoNotExist.Really.I.Do.Not" - del $nonExistingPath -ErrorAction SilentlyContinue + It 'Should throw when passing a relative path to stream or working directory parameters' { + $invalidRelativePath = '..\RelativePath' + $pathParameters = @( 'StandardOutputPath', 'StandardErrorPath', 'StandardInputPath', 'WorkingDirectory' ) - foreach($writeProperty in "StandardOutputPath","StandardErrorPath","StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $null=$args.Add($writeProperty,$relativePath) - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set to relative path" -f $writeProperty) + foreach($pathParameter in $pathParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathParameter = $invalidRelativePath } + + { Set-TargetResource @setTargetResourceParameters } | Should Throw $LocalizedData.PathShouldBeAbsolute + } + } + It 'Should throw when providing a nonexistent path for StandardInputPath or WorkingDirectory' { + $invalidNonexistentPath = Join-Path -Path (Get-Location) -ChildPath 'NonexistentPath' - foreach($writeProperty in "StandardInputPath","WorkingDirectory") - { - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $args[$writeProperty] = $nonExistingPath - $thrown = $false - try - { - MSFT_ProcessResource\Set-TargetResource @args - } - catch - { - $thrown = $true - } - Assert $thrown ("{0} cannot be set to nonexisting path" -f $writeProperty) - } + if (Test-Path -Path $invalidNonexistentPath) + { + Remove-Item -Path $invalidNonexistentPath -Recurse -Force + } - foreach($writeProperty in "StandardOutputPath","StandardErrorPath") - { - # Paths that need not exist so no exception should be thrown - $args=@{Path=$exePath;Ensure="Present";Arguments=""} - $null=$args.Add($writeProperty,$nonExistingPath) - MSFT_ProcessResource\Set-TargetResource @args - get-process ProcessTest | stop-process - del $nonExistingPath -ErrorAction SilentlyContinue + $pathMustExistParameters = @( 'StandardInputPath', 'WorkingDirectory' ) + + foreach ($pathMustExistParameter in $pathMustExistParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathMustExistParameter = $invalidNonexistentPath } + + { Set-TargetResource @setTargetResourceParameters } | Should Throw $LocalizedData.PathShouldExist } } - It 'TestGetWmiObject' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "" - MSFT_ProcessResource\Set-TargetResource $exePath -Arguments "abc" - $r=@(GetWin32_Process $exePath -useWmiObjectCount 0) - AssertEquals $r.Count 1 "get-wmiobject with filter" - $a=@(GetWin32_Process $exePath -useWmiObjectCount 5) - AssertEquals $a.Count 1 "through get-process" - MSFT_ProcessResource\Set-TargetResource $exePath -Ensure Absent -Arguments "" + It 'Should not throw when providing a nonexistent path for StandardOutputPath or StandardErrorPath' { + $invalidNonexistentPath = Join-Path -Path (Get-Location) -ChildPath 'NonexistentPath' + + if (Test-Path -Path $invalidNonexistentPath) + { + Remove-Item -Path $invalidNonexistentPath -Recurse -Force + } + + $pathNotNeedExistParameters = @( 'StandardOutputPath', 'StandardErrorPath' ) + + foreach ($pathNotNeedExistParameter in $pathNotNeedExistParameters) + { + $setTargetResourceParameters = @{ + Path = $script:cmdProcessFullPath + Ensure = 'Present' + Arguments = '' + $pathNotNeedExistParameter = $invalidNonexistentPath + } + + { Set-TargetResource @setTargetResourceParameters } | Should Not Throw } } } @@ -295,7 +370,7 @@ try Test-TargetResource -Path $script:cmdProcessFullPath -Arguments '' | Should Be $false Test-TargetResource -Path $script:cmdProcessFullPath -Arguments 'NotTheOriginalArguments' | Should Be $false - + Test-TargetResource -Path $script:cmdProcessFullPath -Arguments $actualArguments | Should Be $true } @@ -310,190 +385,100 @@ try $testTargetResourceResult | Should Be $false } - } - - <# - SPLIT into 3 - - .Synopsis - Tests a process with an argument. - .DESCRIPTION - - Starts a process with an argument - - Ensures Test with "" as Arguments is false - - Ensures Test with the wrong arguments is false - - Ensures Test with no Arguments key is true - - Ensures Test with the right Arguments key is true - - Ensures Get with no arguments key is 1 Present - - Ensures Get with "" as Arguments is 1 Absent - - - Starts process with no arguments - - Ensures Get with no arguments key is 2 - - Ensures Get with null arguments is 1 Present with no Arguments - - Ensures Get with argument is 1 Present with the Arguments - - Set All process to be absent - - Ensure none are left - #> - It 'TestGetSetProcessResourceWithArguments' -Pending { - Invoke-Remotely { - $exePath = $script:cmdProcessFullPath - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "before set, there should be no process" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments" - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set, cannot find process with no arguments" - } - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "NotTheOriginalArguments") - { - throw "after set, cannot find process with arguments that were not the ones we set" - } - - if(!(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments")) - { - throw "after set, there should be a process if we specify arguments" - } - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Absent') - { - throw "there should be no process without arguments" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Arguments "" - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "") - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with no argument" - } - - if($processes[0].Arguments.length -ne 0) - { - throw "there should no arguments in the process" - } - - $processes = @(MSFT_ProcessResource\Get-TargetResource -Path $exePath -Arguments "TestGetSetProcessResourceWithArguments") - - if($processes.Count -ne 1 -or $processes[0].Ensure -ne 'Present') - { - throw "there should be only one process present with TestGetSetProcessResourceWithArguments as argument" - } - - if($processes[0].Arguments -ne 'TestGetSetProcessResourceWithArguments') - { - throw "the argument should be TestGetSetProcessResourceWithArguments" - } - - MSFT_ProcessResource\Set-TargetResource -Path $exePath -Ensure Absent -Arguments "" - - if(MSFT_ProcessResource\Test-TargetResource -Path $exePath -Arguments "") - { - throw "after set absent, there should be no process" - } - } + } - Context 'WQLEscape' { - It 'TestWQLEscape' -Pending { - WQLEscape "a'`"\b" | Should Be "a\'\`"\\b" + Context 'Get-Win32Process' { + It 'Should only return one process when arguments were changed for that process' { + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments '' + Set-TargetResource -Path $script:cmdProcessFullPath -Arguments 'abc' + + $processes = @( Get-Win32Process -Path $script:cmdProcessFullPath -UseGetCimInstanceThreshold 0 ) + $processes.Count | Should Be 1 + + $processes = @( Get-Win32Process -Path $script:cmdProcessFullPath -UseGetCimInstanceThreshold 5 ) + $processes.Count | Should Be 1 } } - - Context 'Get-ProcessArgumentsFromCommandLine' { - It 'TestGetProcessArgumentsFromCommandLine' -Pending { - $testCases=(("c a ","a"),('"c b d" e ',"e"),(" a b","b"), (" abc ","")) - foreach($testCase in $testCases) + + Context 'Get-ArgumentsFromCommandLineInput' { + It 'Should retrieve expected arguments from command line input' { + $testCases = @( @{ + CommandLineInput = "c a " + ExpectedArguments = "a" + }, + @{ + CommandLineInput = '"c b d" e ' + ExpectedArguments = "e" + }, + @{ + CommandLineInput = " a b" + ExpectedArguments = "b" + }, + @{ + CommandLineInput = " abc " + ExpectedArguments = "" + } + ) + + foreach ($testCase in $testCases) { - $test=$testCase[0] - $expected=$testCase[1] - $actual = GetProcessArgumentsFromCommandLine $test + $commandLineInput = $testCase.CommandLineInput + $expectedArguments = $testCase.ExpectedArguments + $actualArguments = Get-ArgumentsFromCommandLineInput -CommandLineInput $commandLineInput - $actual | Should Be $expected + $actualArguments | Should Be $expectedArguments } } } - Context 'Get-DomainAndUserName' { - It 'TestDomainUserNameParseAt' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "user@domain", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseSlash' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq "domain") "wrong domain $Domain" - Assert ($UserName -eq "user") "wrong user $UserName" - } - } - - It 'TestDomainUserNameParseImplicitDomain' -Pending { - Invoke-Remotely { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "localuser", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - Assert ($Domain -eq $env:COMPUTERNAME) "wrong domain $Domain" - Assert ($UserName -eq "localuser") "wrong user $UserName" - } + Context 'Split-Credential' { + It 'Should return correct domain and username with @ seperator' { + $testUsername = 'user@domain' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + $splitCredentialResult = Split-Credential -Credential $testCredential + + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' } - It 'TestDomainUserNameParseSlashFail' -Pending { - $originalErrorActionPreference = $global:ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + It 'Should return correct domain and username with \ seperator' { + $testUsername = 'domain\user' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain\user\foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + $splitCredentialResult = Split-Credential -Credential $testCredential - $global:ErrorActionPreference = $script:originalErrorActionPreference + $splitCredentialResult.Domain | Should Be 'domain' + $splitCredentialResult.Username | Should Be 'user' } - It 'TestDomainUserNameParseAtFail' -Pending { - $originalErrorActionPreference = $global:ErrorActionPreference - $global:ErrorActionPreference = 'Stop' + It 'Should return correct domain and username with a local user' { + $testUsername = 'localuser' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) - try - { - $Domain, $UserName = Get-DomainAndUserName ([System.Management.Automation.PSCredential]::new( - "domain@user@foo", - ("dummy" | ConvertTo-SecureString -asPlainText -Force) - ) ) - } - catch - { - $exceptionThrown = $true - Assert ($_.Exception -is [System.ArgumentException]) "Exception of type $($_.Exception.GetType().ToString()) was not expected" - } - Assert ($exceptionThrown) "no exception thrown" + $splitCredentialResult = Split-Credential -Credential $testCredential - $global:ErrorActionPreference = $script:originalErrorActionPreference + $splitCredentialResult.Username | Should Be 'localuser' + } + + It 'Should throw when more than one \ in username' { + $testUsername = 'user\domain\foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw + } + + It 'Should throw when more than one @ in username' { + $testUsername = 'user@domain@foo' + $testPassword = ConvertTo-SecureString -String 'dummy' -AsPlainText -Force + $testCredential = New-Object -TypeName 'PSCredential' -ArgumentList @($testUsername, $testPassword) + + { $splitCredentialResult = Split-Credential -Credential $testCredential } | Should Throw } } } From 06371a49769992cf113eddbc9450067ca7bd6f06 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:09:31 -0700 Subject: [PATCH 35/49] Updating README and HQRM plan. --- HighQualityResourceModulePlan.md | 4 ++-- README.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/HighQualityResourceModulePlan.md b/HighQualityResourceModulePlan.md index 634bb12b4..36f4082cb 100644 --- a/HighQualityResourceModulePlan.md +++ b/HighQualityResourceModulePlan.md @@ -40,8 +40,8 @@ The PSDesiredStateConfiguration High Quality Resource Module will consist of the - [x] Archive - [x] Group - [x] Package - - [ ] Process (In Progress) - - [ ] Registry + - [X] Process + - [ ] Registry (In Progress) - [x] Service - [ ] WindowsOptionalFeature - [ ] [3. Resolve Nano Server vs. Full Server Resources](#resolve-nano-server-vs-full-server-resources) diff --git a/README.md b/README.md index 3734da8e1..ddcf82148 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,7 @@ These parameters will be the same for each Windows optional feature in the set. * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. * Fix bugs in Assert-GroupNameValid to throw correct exception. +* Merged xProcess with in-box Process resource and added tests. ### 3.12.0.0 From 71336d5673764c25d391fae52b5caaf15650b04d Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Fri, 29 Jul 2016 17:12:19 -0700 Subject: [PATCH 36/49] Removing extra function from CommonResourceHelper. --- DSCResources/CommonResourceHelper.psm1 | 52 ------------------- .../MSFT_xProcessResource.psm1 | 2 +- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/DSCResources/CommonResourceHelper.psm1 b/DSCResources/CommonResourceHelper.psm1 index 587de66ff..39dc0b203 100644 --- a/DSCResources/CommonResourceHelper.psm1 +++ b/DSCResources/CommonResourceHelper.psm1 @@ -84,58 +84,6 @@ function New-InvalidOperationException throw $errorRecordToThrow } -<# -# The goal of this function is to get domain and username from PSCredential -# without calling GetNetworkCredential() method. -# Call to GetNetworkCredential() expose password as a plain text in memory. -#> -function Get-DomainAndUserName([PSCredential]$Credential) -{ - # - # Supported formats: DOMAIN\username, username@domain - # - $wrongFormat = $false - if ($Credential.UserName.Contains('\')) - { - $segments = $Credential.UserName.Split('\') - if ($segments.Length -gt 2) - { - # i.e. domain\user\foo - $wrongFormat = $true - } else { - $Domain = $segments[0] - $UserName = $segments[1] - } - } - elseif ($Credential.UserName.Contains('@')) - { - $segments = $Credential.UserName.Split('@') - if ($segments.Length -gt 2) - { - # i.e. user@domain@foo - $wrongFormat = $true - } else { - $UserName = $segments[0] - $Domain = $segments[1] - } - } - else - { - # support for default domain (localhost) - return @( $env:COMPUTERNAME, $Credential.UserName ) - } - - if ($wrongFormat) - { - $message = $LocalizedData.ErrorInvalidUserName -f $Credential.UserName - Write-Verbose $message - $exception = New-Object System.ArgumentException $message - throw New-Object System.Management.Automation.ErrorRecord $exception, "InvalidUserName", InvalidArgument, $null - } - - return @( $Domain, $UserName ) -} - Export-ModuleMember -Function ` Test-IsNanoServer, ` New-InvalidArgumentException, ` diff --git a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 index 11403b890..15fc1cbcf 100644 --- a/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 +++ b/DSCResources/MSFT_xProcessResource/MSFT_xProcessResource.psm1 @@ -28,7 +28,7 @@ ErrorCredentialParameterNotSupportedWithRunAsCredential = The PsDscRunAsCredenti } # Commented out until more languages are supported -# Import-LocalizedData LocalizedData -filename MSFT_ProcessResource.strings.psd1 +# Import-LocalizedData LocalizedData -filename MSFT_xProcessResource.strings.psd1 Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1" From d539b0f1eb36bd873f682dd567b157a647ff9867 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Mon, 1 Aug 2016 15:38:23 -0700 Subject: [PATCH 37/49] Suppressing PSSA rule in tests. --- Tests/Unit/MSFT_xProcessResource.Tests.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 index d3346eb0f..be65087e5 100644 --- a/Tests/Unit/MSFT_xProcessResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xProcessResource.Tests.ps1 @@ -1,3 +1,6 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] +param () + Import-Module -Name "$PSScriptRoot\..\CommonTestHelper.psm1" -Force $script:testEnvironment = Enter-DscResourceTestEnvironment ` From 0a30ab14410a0e097a4097807db4ae967cdc6ed3 Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Tue, 2 Aug 2016 08:46:10 +0700 Subject: [PATCH 38/49] Change parameter validation of Description to ValidateNotNull --- DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 b/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 index 3bc8311e2..20bd67648 100644 --- a/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 +++ b/DSCResources/MSFT_xServiceResource/MSFT_xServiceResource.psm1 @@ -135,7 +135,7 @@ function Test-TargetResource [String] $DisplayName, - [ValidateNotNullOrEmpty()] + [ValidateNotNull()] [String] $Description, @@ -245,7 +245,7 @@ function Set-TargetResource [String] $DisplayName, - [ValidateNotNullOrEmpty()] + [ValidateNotNull()] [String] $Description, From 516e3c73d794f8a8baebc244735e82996ccc340c Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Tue, 2 Aug 2016 09:04:31 +0700 Subject: [PATCH 39/49] Remove invalid parameters The parameters CompressionLevel, DestinationType and MatchSource are not valid and need to be removed for the example to work. --- Examples/Sample_xArchive_ExpandArchive.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/Examples/Sample_xArchive_ExpandArchive.ps1 b/Examples/Sample_xArchive_ExpandArchive.ps1 index 5854205d4..796ac7016 100644 --- a/Examples/Sample_xArchive_ExpandArchive.ps1 +++ b/Examples/Sample_xArchive_ExpandArchive.ps1 @@ -28,9 +28,6 @@ Configuration Sample_xArchive_ExpandArchive { Path = $Path Destination = $Destination - CompressionLevel = $CompressionLevel - DestinationType="Directory" - MatchSource=$MatchSource } } } From 72978102696ab8644b7f5a3b4f4b51d2a8d7e8ec Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Tue, 2 Aug 2016 09:06:35 +0700 Subject: [PATCH 40/49] Remove obsolete configuration parameters The parameters CompressionLevel and MatchSource are not valid for this resource and should be removed to avoid confusion. --- Examples/Sample_xArchive_ExpandArchive.ps1 | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Examples/Sample_xArchive_ExpandArchive.ps1 b/Examples/Sample_xArchive_ExpandArchive.ps1 index 796ac7016..371e7bdf6 100644 --- a/Examples/Sample_xArchive_ExpandArchive.ps1 +++ b/Examples/Sample_xArchive_ExpandArchive.ps1 @@ -9,16 +9,7 @@ Configuration Sample_xArchive_ExpandArchive [parameter (mandatory=$true)] [ValidateNotNullOrEmpty()] - [string] $Destination, - - [parameter (mandatory=$false)] - [ValidateSet("Optimal","NoCompression","Fastest")] - [string] - $CompressionLevel = "Optimal", - - [parameter (mandatory=$false)] - [boolean] - $MatchSource = $false + [string] $Destination ) Import-DscResource -ModuleName xPSDesiredStateConfiguration From f04ddc8b3898af685aef4254e0341621086aa04b Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Tue, 2 Aug 2016 09:13:16 +0700 Subject: [PATCH 41/49] Convert Path parameter to single string The xArchive resource does not support the extraction of multiple files into the same destination. --- Examples/Sample_xArchive_ExpandArchive.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Sample_xArchive_ExpandArchive.ps1 b/Examples/Sample_xArchive_ExpandArchive.ps1 index 371e7bdf6..c4d4f1166 100644 --- a/Examples/Sample_xArchive_ExpandArchive.ps1 +++ b/Examples/Sample_xArchive_ExpandArchive.ps1 @@ -5,7 +5,7 @@ Configuration Sample_xArchive_ExpandArchive ( [parameter(mandatory=$true)] [ValidateNotNullOrEmpty()] - [string[]] $Path, + [string] $Path, [parameter (mandatory=$true)] [ValidateNotNullOrEmpty()] From cfc6920d9a8e55594a421ed5398d2b4b0fe338b1 Mon Sep 17 00:00:00 2001 From: Daniel Krebs Date: Tue, 2 Aug 2016 09:36:19 +0700 Subject: [PATCH 42/49] Update Unreleased section of README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3734da8e1..08c24ca54 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,8 @@ These parameters will be the same for each Windows optional feature in the set. * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. * Fix bugs in Assert-GroupNameValid to throw correct exception. +* xService + * Updated xService resource to allow empty string for Description parameter. ### 3.12.0.0 From 61002f61e2b93683dd8cc84b0efbed53677af140 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 2 Aug 2016 18:53:47 -0700 Subject: [PATCH 43/49] Adding registry parameters back into xPackage. --- .../MSFT_xPackageResource.psm1 | 330 +++++++++++++++++- .../MSFT_xPackageResource.schema.mof | Bin 906 -> 2146 bytes 2 files changed, 320 insertions(+), 10 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index a69c214f8..7baf7cf42 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -63,6 +63,7 @@ Redirectingpackagepathtocachefilelocation = Redirecting package path to cache fi ThebinaryisanEXE = The binary is an EXE Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1} +ProvideParameterForRegistryCheck = "Please provide the {0} parameter in order to check for installation status from a registry key." '@ } @@ -176,6 +177,65 @@ function Convert-PathToUri return $uri } +<# + .SYNOPSIS + Retrieves a value from a registry without throwing errors. + + .PARAMETER Key + The key of the registry to get the value from. + + .PARAMETER Value + The name of the value to retrieve. + + .PARAMETER RegistryHive + The registry hive to retrieve the value from. + + .PARAMETER RegistyView + The registry view to retrieve the value from. +#> +function Get-RegistryValueWithErrorsIgnored +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Key, + + [Parameter(Mandatory = $true)] + [String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryView] + $RegistryView + ) + + $registryValue = $null + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key) + + if ($null -ne $subRegistryKey) + { + $registryValue = $subRegistryKey.GetValue($Value) + } + } + catch + { + $exceptionText = ($_ | Out-String).Trim() + Write-Verbose -Message "An exception occured while attempting to retrieve a registry value: $exceptionText" + } + + return $registryValue +} + <# .SYNOPSIS Retrieves the product entry for the package with the given name and/or identifying number. @@ -183,8 +243,23 @@ function Convert-PathToUri .PARAMETER Name The name of the product entry to retrieve. + .PARAMETER CreateCheckRegValue + Indicates whether or not to retrieve the package installation status from a registry. + .PARAMETER IdentifyingNumber The identifying number of the product entry to retrieve. + + .PARAMETER InstalledCheckRegHive + The registry hive to check for package installation status. + + .PARAMETER InstalledCheckRegKey + The registry key to open to check for package installation status. + + .PARAMETER InstalledCheckRegValueName + The registry value name to check for package installation status. + + .PARAMETER InstalledCheckRegValueData + The value to compare against the retrieved registry value to check for package installation. #> function Get-ProductEntry { @@ -195,7 +270,23 @@ function Get-ProductEntry $Name, [String] - $IdentifyingNumber + $IdentifyingNumber, + + [Switch] + $CreateCheckRegValue, + + [ValidateSet('LocalMachine', 'CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData ) $uninstallRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' @@ -227,6 +318,37 @@ function Get-ProductEntry } } + if ($null -eq $productEntry) + { + if ($CreateCheckRegValue) + { + $installValue = $null + + $win32OperatingSystem = Get-CimInstance -ClassName 'Win32_OperatingSystem' -ErrorAction 'SilentlyContinue' + + # If 64-bit OS, check 64-bit registry view first + if ($win32OperatingSystem.OSArchitecture -ieq '64-bit') + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry64' + } + + if ($null -eq $installValue) + { + $installValue = Get-RegistryValueWithErrorsIgnored -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -RegistryHive $InstalledCheckRegHive -RegistryView 'Registry32' + } + + if ($null -ne $installValue) + { + if ($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + { + $productEntry = @{ + Installed = $true + } + } + } + } + } + return $productEntry } @@ -283,7 +405,23 @@ function Test-TargetResource $SignerThumbprint, [String] - $ServerCertificateValidationCallback + $ServerCertificateValidationCallback, + + [Boolean] + $CreateCheckRegValue = $false, + + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData ) Assert-PathExtensionValid -Path $Path @@ -294,7 +432,30 @@ function Test-TargetResource $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } - $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + Assert-RegistryParametersValid -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName + + foreach ($parameterName in $checkRegistryValueParameters.Keys) + { + $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] + } + } + + $productEntry = Get-ProductEntry @getProductEntryParameters Write-Verbose -Message ($LocalizedData.EnsureIsEnsure -f $Ensure) @@ -311,8 +472,15 @@ function Test-TargetResource if ($null -ne $productEntry) { - $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' - Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $displayName) + if ($CreateCheckRegValue) + { + Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $Name) + } + else + { + $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' + Write-Verbose -Message ($LocalizedData.PackageAppearsInstalled -f $displayName) + } } else { @@ -386,7 +554,23 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [AllowEmptyString()] [String] - $ProductId + $ProductId, + + [Boolean] + $CreateCheckRegValue = $false, + + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData ) Assert-PathExtensionValid -Path $Path @@ -397,7 +581,30 @@ function Get-TargetResource $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } - $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + Assert-RegistryParametersValid -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName + + foreach ($parameterName in $checkRegistryValueParameters.Keys) + { + $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] + } + } + + $productEntry = Get-ProductEntry @getProductEntryParameters if ($null -eq $productEntry) { @@ -408,6 +615,15 @@ function Get-TargetResource Installed = $false } } + elseif ($CreateCheckRegValue) + { + return @{ + Ensure = 'Present' + Name = $Name + ProductId = $identifyingNumber + Installed = $true + } + } <# Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE). @@ -590,6 +806,48 @@ function Get-MsiProductCode return $productCode } +<# + .SYNOPSIS + Asserts that the InstalledCheckRegKey, InstalledCheckRegValueName, and + InstalledCheckRegValueData parameter required for retrieving package installation status + from a registry are not null or empty. + + .PARAMETER InstalledCheckRegKey + The InstalledCheckRegKey parameter to check. + + .PARAMETER InstalledCheckRegValueName + The InstalledCheckRegValueName parameter to check. + + .PARAMETER InstalledCheckRegValueData + The InstalledCheckRegValueData parameter to check. + + .NOTES + This could be done with parameter validation. + It is implemented this way to provide a clearer error message. +#> +function Assert-RegistryParametersValid { + [CmdletBinding()] + param + ( + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData + ) + + foreach ($parameter in $PSBoundParameters.Keys) + { + if ([String]::IsNullOrEmpty($PSBoundParameters[$parameter])) + { + New-InvalidArgumentException -ArgumentName $parameter -Message ($LocalizedData.ProvideParameterForRegistryCheck -f $parameter) "Please provide the $parameter parameter in order to check for installation status from a registry key." + } + } +} + function Set-TargetResource { [CmdletBinding(SupportsShouldProcess = $true)] @@ -642,12 +900,28 @@ function Set-TargetResource $SignerThumbprint, [String] - $ServerCertificateValidationCallback + $ServerCertificateValidationCallback, + + [Boolean] + $CreateCheckRegValue = $false, + + [ValidateSet('LocalMachine','CurrentUser')] + [String] + $InstalledCheckRegHive = 'LocalMachine', + + [String] + $InstalledCheckRegKey, + + [String] + $InstalledCheckRegValueName, + + [String] + $InstalledCheckRegValueData ) $ErrorActionPreference = 'Stop' - if (Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId) + if (Test-TargetResource @PSBoundParameters) { return } @@ -1010,6 +1284,21 @@ function Set-TargetResource $operationMessageString = $LocalizedData.PackageInstalled } + if ($CreateCheckRegValue) + { + $registryValueString = '{0}\{1}\{2}' -f $InstalledCheckRegHive, $InstalledCheckRegKey, $InstalledCheckRegValueName + if ($Ensure -eq 'Present') + { + Write-Verbose -Message ($LocalizedData.CreatingRegistryValue -f $registryValueString) + Set-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName -Data $InstalledCheckRegValueData + } + else + { + Write-Verbose ($LocalizedData.RemovingRegistryValue -f $registryValueString) + Remove-RegistryValue -RegistryHive $InstalledCheckRegHive -Key $InstalledCheckRegKey -Value $InstalledCheckRegValueName + } + } + <# Check if a reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on some client SKUs (worked on both Server and Client Skus in Windows 10). @@ -1026,7 +1315,28 @@ function Set-TargetResource if ($Ensure -eq 'Present') { - $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber + $getProductEntryParameters = @{ + Name = $Name + IdentifyingNumber = $identifyingNumber + } + + $checkRegistryValueParameters = @{ + CreateCheckRegValue = $CreateCheckRegValue + InstalledCheckRegHive = $InstalledCheckRegHive + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + + if ($CreateCheckRegValue) + { + foreach ($parameterName in $checkRegistryValueParameters.Keys) + { + $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] + } + } + + $productEntry = Get-ProductEntry @getProductEntryParameters if (-not $productEntry) { diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 89ac4017dd37e593e0c6c47043fcf54b3f7b9835..5f003c438739ceaf44e6a9940b760192fd196817 100644 GIT binary patch literal 2146 zcmchYOK;Oq5QWbgiT_|_1yY3Clp+MH;80OPXw_6~stV$0EV^yTQw?Vbk)ri>n*{z37?trt3 z6Z{%a3wQ|hl6&H6n(BEe&(}n|aSZwPIj3b?%lHdEVwd}*24&o*`nXdBV_-=KSFoJI z?ZiVB{8ek|f{egf&yBsL0okVNIgC#6J#oLsj)jyg7p}Lam_iBRx~|M??7Eig$7iR` zvdom9y)kBa9;ibnOgX9R!~)eD15@;J*-%5Z`Zr)Ar}9mvtv^HjRWbfd^sq~)-_&tM zF4?4&^WWxa1*0=C34OLn-3?bTKLc$YIf0WVtk2khXg|O-S^K^5_eT?E1dB_TiD@A~ ze>NYcj6A*!EVypsbx)no7E5KL!% z(pL{e=HZMwPEoy`?c-&S^SUkaf8thkm~+&Hu?YT6fu`bl18Y?eriF-Jneqlq(MX?o zwhdCx`*siK#I`*$Vh*xhc^5yH!&jF{=zeXIK5&guf^Gh6@5gA}-m@tS*9sePx3AQ$ zSy%NLy7@C7woxKwoBZGTE52HF--2xu@%LN{-#y38a0?HclbY?`jNwfV3cnc-w`;$D E0ACklp#T5? literal 906 zcmbVLO;6)65WQFGf3SQ4DMD@8A_S+BwxU|HL=ECnRmh2l)ZjQ}Z0{~0{yWYGX$s+j zq9mhv^TsbTFDqX%t#1Lf$khQI9u1BL^w6850LA6=AC|!Z{U5UAk)=Qn*QDP|3I++H z`OV!2umqi3On{t`>tJzr!8ClN`n}hFkC4?r6b8IoCM^VP`$|Iu4a!iD&`DfIZxyOL zu{F^Kg-XdxX^U`P_WTXcO}o^FBO zEQ^72;0vViu5#osTou@rg(*nySer38Mx$E<}#;-#5l>(tsG?RIgVOJIsL{kpzm zE+kwsT~GhB1)|Hzkb2a;M&pwzI6nPV1>@n!1;KRep36lz3#P-90o_^T2i#^ig`$Z} zbLYrS)+IXSA! Date: Thu, 4 Aug 2016 10:00:59 +0700 Subject: [PATCH 44/49] Replace alias ipmo with full cmdlet name --- ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 b/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 index eaa161f32..629a8a217 100644 --- a/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 +++ b/ResourceDesignerScripts/New-PSSessionConfigurationResource.ps1 @@ -1,4 +1,4 @@ -ipmo xDSCResourceDesigner +Import-Module -Name 'xDSCResourceDesigner' $resProperties = @{ Name = New-xDscResourceProperty -Description 'Name of the PS Remoting Endpoint' ` From 20f934eaa7f972ce8cc156845961394f6daae725 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 9 Aug 2016 11:37:29 -0700 Subject: [PATCH 45/49] Adding tests for registry parameters and fixing xPackage for new params. --- .../MSFT_xPackageResource.psm1 | 199 ++++++++++++--- Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 235 +++++++++++++++++- 2 files changed, 392 insertions(+), 42 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 index 7baf7cf42..fc653af7f 100644 --- a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 +++ b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.psm1 @@ -63,7 +63,10 @@ Redirectingpackagepathtocachefilelocation = Redirecting package path to cache fi ThebinaryisanEXE = The binary is an EXE Userhasrequestedloggingneedtoattacheventhandlerstotheprocess = User has requested logging, need to attach event handlers to the process StartingwithstartInfoFileNamestartInfoArguments = Starting {0} with {1} -ProvideParameterForRegistryCheck = "Please provide the {0} parameter in order to check for installation status from a registry key." +ProvideParameterForRegistryCheck = Please provide the {0} parameter in order to check for installation status from a registry key. +ErrorSettingRegistryValue = An error occured while attempting to set the registry key {0} value {1} to {2} +ErrorRemovingRegistryValue = An error occured while attempting to remove the registry key {0} value {1} +ExeCouldNotBeUninstalled = The .exe file found at {0} could not be uninstalled. The uninstall functionality may not be implemented in this .exe file. '@ } @@ -133,7 +136,7 @@ function Convert-ProductIdToIdentifyingNumber } catch { - New-InvalidArgumentException -ArgumentName 'ProductId' -Messsage ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) + New-InvalidArgumentException -ArgumentName 'ProductId' -Message ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) } } @@ -447,12 +450,8 @@ function Test-TargetResource if ($CreateCheckRegValue) { - Assert-RegistryParametersValid -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName - - foreach ($parameterName in $checkRegistryValueParameters.Keys) - { - $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] - } + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData + $getProductEntryParameters += $checkRegistryValueParameters } $productEntry = Get-ProductEntry @getProductEntryParameters @@ -580,6 +579,12 @@ function Get-TargetResource { $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } + else + { + $identifyingNumber = $ProductId + } + + $packageResourceResult = @{} $getProductEntryParameters = @{ Name = $Name @@ -596,40 +601,44 @@ function Get-TargetResource if ($CreateCheckRegValue) { - Assert-RegistryParametersValid -InstalledCheckRegHive $InstalledCheckRegHive -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName + Assert-RegistryParametersValid -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName -InstalledCheckRegValueData $InstalledCheckRegValueData - foreach ($parameterName in $checkRegistryValueParameters.Keys) - { - $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] - } + $getProductEntryParameters += $checkRegistryValueParameters + $packageResourceResult += $checkRegistryValueParameters } $productEntry = Get-ProductEntry @getProductEntryParameters if ($null -eq $productEntry) { - return @{ + $packageResourceResult += @{ Ensure = 'Absent' Name = $Name ProductId = $identifyingNumber + Path = $Path Installed = $false } + + return $packageResourceResult } elseif ($CreateCheckRegValue) { - return @{ + $packageResourceResult += @{ Ensure = 'Present' Name = $Name ProductId = $identifyingNumber + Path = $Path Installed = $true } + + return $packageResourceResult } <# Identifying number can still be null here (e.g. remote MSI with Name specified, local EXE). If the user gave a product ID just pass it through, otherwise get it from the product. #> - if ($null -eq $identifyingNumber) + if ($null -eq $identifyingNumber -and $null -ne $productEntry.Name) { $identifyingNumber = Split-Path -Path $productEntry.Name -Leaf } @@ -663,7 +672,7 @@ function Get-TargetResource $displayName = Get-LocalizedRegistryKeyValue -RegistryKey $productEntry -ValueName 'DisplayName' - return @{ + $packageResourceResult += @{ Ensure = 'Present' Name = $displayName Path = $Path @@ -675,6 +684,8 @@ function Get-TargetResource PackageDescription = $comments Publisher = $publisher } + + return $packageResourceResult } <# @@ -825,7 +836,8 @@ function Get-MsiProductCode This could be done with parameter validation. It is implemented this way to provide a clearer error message. #> -function Assert-RegistryParametersValid { +function Assert-RegistryParametersValid +{ [CmdletBinding()] param ( @@ -843,8 +855,114 @@ function Assert-RegistryParametersValid { { if ([String]::IsNullOrEmpty($PSBoundParameters[$parameter])) { - New-InvalidArgumentException -ArgumentName $parameter -Message ($LocalizedData.ProvideParameterForRegistryCheck -f $parameter) "Please provide the $parameter parameter in order to check for installation status from a registry key." + New-InvalidArgumentException -ArgumentName $parameter -Message ($LocalizedData.ProvideParameterForRegistryCheck -f $parameter) + } + } +} + +<# + .SYNOPSIS + Sets the value of a registry key to the specified data. + + .PARAMETER Key + The registry key that contains the value to set. + + .PARAMETER Value + The value name of the registry key value to set. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to set. + + .PARAMETER Data + The data to set the registry key value to. +#> +function Set-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Key, + + [Parameter(Mandatory = $true)] + [String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $Data + ) + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) + + # Opens the subkey with write access + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) + + if ($null -eq $subRegistryKey) + { + Write-Verbose "Key: '$Key'" + $subRegistryKey = $baseRegistryKey.CreateSubKey($Key) } + + $subRegistryKey.SetValue($Value, $Data) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($LocalizedData.ErrorSettingRegistryValue -f $Key, $Value, $Data) -ErrorRecord $_ + } +} + +<# + .SYNOPSIS + Removes the specified value of a registry key. + + .PARAMETER Key + The registry key that contains the value to remove. + + .PARAMETER Value + The value name of the registry key value to remove. + + .PARAMETER RegistryHive + The registry hive that contains the registry key to remove. +#> +function Remove-RegistryValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [String] + $Key, + + [Parameter(Mandatory = $true)] + [String] + $Value, + + [Parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive + ) + + try + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, [Microsoft.Win32.RegistryView]::Default) + + $subRegistryKey = $baseRegistryKey.OpenSubKey($Key, $true) + $subRegistryKey.DeleteValue($Value) + $subRegistryKey.Close() + } + catch + { + New-InvalidOperationException -Message ($LocalizedData.ErrorRemovingRegistryValue -f $Key, $Value) -ErrorRecord $_ } } @@ -933,6 +1051,10 @@ function Set-TargetResource { $identifyingNumber = Convert-ProductIdToIdentifyingNumber -ProductId $ProductId } + else + { + $identifyingNumber = $ProductId + } $productEntry = Get-ProductEntry -Name $Name -IdentifyingNumber $identifyingNumber @@ -1181,18 +1303,24 @@ function Set-TargetResource { # Absent case $startInfo.FileName = "$env:winDir\system32\msiexec.exe" - - $id = Split-Path -Path $productEntry.Name -Leaf - $startInfo.Arguments = ('/x{0} /quiet' -f $id) - - # Never let msiexec restart automatically. DSC should handle reboot requests. - $startInfo.Arguments += ' /norestart' + + # We may have used the Name earlier, now we need the actual ID + if ($null -eq $productEntry.Name) + { + $id = $Path + } + else + { + $id = Split-Path -Path $productEntry.Name -Leaf + } + + $startInfo.Arguments = "/x $id /quiet /norestart" if ($LogPath) { $startInfo.Arguments += ' /log "{0}"' -f $LogPath } - + if ($Arguments) { $startInfo.Arguments += "$Arguments" @@ -1250,9 +1378,17 @@ function Set-TargetResource Remove-Event -SourceIdentifier $errLogPath } - if(-not ($ReturnCode -contains $exitCode)) + if (-not ($ReturnCode -contains $exitCode)) { - New-InvalidOperationException ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + # Some .exe files do not support uninstall + if ($Ensure -eq 'Absent' -and $fileExtension -eq '.exe' -and $exitCode -eq '1620') + { + Write-Warning -Message ($LocalizedData.ExeCouldNotBeUninstalled -f $Path) + } + else + { + New-InvalidOperationException ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + } } } } @@ -1330,15 +1466,12 @@ function Set-TargetResource if ($CreateCheckRegValue) { - foreach ($parameterName in $checkRegistryValueParameters.Keys) - { - $getProductEntryParameters[$parameterName] = $checkRegistryValueParameters[$parameterName] - } + $getProductEntryParameters += $checkRegistryValueParameters } $productEntry = Get-ProductEntry @getProductEntryParameters - if (-not $productEntry) + if ($null -eq $productEntry) { New-InvalidOperationException -Message ($LocalizedData.PostValidationError -f $originalPath) } diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 43517d6d5..86f2bcf30 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -1,13 +1,9 @@ Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force -$script:dscResourceModuleName = 'xPSDesiredStateConfiguration' -$script:dscResourceName = 'MSFT_xPackageResource' -$script:testType = 'Unit' - $script:testEnvironment = Enter-DscResourceTestEnvironment ` - -DscResourceModuleName $script:dscResourceModuleName ` - -DscResourceName $script:dscResourceName ` - -TestType $script:testType + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xPackageResource' ` + -TestType 'Unit' try { @@ -15,6 +11,7 @@ try Describe 'MSFT_xPackageResource Unit Tests' { BeforeAll { Import-Module "$PSScriptRoot\MSFT_xPackageResource.TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" $script:skipHttpsTest = $true @@ -76,6 +73,87 @@ try } } + Context 'Get-TargetResource' { + It 'Should return only basic properties for absent package' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + } + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + + It 'Should return only basic properties for present package with registry check parameters specified and CreateCheckRegValue true' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + + It 'Should return full properties for present package with registry check parameters specified and CreateCheckRegValue false' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + CreateCheckRegValue = $false + InstalledCheckRegKey = '' + InstalledCheckRegValueName = '' + InstalledCheckRegValueData = '' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + + It 'Should return full properties for present package without registry check parameters specified' { + $packageParameters = @{ + Path = $script:msiLocation + Name = $script:packageName + ProductId = $script:packageId + } + + Set-TargetResource -Ensure 'Present' @packageParameters + Clear-xPackageCache + + $getTargetResourceResult = Get-TargetResource @packageParameters + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'Path', 'InstalledOn', 'Size', 'Version', 'PackageDescription', 'Publisher' ) + + Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties + } + } + Context 'Test-TargetResource' { It 'Should return correct value when package is absent' { $testTargetResourceResult = Test-TargetResource ` @@ -111,7 +189,7 @@ try $testTargetResourceResult | Should Be $true } - It 'Should return correct value when package is present' { + It 'Should return correct value when package is present without registry parameters' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) Clear-xPackageCache @@ -150,10 +228,67 @@ try $testTargetResourceResult | Should Be $false } + + $existingPackageParameters = @{ + Path = $script:testExecutablePath + Name = [String]::Empty + ProductId = [String]::Empty + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + It 'Should return present with existing exe and matching registry parameters' { + Set-TargetResource -Ensure 'Present' @existingPackageParameters + + try + { + $testTargetResourceResult = Test-TargetResource -Ensure 'Present' @existingPackageParameters + $testTargetResourceResult | Should Be $true + + $testTargetResourceResult = Test-TargetResource -Ensure 'Absent' @existingPackageParameters + $testTargetResourceResult | Should Be $false + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($existingPackageParameters.InstalledCheckRegKey) + } + } + + $parametersToMismatchCheck = @( 'InstalledCheckRegKey', 'InstalledCheckRegValueName', 'InstalledCheckRegValueData' ) + + foreach ($parameterToMismatchCheck in $parametersToMismatchCheck) + { + It "Should return not present with existing exe and mismatching parameter $parameterToMismatchCheck" { + Set-TargetResource -Ensure 'Present' @existingPackageParameters + + try + { + $mismatchingParameters = $existingPackageParameters.Clone() + $mismatchingParameters[$parameterToMismatchCheck] = 'not original value' + + Write-Verbose -Message "Test target resource parameters: $( Out-String -InputObject $mismatchingParameters)" + + $testTargetResourceResult = Test-TargetResource -Ensure 'Present' @mismatchingParameters + $testTargetResourceResult | Should Be $false + + $testTargetResourceResult = Test-TargetResource -Ensure 'Absent' @mismatchingParameters + $testTargetResourceResult | Should Be $true + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($existingPackageParameters.InstalledCheckRegKey) + } + } + } } Context 'Set-TargetResource' { - It 'Should correctly install and remove a package' { + It 'Should correctly install and remove a .msi package without registry parameters' { Set-TargetResource -Ensure 'Present' -Path $script:msiLocation -ProductId $script:packageId -Name ([String]::Empty) Test-PackageInstalledByName -Name $script:packageName | Should Be $true @@ -176,6 +311,88 @@ try Test-PackageInstalledByName -Name $script:packageName | Should Be $false } + It 'Should correctly install and remove a .msi package with registry parameters' { + $packageParameters = @{ + Path = $script:msiLocation + Name = [String]::Empty + ProductId = $script:packageId + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + + $getTargetResourceResult = Get-TargetResource @packageParameters + + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $packageParameters.ProductId + $getTargetResourceResult.Path | Should Be $packageParameters.Path + $getTargetResourceResult.Name | Should Be $packageParameters.Name + $getTargetResourceResult.CreateCheckRegValue | Should Be $packageParameters.CreateCheckRegValue + $getTargetResourceResult.InstalledCheckRegHive | Should Be $packageParameters.InstalledCheckRegHive + $getTargetResourceResult.InstalledCheckRegKey | Should Be $packageParameters.InstalledCheckRegKey + $getTargetResourceResult.InstalledCheckRegValueName | Should Be $packageParameters.InstalledCheckRegValueName + $getTargetResourceResult.InstalledCheckRegValueData | Should Be $packageParameters.InstalledCheckRegValueData + + Set-TargetResource -Ensure 'Absent' @packageParameters + + Test-PackageInstalledByName -Name $script:packageName | Should Be $false + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + + It 'Should correctly install and remove a .exe package with registry parameters' { + $packageParameters = @{ + Path = $script:testExecutablePath + Name = [String]::Empty + ProductId = [String]::Empty + CreateCheckRegValue = $true + InstalledCheckRegHive = 'LocalMachine' + InstalledCheckRegKey = 'SOFTWARE\xPackageTestKey' + InstalledCheckRegValueName = 'xPackageTestValue' + InstalledCheckRegValueData = 'installed' + } + + Set-TargetResource -Ensure 'Present' @packageParameters + + try + { + Test-TargetResource -Ensure 'Present' @packageParameters | Should Be $true + + $getTargetResourceResult = Get-TargetResource @packageParameters + + $getTargetResourceResult.Installed | Should Be $true + $getTargetResourceResult.ProductId | Should Be $packageParameters.ProductId + $getTargetResourceResult.Path | Should Be $packageParameters.Path + $getTargetResourceResult.Name | Should Be $packageParameters.Name + $getTargetResourceResult.CreateCheckRegValue | Should Be $packageParameters.CreateCheckRegValue + $getTargetResourceResult.InstalledCheckRegHive | Should Be $packageParameters.InstalledCheckRegHive + $getTargetResourceResult.InstalledCheckRegKey | Should Be $packageParameters.InstalledCheckRegKey + $getTargetResourceResult.InstalledCheckRegValueName | Should Be $packageParameters.InstalledCheckRegValueName + $getTargetResourceResult.InstalledCheckRegValueData | Should Be $packageParameters.InstalledCheckRegValueData + + Set-TargetResource -Ensure 'Absent' @packageParameters + + Test-TargetResource -Ensure 'Absent' @packageParameters | Should Be $true + } + finally + { + $baseRegistryKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) + $baseRegistryKey.DeleteSubKeyTree($packageParameters.InstalledCheckRegKey) + } + } + It 'Should throw with incorrect product id' { $wrongPackageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a050272}' From b0c50793c3513ff5234510af6944fca32fa956b4 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 9 Aug 2016 11:39:13 -0700 Subject: [PATCH 46/49] Updating README. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ead103bfa..074e3145b 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ These parameters will be the same for each Windows optional feature in the set. * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. * Updated appveyor.yml to use the default image. * Merged xPackage with in-box Package resource and added tests. +* xPackage: Re-implemented parameters for installation check from registry key value. * xGroup: * Fixed Verbose output in Get-MembersAsPrincipals function. * Fixed bug when credential parameter passed does not contain local or domain context. From ffc403394a83603747fbd7b2af94574fbac86b46 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 9 Aug 2016 12:01:31 -0700 Subject: [PATCH 47/49] Adding an integration test for xPackage. --- .../MSFT_xPackageResource.schema.mof | Bin 2146 -> 2380 bytes .../MSFT_xPackageResource.Tests.ps1 | 102 ++++++++++++++++++ Tests/Unit/MSFT_xPackageResource.Tests.ps1 | 10 +- 3 files changed, 107 insertions(+), 5 deletions(-) create mode 100644 Tests/Integration/MSFT_xPackageResource.Tests.ps1 diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 5f003c438739ceaf44e6a9940b760192fd196817..719afaf83d4a92984ab8c5def9a37a79ba52db87 100644 GIT binary patch delta 166 zcmaDPa7Ji@5c|Y?V)Z@@`3%Vni3~XmzCbdWA%h{4A&()IL5V>Jh@BZq8H#`~6)0B1 z5Xw*tWEBC$Yk{(1U|pp^kz$5wpcq*uF)`Fm7GyVN_W)X3#*jMMkx6{=K6aDMZtOCQ E0FvV(L;wH) delta 16 XcmX>j^hjWX5c^~~CZWxI9CnNVFE<2d diff --git a/Tests/Integration/MSFT_xPackageResource.Tests.ps1 b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 new file mode 100644 index 000000000..52f45f358 --- /dev/null +++ b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 @@ -0,0 +1,102 @@ +Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force + +$script:testEnvironment = Enter-DscResourceTestEnvironment ` + -DscResourceModuleName 'xPSDesiredStateConfiguration' ` + -DscResourceName 'MSFT_xPackageResource' ` + -TestType 'Integration' + +Describe "xPackage Integration Tests" { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xPackageResource.TestHelper.psm1" -Force + + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' + + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null + + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName + + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + + New-TestMsi -DestinationPath $script:msiLocation | Out-Null + + Clear-xPackageCache | Out-Null + } + + BeforeEach { + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Package could not be removed.' + } + } + + AfterAll { + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } + + Clear-xPackageCache | Out-Null + + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } + + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } + } + + It "Install a .msi package" { + $configurationName = "EnsureProcessIsPresent" + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + $errorPath = Join-Path -Path (Get-Location) -ChildPath "StdErrorPath.txt" + $outputPath = Join-Path -Path (Get-Location) -ChildPath "StdOutputPath.txt" + + try + { + Configuration $configurationName + { + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xPackage Package1 + { + Path = $script:msiLocation + Ensure = "Present" + Name = $script:packageName + ProductId = $script:packageId + } + } + + & $configurationName -OutputPath $configurationPath + + Start-DscConfiguration -Path $configurationPath -Wait -Force -Verbose + + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + } + finally + { + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } + } + } +} diff --git a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 index 86f2bcf30..a2ca433b4 100644 --- a/Tests/Unit/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Unit/MSFT_xPackageResource.Tests.ps1 @@ -87,7 +87,7 @@ try Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties } - It 'Should return only basic properties for present package with registry check parameters specified and CreateCheckRegValue true' { + It 'Should return basic and registry properties for present package with registry check parameters specified and CreateCheckRegValue true' { $packageParameters = @{ Path = $script:msiLocation Name = $script:packageName @@ -106,7 +106,7 @@ try Clear-xPackageCache $getTargetResourceResult = Get-TargetResource @packageParameters - $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'CreateCheckRegValue', 'InstalledCheckRegHive', 'InstalledCheckRegKey', 'InstalledCheckRegValueName', 'InstalledCheckRegValueData' ) Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties } @@ -117,7 +117,7 @@ try } } - It 'Should return full properties for present package with registry check parameters specified and CreateCheckRegValue false' { + It 'Should return full package properties for present package with registry check parameters specified and CreateCheckRegValue false' { $packageParameters = @{ Path = $script:msiLocation Name = $script:packageName @@ -132,12 +132,12 @@ try Clear-xPackageCache $getTargetResourceResult = Get-TargetResource @packageParameters - $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed' ) + $getTargetResourceResultProperties = @( 'Ensure', 'Name', 'ProductId', 'Installed', 'Path', 'InstalledOn', 'Size', 'Version', 'PackageDescription', 'Publisher' ) Test-GetTargetResourceResult -GetTargetResourceResult $getTargetResourceResult -GetTargetResourceResultProperties $getTargetResourceResultProperties } - It 'Should return full properties for present package without registry check parameters specified' { + It 'Should return full package properties for present package without registry check parameters specified' { $packageParameters = @{ Path = $script:msiLocation Name = $script:packageName From 76c64965d055355c1d8c8cfe20f1014007cf90d1 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Tue, 9 Aug 2016 12:38:07 -0700 Subject: [PATCH 48/49] Removing force on import in common test helper and converting xPackage mof to ASCII. --- .../MSFT_xPackageResource.schema.mof | Bin 2380 -> 1189 bytes Tests/CommonTestHelper.psm1 | 2 +- .../MSFT_xPackageResource.Tests.ps1 | 142 +++++++++--------- 3 files changed, 75 insertions(+), 69 deletions(-) diff --git a/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof b/DSCResources/MSFT_xPackageResource/MSFT_xPackageResource.schema.mof index 719afaf83d4a92984ab8c5def9a37a79ba52db87..d143c4751a37b0658cad70923068e3ee2fa59171 100644 GIT binary patch literal 1189 zcmbVLO>f&U47~@)e-M0X0i#ILr5M&z<|Ih4dNty-*1C!ND~gybK3~Y=0ux;NDSTz^D5cCqPgjwc`?J7M0d( z6;-{l9Z?#&OvpkigK$~a`~uI7zhGK<5q=ns5I3QK>hx_wfO%|~UdL6a zr&>XGkwp;45U-`uR5Cl5atNONmxXdFiuu$ukIq(w9Wh+$^9k92HYmM3hV8Bsud}c6 z_1j5y9?6{8oOr{r=asKf`WlYBtYxwi2eQ(ym!k>>XM$S75 zreHJ^Np2OHW_`3no=CvZMBjnw_W?Jl$)degWj_Y+5zqtF{2OOfgVjEdX|Xbo3K7|p zYL#wX##PR!@F`2V1lzr55Usp!6=19Xzk!-Yo85RRu!jvKEB;uow=4HIKzms&a_<)` f@A-Io|EOA`=Ty^9?Sqo+$=E3!sFqlZd$rGRW$=ra literal 2380 zcmchYTW`}q5QXO%iT_~b38V&3y#cwH|g(>|+UW3@X9*0xv_}1AESO2;zX< zFI|*lTi+%7znqtV-KX~5#h8L`l2ISe3*NiDU9hh~l$XwrvPk)Ef@)aX2Kdg`u03pW z2b?lauq&Qs@DSK5o{7t8Y%f;ge1o?u$B@52=X7V=ETlv^bG4OX3MGWgyfUk?t6Huf zpPj0mt4#UPi!sHyV=XEn<)p3>GgL1I#^~j&CWmtNZ^1-Pa0$^9j;)00$LS0gp)d~&sYy{Kf%=P_G{zMk2=f%7UwP! zX(2#=>JKR+k8c&}*VFg6Pjt%YHCS;{jjNslJy31toBF@OMABEwcI}zBvSWMp)LYpF z(^{@{tpg&na7-RYs9tN^SlME|YKr_Hzhxcf9JOIAf`2JcDxTJ`R<(NC6 z`@*{>NHy=9Eu7$6b7VjtWV7-vb`--m7fI;u>ZDIxLzJM&pUwRkt?N7MvT&)e0Z*M$ z)lpZgMV&9(6L*dbiCuF=XPnd%x|yQR)vS4s78PH0{qJk3%x1Z7LgG?I&fTPo&~)rr zs?PL27PB+!-`Ok9=c-e0Q}6#h`@(j|si0p|aq+M|ld@AL)6zPth24yY>!sa4NF9xo diff --git a/Tests/CommonTestHelper.psm1 b/Tests/CommonTestHelper.psm1 index 9813bd523..a4446f8e0 100644 --- a/Tests/CommonTestHelper.psm1 +++ b/Tests/CommonTestHelper.psm1 @@ -696,7 +696,7 @@ function Enter-DscResourceTestEnvironment } } - Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" -Force + Import-Module "$PSScriptRoot\..\DSCResource.Tests\TestHelper.psm1" return Initialize-TestEnvironment ` -DSCModuleName $DscResourceModuleName ` diff --git a/Tests/Integration/MSFT_xPackageResource.Tests.ps1 b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 index 52f45f358..8de93a967 100644 --- a/Tests/Integration/MSFT_xPackageResource.Tests.ps1 +++ b/Tests/Integration/MSFT_xPackageResource.Tests.ps1 @@ -1,102 +1,108 @@ -Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" -Force +Import-Module "$PSScriptRoot\..\CommonTestHelper.psm1" $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName 'xPSDesiredStateConfiguration' ` -DscResourceName 'MSFT_xPackageResource' ` -TestType 'Integration' +try +{ + Describe "xPackage Integration Tests" { + BeforeAll { + Import-Module "$PSScriptRoot\..\Unit\MSFT_xPackageResource.TestHelper.psm1" -Force -Describe "xPackage Integration Tests" { - BeforeAll { - Import-Module "$PSScriptRoot\..\Unit\MSFT_xPackageResource.TestHelper.psm1" -Force + $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' - $script:testDirectoryPath = Join-Path -Path $PSScriptRoot -ChildPath 'MSFT_xPackageResourceTests' - - if (Test-Path -Path $script:testDirectoryPath) - { - Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null - } + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } - New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null + New-Item -Path $script:testDirectoryPath -ItemType 'Directory' | Out-Null - $script:msiName = 'DSCSetupProject.msi' - $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName + $script:msiName = 'DSCSetupProject.msi' + $script:msiLocation = Join-Path -Path $script:testDirectoryPath -ChildPath $script:msiName - $script:packageName = 'DSCUnitTestPackage' - $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' + $script:packageName = 'DSCUnitTestPackage' + $script:packageId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' - New-TestMsi -DestinationPath $script:msiLocation | Out-Null + New-TestMsi -DestinationPath $script:msiLocation | Out-Null - Clear-xPackageCache | Out-Null - } + Clear-xPackageCache | Out-Null + } - BeforeEach { - Clear-xPackageCache | Out-Null + BeforeEach { + Clear-xPackageCache | Out-Null - if (Test-PackageInstalledByName -Name $script:packageName) - { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null - Start-Sleep -Seconds 1 | Out-Null - } + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } - if (Test-PackageInstalledByName -Name $script:packageName) - { - throw 'Package could not be removed.' + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Package could not be removed.' + } } - } - AfterAll { - if (Test-Path -Path $script:testDirectoryPath) - { - Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null - } + AfterAll { + if (Test-Path -Path $script:testDirectoryPath) + { + Remove-Item -Path $script:testDirectoryPath -Recurse -Force | Out-Null + } - Clear-xPackageCache | Out-Null + Clear-xPackageCache | Out-Null - if (Test-PackageInstalledByName -Name $script:packageName) - { - Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null - Start-Sleep -Seconds 1 | Out-Null - } + if (Test-PackageInstalledByName -Name $script:packageName) + { + Start-Process -FilePath 'msiexec.exe' -ArgumentList @("/x$script:packageId", '/passive') -Wait | Out-Null + Start-Sleep -Seconds 1 | Out-Null + } - if (Test-PackageInstalledByName -Name $script:packageName) - { - throw 'Test output will not be valid - package could not be removed.' + if (Test-PackageInstalledByName -Name $script:packageName) + { + throw 'Test output will not be valid - package could not be removed.' + } } - } - It "Install a .msi package" { - $configurationName = "EnsureProcessIsPresent" - $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName - $errorPath = Join-Path -Path (Get-Location) -ChildPath "StdErrorPath.txt" - $outputPath = Join-Path -Path (Get-Location) -ChildPath "StdOutputPath.txt" + It "Install a .msi package" { + $configurationName = "EnsurePackageIsPresent" + $configurationPath = Join-Path -Path (Get-Location) -ChildPath $configurationName + $errorPath = Join-Path -Path (Get-Location) -ChildPath "StdErrorPath.txt" + $outputPath = Join-Path -Path (Get-Location) -ChildPath "StdOutputPath.txt" - try - { - Configuration $configurationName + try { - Import-DscResource -ModuleName xPSDesiredStateConfiguration - - xPackage Package1 + Configuration $configurationName { - Path = $script:msiLocation - Ensure = "Present" - Name = $script:packageName - ProductId = $script:packageId + Import-DscResource -ModuleName xPSDesiredStateConfiguration + + xPackage Package1 + { + Path = $script:msiLocation + Ensure = "Present" + Name = $script:packageName + ProductId = $script:packageId + } } - } - & $configurationName -OutputPath $configurationPath + & $configurationName -OutputPath $configurationPath - Start-DscConfiguration -Path $configurationPath -Wait -Force -Verbose + Start-DscConfiguration -Path $configurationPath -Wait -Force -Verbose - Test-PackageInstalledByName -Name $script:packageName | Should Be $true - } - finally - { - if (Test-Path -Path $configurationPath) + Test-PackageInstalledByName -Name $script:packageName | Should Be $true + } + finally { - Remove-Item -Path $configurationPath -Recurse -Force + if (Test-Path -Path $configurationPath) + { + Remove-Item -Path $configurationPath -Recurse -Force + } } } } } +finally +{ + Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment +} From 8d06d2b332fcf7da8daad63125af8c2997f92c61 Mon Sep 17 00:00:00 2001 From: Katie Keim Date: Wed, 10 Aug 2016 13:49:21 -0700 Subject: [PATCH 49/49] Releasing version 3.13.0.0 --- README.md | 2 ++ appveyor.yml | 4 ++-- xPSDesiredStateConfiguration.psd1 | 20 ++++++++++++++++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 074e3145b..1a883d4e4 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,8 @@ These parameters will be the same for each Windows optional feature in the set. ### Unreleased +### 3.13.0.0 + * Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. * Updated appveyor.yml to use the default image. * Merged xPackage with in-box Package resource and added tests. diff --git a/appveyor.yml b/appveyor.yml index 3be1cbe06..2408e30f7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ #---------------------------------# # environment configuration # #---------------------------------# -version: 3.12.{build}.0 +version: 3.13.{build}.0 install: - git clone https://github.com/PowerShell/DscResource.Tests - ps: | @@ -40,7 +40,7 @@ deploy_script: # Creating project artifact $stagingDirectory = (Resolve-Path ..).Path $manifest = Join-Path $pwd "xPSDesiredStateConfiguration.psd1" - (Get-Content $manifest -Raw).Replace("3.12.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest + (Get-Content $manifest -Raw).Replace("3.13.0.0", $env:APPVEYOR_BUILD_VERSION) | Out-File $manifest $zipFilePath = Join-Path $stagingDirectory "$(Split-Path $pwd -Leaf).zip" Add-Type -assemblyname System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::CreateFromDirectory($pwd, $zipFilePath) diff --git a/xPSDesiredStateConfiguration.psd1 b/xPSDesiredStateConfiguration.psd1 index 752dc0b83..1db1f4928 100644 --- a/xPSDesiredStateConfiguration.psd1 +++ b/xPSDesiredStateConfiguration.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. -ModuleVersion = '3.12.0.0' +ModuleVersion = '3.13.0.0' # ID used to uniquely identify this module GUID = 'cc8dc021-fa5f-4f96-8ecf-dfd68a6d9d48' @@ -52,7 +52,22 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '* Removed localization for now so that resources can run on non-English systems. + ReleaseNotes = '* Converted appveyor.yml to install Pester from PSGallery instead of from Chocolatey. +* Updated appveyor.yml to use the default image. +* Merged xPackage with in-box Package resource and added tests. +* xPackage: Re-implemented parameters for installation check from registry key value. +* xGroup: + * Fixed Verbose output in Get-MembersAsPrincipals function. + * Fixed bug when credential parameter passed does not contain local or domain context. + * Fixed logic bug in MembersToInclude and MembersToExclude. + * Fixed bug when trying to include the built-in Administrator in Members. + * Fixed bug where Test-TargetResource would check for members when none specified. + * Fix bug in Test-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bug in Set-TargetResourceOnFullSKU function when group being set to a single member. + * Fix bugs in Assert-GroupNameValid to throw correct exception. +* xService + * Updated xService resource to allow empty string for Description parameter. +* Merged xProcess with in-box Process resource and added tests. ' @@ -64,3 +79,4 @@ PrivateData = @{ +