diff --git a/Functions/icon.Tests.ps1 b/Functions/icon.Tests.ps1 new file mode 100644 index 0000000..4106111 --- /dev/null +++ b/Functions/icon.Tests.ps1 @@ -0,0 +1,49 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { +Describe 'Get-IconReferencePath' { + Mock Get-StockIconReferencePath -Verifiable + Context 'StockIconName' { + Mock Get-StockIconReferencePath { 'stock icon reference path' } -Verifiable + It 'returns result based on StockIconName' { + $splat = @{ + StockIconName = 'DocumentNotAssociated' + IconFilePath = '' + IconResourceId = 0 + } + $r = Get-IconReferencePath @splat + $r | Should be 'stock icon reference path' + } + It 'correctly invokes functions' { + Assert-MockCalled Get-StockIconReferencePath 1 { + $StockIconName -eq 'DocumentNotAssociated' + } + } + } + Context 'IconFilePath' { + It 'returns result based on IconFilePath' { + $splat = @{ + StockIconName = '' + IconFilePath = 'c:\Windows\calc.exe' + IconResourceId = 1 + } + $r = Get-IconReferencePath @splat + $r | Should be 'c:\Windows\calc.exe,1' + } + It 'correctly invokes functions' { + Assert-MockCalled Get-StockIconReferencePath 0 -Exactly + } + } + Context 'neither' { + It 'returns nothing' { + $splat = @{ + StockIconName = '' + IconFilePath = '' + IconResourceId = 0 + } + $r = Get-IconReferencePath @splat + $r | Should beNullOrEmpty + } + } +} +} diff --git a/Functions/icon.ps1 b/Functions/icon.ps1 new file mode 100644 index 0000000..cd3d9b7 --- /dev/null +++ b/Functions/icon.ps1 @@ -0,0 +1,29 @@ +function Get-IconReferencePath +{ + [CmdletBinding()] + param + ( + [string] + $StockIconName, + + [string] + $IconFilePath, + + [int] + $IconResourceId + ) + process + { + # return based on the StockIconName + if ( $StockIconName -and $StockIconName -ne 'DoNotSet' ) + { + return Get-StockIconReferencePath $StockIconName + } + + # return based on IconFilePath + if ( $IconFilePath ) + { + return "$IconFilePath,$IconResourceId" + } + } +} diff --git a/Functions/loadTypes.ps1 b/Functions/loadTypes.ps1 index de85b51..462ca1d 100644 --- a/Functions/loadTypes.ps1 +++ b/Functions/loadTypes.ps1 @@ -1,6 +1,8 @@ @( 'shellLibraryType.ps1' + 'shellLibraryFolderType.ps1' 'stockIconInfoType.ps1' + 'shortcutType.ps1' ) | % { . "$($PSCommandPath | Split-Path -Parent)\$_" } diff --git a/Functions/processPersistentItem.Tests.ps1 b/Functions/processPersistentItem.Tests.ps1 new file mode 100644 index 0000000..2a7bb34 --- /dev/null +++ b/Functions/processPersistentItem.Tests.ps1 @@ -0,0 +1,312 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { + +function Get-PersistentItem { param ($Key) } +function Add-PersistentItem { param ($Key) } +function Remove-PersistentItem { param ($Key) } + +Describe 'Invoke-ProcessPersistentItem -Ensure Present: ' { + Mock Get-PersistentItem -Verifiable + Mock Add-PersistentItem -Verifiable + Mock Remove-PersistentItem { 'junk' } -Verifiable + Mock Invoke-ProcessPersistentItemProperty -Verifiable + + $delegates = @{ + Getter = 'Get-PersistentItem' + Adder = 'Add-PersistentItem' + Remover = 'Remove-PersistentItem' + PropertyGetter = 'Get-PersistentItemProperty' + PropertySetter = 'Set-PersistentItemProperty' + PropertyNormalizer = 'Get-NormalizedPersistentItemProperty' + } + $coreDelegates = @{ + Getter = 'Get-PersistentItem' + Adder = 'Add-PersistentItem' + Remover = 'Remove-PersistentItem' + } + + Context '-Ensure Present: absent, Set' { + Mock Add-PersistentItem { 'item' } + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{ P = 'P desired' } + } + $r = Invoke-ProcessPersistentItem Set Present @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 1 { + $Mode -eq 'Set' -and + $_Keys.Key -eq 'key value' -and + $Properties.P -eq 'P desired' -and + $PropertyGetter -eq 'Get-PersistentItemProperty' -and + $PropertySetter -eq 'Set-PersistentItemProperty' -and + $PropertyNormalizer -eq 'Get-NormalizedPersistentItemProperty' + } + } + } + Context '-Ensure Present: absent, Set - omitting properties skips setting properties' { + Mock Add-PersistentItem { 'item' } + It 'returns nothing' { + $splat = @{ Keys = @{ Key = 'key value' } } + $r = Invoke-ProcessPersistentItem Set Present @splat @coreDelegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Present: absent, Test' { + It 'returns false' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Test Present @splat @delegates + $r | Should be $false + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Present: present, Set' { + Mock Get-PersistentItem { 'item' } -Verifiable + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Set Present @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 1 + } + } + Context '-Ensure Present: present, Test' { + Mock Get-PersistentItem { 'item' } -Verifiable + Mock Invoke-ProcessPersistentItemProperty { 'property test result' } -Verifiable + It 'returns result of properties test' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Test Present @splat @delegates + $r | Should be 'property test result' + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 1 + } + } + Context '-Ensure Present: present, Test - omitting properties skips setting properties' { + Mock Get-PersistentItem { 'item' } -Verifiable + It 'returns result of properties test' { + $splat = @{ Keys = @{ Key = 'key value' } } + $r = Invoke-ProcessPersistentItem Test Present @splat @coreDelegates + $r | Should be $true + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Absent: absent, Set' { + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Set Absent @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Absent: absent, Test' { + It 'returns true' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Test Absent @splat @delegates + $r | Should be $true + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Absent: present, Set' { + Mock Get-PersistentItem { 'item' } -Verifiable + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Set Absent @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } + Context '-Ensure Absent: present, Test' { + Mock Get-PersistentItem { 'item' } -Verifiable + It 'returns false' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{} + } + $r = Invoke-ProcessPersistentItem Test Absent @splat @delegates + $r | Should be $false + } + It 'correctly invokes functions' { + Assert-MockCalled Get-PersistentItem 1 { $Key -eq 'key value' } + Assert-MockCalled Add-PersistentItem 0 -Exactly + Assert-MockCalled Remove-PersistentItem 0 -Exactly + Assert-MockCalled Invoke-ProcessPersistentItemProperty 0 -Exactly + } + } +} + + +function Get-PersistentItemProperty { param ($Key,$PropertyName) } +function Set-PersistentItemProperty { param ($Key,$PropertyName,$Value) } +function Get-NormalizedPersistentItemProperty { param ($PropertyName,$Value) } + +Describe 'Invoke-ProcessPersistentItemProperty' { + Mock Get-NormalizedPersistentItemProperty -Verifiable + Mock Get-PersistentItemProperty -Verifiable + Mock Set-PersistentItemProperty { 'junk' } -Verifiable + + $delegates = @{ + PropertyGetter = 'Get-PersistentItemProperty' + PropertySetter = 'Set-PersistentItemProperty' + PropertyNormalizer = 'Get-NormalizedPersistentItemProperty' + } + Context 'Set, property already correct' { + Mock Get-NormalizedPersistentItemProperty { 'already correct' } -Verifiable + Mock Get-PersistentItemProperty { 'already correct' } -Verifiable + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{ P = 'already correct' } + } + $r = Invoke-ProcessPersistentItemProperty Set @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-NormalizedPersistentItemProperty 1 { + $PropertyName -eq 'P' -and + $Value -eq 'already correct' + } + Assert-MockCalled Get-PersistentItemProperty 1 { + $Key -eq 'key value' -and + $PropertyName -eq 'P' + } + Assert-MockCalled Set-PersistentItemProperty 0 -Exactly + } + } + Context 'Test, property correct' { + Mock Get-NormalizedPersistentItemProperty { 'correct' } -Verifiable + Mock Get-PersistentItemProperty { 'correct' } -Verifiable + It 'returns true' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{ P = 'correct' } + } + $r = Invoke-ProcessPersistentItemProperty Test @splat @delegates + $r | Should be $true + } + It 'correctly invokes functions' { + Assert-MockCalled Get-NormalizedPersistentItemProperty 1 { + $PropertyName -eq 'P' -and + $Value -eq 'correct' + } + Assert-MockCalled Get-PersistentItemProperty 1 { + $Key -eq 'key value' -and + $PropertyName -eq 'P' + } + Assert-MockCalled Set-PersistentItemProperty 0 -Exactly + } + } + Context 'Set, correcting property' { + Mock Get-NormalizedPersistentItemProperty { 'normalized' } -Verifiable + Mock Get-PersistentItemProperty { 'original' } -Verifiable + It 'returns nothing' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{ P = 'desired' } + } + $r = Invoke-ProcessPersistentItemProperty Set @splat @delegates + $r | Should beNullOrEmpty + } + It 'correctly invokes functions' { + Assert-MockCalled Get-NormalizedPersistentItemProperty 1 { + $PropertyName -eq 'P' -and + $Value -eq 'desired' + } + Assert-MockCalled Get-PersistentItemProperty 1 { + $Key -eq 'key value' -and + $PropertyName -eq 'P' + } + Assert-MockCalled Set-PersistentItemProperty 1 -Exactly { + $Key -eq 'key value' -and + $PropertyName -eq 'P' -and + $Value -eq 'desired' + } + } + } + Context 'Test, property incorrect' { + Mock Get-NormalizedPersistentItemProperty { 'normalized' } -Verifiable + Mock Get-PersistentItemProperty { 'original' } -Verifiable + It 'returns false' { + $splat = @{ + Keys = @{ Key = 'key value' } + Properties = @{ P = 'desired' } + } + $r = Invoke-ProcessPersistentItemProperty Test @splat @delegates + $r | Should be $false + } + It 'correctly invokes functions' { + Assert-MockCalled Get-NormalizedPersistentItemProperty 1 { + $PropertyName -eq 'P' -and + $Value -eq 'desired' + } + Assert-MockCalled Get-PersistentItemProperty 1 { + $Key -eq 'key value' -and + $PropertyName -eq 'P' + } + Assert-MockCalled Set-PersistentItemProperty 0 -Exactly + } + } +} +} diff --git a/Functions/processPersistentItem.ps1 b/Functions/processPersistentItem.ps1 new file mode 100644 index 0000000..246160b --- /dev/null +++ b/Functions/processPersistentItem.ps1 @@ -0,0 +1,178 @@ +function Invoke-ProcessPersistentItem +{ + [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + param + ( + [Parameter(Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Set','Test')] + $Mode, + + [Parameter(Position = 2, + ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Present','Absent')] + $Ensure = 'Present', + + [Parameter(Mandatory = $true, + Position = 3, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [Alias('Keys')] + [hashtable] + $_Keys, # https://github.com/pester/Pester/issues/776 + + [Parameter(Mandatory = $true)] + [string] + $Getter, + + [Parameter(Mandatory = $true)] + [string] + $Adder, + + [Parameter(Mandatory = $true)] + [string] + $Remover, + + [Parameter(ParameterSetName = 'with_properties', + Mandatory = $true)] + [hashtable] + $Properties, + + [Parameter(ParameterSetName = 'with_properties', + Mandatory = $true)] + [string] + $PropertyGetter, + + [Parameter(ParameterSetName = 'with_properties', + Mandatory = $true)] + [string] + $PropertySetter, + + [Parameter(ParameterSetName = 'with_properties', + Mandatory = $true)] + [string] + $PropertyNormalizer + ) + process + { + # retrieve the item + $item = & $Getter @_Keys + + # process item existence + switch ( $Ensure ) + { + 'Present' { + if ( -not $item ) + { + # add the item + switch ( $Mode ) + { + 'Set' { $item = & $Adder @_Keys } # create the item + 'Test' { return $false } # the item doesn't exist + } + } + } + 'Absent' { + switch ( $Mode ) + { + 'Set' { + if ( $item ) + { + & $Remover @_Keys | Out-Null + } + return + } + 'Test' { return -not $item } + } + } + } + + if ( $PSCmdlet.ParameterSetName -ne 'with_properties' ) + { + # we are not processing properties + if ( $Mode -eq 'Test' ) + { + return $true + } + return + } + + # process the item's properties + $splat = @{ + Mode = $Mode + Keys = $_Keys + Properties = $Properties + PropertyGetter = $PropertyGetter + PropertySetter = $PropertySetter + PropertyNormalizer = $PropertyNormalizer + } + Invoke-ProcessPersistentItemProperty @splat + } +} + +function Invoke-ProcessPersistentItemProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + Position = 1)] + [ValidateSet('Set','Test')] + $Mode, + + [Parameter(Mandatory = $true)] + [Alias('Keys')] + [hashtable] + $_Keys, # https://github.com/pester/Pester/issues/776 + + [hashtable] + $Properties, + + [Parameter(Mandatory = $true)] + [string] + $PropertyGetter, + + [Parameter(Mandatory = $true)] + [string] + $PropertySetter, + + [Parameter(Mandatory = $true)] + [string] + $PropertyNormalizer + ) + process + { + # process each property + foreach ( $propertyName in $Properties.Keys ) + { + # this is the desired value provided by the user + $desired = $Properties.$propertyName + + # normalize the desired value + $normalized = & $PropertyNormalizer -PropertyName $propertyName -Value $desired + + # get the existing value + $existing = & $PropertyGetter @_Keys -PropertyName $propertyName + + if ( $existing -ne $normalized ) + { + if ( $Mode -eq 'Test' ) + { + # we're testing and we've found a property mismatch + return $false + } + + # the existing property does not match the desired property + # so fix it + & $PropertySetter @_Keys -PropertyName $propertyName -Value $desired | + Out-Null + } + } + + if ( $Mode -eq 'Test' ) + { + return $true + } + } +} diff --git a/Functions/processShellLibrary.Tests.ps1 b/Functions/processShellLibrary.Tests.ps1 index 7779c6f..3778f94 100644 --- a/Functions/processShellLibrary.Tests.ps1 +++ b/Functions/processShellLibrary.Tests.ps1 @@ -1,11 +1,5 @@ Import-Module WindowsShell -Force -Describe 'set up environment' { - It 'add the Windows API Code Pack assembly' { - Add-Type -Path "$PSScriptRoot\..\bin\winapicp\Microsoft.WindowsAPICodePack.Shell.dll" - } -} - InModuleScope WindowsShell { Describe Test-ValidShellLibraryTypeName { @@ -27,397 +21,63 @@ Describe Test-ValidShellLibraryTypeName { } } -Describe Test-ValidStockIconName { - It 'returns true for valid name' { - $r = 'Application' | Test-ValidStockIconName - $r | Should be $true - } - It 'returns true for DoNotSet' { - $r = 'DoNotSet' | Test-ValidStockIconName - $r | Should be $true - } - It 'returns false for invalid name' { - $r = 'Invalid Icon Name' | Test-ValidStockIconName - $r | Should be $false - } - It 'throws for invalid name' { - { 'Invalid Type Name' | Test-ValidStockIconName -ea Stop } | - Should throw 'not a valid' - } -} - -Describe 'Invoke-ProcessShellLibrary -Ensure Present' { - Mock Get-ShellLibrary -Verifiable - Mock Add-ShellLibrary -Verifiable - Mock Remove-ShellLibrary -Verifiable - Mock Get-StockIconReferencePath { - 'C:\WINDOWS\system32\imageres.dll,-152' - } -Verifiable - Mock Set-ShellLibraryProperty -Verifiable - Context 'absent, Set' { - Mock Add-ShellLibrary { - New-Object psobject -Property @{ - Name = 'libary name' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'TypeName' -and - $Value -eq 'Pictures' - } - Assert-MockCalled Get-StockIconReferencePath 1 { - $StockIconName -eq 'Application' - } - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'IconReferencePath' -and - $Value -eq 'C:\WINDOWS\system32\imageres.dll,-152' - } - } - } - Context 'IconFilePath' { - Mock Add-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - IconFilePath = 'c:\folder\some.exe' - IconResourceId = 0 - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'TypeName' -and - $Value -eq 'Pictures' - } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'IconReferencePath' -and - $Value -eq 'C:\folder\some.exe,0' - } - } - } - Context 'StockIcon and IconFilePath' { - Mock Add-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - IconFilePath = 'c:\folder\some.exe' - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'TypeName' -and - $Value -eq 'Pictures' - } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 1 { - $PropertyName -eq 'IconReferencePath' -and - $Value -eq 'C:\folder\some.exe,0' - } - } - } - Context 'null optional properties' { - Mock Add-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - } - } - Context 'omit optional properties' { - Mock Add-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - } - } -Verifiable - It 'returns nothing' { - $r = Invoke-ProcessShellLibrary Set Present 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - } - } - Context 'absent, Test' { - It 'returns false' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Test +Describe 'Invoke-ProcessShellLibrary' { + Mock Get-IconReferencePath -Verifiable + Mock Invoke-ProcessPersistentItem { 'return value' } -Verifiable + Context 'plumbing' { + Mock Get-IconReferencePath { 'icon reference path' } -Verifiable + It 'returns exactly one item' { + $params = New-Object psobject -Property @{ + Mode = 'Set' + Ensure = 'Present' + Name = 'name' + TypeName = 'Pictures' + StockIconName = 'AudioFiles' + IconFilePath = 'c:/filepath' + IconResourceId = 1 + } + $r = $params | Invoke-ProcessShellLibrary $r.Count | Should be 1 - $r | Should be $false + $r | Should be 'return value' } It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present, Set' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-152' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 1 - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present, Test' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-152' - } - } -Verifiable - It 'returns true' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Test - $r.Count | Should be 1 - $r | Should be $true - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 1 - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present wrong type, Test' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-152' - } - } -Verifiable - It 'returns false' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Music' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Test - $r.Count | Should be 1 - $r | Should be $false - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present wrong icon, Test' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-94' - } - } -Verifiable - It 'returns false' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - } - $r = $object | Invoke-ProcessShellLibrary Test - $r.Count | Should be 1 - $r | Should be $false - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 1 - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } -} + Assert-MockCalled Get-IconReferencePath 1 { + $StockIconName -eq 'AudioFiles' -and + $IconFilePath -eq 'c:/filepath' -and + $IconResourceId -eq 1 + } + Assert-MockCalled Invoke-ProcessPersistentItem 1 { + $Mode -eq 'Set' -and + $Ensure -eq 'Present' -and + $_Keys.Name -eq 'name' -and -Describe 'Invoke-ProcessShellLibrary -Ensure Absent' { - Mock Get-ShellLibrary -Verifiable - Mock Add-ShellLibrary -Verifiable - Mock Remove-ShellLibrary -Verifiable - Mock Get-StockIconReferencePath { - 'C:\WINDOWS\system32\imageres.dll,-152' - } -Verifiable - Mock Set-ShellLibraryProperty -Verifiable - Context 'absent, Set' { - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - Ensure = 'Absent' + #Properties + $Properties.TypeName -eq 'Pictures' -and + $Properties.IconReferencePath -eq 'icon reference path' # <- icon } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } } } - Context 'absent, Test' { - It 'returns true' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - Ensure = 'Absent' + Context 'omit optional' { + It 'returns exactly one item' { + $params = New-Object psobject -Property @{ + Mode = 'Set' + Ensure = 'Present' + Name = 'name' + TypeName = 'DoNotSet' } - $r = $object | Invoke-ProcessShellLibrary Test + $r = $params | Invoke-ProcessShellLibrary $r.Count | Should be 1 - $r | Should be $true + $r | Should be 'return value' } It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present, Set' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-152' - } - } -Verifiable - It 'returns nothing' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - Ensure = 'Absent' - } - $r = $object | Invoke-ProcessShellLibrary Set - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } - } - } - Context 'present, Test' { - Mock Get-ShellLibrary { - New-Object ShellLibraryInfo -Property @{ - Name = 'libary name' - TypeName = 'Pictures' - IconReferencePath = 'C:\WINDOWS\system32\imageres.dll,-152' - } - } -Verifiable - It 'returns false' { - $object = New-Object psobject -Property @{ - Name = 'library name' - TypeName = 'Pictures' - StockIconName = 'Application' - Ensure = 'Absent' + Assert-MockCalled Invoke-ProcessPersistentItem 1 { + $Mode -eq 'Set' -and + $Ensure -eq 'Present' -and + $_Keys.Name -eq 'name' -and + + #Properties + $Properties.Count -eq 0 } - $r = $object | Invoke-ProcessShellLibrary Test - $r.Count | Should be 1 - $r | Should be $false - } - It 'correctly invokes functions' { - Assert-MockCalled Get-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Add-ShellLibrary 0 -Exactly - Assert-MockCalled Remove-ShellLibrary 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'TypeName' } - Assert-MockCalled Get-StockIconReferencePath 0 -Exactly - Assert-MockCalled Set-ShellLibraryProperty 0 -Exactly -ParameterFilter { $PropertyName -eq 'IconReferencePath' } } } } diff --git a/Functions/processShellLibrary.ps1 b/Functions/processShellLibrary.ps1 index 2b3a442..2b3ea67 100644 --- a/Functions/processShellLibrary.ps1 +++ b/Functions/processShellLibrary.ps1 @@ -25,31 +25,6 @@ function Test-ValidShellLibraryTypeName return $true } } -function Test-ValidStockIconName -{ - [CmdletBinding()] - param - ( - [Parameter(ValueFromPipeline = $true, - Mandatory = $true)] - [string] - $StockIconName - ) - process - { - $out = New-Object Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier - if - ( - $StockIconName -ne 'DoNotSet' -and - -not [Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier]::TryParse($StockIconName,[ref]$out) - ) - { - &(Publish-Failure "$StockIconName is not a valid stock icon name",'IconName' ([System.ArgumentException])) - return $false - } - return $true - } -} function Invoke-ProcessShellLibrary { @@ -90,11 +65,7 @@ function Invoke-ProcessShellLibrary [Parameter(ValueFromPipelineByPropertyName = $true)] [int] - $IconResourceId=0, - - [Parameter(ValueFromPipelineByPropertyName = $true)] - [string[]] - $FolderOrder + $IconResourceId=0 ) process { @@ -103,94 +74,39 @@ function Invoke-ProcessShellLibrary $TypeName | ? {$_} | Test-ValidShellLibraryTypeName -ea Stop | Out-Null $StockIconName | ? {$_} | Test-ValidStockIconName -ea Stop | Out-Null - # retrieve the library - $library = $Name | Get-ShellLibrary + # pass through properties + $properties = @{} + 'TypeName' | + ? { $_ -in $PSCmdlet.MyInvocation.BoundParameters.Keys } | + ? { (Get-Variable $_ -ValueOnly) -ne 'DoNotSet' } | + % { $properties.$_ = Get-Variable $_ -ValueOnly } - # process library existence - switch ( $Ensure ) - { - 'Present' { - if ( -not $library ) - { - switch( $Mode ) - { - 'Set' { $library = $Name | Add-ShellLibrary } # create the library - 'Test' { return $false } # the library doesn't exist - } - } - } - 'Absent' { - switch ( $Mode ) - { - 'Set' { - if ( $library ) - { - # the library exists, remove it - $Name | Remove-ShellLibrary - } - return - } - 'Test' - { - return -not $library - } - } - } - } - # process library type - if - ( - ( $TypeName -ne [string]::Empty -and $TypeName -ne 'DoNotSet' ) -and - $library.TypeName -ne $TypeName - ) - { - switch ( $Mode ) - { - 'Set' { - # correct the property - $library | Set-ShellLibraryProperty TypeName $TypeName - } - 'Test' { return $false } # the property is incorrect - } + # work out the icon + $splat = @{ + StockIconName = $StockIconName + IconFilePath = $IconFilePath + IconResourceId = $IconResourceId } - - # process the icon name - if - ( - ( $StockIconName -ne [string]::Empty -and $StockIconName -ne 'DoNotSet' ) -or - $IconFilePath - ) + if ( $iconReferencePath = Get-IconReferencePath @splat ) { - # compose the icon reference path - if ( $IconFilePath ) - { - $iconReferencePath = "$IconFilePath,$IconResourceId" - } - else - { - $iconReferencePath = $StockIconName | Get-StockIconReferencePath - } - - if ( $library.IconReferencePath -ne $iconReferencePath ) - { - switch ( $Mode ) - { - 'Set' { - # correct the property - $library | Set-ShellLibraryProperty IconReferencePath $iconReferencePath - } - 'Test' { return $false } # the property is incorrect - } - } + $properties.IconReferencePath = $iconReferencePath } - # if a folder order is provided invoke Test-ShellLibraryFoldersSortOrder, Sort-ShellLibraryFolders - - if ( $Mode -eq 'Test' ) - { - return $true + # process + $splat = @{ + Mode = $Mode + Ensure = $Ensure + Keys = @{ Name = $Name } + Properties = $properties + Getter = 'Get-ShellLibrary' + Adder = 'Add-ShellLibrary' + Remover = 'Remove-ShellLibrary' + PropertyGetter = 'Get-ShellLibraryProperty' + PropertySetter = 'Set-ShellLibraryProperty' + PropertyNormalizer = 'Get-NormalizedShellLibraryProperty' } + Invoke-ProcessPersistentItem @splat } } diff --git a/Functions/processShellLibraryFolder.Tests.ps1 b/Functions/processShellLibraryFolder.Tests.ps1 index 90f12d7..8e2550b 100644 --- a/Functions/processShellLibraryFolder.Tests.ps1 +++ b/Functions/processShellLibraryFolder.Tests.ps1 @@ -1,167 +1,33 @@ Import-Module WindowsShell -Force -Describe 'set up environment' { - It 'add the Windows API Code Pack assembly' { - Add-Type -Path "$PSScriptRoot\..\bin\winapicp\Microsoft.WindowsAPICodePack.Shell.dll" - } -} +InModuleScope WindowsShell { -Describe 'Invoke-ProcessShellLibraryFolder -Ensure Present' { - InModuleScope WindowsShell { - Mock Test-ShellLibrary { $true } -Verifiable - Mock Test-ShellLibraryFolder -Verifiable - Mock Add-ShellLibraryFolder -Verifiable - Mock Remove-ShellLibraryFolder -Verifiable - Context 'absent, Set'{ - It 'returns nothing' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Set Present 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'library missing, Set' { - Mock Test-ShellLibrary { $false } -Verifiable - It 'returns nothing' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Set Present 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'absent, Test' { - It 'returns false' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Test Present 'library name' - $r | Should be $false - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'present, Set' { - Mock Test-ShellLibraryFolder { $true } -Verifiable - It 'returns nothing' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Set Present 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'present, Test' { - Mock Test-ShellLibraryFolder { $true } -Verifiable - It 'returns true' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Test Present 'library name' - $r | Should be $true - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly +Describe 'Invoke-ProcessShellLibraryFolder' { + Mock Invoke-ProcessPersistentItem { 'return value' } -Verifiable + Context 'plumbing'{ + It 'returns exactly one item' { + $params = New-Object psobject -Property @{ + Mode = 'Set' + Ensure = 'Present' + FolderPath = 'c:\folder\path' + LibraryName = 'LibraryName' + } + $r = $params | Invoke-ProcessShellLibraryFolder + $r.Count | Should be 1 + $r | Should be 'return value' + } + It 'correctly invokes functions' { + Assert-MockCalled Invoke-ProcessPersistentItem 1 { + $Mode -eq 'Set' -and + $Ensure -eq 'Present' -and + $_Keys.FolderPath -eq 'c:\folder\path' -and + $_Keys.LibraryName -eq 'LibraryName' -and + + #Properties + $Properties.Count -eq 0 } } } } -Describe 'Invoke-ProcessShellLibraryFolder -Ensure Absent' { - InModuleScope WindowsShell { - Mock Test-ShellLibrary { $true } -Verifiable - Mock Test-ShellLibraryFolder -Verifiable - Mock Add-ShellLibraryFolder -Verifiable - Mock Remove-ShellLibraryFolder -Verifiable - Context 'absent, Set'{ - It 'returns nothing' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Set Absent 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'absent, Test' { - It 'returns true' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Test Absent 'library name' - $r | Should be $true - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - Context 'present, Set' { - Mock Test-ShellLibraryFolder { $true } -Verifiable - It 'returns nothing' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Set Absent 'library name' - $r | Should beNullOrEmpty - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - } - } - Context 'present, Test' { - Mock Test-ShellLibraryFolder { $true } -Verifiable - It 'returns false' { - $r = 'c:\folder' | Invoke-ProcessShellLibraryFolder Test Absent 'library name' - $r | Should be $false - } - It 'correctly invokes functions ' { - Assert-MockCalled Test-ShellLibrary 1 { $Name -eq 'library name' } - Assert-MockCalled Test-ShellLibraryFolder 1 { - $LibraryName -eq 'library name' -and - $FolderPath -eq 'c:\folder' - } - Assert-MockCalled Add-ShellLibraryFolder 0 -Exactly - Assert-MockCalled Remove-ShellLibraryFolder 0 -Exactly - } - } - } } + diff --git a/Functions/processShellLibraryFolder.ps1 b/Functions/processShellLibraryFolder.ps1 index f5c2d25..032604f 100644 --- a/Functions/processShellLibraryFolder.ps1 +++ b/Functions/processShellLibraryFolder.ps1 @@ -32,50 +32,17 @@ function Invoke-ProcessShellLibraryFolder ) process { - if ( -not (Test-ShellLibrary $LibraryName) ) - { - # the library doesn't exist - if ( $Mode -eq 'Set' ) - { - return - } - return $false - } - - $folderExists = $FolderPath | Test-ShellLibraryFolder $LibraryName - - switch ( $Ensure ) - { - 'Present' { - if ( -not $folderExists ) - { - switch ( $Mode ) - { - 'Set' { $FolderPath | Add-ShellLibraryFolder $LibraryName } # create the folder - 'Test' { return $false } # the folder doesn't exist - } - } - switch ( $Mode ) - { - 'Set' {} - 'Test' { return $true } # the folder exists - } - } - 'Absent' { - switch ( $Mode ) - { - 'Set' { - if ( $folderExists ) - { - # the library exists, remove it - $FolderPath | Remove-ShellLibraryFolder $LibraryName - } - } - 'Test' { - return -not $folderExists - } - } + $splat = @{ + Mode = $Mode + Ensure = $Ensure + Keys = @{ + LibraryName = $LibraryName + FolderPath = $FolderPath } + Getter = 'Get-ShellLibraryFolder' + Adder = 'Add-ShellLibraryFolder' + Remover = 'Remove-ShellLibraryFolder' } + Invoke-ProcessPersistentItem @splat } } diff --git a/Functions/processShortcut.Tests.ps1 b/Functions/processShortcut.Tests.ps1 new file mode 100644 index 0000000..a80427c --- /dev/null +++ b/Functions/processShortcut.Tests.ps1 @@ -0,0 +1,75 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { + +Describe 'Invoke-ProcessShortcut' { + Mock Get-IconReferencePath -Verifiable + Mock Invoke-ProcessPersistentItem -Verifiable + Context 'plumbing' { + Mock Get-IconReferencePath { 'icon reference path' } -Verifiable + Mock Invoke-ProcessPersistentItem { 'return value' } -Verifiable + It 'returns exactly one item' { + $params = New-Object psobject -Property @{ + Mode = 'Set' + Ensure = 'Present' + Path = 'path' + TargetPath = 'target path' + Arguments = 'arguments' + WorkingDirectory = 'working directory' + WindowStyle = 'Normal' + Hotkey = 'hotkey' + StockIconName = 'stock icon name' + IconFilePath = 'icon file path' + IconResourceId = 1 + Description = 'description' + } + $r = $params | Invoke-ProcessShortcut + $r.Count | Should be 1 + $r | Should be 'return value' + } + It 'correctly invokes functions' { + Assert-MockCalled Get-IconReferencePath 1 { + $StockIconName -eq 'stock icon name' -and + $IconFilePath -eq 'icon file path' -and + $IconResourceId -eq 1 + } + Assert-MockCalled Invoke-ProcessPersistentItem 1 { + $Mode -eq 'Set' -and + $Ensure -eq 'Present' -and + $_Keys.Path -eq 'path' -and + + #Properties + $Properties.TargetPath -eq 'target path' -and + $Properties.Arguments -eq 'arguments' -and + $Properties.WorkingDirectory -eq 'working directory' -and + $Properties.WindowStyle -eq 'Normal' -and + $Properties.IconLocation -eq 'icon reference path' -and # <- icon + $Properties.Description -eq 'description' + } + } + } + Context 'omit optional' { + Mock Invoke-ProcessPersistentItem { 'return value' } -Verifiable + It 'returns exactly one item' { + $params = New-Object psobject -Property @{ + Mode = 'Set' + Ensure = 'Present' + Path = 'path' + } + $r = $params | Invoke-ProcessShortcut + $r.Count | Should be 1 + $r | Should be 'return value' + } + It 'correctly invokes functions' { + Assert-MockCalled Invoke-ProcessPersistentItem 1 { + $Mode -eq 'Set' -and + $Ensure -eq 'Present' -and + $_Keys.Path -eq 'path' -and + + #Properties + $Properties.Count -eq 0 + } + } + } +} +} diff --git a/Functions/processShortcut.ps1 b/Functions/processShortcut.ps1 new file mode 100644 index 0000000..7604218 --- /dev/null +++ b/Functions/processShortcut.ps1 @@ -0,0 +1,101 @@ +function Invoke-ProcessShortcut +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + Position = 1, + ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Set','Test')] + $Mode, + + [Parameter(Position = 2, + ValueFromPipelineByPropertyName = $true)] + [ValidateSet('Present','Absent')] + $Ensure = 'Present', + + [Parameter(Mandatory = $true, + Position = 3, + ValueFromPipelineByPropertyName = $true)] + [Alias('FullName')] + [string] + $Path, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('Target')] + [string] + $TargetPath, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $Arguments, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('StartIn','WorkingFolder')] + [string] + $WorkingDirectory, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('Run')] + [WindowStyle] + $WindowStyle, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('ShortcutKey')] + [string] + $Hotkey, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $StockIconName, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string] + $IconFilePath, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [int] + $IconResourceId=0, + + [Parameter(ValueFromPipelineByPropertyName = $true)] + [Alias('Comment')] + [string] + $Description + ) + process + { + # pass through shortcut properties + $properties = @{} + 'TargetPath','Arguments','WorkingDirectory', + 'WindowStyle','Hotkey','Description' | + ? { $_ -in $PSCmdlet.MyInvocation.BoundParameters.Keys } | + % { $properties.$_ = Get-Variable $_ -ValueOnly } + + + # work out the icon + $splat = @{ + StockIconName = $StockIconName + IconFilePath = $IconFilePath + IconResourceId = $IconResourceId + } + if ( $iconReferencePath = Get-IconReferencePath @splat ) + { + $properties.IconLocation = $iconReferencePath + } + + # process + $splat = @{ + Mode = $Mode + Ensure = $Ensure + Keys = @{ Path = $Path } + Properties = $properties + Getter = 'Get-ShortCut' + Adder = 'Add-ShortCut' + Remover = 'Remove-ShortCut' + PropertyGetter = 'Get-ShortCutProperty' + PropertySetter = 'Set-ShortCutProperty' + PropertyNormalizer = 'Get-NormalizedShortCutProperty' + } + Invoke-ProcessPersistentItem @splat + } +} diff --git a/Functions/shellLibrary.ps1 b/Functions/shellLibrary.ps1 index fd97f7b..6235217 100644 --- a/Functions/shellLibrary.ps1 +++ b/Functions/shellLibrary.ps1 @@ -61,6 +61,7 @@ function Get-ShellLibrary ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('Key')] [string] $Name ) @@ -96,6 +97,7 @@ function Add-ShellLibrary ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('Key')] $Name ) process @@ -134,6 +136,7 @@ function Remove-ShellLibrary ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('Key')] [string] $Name ) @@ -154,6 +157,30 @@ function Remove-ShellLibrary } } +function Get-ShellLibraryProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [string] + [Alias('Key','Name')] + $LibraryName, + + [Parameter(Mandatory = $true, + position = 1)] + [ValidateSet('TypeName','IconReferencePath')] + [string] + $PropertyName + ) + process + { + ( $LibraryName | Get-ShellLibrary ).$PropertyName + } +} + function Set-ShellLibraryProperty { [CmdletBinding()] @@ -163,7 +190,7 @@ function Set-ShellLibraryProperty ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string] - [Alias('Name')] + [Alias('Key','Name')] $LibraryName, [Parameter(Mandatory = $true, @@ -208,18 +235,25 @@ function Set-ShellLibraryProperty } } -function Get-StockIconReferencePath +function Get-NormalizedShellLibraryProperty { [CmdletBinding()] param ( [Parameter(Mandatory = $true, + Position = 1)] + [string] + $PropertyName, + + [Parameter(Mandatory = $true, + Position = 2, ValueFromPipeline = $true)] - [Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier] - $StockIconName + [AllowNull()] + [AllowEmptyString()] + $Value ) process { - return [StockIconInfo.StockIconInfo]::GetIconRefPath([int]$StockIconName) + $Value } } diff --git a/Functions/shellLibraryFolder.ps1 b/Functions/shellLibraryFolder.ps1 index 4c31766..932c360 100644 --- a/Functions/shellLibraryFolder.ps1 +++ b/Functions/shellLibraryFolder.ps1 @@ -66,13 +66,38 @@ function Test-ShellLibraryFolder { if ( $null -ne $l ) { $l.Dispose() } } - # new up a [ShellFileSystemFolder] from $FolderPath - # test if the library .Contains() the folder return $true } } +function Get-ShellLibraryFolder +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [ValidateScript({ $_ | Test-ValidFilePath })] + $FolderPath, + + [Parameter(Mandatory = $true, + Position = 1)] + [ValidateScript({ $_ | Test-ValidShellLibraryName })] + $LibraryName + ) + process + { + if ( $FolderPath | Test-ShellLibraryFolder $LibraryName ) + { + return New-Object ShellLibraryFolderInfo -Property @{ + LibraryName = $LibraryName + FolderPath = $FolderPath + } + } + } +} + function Add-ShellLibraryFolder { [CmdletBinding()] diff --git a/Functions/shellLibraryFolderType.ps1 b/Functions/shellLibraryFolderType.ps1 new file mode 100644 index 0000000..c656aa3 --- /dev/null +++ b/Functions/shellLibraryFolderType.ps1 @@ -0,0 +1,5 @@ +class ShellLibraryFolderInfo +{ + [string] $LibraryName + [string] $FolderPath +} diff --git a/Functions/shortcut.Tests.ps1 b/Functions/shortcut.Tests.ps1 new file mode 100644 index 0000000..cdc2278 --- /dev/null +++ b/Functions/shortcut.Tests.ps1 @@ -0,0 +1,33 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { + +Describe Test-ValidShortcutObject { + It 'returns false' { + $r = 'not a shortcut object' | Test-ValidShortcutObject + $r | Should be $false + } + + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + It 'returns true' { + $r = $shortcutPath | + Add-Shortcut | + Test-ValidShortcutObject + $r | Should be $true + } +} + +Describe Get-NormalizedShortcutProperty { + It 'Hotkey' { + $r = 'Ctrl+Alt+f' | Get-NormalizedShortcutProperty Hotkey + $r | Should be 'Alt+Ctrl+f' + } + It 'TargetPath' { + $r = 'c:/bogus/path.exe' | Get-NormalizedShortcutProperty TargetPath + $r | Should be 'c:\bogus\path.exe' + } +} +} diff --git a/Functions/shortcut.ps1 b/Functions/shortcut.ps1 new file mode 100644 index 0000000..1f0f8df --- /dev/null +++ b/Functions/shortcut.ps1 @@ -0,0 +1,221 @@ +function Test-ValidShortcutObject +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + $InputObject + ) + process + { + $memberNames = $InputObject | + Get-Member | + % Name + + foreach ( $expectedMemberName in @( + 'Load','Save','Arguments','Description','FullName','Hotkey', + 'IconLocation','RelativePath','TargetPath','WindowStyle', + 'WorkingDirectory' + ) + ) + { + if ( $expectedMemberName -notin $memberNames ) + { + return $false + } + } + + return $true + } +} + +function Get-Shortcut +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + $Path + ) + process + { + if ( -not ($Path | Test-Shortcut) ) + { + return + } + + (New-Object -ComObject WScript.Shell). + CreateShortcut($Path) + } +} + +function Test-Shortcut +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + $Path + ) + process + { + Test-Path $Path -PathType Leaf + } +} + +function Add-Shortcut +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + $Path + ) + process + { + if ( $Path | Test-Shortcut ) + { + throw [System.IO.IOException]::new( + "Shortcut at $Path already exists." + ) + } + + $shortcut = (New-Object -ComObject WScript.Shell).CreateShortcut($Path) + $shortcut.Save() + return $shortcut + } +} + +function Remove-Shortcut +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [Alias('Key')] + $Path + ) + process + { + try + { + Remove-Item $Path -ea Stop | Out-Null + } + catch [System.Management.Automation.ItemNotFoundException] + { + throw [System.Management.Automation.ItemNotFoundException]::new( + "Shortcut not found at $Path", + $_.Exception + ) + } + } +} + +function Get-ShortcutProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true, + position = 1)] + [string] + $PropertyName + ) + process + { + if ( -not ( $Path | Test-Shortcut ) ) + { + # the shortcut doesn't exist + throw [System.Management.Automation.ItemNotFoundException]::new( + "Shortcut not found at $path" + ) + } + + # the shortcut exists + + # get the property and return it + $shortcut = $Path | Get-Shortcut + $shortcut.$PropertyName + } +} + +function Set-ShortcutProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [string] + $Path, + + [Parameter(Mandatory = $true, + position = 1)] + [string] + $PropertyName, + + [Parameter(Mandatory = $true, + position = 2)] + $Value + ) + process + { + if ( -not ( $Path | Test-Shortcut ) ) + { + # the shortcut doesn't exist + throw [System.Management.Automation.ItemNotFoundException]::new( + "Shortcut not found at $path" + ) + } + + # the shortcut exists + + # change the property and save it + $shortcut = $Path | Get-Shortcut + $shortcut.$PropertyName = $Value + $shortcut.Save() + } +} + +function Get-NormalizedShortcutProperty +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + Position = 1)] + [string] + $PropertyName, + + [Parameter(Mandatory = $true, + Position = 2, + ValueFromPipeline = $true)] + [AllowNull()] + [AllowEmptyString()] + $Value + ) + process + { + # create the object we'll use to perform the normalization + $shortcut = (New-Object -ComObject WScript.Shell). + CreateShortcut("$([guid]::NewGuid().Guid).lnk") + + # set the property + $shortcut.$PropertyName = $Value + + # return the normalized property + return $shortcut.$PropertyName + } +} diff --git a/Functions/shortcutType.ps1 b/Functions/shortcutType.ps1 new file mode 100644 index 0000000..7ddca21 --- /dev/null +++ b/Functions/shortcutType.ps1 @@ -0,0 +1,18 @@ +<# +WindowStyle Property + +https://msdn.microsoft.com/fr-fr/library/w88k7fw2(v=vs.84).aspx + +intWindowStyle Description +1 Activates and displays a window. If the window is minimized + or maximized, the system restores it to its original size and + position. +3 Activates the window and displays it as a maximized window. +7 Minimizes the window and activates the next top-level window. +#> +enum WindowStyle +{ + Normal = 1 + Maximized = 3 + Minimized = 7 +} diff --git a/Functions/stockIcon.Tests.ps1 b/Functions/stockIcon.Tests.ps1 new file mode 100644 index 0000000..1fab142 --- /dev/null +++ b/Functions/stockIcon.Tests.ps1 @@ -0,0 +1,29 @@ +Import-Module WindowsShell -Force + +Describe 'set up environment' { + It 'add the Windows API Code Pack assembly' { + Add-Type -Path "$PSScriptRoot\..\bin\winapicp\Microsoft.WindowsAPICodePack.Shell.dll" + } +} + +InModuleScope WindowsShell { + +Describe Test-ValidStockIconName { + It 'returns true for valid name' { + $r = 'Application' | Test-ValidStockIconName + $r | Should be $true + } + It 'returns true for DoNotSet' { + $r = 'DoNotSet' | Test-ValidStockIconName + $r | Should be $true + } + It 'returns false for invalid name' { + $r = 'Invalid Icon Name' | Test-ValidStockIconName + $r | Should be $false + } + It 'throws for invalid name' { + { 'Invalid Type Name' | Test-ValidStockIconName -ea Stop } | + Should throw 'not a valid' + } +} +} diff --git a/Functions/stockIcon.ps1 b/Functions/stockIcon.ps1 new file mode 100644 index 0000000..f0ebf7a --- /dev/null +++ b/Functions/stockIcon.ps1 @@ -0,0 +1,41 @@ +function Get-StockIconReferencePath +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true)] + [Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier] + $StockIconName + ) + process + { + return [StockIconInfo.StockIconInfo]::GetIconRefPath([int]$StockIconName) + } +} + +function Test-ValidStockIconName +{ + [CmdletBinding()] + param + ( + [Parameter(ValueFromPipeline = $true, + Mandatory = $true)] + [string] + $StockIconName + ) + process + { + $out = New-Object Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier + if + ( + $StockIconName -ne 'DoNotSet' -and + -not [Microsoft.WindowsAPICodePack.Shell.StockIconIdentifier]::TryParse($StockIconName,[ref]$out) + ) + { + &(Publish-Failure "$StockIconName is not a valid stock icon name",'IconName' ([System.ArgumentException])) + return $false + } + return $true + } +} diff --git a/IntegrationTests/shellLibrary.Tests.ps1 b/IntegrationTests/shellLibrary.Tests.ps1 index c2f4169..11762bc 100644 --- a/IntegrationTests/shellLibrary.Tests.ps1 +++ b/IntegrationTests/shellLibrary.Tests.ps1 @@ -1,11 +1,5 @@ Import-Module WindowsShell -Force -Describe 'set up environment' { - It 'add the Windows API Code Pack assembly' { - Add-Type -Path "$PSScriptRoot\..\bin\winapicp\Microsoft.WindowsAPICodePack.Shell.dll" - } -} - InModuleScope WindowsShell { Describe Get-ShellLibrary { @@ -147,10 +141,14 @@ foreach ( $values in @( $r = $libraryName | Set-ShellLibraryProperty $propertyName $propertyValue $r | Should beNullOrEmpty } - It 'the type name is correct' { + It 'the property value is correct (Get-ShellLibrary)' { $r = $libraryName | Get-ShellLibrary $r.$propertyName | Should be $propertyValue } + It 'the property value is correct (Get-ShellLibraryProperty)' { + $r = $libraryName | Get-ShellLibraryProperty $propertyName + $r | Should be $propertyValue + } } Context 'cleanup' { It 'remove the library' { diff --git a/IntegrationTests/shortcut.Tests.ps1 b/IntegrationTests/shortcut.Tests.ps1 new file mode 100644 index 0000000..c34a79d --- /dev/null +++ b/IntegrationTests/shortcut.Tests.ps1 @@ -0,0 +1,178 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { + +Describe Get-Shortcut { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + Context 'shortcut exists' { + It 'manually create a real shortcut' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Save() + } + It 'returns exactly one object that looks like a shortcut' { + $r = $shortcutPath | Get-Shortcut + $r | Measure | % Count | Should be 1 + $r | Test-ValidShortcutObject | + Should be $true + } + It 'cleanup' { + Remove-Item $shortcutPath + } + } + Context 'shortcut does not exist' { + It 'returns nothing' { + $r = $shortcutPath | Get-Shortcut + $r | Should beNullOrEmpty + } + } +} + +Describe Add-Shortcut { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + Context 'shortcut doesn''t exist' { + It 'returns exactly one object that looks like a shortcut' { + $r = $shortcutPath | Add-Shortcut + $r | Measure | % Count | Should be 1 + $r | Test-ValidShortcutObject | + Should be $true + } + } + Context 'shortcut exists' { + It 'throws correct exception' { + { $shortcutPath | Add-Shortcut } | + Should throw 'already exists' + } + } + It 'cleanup' { + Remove-Item $shortcutPath + } +} + +Describe Remove-Shortcut { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + Context 'shortcut exists' { + It 'create the shortcut' { + $shortcutPath | Add-Shortcut + } + It 'the shortcut exists' { + $shortcutPath | Test-Shortcut | Should be $true + $shortcutPath | Get-Shortcut | % FullName | Should be $shortcutPath + } + It 'returns nothing' { + $r = $shortcutPath | Remove-Shortcut + $r | Should beNullOrEmpty + } + It 'the shortcut no longer exists' { + $shortcutPath | Test-Shortcut | Should be $false + $shortcutPath | Get-Shortcut | Should beNullOrEmpty + } + } + Context 'shortcut does not exist' { + It 'the shortcut does not exist' { + $shortcutPath | Test-Shortcut | Should be $false + $shortcutPath | Get-Shortcut | Should beNullOrEmpty + } + It 'throws correct exception' { + { $shortcutPath | Remove-Shortcut } | + Should throw 'Shortcut not found' + } + } +} + + +foreach ( $values in @( + @('TargetPath', [string]::Empty,'C:\Windows\System32\WindowsPowershell\v1.0\powershell.exe'), + @('WindowStyle', 1, 7), + @('Hotkey', [string]::Empty,'Alt+Ctrl+f'), + @('IconLocation', ',0', 'notepad.exe,0'), + @('Description', [string]::Empty,'Shortcut script'), + @('WorkingDirectory',[string]::Empty,[System.IO.Path]::GetTempPath()), + @('Arguments', [string]::Empty,'c:\myFile.txt') + ) +) +{ + $propertyName,$initialValue,$propertyValue = $values + Describe "Set- and Get-ShortcutProperty $propertyName $propertyValue" { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + Context 'shortcut exists' { + It 'create the shortcut' { + $shortcutPath | Add-Shortcut + } + It 'the shortcut exists' { + $r = $shortcutPath | Test-Shortcut + $r | Should be $true + } + It "the property $propertyName has initial value $initialValue" { + $r = $shortcutPath | Get-Shortcut + $initialValue -eq $r.$propertyName | Should be $true + } + It 'returns nothing' { + $r = $shortcutPath | Set-ShortcutProperty $propertyName $propertyValue + $r | Should beNullOrEmpty + } + It 'the property value is correct (Get-Shortcut)' { + $r = $shortcutPath | Get-Shortcut + $r.$propertyName | Should be $propertyValue + } + It 'the property value is correct (Get-ShortcutProperty)' { + $r = $shortcutPath | Get-ShortcutProperty $propertyName + $r | Should be $propertyValue + } + } + Context 'cleanup' { + It 'remove the shortcut' { + $shortcutPath | Remove-Shortcut + } + } + Context 'the shortcut does not exist' { + It 'the shortcut does not exist' { + $r = $shortcutPath | Test-Shortcut + $r | Should be $false + } + It 'throws correct exception' { + { $shortcutPath | Set-ShortcutProperty $propertyName $propertyValue } | + Should throw 'Shortcut not found' + } + } + } +} + +Describe "Get-NormalizedShortcutProperty" { + foreach ( $values in @( + @('TargetPath', 'c:/Windows/system32/calc.exe','c:\Windows\system32\calc.exe'), + @('WindowStyle', 0, 0 ), + @('Hotkey', 'Ctrl+Alt+f','Alt+Ctrl+f'), + @('IconLocation', 'c:/Windows/system32/calc.exe,0','c:/Windows/system32/calc.exe,0'), + @('Description', 'description','description'), + @('WorkingDirectory','c:/Windows','c:/Windows'), + @('Arguments', 'arguments','arguments') + ) + ) + { + $propertyName,$original,$normalized= $values + Context "Get-NormalizedShortcutProperty $propertyName $original" { + It "returns $normalized" { + $r = Get-NormalizedShortcutProperty $propertyName $original + $r | Should be $normalized + } + } + } +} +} diff --git a/IntegrationTests/zeroDsc.Tests.ps1 b/IntegrationTests/zeroDsc.shellLibrary.Tests.ps1 similarity index 98% rename from IntegrationTests/zeroDsc.Tests.ps1 rename to IntegrationTests/zeroDsc.shellLibrary.Tests.ps1 index 46559ad..9a6d4c8 100644 --- a/IntegrationTests/zeroDsc.Tests.ps1 +++ b/IntegrationTests/zeroDsc.shellLibrary.Tests.ps1 @@ -6,7 +6,7 @@ if ( -not (Get-Module ZeroDsc -ListAvailable) ) Remove-Module WindowsShell -fo -ea si; Import-Module WindowsShell Import-Module PSDesiredStateConfiguration, ZeroDsc -Describe 'Invoke with ZeroDsc' { +Describe 'Invoke with ZeroDsc (ShellLibrary)' { $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] $libraryName1 = "ZeroDsc1-$guidFrag" $libraryName2 = "ZeroDsc2-$guidFrag" @@ -92,3 +92,4 @@ Describe 'Invoke with ZeroDsc' { } } } + diff --git a/IntegrationTests/zeroDsc.shortcut.Tests.ps1 b/IntegrationTests/zeroDsc.shortcut.Tests.ps1 new file mode 100644 index 0000000..ced1e12 --- /dev/null +++ b/IntegrationTests/zeroDsc.shortcut.Tests.ps1 @@ -0,0 +1,54 @@ +if ( -not (Get-Module ZeroDsc -ListAvailable) ) +{ + return +} + +Remove-Module WindowsShell -fo -ea si; Import-Module WindowsShell +Import-Module PSDesiredStateConfiguration, ZeroDsc + +Describe 'Invoke with ZeroDsc (Shortcut)' { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + $tests = [ordered]@{ + basic = @" + Get-DscResource Shortcut WindowsShell | Import-DscResource + Shortcut MyShortcut @{ Path = "$shortcutPath" } +"@ + full = @" + Get-DscResource Shortcut WindowsShell | Import-DscResource + Shortcut MyShortcut @{ + Path = "$shortcutPath" + Arguments = 'arguments' + Hotkey = 'Ctrl+Alt+f' + StockIconName = 'AudioFiles' + TargetPath = 'C:\Windows\System32\calc.exe' + WindowStyle = 'Maximized' + WorkingDirectory = 'c:\temp' + Description = 'some description' + } +"@ + } + foreach ( $testName in $tests.Keys ) + { + Context $testName { + $document = [scriptblock]::Create($tests.$testName) + $h = @{} + It 'create instructions' { + $h.i = ConfigInstructions SomeName $document + } + foreach ( $step in $h.i ) + { + It $step.Message { + $r = $step | Invoke-ConfigStep + $r.Progress | Should not be 'failed' + } + } + } + } + It 'cleanup' { + Remove-Item $shortcutPath + } +} diff --git a/Riders/shellLibrary.Tests.ps1 b/Riders/shellLibrary.Tests.ps1 index 615dad7..170939b 100644 --- a/Riders/shellLibrary.Tests.ps1 +++ b/Riders/shellLibrary.Tests.ps1 @@ -1,7 +1,7 @@ <# Keeping any reference to an object returned by a call to WindowsAPICodePack.Shell.ShellLibrary -can cause errors on subsequent calls. The solution to this seems to be to relinquish all -references to WindowsAPICodePack.Shell.ShellLibrary then collect garbage between calls. +can cause errors on subsequent calls. The solution to this seems to be to call .Dispose() on +all objects from WindowsAPICodePack.Shell.ShellLibrary between calls. #> Describe 'set up environment' { diff --git a/Riders/shortcut.Tests.ps1 b/Riders/shortcut.Tests.ps1 new file mode 100644 index 0000000..b9ba539 --- /dev/null +++ b/Riders/shortcut.Tests.ps1 @@ -0,0 +1,264 @@ +Import-Module WindowsShell -Force + +InModuleScope WindowsShell { + +Describe 'Shortcut object' { + Context 'create' { + $h = @{} + It 'create with no arguments throws' { + $wshShell = New-Object -ComObject WScript.Shell + { $wshShell.CreateShortcut() } | + Should throw 'Cannot find an overload' + } + It 'create with bogus file path...' { + $name = "$([guid]::NewGuid().Guid).lnk" + $wshShell = New-Object -ComObject WScript.Shell + $h.shortcut = $wshShell.CreateShortcut($name) + $h.shortcut.FullName | + Should match $name.Split('.')[0] + } + It '...then changing to another bogus file path fails' { + $name = "$([guid]::NewGuid().Guid).lnk" + { $h.shortcut.FullName = $name } | + Should throw 'Cannot find an overload' + } + It 'the .FullName property has no setter' { + $h.shortcut | + Get-Member FullName | + % Definition | + Should not match 'set' + } + } +} +Describe 'Shortcut CRUD' { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + Context 'create' { + It 'nothing exists at the path' { + Test-Path $shortcutPath | Should be $false + } + It 'create' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Save() + } + It 'file exists at the path' { + Test-Path $shortcutPath | Should be $true + } + It 'cleanup' { + Remove-Item $shortcutPath + } + } + Context 'create using bad path' { + $badFolderPath = "$tempPath\$guidFrag" + $badShortcutPath = "$badFolderPath\badpath.lnk" + It 'the folder for the shortcut doesn''t exist' { + Test-Path $badFolderPath | Should be $false + } + It 'creating a new shortcut throws' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($badShortcutPath) + { $shortcut.Save() } | + Should throw 'Unable to save shortcut' + } + It 'nothing exists at the path' { + Test-Path $badShortcutPath | Should be $false + } + } + Context 'read and update' { + It 'create' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.TargetPath = 'C:\Windows\System32\WindowsPowershell\v1.0\powershell.exe' + $shortcut.Save() + } + It 'creating another shortcut object reads the existing shortcut...' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.TargetPath | Should match 'powershell' + } + It '...that object can be modified and saved' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.TargetPath = 'C:\Windows\system32\calc.exe' + $shortcut.Save() + } + It '...the modifications persists.' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.TargetPath | Should match 'calc' + } + It 'cleanup' { + Remove-Item $shortcutPath + } + } + Context 'remove' { + It 'create' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Save() + } + It 'exists' { + Test-Path $shortcutPath | Should be $true + } + It 'remove' { + Remove-Item $shortcutPath + } + It 'no longer exists' { + Test-Path $shortcutPath | Should be $false + } + } +} +Describe 'Shortcut properties' { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + foreach ( $values in @( + @('TargetPath','c:\bogus\path.exe'), + @('WindowStyle',1), + @('Hotkey', 'Alt+Ctrl+f'), + @('IconLocation','notepad.exe,0'), + @('Description','Shortcut script'), + @('WorkingDirectory',$tempPath), + @('Arguments', 'c:\myFile.txt') + ) + ) + { + $propertyName,$value = $values + Context ".$propertyName = $value" { + It 'create with property' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.$propertyName = $value + $shortcut.Save() + } + It 'the property persists' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.$propertyName | + Should be $value + } + } + } + It 'cleanup' { + Remove-Item $shortcutPath + } +} + +Describe 'Shortcut Hotkey String' { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + foreach ( $values in @( + @('Ctrl+Alt+f','Alt+Ctrl+f'), + @('Ctrl+Alt+Shift+f','Alt+Ctrl+Shift+f') + ) + ) + { + $original,$final = $values + Context "$original becomes $final" { + It "create with $original" { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Hotkey = $original + $shortcut.Save() + } + It "reading back results in $final"{ + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Hotkey | + Should be $final + } + } + } + Context 'allowed characters' { + foreach ( $char in 'azAZ09'.GetEnumerator() ) + { + $character = $char.ToString() + It "$character" { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Hotkey = $character + $shortcut.Save() + + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Hotkey | + Should be $character + } + } + } + Context 'disallowed letters' { + foreach ( $char in '`-=[]\;'',./*-+'.GetEnumerator() ) + { + $character = $char.ToString() + It "$character" { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + { $shortcut.Hotkey = $character } | + Should throw 'Value does not fall within the expected range.' + } + } + } + Context 'use Shell.Shortcut method to normalize' { + $h = @{} + It 'create shortcut object' { + $wshShell = New-Object -ComObject WScript.Shell + $h.shortcut = $wshShell.CreateShortcut("$([guid]::NewGuid().Guid).lnk") + } + It 'set Hotkey property' { + $h.shortcut.Hotkey = 'Ctrl+Alt+f' + } + It 'the string in the property is normalized' { + $h.shortcut.Hotkey | + Should be 'Alt+Ctrl+f' + } + } + It 'cleanup' { + Remove-Item $shortcutPath + } +} + +Describe 'Shortcut existence' { + $guidFrag = [guid]::NewGuid().Guid.Split('-')[0] + $shortcutFilename = "Shortcut-$guidFrag.lnk" + $tempPath = [System.IO.Path]::GetTempPath() + $shortcutPath = Join-Path $tempPath $shortcutFilename + + Context 'bogus shortcut file' { + It 'create bogus file' { + 'bogus' | Set-Content $shortcutPath + Test-Path $shortcutPath | Should be $true + } + It 'read file as shortcut produces reasonable object' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + 'Arguments','Description','Hotkey','RelativePath', + 'TargetPath','WorkingDirectory' | + % { $shortcut.$_ | Should beNullOrEmpty } + + } + It 'overwrite file' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Description = 'some description' + $shortcut.Save() + } + It 'overwritten file reads back correctly' { + $wshShell = New-Object -comObject WScript.Shell + $shortcut = $wshShell.CreateShortcut($shortcutPath) + $shortcut.Description | + Should be 'some description' + } + } + It 'cleanup' { + Remove-Item $shortcutPath + } +} +} diff --git a/Shortcut.psm1 b/Shortcut.psm1 new file mode 100644 index 0000000..2dda092 --- /dev/null +++ b/Shortcut.psm1 @@ -0,0 +1,86 @@ +enum Ensure +{ + Present + Absent +} + +enum WindowStyle +{ + Normal = 1 + Maximized = 3 + Minimized = 7 +} + +enum StockIconName +{ + DocumentNotAssociated; DocumentAssociated; Application; Folder; FolderOpen; Drive525; Drive35; DriveRemove; + DriveFixed; DriveNetwork; DriveNetworkDisabled; DriveCD; DriveRam; World; Server; Printer; MyNetwork; Find; Help; + Share; Link; SlowFile; Recycler; RecyclerFull; MediaCDAudio; Lock; AutoList; PrinterNet; ServerShare; PrinterFax; + PrinterFaxNet; PrinterFile; Stack; MediaSvcd; StuffedFolder; DriveUnknown; DriveDvd; MediaDvd; MediaDvdRam; + MediaDvdRW; MediaDvdR; MediaDvdRom; MediaCDAudioPlus; MediaCDRW; MediaCDR; MediaCDBurn; MediaBlankCD; MediaCDRom; + AudioFiles; ImageFiles; VideoFiles; MixedFiles; FolderBack; FolderFront; Shield; Warning; Info; Error; Key; + Software; Rename; Delete; MediaAudioDvd; MediaMovieDvd; MediaEnhancedCD; MediaEnhancedDvd; MediaHDDvd; + MediaBluRay; MediaVcd; MediaDvdPlusR; MediaDvdPlusRW; DesktopPC; MobilePC; Users; MediaSmartMedia; + MediaCompactFlash; DeviceCellPhone; DeviceCamera; DeviceVideoCamera; DeviceAudioPlayer; NetworkConnect; Internet; + ZipFile; Settings; DriveHDDVD; DriveBluRay; MediaHDDVDROM; MediaHDDVDR; MediaHDDVDRAM; MediaBluRayROM; + MediaBluRayR; MediaBluRayRE; ClusteredDisk; + + DoNotSet +} + +[DscResource()] +class Shortcut +{ + [DscProperty(Key)] + [string] + $Path + + [DscProperty()] + [Ensure] + $Ensure + + [DscProperty()] + [string] + $TargetPath + + [DscProperty()] + [string] + $Arguments + + [DscProperty()] + [string] + $WorkingDirectory + + [DscProperty()] + [WindowStyle] + $WindowStyle=[WindowStyle]::Normal + + [DscProperty()] + [string] + $Hotkey + + [DscProperty()] + [StockIconName] + $StockIconName = [StockIconName]::DoNotSet + + [DscProperty()] + [string] + $IconFilePath + + [DscProperty()] + [int] + $IconResourceId + + [DscProperty()] + [string] + $Description + + [void] Set() { + $this | Invoke-ProcessShortcut Set + } + [bool] Test() { + return $this | Invoke-ProcessShortcut Test + } + + [Shortcut] Get() { return $this } +} \ No newline at end of file diff --git a/WindowsShell.psd1 b/WindowsShell.psd1 index 7a950ca..d300a68 100644 --- a/WindowsShell.psd1 +++ b/WindowsShell.psd1 @@ -2,7 +2,7 @@ # Script module or binary module file associated with this manifest. RootModule = 'WindowsShell.psm1' -NestedModules = 'ShellLibrary.psm1','ShellLibraryFolder.psm1' +NestedModules = 'ShellLibrary.psm1','ShellLibraryFolder.psm1','Shortcut.psm1' DscResourcesToExport = '*' diff --git a/WindowsShell.psm1 b/WindowsShell.psm1 index b20de5a..0f2beea 100644 --- a/WindowsShell.psm1 +++ b/WindowsShell.psm1 @@ -24,4 +24,5 @@ Add-Type -Path "$moduleRoot\bin\winapicp\Microsoft.WindowsAPICodePack.Shell.dll" Export-ModuleMember -Function @( 'Invoke-ProcessShellLibrary' 'Invoke-ProcessShellLibraryFolder' + 'Invoke-ProcessShortcut' )