Tests/Unit/MSFT_xMsiPackage.Tests.ps1
<#
Suppress PSAvoidUsingConvertToSecureStringWithPlainText since SecureString objects are used for test passwords. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param () $errorActionPreference = 'Stop' Set-StrictMode -Version 'Latest' $script:testsFolderFilePath = Split-Path $PSScriptRoot -Parent $script:commonTestHelperFilePath = Join-Path -Path $testsFolderFilePath -ChildPath 'CommonTestHelper.psm1' Import-Module -Name $commonTestHelperFilePath if (Test-SkipContinuousIntegrationTask -Type 'Unit') { return } Describe 'xMsiPackage Unit Tests' { BeforeAll { # Import CommonTestHelper for Enter-DscResourceTestEnvironment, Exit-DscResourceTestEnvironment $script:testsFolderFilePath = Split-Path $PSScriptRoot -Parent $script:commonTestHelperFilePath = Join-Path -Path $testsFolderFilePath -ChildPath 'CommonTestHelper.psm1' Import-Module -Name $commonTestHelperFilePath -Force $script:testEnvironment = Enter-DscResourceTestEnvironment ` -DscResourceModuleName 'xPSDesiredStateConfiguration' ` -DscResourceName 'MSFT_xMsiPackage' ` -TestType 'Unit' } AfterAll { Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment } InModuleScope 'MSFT_xMsiPackage' { $script:testsFolderFilePath = Split-Path $PSScriptRoot -Parent $script:commonTestHelperFilePath = Join-Path -Path $script:testsFolderFilePath -ChildPath 'CommonTestHelper.psm1' # This must be imported again within the InModuleScope so that the helper functions can be accessed Import-Module -Name $commonTestHelperFilePath $testUsername = 'TestUsername' $testPassword = 'TestPassword' $secureTestPassword = ConvertTo-SecureString -String $testPassword -AsPlainText -Force $script:testCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @( $testUsername, $secureTestPassword ) $script:testProductId = '{deadbeef-80c6-41e6-a1b9-8bdb8a05027f}' $script:testIdentifyingNumber = '{DEADBEEF-80C6-41E6-A1B9-8BDB8A05027F}' $script:testWrongProductId = 'wrongId' $script:testPath = 'file://test.msi' $script:destinationPath = Join-Path -Path $script:packageCacheLocation -ChildPath (Get-Location).Drive.Root $script:testUriHttp = [System.Uri] 'http://test.msi' $script:testUriHttps = [System.Uri] 'https://test.msi' $script:testUriFile = [System.Uri] 'file://test.msi' $script:testUriNonUnc = [System.Uri] 'file:///C:/test.msi' $script:testUriQuery = [System.Uri] 'http://C:/directory/test/test.msi?sv=2017-01-31&spr=https' $script:testUriOnlyFile = [System.Uri] 'test.msi' $script:mockStream = New-MockObject -Type 'System.IO.FileStream' $script:mockWebRequest = New-MockObject -Type 'System.Net.HttpWebRequest' $script:mockStartInfo = New-MockObject -Type 'System.Diagnostics.ProcessStartInfo' $script:mockProcess = New-MockObject -Type 'System.Diagnostics.Process' $script:mockProductEntry = New-MockObject -Type 'Microsoft.Win32.RegistryKey' $script:mockPSDrive = @{ Root = 'mockRoot' } $script:mockProductEntryInfo = @{ Name = 'TestDisplayName' InstallSource = 'TestInstallSource' InstalledOn = '4/4/2017' Size = 2048 Version = '1.2.3.4' PackageDescription = 'Test Description' Publisher = 'Test Publisher' Ensure = 'Present' } # Used to create the names of the tests that check to ensure the correct error is thrown. $script:errorMessageTitles = @{ CouldNotOpenLog = 'not being able to open the log path' InvalidId = 'the specified product ID not matching the actual product ID' CouldNotOpenDestFile = 'not being able to open the destination file to write to' PathDoesNotExist = 'not being able to find the path' CouldNotStartProcess = 'not being able to start the process' PostValidationError = 'not being able to find the package after installation' } Describe 'Get-TargetResource' { Mock -CommandName 'Convert-ProductIdToIdentifyingNumber' -MockWith { return $script:testIdentifyingNumber } Mock -CommandName 'Get-ProductEntry' -MockWith { return $null } Mock -CommandName 'Get-ProductEntryInfo' -MockWith { return $script:mockProductEntryInfo } Context 'MSI package does not exist' { $getTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryInfo'; Times = 0 } ) $expectedReturnValue = @{ Ensure = 'Absent' ProductId = $script:testIdentifyingNumber } Invoke-GetTargetResourceUnitTest -GetTargetResourceParameters $getTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $expectedReturnValue } Mock -CommandName 'Get-ProductEntry' -MockWith { return $script:mockProductEntry } Context 'MSI package does exist' { $getTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryInfo'; Times = 1 } ) $expectedReturnValue = $script:mockProductEntryInfo Invoke-GetTargetResourceUnitTest -GetTargetResourceParameters $getTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $expectedReturnValue } } Describe 'Set-TargetResource' { $setTargetResourceParameters = @{ ProductId = 'TestProductId' Path = $script:testPath Ensure = 'Present' Arguments = 'TestArguments' LogPath = 'TestLogPath' FileHash = 'TestFileHash' HashAlgorithm = 'Sha256' SignerSubject = 'TestSignerSubject' SignerThumbprint = 'TestSignerThumbprint' ServerCertificateValidationCallback = 'TestValidationCallback' RunAsCredential = $script:testCredential } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriNonUnc } Mock -CommandName 'Convert-ProductIdToIdentifyingNumber' -MockWith { return $script:testIdentifyingNumber } Mock -CommandName 'Assert-PathExtensionValid' -MockWith {} Mock -CommandName 'New-LogFile' -MockWith {} Mock -CommandName 'New-PSDrive' -MockWith { return $script:mockPSDrive } Mock -CommandName 'Test-Path' -MockWith { return $true } Mock -CommandName 'New-Item' -MockWith {} Mock -CommandName 'New-Object' -MockWith { return $script:mockStream } Mock -CommandName 'Get-WebRequestResponse' -MockWith { return $script:mockStream } Mock -CommandName 'Copy-ResponseStreamToFileStream' -MockWith {} Mock -CommandName 'Close-Stream' -MockWith {} Mock -CommandName 'Assert-FileValid' -MockWith {} Mock -CommandName 'Get-MsiProductCode' -MockWith { return $script:testIdentifyingNumber } Mock -CommandName 'Start-MsiProcess' -MockWith { # Returns the exit code return 0 } Mock -CommandName 'Remove-PSDrive' -MockWith {} Mock -CommandName 'Remove-Item' -MockWith {} Mock -CommandName 'Invoke-CimMethod' -MockWith {} Mock -CommandName 'Get-ItemProperty' -MockWith { return $null } Mock -CommandName 'Get-ProductEntry' -MockWith { return $script:mockProductEntry } Context 'Uri scheme is non-UNC file and installation succeeds' { $mocksCalled = @( @{ Command = 'Convert-PathToUri'; Times = 1 } @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } @{ Command = 'Get-WebRequestResponse'; Times = 0 } @{ Command = 'Test-Path'; Times = 1; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @{ Command = 'Remove-PSDrive'; Times = 0 } @{ Command = 'Remove-Item'; Times = 0; Custom = 'the downloaded file from the http server' } @{ Command = 'Invoke-CimMethod'; Times = 1 } @{ Command = 'Get-ItemProperty'; Times = 1; Custom = 'the registry data for pending file rename operations' } @{ Command = 'Get-ProductEntry'; Times = 1 } ) Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriFile } $setTargetResourceParameters.Ensure = 'Present' Context 'Uri scheme is UNC file and installation succeeds' { $mocksCalled = @( @{ Command = 'Convert-PathToUri'; Times = 1 } @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 1 } @{ Command = 'Get-WebRequestResponse'; Times = 0 } @{ Command = 'Test-Path'; Times = 1; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @{ Command = 'Remove-PSDrive'; Times = 1 } @{ Command = 'Remove-Item'; Times = 0; Custom = 'the downloaded file from the http server' } @{ Command = 'Invoke-CimMethod'; Times = 1 } @{ Command = 'Get-ItemProperty'; Times = 1; Custom = 'the registry data for pending file rename operations' } @{ Command = 'Get-ProductEntry'; Times = 1 } ) Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriHttp } Context 'Uri scheme is Http and installation succeeds' { $mocksCalled = @( @{ Command = 'Convert-PathToUri'; Times = 1 } @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } @{ Command = 'Test-Path'; Times = 3; Custom = 'to the package cache' } @{ Command = 'New-Item'; Times = 0; Custom = 'directory for the package cache' } @{ Command = 'New-Object'; Times = 1; Custom = 'file stream to copy the response to' } @{ Command = 'Get-WebRequestResponse'; Times = 1 } @{ Command = 'Copy-ResponseStreamToFileStream'; Times = 1 } @{ Command = 'Close-Stream'; Times = 2 } @{ Command = 'Test-Path'; Times = 3; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @{ Command = 'Remove-PSDrive'; Times = 0 } @{ Command = 'Remove-Item'; Times = 1; Custom = 'the directory used for the package cache' } @{ Command = 'Invoke-CimMethod'; Times = 1 } @{ Command = 'Get-ItemProperty'; Times = 1; Custom = 'the registry data for pending file rename operations' } @{ Command = 'Get-ProductEntry'; Times = 1 } ) Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriHttps } Context 'Uri scheme is Https and installation succeeds' { $mocksCalled = @( @{ Command = 'Convert-PathToUri'; Times = 1 } @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } @{ Command = 'Test-Path'; Times = 3; Custom = 'to the package cache' } @{ Command = 'New-Item'; Times = 0; Custom = 'directory for the package cache' } @{ Command = 'New-Object'; Times = 1; Custom = 'file stream to copy the response to' } @{ Command = 'Get-WebRequestResponse'; Times = 1 } @{ Command = 'Copy-ResponseStreamToFileStream'; Times = 1 } @{ Command = 'Close-Stream'; Times = 2 } @{ Command = 'Test-Path'; Times = 3; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 1 } @{ Command = 'Get-MsiProductCode'; Times = 1 } @{ Command = 'Start-MsiProcess'; Times = 1 } @{ Command = 'Remove-PSDrive'; Times = 0 } @{ Command = 'Remove-Item'; Times = 1; Custom = 'the directory used for the package cache' } @{ Command = 'Invoke-CimMethod'; Times = 1 } @{ Command = 'Get-ItemProperty'; Times = 1; Custom = 'the registry data for pending file rename operations' } @{ Command = 'Get-ProductEntry'; Times = 1 } ) Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Context 'Reboot handling' { Mock -CommandName 'Start-MsiProcess' -MockWith { return 3010 } Mock -CommandName 'Set-DSCMachineRebootRequired' -MockWith {} It 'Should request reboot by default' { $setTargetResourceParameters.IgnoreReboot = $false { $null = Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw Assert-MockCalled -CommandName 'Set-DSCMachineRebootRequired' -Exactly 1 -Scope 'It' } It 'Should not request reboot if IgnoreReboot specified' { $setTargetResourceParameters.IgnoreReboot = $true { $null = Set-TargetResource @setTargetResourceParameters } | Should -Not -Throw Assert-MockCalled -CommandName 'Set-DSCMachineRebootRequired' -Exactly 0 -Scope 'It' } } $setTargetResourceParameters.Ensure = 'Absent' # The URI scheme doesn't matter for uninstallation - it will always do the same thing Context 'Uninstallation succeeds' { $mocksCalled = @( @{ Command = 'Convert-PathToUri'; Times = 1 } @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Assert-PathExtensionValid'; Times = 1 } @{ Command = 'New-LogFile'; Times = 1 } @{ Command = 'New-PSDrive'; Times = 0 } @{ Command = 'Get-WebRequestResponse'; Times = 0 } @{ Command = 'Test-Path'; Times = 0; Custom = 'to the MSI file' } @{ Command = 'Assert-FileValid'; Times = 0 } @{ Command = 'Get-MsiProductCode'; Times = 0 } @{ Command = 'Start-MsiProcess'; Times = 1 } @{ Command = 'Remove-PSDrive'; Times = 0 } @{ Command = 'Remove-Item'; Times = 0; Custom = 'the downloaded file from the http server' } @{ Command = 'Invoke-CimMethod'; Times = 1 } @{ Command = 'Get-ItemProperty'; Times = 1; Custom = 'the registry data for pending file rename operations' } @{ Command = 'Get-ProductEntry'; Times = 0 } ) Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriQuery } Context 'Path is a query path' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $false It 'Should assert that the file without the query string has a valid extension' { Assert-MockCalled -CommandName 'Assert-PathExtensionValid' -Exactly 1 -Scope 'Context' -ParameterFilter { $Path -eq (Split-Path -Path $script:testuriQuery.LocalPath -Leaf) } } } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriOnlyFile } Context 'Converted URI does not have a local path' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $false It 'Should assert that the original path has a valid extension' { Assert-MockCalled -CommandName 'Assert-PathExtensionValid' -Exactly 1 -Scope 'Context' -ParameterFilter { $Path -eq $script:testPath } } } $setTargetResourceParameters.Remove('LogPath') Context 'Converted URI does not have a local path' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled @(@{ Command = 'New-LogFile'; Times = 0 }) ` -ShouldThrow $false } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriHttp } Mock -CommandName 'Test-Path' -MockWith { return $false } -ParameterFilter { $Path -eq $script:packageCacheLocation } $setTargetResourceParameters.Ensure = 'Present' Context 'URI scheme is Http and package cache location does not exist yet' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -MocksCalled @(@{ Command = 'New-Item'; Times = 1; Custom = 'directory for the package cache' }) ` -ShouldThrow $false } # Error Tests Mock -CommandName 'Get-ProductEntry' -MockWith { return $null } Context 'Package could not be found after installation' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.PostValidationError -f $setTargetResourceParameters.Path) ` -ErrorTestName $script:errorMessageTitles.PostValidationError } Mock -CommandName 'Get-MsiProductCode' -MockWith { return $script:testWrongProductId } Context 'Product code from downloaded MSI package does not match specified ID' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.InvalidId -f $script:testIdentifyingNumber, $script:testWrongProductId) ` -ErrorTestName $script:errorMessageTitles.InvalidId } Mock -CommandName 'New-Object' -MockWith { Throw } Context 'Failure while creating the file stream object to download to' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.CouldNotOpenDestFile -f $script:destinationPath) ` -ErrorTestName $script:errorMessageTitles.CouldNotOpenDestFile } Mock -CommandName 'Convert-PathToUri' -MockWith { return $script:testUriNonUnc } Mock -CommandName 'Test-Path' -MockWith { return $false } -ParameterFilter { $Path -eq $script:testPath } Context 'Invalid path was passed in' { Invoke-SetTargetResourceUnitTest -SetTargetResourceParameters $setTargetResourceParameters ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.PathDoesNotExist -f $script:testPath) ` -ErrorTestName $script:errorMessageTitles.PathDoesNotExist } } Describe 'Test-TargetResource' { Mock -CommandName 'Convert-ProductIdToIdentifyingNumber' -MockWith { return $script:testIdentifyingNumber } Mock -CommandName 'Get-ProductEntry' -MockWith { return $script:mockProductEntry } Mock -CommandName 'Get-ProductEntryValue' -MockWith { return $script:mockProductEntryInfo.Name } Context 'Specified package is present and should be' { $testTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath Ensure = 'Present' } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryValue'; Times = 1 } ) Invoke-TestTargetResourceUnitTest -TestTargetResourceParameters $testTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $true } Context 'Specified package is present but should not be' { $testTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath Ensure = 'Absent' } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryValue'; Times = 1 } ) Invoke-TestTargetResourceUnitTest -TestTargetResourceParameters $testTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $false } Mock -CommandName 'Get-ProductEntry' -MockWith { return $null } Context 'Specified package is Absent but should not be' { $testTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath Ensure = 'Present' } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryValue'; Times = 0 } ) Invoke-TestTargetResourceUnitTest -TestTargetResourceParameters $testTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $false } Context 'Specified package is Absent and should be' { $testTargetResourceParameters = @{ ProductId = $script:testProductId Path = $script:testPath Ensure = 'Absent' } $mocksCalled = @( @{ Command = 'Convert-ProductIdToIdentifyingNumber'; Times = 1 } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Get-ProductEntryValue'; Times = 0 } ) Invoke-TestTargetResourceUnitTest -TestTargetResourceParameters $testTargetResourceParameters ` -MocksCalled $mocksCalled ` -ExpectedReturnValue $true } } Describe 'Assert-PathExtensionValid' { Context 'Path is a valid .msi path' { It 'Should not throw' { { Assert-PathExtensionValid -Path 'testMsiFile.msi' } | Should -Not -Throw } } Context 'Path is not a valid .msi path' { It 'Should throw an invalid argument exception when an EXE file is passed in' { $invalidPath = 'testMsiFile.exe' $expectedErrorMessage = ($script:localizedData.InvalidBinaryType -f $invalidPath) { Assert-PathExtensionValid -Path $invalidPath } | Should -Throw -ExpectedMessage $expectedErrorMessage } It 'Should throw an invalid argument exception when an invalid file type is passed in' { $invalidPath = 'testMsiFilemsi' $expectedErrorMessage = ($script:localizedData.InvalidBinaryType -f $invalidPath) { Assert-PathExtensionValid -Path $invalidPath } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Convert-PathToUri' { Context 'Path has a valid URI scheme' { It 'Should return the expected URI when scheme is a file' { $filePath = (Join-Path -Path $PSScriptRoot -ChildPath 'testMsi.msi') $expectedReturnValue = [System.Uri] $filePath Convert-PathToUri -Path $filePath | Should -Be $expectedReturnValue } It 'Should return the expected URI when scheme is http' { $uriBuilder = [System.UriBuilder]::new('http', 'localhost') $uriBuilder.Path = 'testMsi.msi' $filePath = $uriBuilder.Uri.AbsoluteUri Convert-PathToUri -Path $filePath | Should -Be $uriBuilder.Uri } It 'Should return the expected URI when scheme is https' { $uriBuilder = [System.UriBuilder]::new('https', 'localhost') $uriBuilder.Path = 'testMsi.msi' $filePath = $uriBuilder.Uri.AbsoluteUri Convert-PathToUri -Path $filePath | Should -Be $uriBuilder.Uri } } Context 'Invalid path passed in' { It 'Should throw an error when uri scheme is invalid' { $filePath = 'ht://localhost/testMsi.msi' $expectedErrorMessage = ($script:localizedData.InvalidPath -f $filePath) { Convert-PathToUri -Path $filePath } | Should -Throw -ExpectedMessage $expectedErrorMessage } It 'Should throw an error when path is not in valid format' { $filePath = 'mri' $expectedErrorMessage = ($script:localizedData.InvalidPath -f $filePath) { Convert-PathToUri -Path $filePath } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Convert-ProductIdToIdentifyingNumber' { Context 'Valid Product ID is passed in' { It 'Should return the same value that is passed in when the Product ID is already in the correct format' { Convert-ProductIdToIdentifyingNumber -ProductId $script:testIdentifyingNumber | Should -Be $script:testIdentifyingNumber } It 'Should convert a valid poduct ID to the identifying number format' { Convert-ProductIdToIdentifyingNumber -ProductId $script:testProductId | Should -Be $script:testIdentifyingNumber } } Context 'Invalid Product ID is passed in' { It 'Should throw an exception when an invalid product ID is passed in' { $expectedErrorMessage = ($script:localizedData.InvalidIdentifyingNumber -f $script:testWrongProductId) { Convert-ProductIdToIdentifyingNumber -ProductId $script:testWrongProductId } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Get-ProductEntry' { $uninstallRegistryKeyLocation = (Join-Path -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' -ChildPath $script:testIdentifyingNumber) $uninstallRegistryKeyWow64Location = (Join-Path -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall' -ChildPath $script:testIdentifyingNumber) Mock -CommandName 'Get-Item' -MockWith { return $script:mockProductEntry } -ParameterFilter { $Path -eq $uninstallRegistryKeyLocation } Mock -CommandName 'Get-Item' -MockWith { return $script:mockProductEntry } -ParameterFilter { $Path -eq $uninstallRegistryKeyWow64Location } Context 'Product entry is found in the expected location' { It 'Should return the expected product entry' { Get-ProductEntry -IdentifyingNumber $script:testIdentifyingNumber | Should -Be $script:mockProductEntry } It 'Should retrieve the item' { Assert-MockCalled -CommandName 'Get-Item' -Exactly 1 -Scope 'Context' } } Mock -CommandName 'Get-Item' -MockWith { return $null } -ParameterFilter { $Path -eq $uninstallRegistryKeyLocation } Context 'Product entry is found under Wow6432Node' { It 'Should return the expected product entry' { Get-ProductEntry -IdentifyingNumber $script:testIdentifyingNumber | Should -Be $script:mockProductEntry } It 'Should attempt to retrieve the item twice' { Assert-MockCalled -CommandName 'Get-Item' -Exactly 2 -Scope 'Context' } } Mock -CommandName 'Get-Item' -MockWith { return $null } -ParameterFilter { $Path -eq $uninstallRegistryKeyWow64Location } Context 'Product entry is not found' { It 'Should return $null' { Get-ProductEntry -IdentifyingNumber $script:testIdentifyingNumber | Should -Be $null } It 'Should attempt to retrieve the item twice' { Assert-MockCalled -CommandName 'Get-Item' -Exactly 2 -Scope 'Context' } } } Describe 'Get-ProductEntryInfo' { Mock -CommandName Get-ProductEntryValue -MockWith { return '20170404' } -ParameterFilter { $Property -eq 'InstallDate' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.Publisher } -ParameterFilter { $Property -eq 'Publisher' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.Size } -ParameterFilter { $Property -eq 'EstimatedSize' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.Version } -ParameterFilter { $Property -eq 'DisplayVersion' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.PackageDescription } -ParameterFilter { $Property -eq 'Comments' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.Name } -ParameterFilter { $Property -eq 'DisplayName' } Mock -CommandName Get-ProductEntryValue -MockWith { return $script:mockProductEntryInfo.InstallSource } -ParameterFilter { $Property -eq 'InstallSource' } Context 'All properties are retrieved successfully' { $getProductEntryInfoResult = Get-ProductEntryInfo -ProductEntry $script:mockProductEntry It 'Should return the expected installed date' { $getProductEntryInfoResult.InstalledOn | Should -Be $script:mockProductEntryInfo.InstalledOn } It 'Should return the expected publisher' { $getProductEntryInfoResult.Publisher | Should -Be $script:mockProductEntryInfo.Publisher } It 'Should return the expected size' { $getProductEntryInfoResult.Size | Should -Be ($script:mockProductEntryInfo.Size / 1024) } It 'Should return the expected Version' { $getProductEntryInfoResult.Version | Should -Be $script:mockProductEntryInfo.Version } It 'Should return the expected package description' { $getProductEntryInfoResult.PackageDescription | Should -Be $script:mockProductEntryInfo.PackageDescription } It 'Should return the expected name' { $getProductEntryInfoResult.Name | Should -Be $script:mockProductEntryInfo.Name } It 'Should return the expected install source' { $getProductEntryInfoResult.InstallSource | Should -Be $script:mockProductEntryInfo.InstallSource } It 'Should retrieve 7 product entry values' { Assert-MockCalled -CommandName 'Get-ProductEntryValue' -Exactly 7 -Scope 'Context' } } Mock -CommandName Get-ProductEntryValue -MockWith { return '4/4/2017' } -ParameterFilter { $Property -eq 'InstallDate' } Context 'Install date is in incorrect format' { $getProductEntryInfoResult = Get-ProductEntryInfo -ProductEntry $script:mockProductEntry It 'Should return $null for InstalledOn' { $getProductEntryInfoResult.InstalledOn | Should -Be $null } } } Describe 'New-LogFile' { Mock -CommandName 'Test-Path' -MockWith { return $true } Mock -CommandName 'Remove-Item' -MockWith {} Mock -CommandName 'New-Item' -MockWith {} Context 'File with name of given log file already exists and creation of new log file succeeds' { $mocksCalled = @( @{ Command = 'Test-Path'; Times = 1; Custom = 'to the log file' } @{ Command = 'Remove-Item'; Times = 1; Custom = 'any file with the same name as the log file' } @{ Command = 'New-Item'; Times = 1; Custom = 'log file' } ) Invoke-GenericUnitTest -Function { Param($script:testPath) New-LogFile $script:testPath } ` -FunctionParameters @{ LogPath = $script:testPath } ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Test-Path' -MockWith { return $false } Context 'File with name of given log file does not exist and creation of new log file succeeds' { $mocksCalled = @( @{ Command = 'Test-Path'; Times = 1; Custom = 'to the log file' } @{ Command = 'Remove-Item'; Times = 0; Custom = 'any file with the same name as the log file' } @{ Command = 'New-Item'; Times = 1; Custom = 'log file' } ) Invoke-GenericUnitTest -Function { Param($script:testPath) New-LogFile $script:testPath } ` -FunctionParameters @{ LogPath = $script:testPath } ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'New-Item' -MockWith { Throw } Context 'Creation of new log file fails' { Invoke-GenericUnitTest -Function { Param($script:testPath) New-LogFile $script:testPath } ` -FunctionParameters @{ LogPath = $script:testPath } ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.CouldNotOpenLog -f $script:testPath) ` -ErrorTestName $script:errorMessageTitles.CouldNotOpenLog } } Describe 'Get-WebRequestResponse' { Mock -CommandName 'Get-WebRequest' -MockWith { return $script:mockWebRequest } Mock -CommandName 'Get-ScriptBlock' -MockWith { return { Write-Verbose 'Hello World' } } Mock -CommandName 'Get-WebRequestResponseStream' -MockWith { return $script:mockStream } Context 'URI scheme is Http and response is successfully retrieved' { $mocksCalled = @( @{ Command = 'Get-WebRequest'; Times = 1 } @{ Command = 'Get-ScriptBlock'; Times = 0 } @{ Command = 'Get-WebRequestResponseStream'; Times = 1 } ) It 'Should return the expected response stream' { Get-WebRequestResponse -Uri $script:testUriHttp | Should -Be $script:mockStream } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'URI scheme is Https with no callback and response is successfully retrieved' { $mocksCalled = @( @{ Command = 'Get-WebRequest'; Times = 1 } @{ Command = 'Get-ScriptBlock'; Times = 0 } @{ Command = 'Get-WebRequestResponseStream'; Times = 1 } ) It 'Should return the expected response stream' { Get-WebRequestResponse -Uri $script:testUriHttps | Should -Be $script:mockStream } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'URI scheme is Https with callback and response is successfully retrieved' { $mocksCalled = @( @{ Command = 'Get-WebRequest'; Times = 1 } @{ Command = 'Get-ScriptBlock'; Times = 1 } @{ Command = 'Get-WebRequestResponseStream'; Times = 1 } ) It 'Should return the expected response stream' { Get-WebRequestResponse -Uri $script:testUriHttps -ServerCertificateValidationCallback 'TestCallbackFunction' | Should -Be $script:mockStream } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Mock -CommandName 'Get-WebRequestResponseStream' -MockWith { Throw } Context 'Error occurred during while retrieving the response' { It 'Should throw the expected exception' { $expectedErrorMessage = ($script:localizedData.CouldNotGetResponseFromWebRequest -f $script:testUriHttp.Scheme, $script:testUriHttp.OriginalString) { Get-WebRequestResponse -Uri $script:testUriHttp } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Assert-FileValid' { Mock -CommandName 'Assert-FileHashValid' -MockWith {} Mock -CommandName 'Assert-FileSignatureValid' -MockWith {} Context 'FileHash is passed in and SignerThumbprint and SignerSubject are not' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 1 } @{ Command = 'Assert-FileSignatureValid'; Times = 0 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath -FileHash 'mockFileHash' } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'FileHash and SignerThumbprint are passed in but SignerSubject is not' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 1 } @{ Command = 'Assert-FileSignatureValid'; Times = 1 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath -FileHash 'mockFileHash' -SignerThumbprint 'mockSignerThumbprint' } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'Only Path and SignerSubject are passed in' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 0 } @{ Command = 'Assert-FileSignatureValid'; Times = 1 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath -SignerSubject 'mockSignerSubject' } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'FileHash, SignerThumbprint, and SignerSubject are passed in' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 1 } @{ Command = 'Assert-FileSignatureValid'; Times = 1 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath -FileHash 'mockFileHash' ` -SignerThumbprint 'mockSignerThumbprint' ` -SignerSubject 'mockSignerSubject' } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'SignerThumbprint and SignerSubject are passed in but FileHash is not' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 0 } @{ Command = 'Assert-FileSignatureValid'; Times = 1 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath -SignerThumbprint 'mockSignerThumbprint' ` -SignerSubject 'mockSignerSubject' } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } Context 'Only path is passed in' { $mocksCalled = @( @{ Command = 'Assert-FileHashValid'; Times = 0 } @{ Command = 'Assert-FileSignatureValid'; Times = 0 } ) It 'Should not throw' { { Assert-FileValid -Path $script:testPath } | Should -Not -Throw } Invoke-ExpectedMocksAreCalledTest -MocksCalled $mocksCalled } } Describe 'Assert-FileHashValid' { $mockHash = @{ Hash = 'testHash' } Mock -CommandName 'Get-FileHash' -MockWith { return $mockHash } Context 'File hash is valid' { It 'Should not throw when hashes match' { { Assert-FileHashValid -Path $script:testPath -Hash $mockHash.Hash -Algorithm 'SHA256' } | Should -Not -Throw } It 'Should fetch the file hash' { Assert-MockCalled -CommandName 'Get-FileHash' -Exactly 1 -Scope 'Context' } } Context 'File hash is invalid' { $badHash = 'BadHash' $expectedErrorMessage = ($script:localizedData.InvalidFileHash -f $script:testPath, $badHash, 'SHA256') It 'Should throw when hashes do not match' { { Assert-FileHashValid -Path $script:testPath -Hash $badHash -Algorithm 'SHA256' } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Assert-FileSignatureValid' { $mockThumbprint = 'mockThumbprint' $mockSubject = 'mockSubject' $mockSignature = @{ Status = [System.Management.Automation.SignatureStatus]::Valid SignerCertificate = @{ Thumbprint = $mockThumbprint; Subject = $mockSubject } } Mock -CommandName 'Get-AuthenticodeSignature' -MockWith { return $mockSignature } Context 'File signature status, thumbprint and subject are valid' { It 'Should not throw' { { Assert-FileSignatureValid -Path $script:testPath -Thumbprint $mockThumbprint -Subject $mockSubject } | Should -Not -Throw } } Context 'File signature status and thumbprint are valid and Subject not passed in' { It 'Should not throw' { { Assert-FileSignatureValid -Path $script:testPath -Thumbprint $mockThumbprint } | Should -Not -Throw } } Context 'File signature status and subject are valid and Thumbprint not passed in' { It 'Should not throw' { { Assert-FileSignatureValid -Path $script:testPath -Subject $mockSubject } | Should -Not -Throw } } Context 'Only Path is passed in' { It 'Should not throw' { { Assert-FileSignatureValid -Path $script:testPath } | Should -Not -Throw } } Context 'File signature status and thumbprint are valid and subject is invalid' { $badSubject = 'BadSubject' $expectedErrorMessage = ($script:localizedData.WrongSignerSubject -f $script:testPath, $badSubject) It 'Should throw expected error message' { { Assert-FileSignatureValid -Path $script:testPath -Thumbprint $mockThumbprint -Subject $badSubject } | Should -Throw -ExpectedMessage $expectedErrorMessage } } Context 'File signature status and subject are valid and thumbprint is invalid' { $badThumbprint = 'BadThumbprint' $expectedErrorMessage = ($script:localizedData.WrongSignerThumbprint -f $script:testPath, $badThumbprint) It 'Should throw expected error message' { { Assert-FileSignatureValid -Path $script:testPath -Thumbprint $badThumbprint -Subject $mockSubject } | Should -Throw -ExpectedMessage $expectedErrorMessage } } Context 'File signature status is invalid and subject and thumbprint are valid' { $mockSignature.Status = 'Invalid' $expectedErrorMessage = ($script:localizedData.InvalidFileSignature -f $script:testPath, $mockSignature.Status) It 'Should throw expected error message' { { Assert-FileSignatureValid -Path $script:testPath -Thumbprint $mockThumbprint -Subject $mockSubject } | Should -Throw -ExpectedMessage $expectedErrorMessage } } } Describe 'Start-MsiProcess' { Mock -CommandName 'New-Object' -MockWith { return $script:mockStartInfo } -ParameterFilter { $TypeName -eq 'System.Diagnostics.ProcessStartInfo' } Mock -CommandName 'New-Object' -MockWith { return $script:mockProcess } -ParameterFilter { $TypeName -eq 'System.Diagnostics.Process' } Mock -CommandName 'Get-ProductEntry' -MockWith { return $script:mockProductEntryInfo } Mock -CommandName 'Invoke-PInvoke' -MockWith { return 0 } Mock -CommandName 'Invoke-Process' -MockWith { return 0 } $startMsiProcessParameters = @{ IdentifyingNumber = $script:testIdentifyingNumber Path = $script:testPath Ensure = 'Present' Arguments = 'TestArguments' LogPath = 'TestLogPath' RunAsCredential = $script:testCredential } Context 'Install MSI package with RunAsCredential specified' { $mocksCalled = @( @{ Command = 'New-Object'; Times = 1; Custom = 'process start info object' } @{ Command = 'Get-ProductEntry'; Times = 0 } @{ Command = 'Invoke-PInvoke'; Times = 1; Custom = 'install' } @{ Command = 'Invoke-Process'; Times = 0; Custom = 'install' } ) Invoke-GenericUnitTest -Function { Param($startMsiProcessParameters) Start-MsiProcess @startMsiProcessParameters } ` -FunctionParameters $startMsiProcessParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } $startMsiProcessParameters.Ensure = 'Absent' Context 'Uninstall MSI package with RunAsCredential specified' { $mocksCalled = @( @{ Command = 'New-Object'; Times = 1; Custom = 'process start info object' } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Invoke-PInvoke'; Times = 1; Custom = 'uninstall' } @{ Command = 'Invoke-Process'; Times = 0; Custom = 'uninstall' } ) Invoke-GenericUnitTest -Function { Param($startMsiProcessParameters) Start-MsiProcess @startMsiProcessParameters } ` -FunctionParameters $startMsiProcessParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } $startMsiProcessParameters.Ensure = 'Present' $startMsiProcessParameters.Remove('RunAsCredential') Context 'Install MSI package without RunAsCredential' { $mocksCalled = @( @{ Command = 'New-Object'; Times = 2; Custom = 'process start info object and process' } @{ Command = 'Get-ProductEntry'; Times = 0 } @{ Command = 'Invoke-PInvoke'; Times = 0; Custom = 'install' } @{ Command = 'Invoke-Process'; Times = 1; Custom = 'install' } ) Invoke-GenericUnitTest -Function { Param($startMsiProcessParameters) Start-MsiProcess @startMsiProcessParameters } ` -FunctionParameters $startMsiProcessParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } $startMsiProcessParameters.Ensure = 'Absent' Context 'Uninstall MSI package without RunAsCredential' { $mocksCalled = @( @{ Command = 'New-Object'; Times = 2; Custom = 'process start info object and process object' } @{ Command = 'Get-ProductEntry'; Times = 1 } @{ Command = 'Invoke-PInvoke'; Times = 0; Custom = 'uninstall' } @{ Command = 'Invoke-Process'; Times = 1; Custom = 'uninstall' } ) Invoke-GenericUnitTest -Function { Param($startMsiProcessParameters) Start-MsiProcess @startMsiProcessParameters } ` -FunctionParameters $startMsiProcessParameters ` -MocksCalled $mocksCalled ` -ShouldThrow $false } Mock -CommandName 'Invoke-Process' -MockWith { Throw } Context 'Error occurred while trying to invoke the process' { Invoke-GenericUnitTest -Function { Param($startMsiProcessParameters) Start-MsiProcess @startMsiProcessParameters } ` -FunctionParameters $startMsiProcessParameters ` -ShouldThrow $true ` -ErrorMessage ($script:localizedData.CouldNotStartProcess -f $script:testPath) ` -ErrorTestName $script:errorMessageTitles.CouldNotStartProcess } } } } |