Files

1017 lines
53 KiB
PowerShell

$errorActionPreference = 'Stop'
Set-StrictMode -Version 'Latest'
Describe 'MsiPackage Unit Tests' {
BeforeAll {
# Import CommonTestHelper
$testsFolderFilePath = Split-Path $PSScriptRoot -Parent
$testHelperFolderFilePath = Join-Path -Path $testsFolderFilePath -ChildPath 'TestHelpers'
$commonTestHelperFilePath = Join-Path -Path $testHelperFolderFilePath -ChildPath 'CommonTestHelper.psm1'
Import-Module -Name $commonTestHelperFilePath
$script:testEnvironment = Enter-DscResourceTestEnvironment `
-DscResourceModuleName 'PSDscResources' `
-DscResourceName 'MSFT_MsiPackage' `
-TestType 'Unit'
}
AfterAll {
Exit-DscResourceTestEnvironment -TestEnvironment $script:testEnvironment
}
InModuleScope 'MSFT_MsiPackage' {
$script:testsFolderFilePath = Split-Path $PSScriptRoot -Parent
$testHelperFolderFilePath = Join-Path -Path $testsFolderFilePath -ChildPath 'TestHelpers'
$script:commonTestHelperFilePath = Join-Path -Path $testHelperFolderFilePath -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 'C:\'
$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 = ([DateTime]::new(2017, 4, 24).ToShortDateString())
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
}
$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 '20170424' } -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
}
}
}
}