diff --git a/src/common/ModuleUtils.psm1 b/src/common/ModuleUtils.psm1 index b5462e30..5e4834a6 100644 --- a/src/common/ModuleUtils.psm1 +++ b/src/common/ModuleUtils.psm1 @@ -47,6 +47,10 @@ function Export-Types { [PSModuleInfo]$Module = (Get-PSCallStack)[0].InvocationInfo.MyCommand.ScriptBlock.Module ) + if (-not $Types -or $Types.Count -eq 0) { + return + } + if (-not $Module) { throw [System.InvalidOperationException]::new('This function must be called from within a module.'); } diff --git a/src/common/Registry.psm1 b/src/common/Registry.psm1 index 3325abe3..aab6d3b1 100644 --- a/src/common/Registry.psm1 +++ b/src/common/Registry.psm1 @@ -213,4 +213,4 @@ function Invoke-OnEachUserHive { } } -Export-ModuleMember -Function New-RegistryKey, Remove-RegistryKey, Test-RegistryKey, Get-RegistryKey, Set-RegistryKey, Invoke-OnEachUserHive; +Export-ModuleMember -Function Invoke-EnsureRegistryPath, Remove-RegistryKey, Test-RegistryKey, Get-RegistryKey, Set-RegistryKey, Invoke-OnEachUserHive; diff --git a/tests/common/Analyser/Analyser.Tests.ps1 b/tests/common/Analyser/Analyser.Tests.ps1 new file mode 100644 index 00000000..a11e1669 --- /dev/null +++ b/tests/common/Analyser/Analyser.Tests.ps1 @@ -0,0 +1,26 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Analyser.psm1" } + +Describe 'Analyser Module Tests' { + Context 'SuppressAnalyserAttribute Functionality' { + It 'Should create SuppressAnalyserAttribute with all properties' { + $Attribute = [Compiler.Analyser.SuppressAnalyserAttribute]::new('TestCheck', 'TestData') + $Attribute.Justification = 'This is a test justification' + + $Attribute | Should -Not -BeNullOrEmpty + $Attribute.CheckType | Should -Be 'TestCheck' + $Attribute.Data | Should -Be 'TestData' + $Attribute.Justification | Should -Be 'This is a test justification' + } + + It 'Should support various data types for Data parameter' { + $StringAttr = [Compiler.Analyser.SuppressAnalyserAttribute]::new('StringCheck', 'StringData') + $StringAttr.Data | Should -Be 'StringData' + + $NumberAttr = [Compiler.Analyser.SuppressAnalyserAttribute]::new('NumberCheck', 42) + $NumberAttr.Data | Should -Be 42 + + $NullAttr = [Compiler.Analyser.SuppressAnalyserAttribute]::new('NullCheck', $null) + $NullAttr.Data | Should -Be $null + } + } +} diff --git a/tests/common/Assert/Assert-Equal.Tests.ps1 b/tests/common/Assert/Assert-Equal.Tests.ps1 index 38b32094..9d30579d 100644 --- a/tests/common/Assert/Assert-Equal.Tests.ps1 +++ b/tests/common/Assert/Assert-Equal.Tests.ps1 @@ -8,7 +8,4 @@ Describe 'Assert-Equal Tests' { It 'Should not throw an error if the object equals the expected value' { Assert-Equal -Object 'foo' -Expected 'foo'; } - - Context 'Error Message Formatting' { - } } diff --git a/tests/common/Cache/Get-CachedLocation.Tests.ps1 b/tests/common/Cache/Get-CachedLocation.Tests.ps1 index 43c2408b..ef1ba340 100644 --- a/tests/common/Cache/Get-CachedLocation.Tests.ps1 +++ b/tests/common/Cache/Get-CachedLocation.Tests.ps1 @@ -2,7 +2,10 @@ BeforeDiscovery { Import-Module -Force -Name $PSScriptRoot/../../../src/common/C Describe 'Get-CachedLocation Tests' { BeforeAll { - $CacheName = "UNIQUE_CACHE_NAME"; + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'UseDeclaredVarsMoreThanAssignments', + $null + )]$CacheName = 'UNIQUE_CACHE_NAME'; InModuleScope Cache { $Script:Folder = "$((Get-PSDrive TestDrive).Root)\PSCache" } diff --git a/tests/common/Connection/Connection.Tests.ps1 b/tests/common/Connection/Connection.Tests.ps1 new file mode 100644 index 00000000..e4de4f43 --- /dev/null +++ b/tests/common/Connection/Connection.Tests.ps1 @@ -0,0 +1,323 @@ +Describe "Connection Module Tests" { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Connection.psm1" -Force + + # Mock external dependencies for cross-platform testing + Mock Connect-ExchangeOnline { } -ModuleName Connection + Mock Disconnect-ExchangeOnline { } -ModuleName Connection + Mock Connect-IPPSSession { } -ModuleName Connection + Mock Connect-MgGraph { } -ModuleName Connection + Mock Disconnect-MgGraph { } -ModuleName Connection + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = 'test-connection-id' + } + } -ModuleName Connection + Mock Get-MgContext { + [PSCustomObject]@{ + Account = 'test@example.com' + Scopes = @('User.Read', 'Mail.Read') + } + } -ModuleName Connection + Mock Get-UserConfirmation { $true } -ModuleName Connection + } + + Context "Module Import" { + It "Should import Connection module successfully" { + Get-Module -Name Connection* | Should -Not -BeNullOrEmpty + } + + It "Should export expected functions" { + $ExportedFunctions = (Get-Module -Name Connection*).ExportedFunctions.Keys + $ExportedFunctions | Should -Contain 'Connect-Service' + } + } + + + + Context "ExchangeOnline Service Tests" { + BeforeEach { + Mock Get-ConnectionInformation { $null } -ModuleName Connection + } + + It "Should handle ExchangeOnline connection" { + Mock Connect-ExchangeOnline { } -ModuleName Connection + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = ange-connection' + } + } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-ExchangeOnline -Times 1 -ModuleName Connection + } + + It "Should handle existing ExchangeOnline connection" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'existing@example.com' + ConnectionId = 'existing-connection' + } + } -ModuleName Connection + Mock Get-UserConfirmation { $true } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' } | Should -Not -Throw + + # Should not call Connect-ExchangeOnline if already connected and user confirms + Should -Invoke Get-UserConfirmation -Times 1 -ModuleName Connection + } + + It "Should disconnect and reconnect if user declines" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'existing@example.com' + ConnectionId = 'existing-connection' + } + } -ModuleName Connection + Mock Get-UserConfirmation { $false } -ModuleName Connection + Mock Disconnect-ExchangeOnline { } -ModuleName Connection + Mock Connect-ExchangeOnline { } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' } | Should -Not -Throw + + Should -Invoke Disconnect-ExchangeOnline -Times 1 -ModuleName Connection + Should -Invoke Connect-ExchangeOnline -Times 1 -ModuleName Connection + } + } + + Context "SecurityComplience Service Tests" { + BeforeEach { + Mock Get-ConnectionInformation { $null } -ModuleName Connection + } + + It "Should handle SecurityComplience connection" { + Mock Connect-IPPSSession { } -ModuleName Connection + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = 'ipps-connection' + } + } -ModuleName Connection + + { Connect-Service -Services 'SecurityComplience' -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-IPPSSession -Times 1 -ModuleName Connection + } + + It "Should handle SecurityComplience disconnection" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = 'ipps-connection' + } + } -ModuleName Connection + Mock Disconnect-ExchangeOnline { } -ModuleName Connection + + # The disconnect for SecurityComplience uses Disconnect-ExchangeOnline + { Connect-Service -Services 'SecurityComplience' -CheckOnly } | Should -Not -Throw + } + } + + Context "Graph Service Tests" { + BeforeEach { + Mock Get-MgContext { $null } -ModuleName Connection + } + + It "Should handle Graph connection without scopes" { + Mock Connect-MgGraph { } -ModuleName Connection + Mock Get-MgContext { + [PSCustomObject]@{ + Account = 'test@example.com' + Scopes = @() + } + } -ModuleName Connection + + { Connect-Service -Services 'Graph' -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection + } + + It "Should handle Graph connection with scopes" { + Mock Connect-MgGraph { } -ModuleName Connection + Mock Get-MgContext { + [PSCustomObject]@{ + Account = 'test@example.com' + Scopes = @('User.Read', 'Mail.Read') + } + } -ModuleName Connection + + $Scopes = @('User.Read', 'Mail.Read') + { Connect-Service -Services 'Graph' -Scopes $Scopes -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection + } + + It "Should handle Graph connection with access token" { + Mock Connect-MgGraph { } -ModuleName Connection + Mock Get-MgContext { + [PSCustomObject]@{ + Account = 'test@example.com' + Scopes = @('User.Read') + } + } -ModuleName Connection + + $SecureToken = ConvertTo-SecureString 'token123' -AsPlainText -Force + { Connect-Service -Services 'Graph' -AccessToken $SecureToken -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection -ParameterFilter { $AccessToken -ne $null } + } + + It "Should handle insufficient scopes in Graph connection" { + Mock Connect-MgGraph { } -ModuleName Connection + Mock Get-MgContext { + [PSCustomObject]@{ + Account = 'test@example.com' + Scopes = @('User.Read') # Missing Mail.Read + } + } -ModuleName Connection + Mock Disconnect-MgGraph { } -ModuleName Connection + + $RequiredScopes = @('User.Read', 'Mail.Read') + { Connect-Service -Services 'Graph' -Scopes $RequiredScopes -DontConfirm } | Should -Not -Throw + + # Should disconnect due to insufficient scopes + Should -Invoke Disconnect-MgGraph -Times 1 -ModuleName Connection + } + + It "Should handle Graph disconnection" { + Mock Disconnect-MgGraph { } -ModuleName Connection + + { Disconnect-MgGraph } | Should -Not -Throw + + Should -Invoke Disconnect-MgGraph -Times 1 -ModuleName Connection + } + } + + Context 'CheckOnly Parameter Tests' { + It 'Should check connection status without connecting' { + Mock Get-ConnectionInformation { $null } -ModuleName Connection + Mock Invoke-FailedExit { throw 'Not connected' } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' -CheckOnly } | Should -Throw 'Not connected' + + # Should not attempt to connect + Should -Invoke Connect-ExchangeOnline -Times 0 -ModuleName Connection + } + + It "Should pass check when already connected" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = 'existing-connection' + } + } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' -CheckOnly } | Should -Not -Throw + } + } + + Context "DontConfirm Parameter Tests" { + It "Should skip confirmation when DontConfirm is used" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'existing@example.com' + ConnectionId = 'existing-connection' + } + } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' -DontConfirm } | Should -Not -Throw + + # Should not call Get-UserConfirmation + Should -Invoke Get-UserConfirmation -Times 0 -ModuleName Connection + } + + It "Should prompt for confirmation by default" { + Mock Get-ConnectionInformation { + [PSCustomObject]@{ + UserPrincipalName = 'existing@example.com' + ConnectionId = 'existing-connection' + } + } -ModuleName Connection + Mock Get-UserConfirmation { $true } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' } | Should -Not -Throw + + Should -Invoke Get-UserConfirmation -Times 1 -ModuleName Connection + } + } + + Context "Error Handling" { + It "Should handle connection failures" { + Mock Get-ConnectionInformation { $null } -ModuleName Connection + Mock Connect-ExchangeOnline { throw "Connection failed" } -ModuleName Connection + Mock Invoke-FailedExit { throw "Failed to connect" } -ModuleName Connection + + { Connect-Service -Services 'ExchangeOnline' -DontConfirm } | Should -Throw + + Should -Invoke Invoke-FailedExit -Times 1 -ModuleName Connection + } + + It "Should handle disconnection failures" { + Mock Disconnect-ExchangeOnline { throw "Disconnect failed" } -ModuleName Connection + Mock Invoke-FailedExit { throw "Failed to disconnect" } -ModuleName Connection + + { Disconnect-ExchangeOnline } | Should -Throw + } + + It "Should handle Graph context retrieval failures" { + Mock Get-MgContext { throw "Graph context error" } -ModuleName Connection + Mock Connect-MgGraph { } -ModuleName Connection + + { Connect-Service -Services 'Graph' -DontConfirm } | Should -Not -Throw + + # Should attempt to connect when context retrieval fails + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection + } + } + + Context "Multiple Services Integration" { + It "Should handle connecting to multiple services" { + Mock Get-ConnectionInformation { $null } -ModuleName Connection + Mock Get-MgContext { $null } -ModuleName Connection + Mock Connect-ExchangeOnline { } -ModuleName Connection + Mock Connect-MgGraph { } -ModuleName Connection + + { Connect-Service -Services @('ExchangeOnline', 'Graph') -DontConfirm } | Should -Not -Throw + + Should -Invoke Connect-ExchangeOnline -Times 1 -ModuleName Connection + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection + } + + It "Should handle mixed connection states" { + # ExchangeOnline already connected, Graph not connected + Mock Get-ConnectionInformation { + param($ConnectionId) + if ($ConnectionId) { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = $ConnectionId + } + } else { + [PSCustomObject]@{ + UserPrincipalName = 'test@example.com' + ConnectionId = 'exchange-connection' + } + } + } -ModuleName Connection + Mock Get-MgContext { $null } -ModuleName Connection + Mock Connect-MgGraph { } -ModuleName Connection + + { Connect-Service -Services @('ExchangeOnline', 'Graph') -DontConfirm } | Should -Not -Throw + + # Should only connect to Graph + Should -Invoke Connect-ExchangeOnline -Times 0 -ModuleName Connection + Should -Invoke Connect-MgGraph -Times 1 -ModuleName Connection + } + } + + +} diff --git a/tests/common/Ensure/Ensure.Tests.ps1 b/tests/common/Ensure/Ensure.Tests.ps1 new file mode 100644 index 00000000..6d2a0957 --- /dev/null +++ b/tests/common/Ensure/Ensure.Tests.ps1 @@ -0,0 +1,356 @@ +Describe "Ensure Module Tests" { + BeforeAll { + Import-Module "$PSScriptRoot/../../../src/common/Ensure.psm1" -Force + } + + Context "Invoke-EnsureAdministrator Tests" { + It "Should handle cross-platform scenarios" { + if ($IsLinux -or $IsMacOS) { + { Invoke-EnsureAdministrator } | Should -Not -Throw + } + } + } + + Context "Invoke-EnsureUser Tests" { + It "Should handle cross-platform scenarios" { + if ($IsLinux -or $IsMacOS) { + { Invoke-EnsureUser } | Should -Not -Throw + } + } + } + + Context 'Invoke-EnsureModule Tests' { + BeforeEach { + Mock Test-NetworkConnection { $true } -ModuleName Ensure + Mock Get-PackageProvider { } -ModuleName Ensure + Mock Install-PackageProvider { } -ModuleName Ensure + Mock Set-PSRepository { } -ModuleName Ensure + Mock Get-Module { $null } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Update-PSResource { } -ModuleName Ensure + Mock Find-PSResource { + [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } + } -ModuleName Ensure + Mock Test-Path { $true } -ModuleName Ensure + } + It 'Should require Modules parameter' { + { Invoke-EnsureModule } | Should -Throw + } + + It 'Should accept string module names' { + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Not -Throw + + Should -Invoke Find-PSResource -Times 1 -ModuleName Ensure + Should -Invoke Install-PSResource -Times 1 -ModuleName Ensure + } + + It 'Should accept hashtable module specifications' { + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '2.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + $ModuleSpec = @{ + Name = 'TestModule' + MinimumVersion = '2.0.0' + } + + { Invoke-EnsureModule -Modules @($ModuleSpec) } | Should -Not -Throw + + Should -Invoke Find-PSResource -Times 1 -ModuleName Ensure + Should -Invoke Install-PSResource -Times 1 -ModuleName Ensure + } + + It 'Should handle already imported modules' { + Mock Get-Module { + [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } + } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Not -Throw + + # Should not attempt to install if already imported + Should -Invoke Install-PSResource -Times 0 -ModuleName Ensure + } + + It 'Should handle local module paths' { + Mock Test-Path { $true } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + $LocalPath = '/path/to/local/module.psm1' + { Invoke-EnsureModule -Modules @($LocalPath) } | Should -Not -Throw + + Should -Invoke Import-Module -Times 1 -ModuleName Ensure + } + + It 'Should handle GitHub repository modules' { + Mock Install-ModuleFromGitHub { '/temp/path/to/module' } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + $GitHubModule = 'owner/repo@main' + { Invoke-EnsureModule -Modules @($GitHubModule) } | Should -Not -Throw + + Should -Invoke Install-ModuleFromGitHub -Times 1 -ModuleName Ensure + } + + It 'Should handle module updates' { + Mock Get-Module { + [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } + } -ModuleName Ensure + Mock Update-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + $ModuleSpec = @{ + Name = 'TestModule' + MinimumVersion = '2.0.0' + } + + { Invoke-EnsureModule -Modules @($ModuleSpec) } | Should -Not -Throw + + Should -Invoke Update-PSResource -Times 1 -ModuleName Ensure + } + + It 'Should handle network connectivity issues' { + Mock Test-NetworkConnection { $false } -ModuleName Ensure + Mock Invoke-Warn { } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Not -Throw + + Should -Invoke Invoke-Warn -Times 1 -ModuleName Ensure + # Should not attempt installation without network + Should -Invoke Install-PSResource -Times 0 -ModuleName Ensure + } + + It 'Should handle NuGet package provider installation' { + Mock Get-PackageProvider { throw 'NuGet not found' } -ModuleName Ensure + Mock Install-PackageProvider { } -ModuleName Ensure + Mock Set-PSRepository { } -ModuleName Ensure + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Not -Throw + + Should -Invoke Install-PackageProvider -Times 1 -ModuleName Ensure + Should -Invoke Set-PSRepository -Times 1 -ModuleName Ensure + } + + It 'Should validate module specifications' { + $InvalidSpec = [PSCustomObject]@{ InvalidProperty = 'Value' } + + { Invoke-EnsureModule -Modules @($InvalidSpec) } | Should -Throw + } + + It 'Should handle DontRemove flag' { + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + $ModuleSpec = @{ + Name = 'TestModule' + DontRemove = $true + } + + { Invoke-EnsureModule -Modules @($ModuleSpec) } | Should -Not -Throw + } + } + + Context 'Invoke-EnsureNetwork Tests' { + BeforeEach { + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $true } -ModuleName Ensure + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'Internet'; IPv6Connectivity = 'Internet' } + } -ModuleName Ensure + } + BeforeEach { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + } + + It 'Should accept Name parameter' { + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $true } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + + { Invoke-EnsureNetwork -Name 'TestNetwork' } | Should -Not -Throw + } + + It 'Should accept optional Password parameter' { + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $true } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + + $SecurePassword = ConvertTo-SecureString 'password123' -AsPlainText -Force + { Invoke-EnsureNetwork -Name 'TestNetwork' -Password $SecurePassword } | Should -Not -Throw + } + + It 'Should detect existing network connection' { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'Internet'; IPv6Connectivity = 'Internet' } + } -ModuleName Ensure + Mock Invoke-Debug { } -ModuleName Ensure + + $Result = Invoke-EnsureNetwork -Name 'TestNetwork' + + $Result | Should -Be $false + Should -Invoke Invoke-Debug -Times 1 -ModuleName Ensure + } + + It 'Should setup network when no connection exists' { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $true } -ModuleName Ensure + Mock Invoke-Info { } -ModuleName Ensure + + $Result = Invoke-EnsureNetwork -Name 'TestNetwork' + + $Result | Should -Be $true + Should -Invoke netsh -Times 3 -ModuleName Ensure # add profile, show profiles, connect + Should -Invoke Test-Connection -Times 1 -ModuleName Ensure + } + + It 'Should handle WhatIf parameter' { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + Mock Invoke-Info { } -ModuleName Ensure + + $Result = Invoke-EnsureNetwork -Name 'TestNetwork' -WhatIf + + $Result | Should -Be $true + Should -Invoke netsh -Times 0 -ModuleName Ensure + } + + It "Should handle network setup timeout" { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $false } -ModuleName Ensure # Simulate connection failure + Mock Invoke-Error { } -ModuleName Ensure + Mock Invoke-FailedExit { throw "Network setup failed" } -ModuleName Ensure + + { Invoke-EnsureNetwork -Name 'TestNetwork' } | Should -Throw "Network setup failed" + + Should -Invoke Invoke-FailedExit -Times 1 -ModuleName Ensure + } + + It "Should generate correct WiFi XML profile" { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + Mock Out-File { } -ModuleName Ensure + Mock netsh { } -ModuleName Ensure + Mock Test-Connection { $true } -ModuleName Ensure + + { Invoke-EnsureNetwork -Name 'TestSSID' } | Should -Not -Throw + + # Should create XML profile and execute netsh commands + Should -Invoke Out-File -Times 1 -ModuleName Ensure + Should -Invoke netsh -Times 3 -ModuleName Ensure + } + } + + Context "Error Handling" { + It "Should handle module installation failures" { + Mock Test-NetworkConnection { $true } -ModuleName Ensure + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { throw "Installation failed" } -ModuleName Ensure + Mock Invoke-FailedExit { throw "Module installation failed" } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Throw "Module installation failed" + + Should -Invoke Invoke-FailedExit -Times 1 -ModuleName Ensure + } + + It "Should handle module import failures" { + Mock Test-NetworkConnection { $true } -ModuleName Ensure + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { throw "Import failed" } -ModuleName Ensure + Mock Invoke-FailedExit { throw "Module import failed" } -ModuleName Ensure + + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Throw "Module import failed" + + Should -Invoke Invoke-FailedExit -Times 1 -ModuleName Ensure + } + + It "Should handle network setup failures" { + Mock Get-NetConnectionProfile { + [PSCustomObject]@{ IPv4Connectivity = 'NoTraffic'; IPv6Connectivity = 'NoTraffic' } + } -ModuleName Ensure + Mock Invoke-WithinEphemeral { + param($ScriptBlock) + & $ScriptBlock + } -ModuleName Ensure + Mock netsh { throw "Network command failed" } -ModuleName Ensure + + { Invoke-EnsureNetwork -Name 'TestNetwork' } | Should -Throw "Network command failed" + } + } + + Context "Parameter Validation" { + It "Should validate Modules parameter is not null or empty" { + { Invoke-EnsureModule -Modules @() } | Should -Throw + { Invoke-EnsureModule -Modules $null } | Should -Throw + } + + It "Should validate Network Name parameter" { + { Invoke-EnsureNetwork -Name '' } | Should -Throw + { Invoke-EnsureNetwork -Name $null } | Should -Throw + } + + It "Should accept valid module specification formats" { + Mock Test-NetworkConnection { $true } -ModuleName Ensure + Mock Get-Module { $null } -ModuleName Ensure + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = '1.0.0' } } -ModuleName Ensure + Mock Install-PSResource { } -ModuleName Ensure + Mock Import-Module { } -ModuleName Ensure + + # String format + { Invoke-EnsureModule -Modules @('TestModule') } | Should -Not -Throw + + # Hashtable format + $HashSpec = @{ Name = 'TestModule'; MinimumVersion = '1.0.0' } + { Invoke-EnsureModule -Modules @($HashSpec) } | Should -Not -Throw + + # GitHub format + { Invoke-EnsureModule -Modules @('owner/repo@branch') } | Should -Not -Throw + } + } +} diff --git a/tests/common/Environment/Invoke-Setup.Tests.ps1 b/tests/common/Environment/Invoke-Setup.Tests.ps1 new file mode 100644 index 00000000..fffeaf03 --- /dev/null +++ b/tests/common/Environment/Invoke-Setup.Tests.ps1 @@ -0,0 +1,137 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Environment.psm1" } + +Describe 'Invoke-Setup Tests' { + BeforeAll { + # Save original values + $Script:OriginalErrorActionPreference = $Global:ErrorActionPreference + $Script:OriginalPSDefaultParameterValues = $Global:PSDefaultParameterValues.Clone() + } + + AfterAll { + # Restore original values + $Global:ErrorActionPreference = $Script:OriginalErrorActionPreference + $Global:PSDefaultParameterValues = $Script:OriginalPSDefaultParameterValues + } + + AfterEach { + # Clean up after each test + $Global:PSDefaultParameterValues.Remove('*:ErrorAction') + $Global:PSDefaultParameterValues.Remove('*:WarningAction') + $Global:PSDefaultParameterValues.Remove('*:InformationAction') + $Global:PSDefaultParameterValues.Remove('*:Verbose') + $Global:PSDefaultParameterValues.Remove('*:Debug') + $Global:PSDefaultParameterValues.Remove('*-Module:Verbose') + } + + Context 'Parameter Value Configuration' { + It 'Should set global PSDefaultParameterValues for ErrorAction' { + InModuleScope Environment { + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:ErrorAction'] | Should -Not -BeNullOrEmpty + } + + It 'Should set global PSDefaultParameterValues for WarningAction' { + InModuleScope Environment { + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:WarningAction'] | Should -Not -BeNullOrEmpty + } + + It 'Should set global PSDefaultParameterValues for InformationAction' { + InModuleScope Environment { + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:InformationAction'] | Should -Not -BeNullOrEmpty + } + + It 'Should configure Verbose parameter based on preferences' { + InModuleScope Environment { + $VerbosePreference = 'Continue' + $DebugPreference = 'Continue' + Invoke-Setup + + $Global:PSDefaultParameterValues['*:Verbose'] | Should -Be $true + } + } + + It 'Should configure Debug parameter based on preferences' { + InModuleScope Environment { + $DebugPreference = 'Continue' + Invoke-Setup + + $Global:PSDefaultParameterValues['*:Debug'] | Should -Be $true + } + } + + It 'Should set module-specific verbose preference based on debug preference' { + InModuleScope Environment { + $DebugPreference = 'Continue' + Invoke-Setup + + $Global:PSDefaultParameterValues['*-Module:Verbose'] | Should -Be $true + } + } + } + + Context 'ErrorActionPreference Configuration' { + It 'Should set global ErrorActionPreference to Stop' { + InModuleScope Environment { + Invoke-Setup + } + + $Global:ErrorActionPreference | Should -Be 'Stop' + } + + It 'Should preserve current preference values in PSDefaultParameterValues' { + $TestErrorActionPreference = 'Continue' + $TestWarningPreference = 'Continue' + $TestInformationPreference = 'Continue' + + $Global:ErrorActionPreference = $TestErrorActionPreference + $Global:WarningPreference = $TestWarningPreference + $Global:InformationPreference = $TestInformationPreference + + InModuleScope Environment { + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:ErrorAction'] | Should -Be $TestErrorActionPreference + $Global:PSDefaultParameterValues['*:WarningAction'] | Should -Be $TestWarningPreference + $Global:PSDefaultParameterValues['*:InformationAction'] | Should -Be $TestInformationPreference + } + } + + Context 'Preference Logic' { + It 'Should set Verbose to false when VerbosePreference is SilentlyContinue' { + InModuleScope Environment { + $VerbosePreference = 'SilentlyContinue' + $DebugPreference = 'SilentlyContinue' + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:Verbose'] | Should -Be $false + } + + It 'Should set Debug to false when DebugPreference is SilentlyContinue' { + InModuleScope Environment { + $DebugPreference = 'SilentlyContinue' + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:Debug'] | Should -Be $false + } + + It 'Should set Debug to false when DebugPreference is Ignore' { + InModuleScope Environment { + $DebugPreference = 'Ignore' + Invoke-Setup + } + + $Global:PSDefaultParameterValues['*:Debug'] | Should -Be $false + } + } +} \ No newline at end of file diff --git a/tests/common/Environment/Invoke-Teardown.Tests.ps1 b/tests/common/Environment/Invoke-Teardown.Tests.ps1 new file mode 100644 index 00000000..e14ccb2d --- /dev/null +++ b/tests/common/Environment/Invoke-Teardown.Tests.ps1 @@ -0,0 +1,101 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Environment.psm1" } + +Describe 'Invoke-Teardown Tests' { + BeforeEach { + $Global:PSDefaultParameterValues['*:ErrorAction'] = 'Stop' + $Global:PSDefaultParameterValues['*:WarningAction'] = 'Continue' + $Global:PSDefaultParameterValues['*:InformationAction'] = 'Continue' + $Global:PSDefaultParameterValues['*:Verbose'] = $true + $Global:PSDefaultParameterValues['*:Debug'] = $true + $Global:PSDefaultParameterValues['*-Module:Verbose'] = $true + } + + Context 'Parameter Value Cleanup' { + It 'Should remove all expected parameters from PSDefaultParameterValues' { + $ExpectedKeys = @( + '*:ErrorAction', + '*:WarningAction', + '*:InformationAction', + '*:Verbose', + '*:Debug', + '*-Module:Verbose' + ) + + # Verify all keys exist before teardown + foreach ($Key in $ExpectedKeys) { + $Global:PSDefaultParameterValues.ContainsKey($Key) | Should -Be $true + } + + InModuleScope Environment { + Invoke-Teardown + } + + # Verify all keys are removed after teardown + foreach ($Key in $ExpectedKeys) { + $Global:PSDefaultParameterValues.ContainsKey($Key) | Should -Be $false + } + } + } + + Context 'Multiple Teardown Calls' { + It 'Should handle being called multiple times without error' { + InModuleScope Environment { + { Invoke-Teardown } | Should -Not -Throw + { Invoke-Teardown } | Should -Not -Throw + { Invoke-Teardown } | Should -Not -Throw + } + } + + It 'Should not throw when keys do not exist' { + $Global:PSDefaultParameterValues.Remove('*:ErrorAction') + $Global:PSDefaultParameterValues.Remove('*:Verbose') + + InModuleScope Environment { + { Invoke-Teardown } | Should -Not -Throw + } + } + } + + Context 'Integration with Invoke-Setup' { + It 'Should clean up all values set by Invoke-Setup' { + InModuleScope Environment { + Invoke-Setup + } + + $Global:PSDefaultParameterValues.ContainsKey('*:ErrorAction') | Should -Be $true + $Global:PSDefaultParameterValues.ContainsKey('*:WarningAction') | Should -Be $true + $Global:PSDefaultParameterValues.ContainsKey('*:InformationAction') | Should -Be $true + + InModuleScope Environment { + Invoke-Teardown + } + + $Global:PSDefaultParameterValues.ContainsKey('*:ErrorAction') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*:WarningAction') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*:InformationAction') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*:Verbose') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*:Debug') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*-Module:Verbose') | Should -Be $false + } + } + + Context 'Selective Removal' { + It 'Should only remove specific keys and leave others intact' { + $Global:PSDefaultParameterValues['Get-Process:Name'] = 'powershell' + $Global:PSDefaultParameterValues['Test-Custom:Param'] = 'value' + + InModuleScope Environment { + Invoke-Teardown + } + + $Global:PSDefaultParameterValues.ContainsKey('*:ErrorAction') | Should -Be $false + $Global:PSDefaultParameterValues.ContainsKey('*:WarningAction') | Should -Be $false + + $Global:PSDefaultParameterValues.ContainsKey('Get-Process:Name') | Should -Be $true + $Global:PSDefaultParameterValues.ContainsKey('Test-Custom:Param') | Should -Be $true + + $Global:PSDefaultParameterValues.Remove('Get-Process:Name') + $Global:PSDefaultParameterValues.Remove('Test-Custom:Param') + } + } +} \ No newline at end of file diff --git a/tests/common/Environment/Test-IsNableRunner.Tests.ps1 b/tests/common/Environment/Test-IsNableRunner.Tests.ps1 new file mode 100644 index 00000000..727957df --- /dev/null +++ b/tests/common/Environment/Test-IsNableRunner.Tests.ps1 @@ -0,0 +1,11 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Environment.psm1" } + +Describe 'Test-IsNableRunner Tests' { + Context 'Basic Functionality' { + It 'Should return False when not running in N-able context' { + $Result = Test-IsNableRunner + + $Result | Should -Be $false + } + } +} \ No newline at end of file diff --git a/tests/common/Logging/Invoke-Level.Tests.ps1 b/tests/common/Logging/Invoke-Level.Tests.ps1 index cf8170b3..f2ed3178 100644 --- a/tests/common/Logging/Invoke-Level.Tests.ps1 +++ b/tests/common/Logging/Invoke-Level.Tests.ps1 @@ -1,9 +1,11 @@ +# TODO - Nested Module Usage BeforeDiscovery { Import-Module -Name "$PSScriptRoot/../../../src/common/Logging.psm1" Import-Module -Name "$PSScriptRoot/Helpers.psm1" } BeforeAll { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', $null)] $Params = @{ Message = 'Test message' }; diff --git a/tests/common/ModuleUtils/Add-ModuleCallback.Tests.ps1 b/tests/common/ModuleUtils/Add-ModuleCallback.Tests.ps1 new file mode 100644 index 00000000..a713038b --- /dev/null +++ b/tests/common/ModuleUtils/Add-ModuleCallback.Tests.ps1 @@ -0,0 +1,57 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/ModuleUtils.psm1" } +Describe 'Add-ModuleCallback Tests' { + BeforeAll { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', $null)] + $ModulePath = "$PSScriptRoot/../../../src/common/ModuleUtils.psm1" + } + + Context 'Basic Functionality' { + It 'Should execute callback when module is removed' { + $TestFile = 'TestDrive:\callback_test.txt' + + $TempModuleFolder = ('TestDrive:\TempExecutionModule_' + [Guid]::NewGuid().ToString()) + New-Item -Path $TempModuleFolder -ItemType Directory -Force | Out-Null + $TempModulePath = Join-Path $TempModuleFolder 'TempExecutionModule.psm1' + $moduleContent = @" +Import-Module "${ModulePath}" -Force + +`$TestCallback = { 'callback executed' | Out-File -FilePath "${TestFile}" } +Add-ModuleCallback -ScriptBlock `$TestCallback.GetNewClosure() +"@ + Set-Content -Path $TempModulePath -Value $moduleContent -Encoding UTF8 + Import-Module -Name $TempModulePath -PassThru | Remove-Module | Out-Null + + Get-Content -Path $TestFile | Should -Be 'callback executed' + } + + It 'Should handle multiple callbacks' { + $TestFile1 = 'TestDrive:\callback_test1.txt' + $TestFile2 = 'TestDrive:\callback_test2.txt' + + $TempModuleFolder = ('TestDrive:\MultiCallbackModule_' + [Guid]::NewGuid().ToString()) + New-Item -Path $TempModuleFolder -ItemType Directory -Force | Out-Null + $TempModulePath = Join-Path $TempModuleFolder 'MultiCallbackModule.psm1' + $moduleContent = @" +Import-Module "${ModulePath}" -Force + +`$TestCallback1 = { 'callback 1 executed' | Out-File -FilePath "${TestFile1}" } +`$TestCallback2 = { 'callback 2 executed' | Out-File -FilePath "${TestFile2}" } + +Add-ModuleCallback -ScriptBlock `$TestCallback1.GetNewClosure() +Add-ModuleCallback -ScriptBlock `$TestCallback2.GetNewClosure() +"@ + Set-Content -Path $TempModulePath -Value $moduleContent -Encoding UTF8 + Import-Module -Name $TempModulePath -PassThru | Remove-Module | Out-Null + + Get-Content -Path $TestFile1 | Should -Be 'callback 1 executed' + Get-Content -Path $TestFile2 | Should -Be 'callback 2 executed' + } + } + + Context 'Error Handling' { + It 'Should throw when not called from within a module' { + $TestCallback = { Write-Output 'test' } + { Add-ModuleCallback -ScriptBlock $TestCallback } | Should -Throw + } + } +} diff --git a/tests/common/ModuleUtils/Export-Types.Tests.ps1 b/tests/common/ModuleUtils/Export-Types.Tests.ps1 new file mode 100644 index 00000000..44e2ec0b --- /dev/null +++ b/tests/common/ModuleUtils/Export-Types.Tests.ps1 @@ -0,0 +1,58 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/ModuleUtils.psm1" } + +Describe 'Export-Types Tests' { + Context 'Basic Functionality' { + BeforeEach { + $ModulePath = "$PSScriptRoot/../../../src/common/ModuleUtils.psm1" + $TypeAcceleratorsClass = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') + $namespace = 'TempExportTypes_' + ([Guid]::NewGuid().ToString().Replace('-', '')) + $TestModulePath = "TestDrive:\$namespace.psm1" + } + + It 'Should export types to TypeAccelerators' { + $CSharpClass = "namespace $namespace { public class TempType1 {} public class TempType2 {} }" + Set-Content -Path $TestModulePath -Value @" +Import-Module "${ModulePath}" -Force +Add-Type -TypeDefinition "$CSharpClass" -Language CSharp +Export-Types -Types @([${namespace}.TempType1], [${namespace}.TempType2]) +"@ + Import-Module $TestModulePath + + $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get + $ExistingTypeAccelerators.Keys -contains "$namespace.TempType1" | Should -Be $true + $ExistingTypeAccelerators.Keys -contains "$namespace.TempType2" | Should -Be $true + } + } + + Context 'Clobber Parameter' { + It 'Should allow clobbering with Clobber switch' { + $csharp = "namespace $namespace { public class TempDateTime {} }" + Set-Content -Path $TestModulePath -Value @" +Import-Module "${ModulePath}" -Force + +Add-Type -TypeDefinition "$csharp" -Language CSharp + +`$TestTypes = @([${namespace}.TempDateTime]) +Export-Types -Types `$TestTypes +Export-Types -Types `$TestTypes -Clobber +"@ + { Import-Module -Name $TestModulePath } | Should -Not -Throw + } + } + + Context 'Module Callback Integration' { + It 'Should register removal callback' { + $csharp = "namespace $namespace { public class TempGuid {} }" + Set-Content -Path $TestModulePath -Value @" +Import-Module "${ModulePath}" -Force + +Add-Type -TypeDefinition "$csharp" -Language CSharp +Export-Types -Types @([${namespace}.TempGuid]) +"@ + Import-Module -Name $TestModulePath + + $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get + $ExistingTypeAccelerators.Keys -contains "$namespace.TempGuid" | Should -Be $false + } + } +} diff --git a/tests/common/PSStyle/Get-ConsoleColour.Tests.ps1 b/tests/common/PSStyle/Get-ConsoleColour.Tests.ps1 new file mode 100644 index 00000000..a94bb097 --- /dev/null +++ b/tests/common/PSStyle/Get-ConsoleColour.Tests.ps1 @@ -0,0 +1,21 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/PSStyle.psm1" } + +Describe 'Get-ConsoleColour Tests' { + Context 'Basic Functionality' { + It 'Should return ANSI escape sequence for valid colors' { + $Colors = @([System.ConsoleColor]::Red, [System.ConsoleColor]::Blue, [System.ConsoleColor]::Green) + + foreach ($Color in $Colors) { + $Result = Get-ConsoleColour -Colour $Color + $Result | Should -Not -BeNullOrEmpty + $Result | Should -BeLike '*[0-9]m*' + } + } + } + + Context 'Error Handling' { + It 'Should handle invalid color values gracefully' { + { Get-ConsoleColour -Colour 'InvalidColor' } | Should -Throw + } + } +} \ No newline at end of file diff --git a/tests/common/PSStyle/PSStyle-Classes.Tests.ps1 b/tests/common/PSStyle/PSStyle-Classes.Tests.ps1 new file mode 100644 index 00000000..df0c5238 --- /dev/null +++ b/tests/common/PSStyle/PSStyle-Classes.Tests.ps1 @@ -0,0 +1,11 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/PSStyle.psm1" } + +Describe 'PSStyle Classes Tests' { + Context 'Class Availability' { + It 'Should load PSStyle classes successfully' { + [ForegroundColor] | Should -Not -BeNullOrEmpty + [BackgroundColor] | Should -Not -BeNullOrEmpty + [FormatData] | Should -Not -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/tests/common/PackageManager/PackageManager.Tests.ps1 b/tests/common/PackageManager/PackageManager.Tests.ps1 new file mode 100644 index 00000000..15d38bf4 --- /dev/null +++ b/tests/common/PackageManager/PackageManager.Tests.ps1 @@ -0,0 +1,332 @@ +Describe "PackageManager Module Tests" { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/PackageManager.psm1" -Force + + # Mock external dependencies for cross-platform testing + Mock Test-NetworkConnection { $true } -ModuleName PackageManager + Mock Get-Command { + [PSCustomObject]@{ Name = 'choco'; Source = 'C:\ProgramData\chocolatey\bin\choco.exe' } + } -ModuleName PackageManager -ParameterFilter { $Name -eq 'choco' } + Mock Test-Path { $true } -ModuleName PackageManager + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + Mock Invoke-Expression { } -ModuleName PackageManager + Mock Import-Module { } -ModuleName PackageManager + } + + Context "Module Import" { + It "Should import PackageManager module successfully" { + Get-Module -Name PackageManager* | Should -Not -BeNullOrEmpty + } + + It "Should export expected functions" { + $ExportedFunctions = (Get-Module -Name PackageManager*).ExportedFunctions.Keys + $ExportedFunctions | Should -Contain 'Test-ManagedPackage' + $ExportedFunctions | Should -Contain 'Install-ManagedPackage' + $ExportedFunctions | Should -Contain 'Uninstall-ManagedPackage' + $ExportedFunctions | Should -Contain 'Update-ManagedPackage' + } + } + + Context "Test-ManagedPackage Tests" { + It "Should require PackageName parameter" { + { Test-ManagedPackage } | Should -Throw + } + + It "Should accept PackageName parameter" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + + { Test-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should return boolean value" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + + $Result = Test-ManagedPackage -PackageName 'git' + $Result | Should -BeOfType [Boolean] + } + + It "Should handle package not found" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 1 } + } -ModuleName PackageManager + + $Result = Test-ManagedPackage -PackageName 'nonexistent-package' + $Result | Should -Be $false + } + + It "Should handle network connection check" { + Mock Test-NetworkConnection { $false } -ModuleName PackageManager + + # Should still work without network for already installed packages + { Test-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + } + + Context "Install-ManagedPackage Tests" { + It "Should require PackageName parameter" { + { Install-ManagedPackage } | Should -Throw + } + + It "Should accept PackageName parameter" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should support ShouldProcess" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' -WhatIf } | Should -Not -Throw + } + + It "Should accept Sha256 parameter" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 0 } + } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' -Sha256 'abc123' } | Should -Not -Throw + } + + It "Should accept NoFail parameter" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 1 } + } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' -NoFail } | Should -Not -Throw + } + + It "Should handle installation failure without NoFail" { + Mock Start-Process { + [PSCustomObject]@{ ExitCode = 1 } + } -ModuleName PackageManager + Mock Invoke-FailedExit { throw "Installation failed" } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' } | Should -Throw + } + + It "Should handle network connectivity check" { + Mock Test-NetworkConnection { $false } -ModuleName PackageManager + Mock Invoke-FailedExit { throw "No network" } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' } | Should -Throw + } + } + + Context "Uninstall-ManagedPackage Tests" { + It "Should require PackageName parameter" { + { Uninstall-ManagedPackage } | Should -Throw + } + + It "Should accept PackageName parameter" { + Mock Invoke-Expression { } -ModuleName PackageManager + + { Uninstall-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should support ShouldProcess" { + Mock Invoke-Expression { } -ModuleName PackageManager + + { Uninstall-ManagedPackage -PackageName 'git' -WhatIf } | Should -Not -Throw + } + + It "Should accept NoFail parameter" { + Mock Invoke-Expression { throw "Uninstall failed" } -ModuleName PackageManager + Mock Invoke-Error { } -ModuleName PackageManager + + { Uninstall-ManagedPackage -PackageName 'git' -NoFail } | Should -Not -Throw + } + + It "Should handle uninstallation failure without NoFail" { + Mock Invoke-Expression { + $global:LASTEXITCODE = 1 + throw "Uninstall failed" + } -ModuleName PackageManager + Mock Invoke-Error { } -ModuleName PackageManager + Mock Invoke-FailedExit { throw "Uninstall failed" } -ModuleName PackageManager + + { Uninstall-ManagedPackage -PackageName 'git' } | Should -Throw + } + } + + Context "Update-ManagedPackage Tests" { + It "Should require PackageName parameter" { + { Update-ManagedPackage } | Should -Throw + } + + It "Should accept PackageName parameter" { + Mock Invoke-Expression { } -ModuleName PackageManager + + { Update-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should support ShouldProcess" { + Mock Invoke-Expression { } -ModuleName PackageManager + + { Update-ManagedPackage -PackageName 'git' -WhatIf } | Should -Not -Throw + } + + It "Should handle update failure" { + Mock Invoke-Expression { + $global:LASTEXITCODE = 1 + throw "Update failed" + } -ModuleName PackageManager + Mock Invoke-Error { } -ModuleName PackageManager + + { Update-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + } + + Context "Package Manager Detection" { + It "Should detect Chocolatey on Windows" { + if ($IsWindows) { + # The module should detect Chocolatey as the package manager + $true | Should -Be $true # This is tested implicitly by other tests + } + } + + It "Should handle unsupported platforms" { + if ($IsLinux -or $IsMacOS) { + # On non-Windows platforms, should handle gracefully or indicate unsupported + # This depends on the module's implementation + $true | Should -Be $true + } + } + } + + Context "Chocolatey Integration" { + BeforeEach { + Mock Get-Command { + [PSCustomObject]@{ Name = 'choco'; Source = 'C:\ProgramData\chocolatey\bin\choco.exe' } + } -ModuleName PackageManager -ParameterFilter { $Name -eq 'choco' } + } + + It "Should detect existing Chocolatey installation" { + # When choco command is available, should not reinstall + Mock Get-Command { + [PSCustomObject]@{ Name = 'choco' } + } -ModuleName PackageManager -ParameterFilter { $Name -eq 'choco' } + + { Install-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should handle missing Chocolatey installation" { + Mock Get-Command { $null } -ModuleName PackageManager -ParameterFilter { $Name -eq 'choco' } + Mock Test-Path { $false } -ModuleName PackageManager + Mock Invoke-Expression { } -ModuleName PackageManager # Mock Chocolatey installation + + { Install-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should handle Chocolatey directory repair" { + Mock Get-Command { $null } -ModuleName PackageManager -ParameterFilter { $Name -eq 'choco' } + Mock Test-Path { + param($Path) + if ($Path -like '*chocolatey') { return $true } + if ($Path -like '*choco.exe') { return $true } + return $false + } -ModuleName PackageManager + Mock Import-Module { } -ModuleName PackageManager + Mock Invoke-Expression { } -ModuleName PackageManager # Mock refreshenv + + { Install-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + } + + Context "Error Handling" { + It "Should handle network connectivity issues" { + Mock Test-NetworkConnection { $false } -ModuleName PackageManager + Mock Invoke-Error { } -ModuleName PackageManager + Mock Invoke-FailedExit { throw "No network" } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' } | Should -Throw + } + + It "Should handle package manager not found" { + Mock Get-Command { $null } -ModuleName PackageManager + Mock Test-Path { $false } -ModuleName PackageManager + Mock Invoke-Error { } -ModuleName PackageManager + + # Should handle gracefully or throw appropriate error + { Test-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + + It "Should handle package installation timeout" { + Mock Start-Process { + Start-Sleep -Seconds 2 + [PSCustomObject]@{ ExitCode = 1 } + } -ModuleName PackageManager + Mock Invoke-FailedExit { throw "Installation timed out" } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' } | Should -Throw + } + } + + Context "Parameter Validation" { + It "Should validate PackageName is not null or empty" { + { Test-ManagedPackage -PackageName '' } | Should -Throw + { Test-ManagedPackage -PackageName $null } | Should -Throw + } + + It "Should accept valid package names" { + Mock Start-Process { [PSCustomObject]@{ ExitCode = 0 } } -ModuleName PackageManager + + { Test-ManagedPackage -PackageName 'git' } | Should -Not -Throw + { Test-ManagedPackage -PackageName 'nodejs' } | Should -Not -Throw + { Test-ManagedPackage -PackageName 'python3' } | Should -Not -Throw + } + + It "Should handle special characters in package names" { + Mock Start-Process { [PSCustomObject]@{ ExitCode = 0 } } -ModuleName PackageManager + + { Test-ManagedPackage -PackageName 'package-with-dashes' } | Should -Not -Throw + { Test-ManagedPackage -PackageName 'package.with.dots' } | Should -Not -Throw + } + } + + Context "Logging and Verbose Output" { + It "Should provide verbose output during operations" { + Mock Start-Process { [PSCustomObject]@{ ExitCode = 0 } } -ModuleName PackageManager + Mock Invoke-Verbose { } -ModuleName PackageManager + Mock Invoke-Info { } -ModuleName PackageManager + + { Install-ManagedPackage -PackageName 'git' -Verbose } | Should -Not -Throw + + # Should call logging functions + } + + It "Should provide debug information" { + Mock Start-Process { [PSCustomObject]@{ ExitCode = 0 } } -ModuleName PackageManager + Mock Invoke-Debug { } -ModuleName PackageManager + + { Test-ManagedPackage -PackageName 'git' -Debug } | Should -Not -Throw + } + } + + Context "Cross-Platform Behavior" { + It "Should handle Windows-specific operations on Windows" { + if ($IsWindows) { + # Should work with Chocolatey + { Test-ManagedPackage -PackageName 'git' } | Should -Not -Throw + } + } + + It "Should handle non-Windows platforms appropriately" { + if ($IsLinux -or $IsMacOS) { + # Should either work with alternative package managers or indicate unsupported + # This test validates that it doesn't crash on non-Windows + { $null } | Should -Not -Throw + } + } + } +} \ No newline at end of file diff --git a/tests/common/Registry/Get-RegistryKey.Tests.ps1 b/tests/common/Registry/Get-RegistryKey.Tests.ps1 new file mode 100644 index 00000000..99deb615 --- /dev/null +++ b/tests/common/Registry/Get-RegistryKey.Tests.ps1 @@ -0,0 +1,199 @@ +Describe "Get-RegistryKey Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock Test-RegistryKey and Get-ItemProperty for testing + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { + [PSCustomObject]@{ + TestKey = 'TestValue' + AnotherKey = 'AnotherValue' + PSPath = 'TestRegistry::HKLM\Software\Test' + } + } + } + + Context "Basic Functionality" { + It "Should return registry key value when key exists" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'ExpectedValue' } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be 'ExpectedValue' + Should -Invoke Test-RegistryKey -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 1 -Scope It + } + + It "Should return null when key does not exist" { + Mock Test-RegistryKey { $false } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'NonExistentKey' + + $Result | Should -Be $null + Should -Invoke Test-RegistryKey -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 0 -Scope It + } + + It "Should return null when registry path does not exist" { + Mock Test-RegistryKey { $false } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\NonExistent' -Key 'TestKey' + + $Result | Should -Be $null + Should -Invoke Test-RegistryKey -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 0 -Scope It + } + } + + Context "Data Type Handling" { + It "Should return string values correctly" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'StringValue' } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be 'StringValue' + $Result | Should -BeOfType [String] + } + + It "Should return integer values correctly" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 42 } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be 42 + $Result | Should -BeOfType [Int32] + } + + It "Should return boolean values correctly" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = $true } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + $Result | Should -BeOfType [Boolean] + } + + It "Should return array values correctly" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = @('Value1', 'Value2', 'Value3') } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be @('Value1', 'Value2', 'Value3') + $Result | Should -BeOfType [Array] + } + + It "Should handle empty string values" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = '' } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be '' + $Result | Should -BeOfType [String] + } + + It "Should handle null values" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = $null } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $null + } + + It "Should handle zero values" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 0 } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be 0 + $Result | Should -BeOfType [Int32] + } + } + + Context "Parameter Validation" { + It "Should require Path parameter" { + { Get-RegistryKey -Key 'TestKey' } | Should -Throw + } + + It "Should require Key parameter" { + { Get-RegistryKey -Path 'HKLM:\Software\Test' } | Should -Throw + } + + It "Should handle various registry path formats" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'TestValue' } } + + Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' | Should -Be 'TestValue' + Get-RegistryKey -Path 'HKCU:\Software\Test' -Key 'TestKey' | Should -Be 'TestValue' + Get-RegistryKey -Path 'HKEY_LOCAL_MACHINE\Software\Test' -Key 'TestKey' | Should -Be 'TestValue' + } + } + + Context "Error Handling" { + It "Should handle Get-ItemProperty exceptions gracefully" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { throw "Access denied" } + + { Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw "Access denied" + } + + It "Should handle Test-RegistryKey exceptions gracefully" { + Mock Test-RegistryKey { throw "Registry access error" } + + { Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw "Registry access error" + } + } + + Context "Property Extraction" { + It "Should extract the correct property from Get-ItemProperty result" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { + [PSCustomObject]@{ + TestKey = 'CorrectValue' + OtherKey = 'OtherValue' + PSPath = 'SomeRegistryPath' + PSChildName = 'SomeChildName' + } + } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be 'CorrectValue' + $Result | Should -Not -Be 'OtherValue' + } + + It "Should handle properties with complex names" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ 'Complex-Property_Name.123' = 'ComplexValue' } } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'Complex-Property_Name.123' + + $Result | Should -Be 'ComplexValue' + } + } + + Context "Select-Object Usage" { + It "Should properly use Select-Object -ExpandProperty" { + Mock Test-RegistryKey { $true } + Mock Get-ItemProperty { + $obj = [PSCustomObject]@{ TestKey = 'TestValue' } + $obj | Add-Member -MemberType ScriptMethod -Name ToString -Value { return 'MockedObject' } -Force + return $obj + } + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + # Should return the actual property value, not the object + $Result | Should -Be 'TestValue' + $Result | Should -Not -Be 'MockedObject' + } + } +} diff --git a/tests/common/Registry/Invoke-EnsureRegistryPath.Tests.ps1 b/tests/common/Registry/Invoke-EnsureRegistryPath.Tests.ps1 new file mode 100644 index 00000000..bec9332d --- /dev/null +++ b/tests/common/Registry/Invoke-EnsureRegistryPath.Tests.ps1 @@ -0,0 +1,146 @@ +Describe "Invoke-EnsureRegistryPath Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock dependencies for cross-platform testing + Mock Test-Path { $false } + Mock New-Item { + [PSCustomObject]@{ + Name = 'MockedKey' + PSPath = "TestRegistry::$Path" + } + } + Mock Join-Path { + param($Path, $ChildPath) + if ($Path.EndsWith(':')) { + return "$Path\$ChildPath" + } + return "$Path\$ChildPath" + } + } + + Context "Basic Functionality" { + It "Should create registry path with HKLM root" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' } | Should -Not -Throw + + Should -Invoke Test-Path -Exactly 2 -Scope It + Should -Invoke New-Item -Exactly 2 -Scope It + } + + It "Should create registry path with HKCU root" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKCU' -Path 'Software\TestPath' } | Should -Not -Throw + + Should -Invoke Test-Path -Exactly 2 -Scope It + Should -Invoke New-Item -Exactly 2 -Scope It + } + + It "Should handle existing registry paths" { + Mock Test-Path { $true } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\ExistingPath' } | Should -Not -Throw + + Should -Invoke Test-Path -AtLeast 1 -Scope It + Should -Invoke New-Item -Exactly 0 -Scope It + } + + It "Should handle nested registry paths" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\Level1\Level2\Level3' } | Should -Not -Throw + + Should -Invoke Test-Path -AtLeast 3 -Scope It + Should -Invoke New-Item -AtLeast 3 -Scope It + } + } + + Context "ShouldProcess Support" { + It "Should support WhatIf parameter" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' -WhatIf } | Should -Not -Throw + + # When WhatIf is used, New-Item should not be called + Should -Invoke New-Item -Exactly 0 -Scope It + } + + It "Should support Confirm parameter" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' -Confirm:$false } | Should -Not -Throw + + Should -Invoke New-Item -AtLeast 1 -Scope It + } + } + + Context "Parameter Validation" { + It "Should accept valid Root values" { + Mock Test-Path { $true } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software' } | Should -Not -Throw + { Invoke-EnsureRegistryPath -Root 'HKCU' -Path 'Software' } | Should -Not -Throw + } + + It "Should reject invalid Root values" { + { Invoke-EnsureRegistryPath -Root 'INVALID' -Path 'Software' } | Should -Throw + } + + It "Should handle empty path segments" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\\TestPath' } | Should -Not -Throw + + # Should filter out empty segments + Should -Invoke Test-Path -AtLeast 1 -Scope It + } + + It "Should handle paths with leading/trailing slashes" { + Mock Test-Path { $false } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path '\Software\TestPath\' } | Should -Not -Throw + + Should -Invoke Test-Path -AtLeast 1 -Scope It + } + } + + Context "Error Handling" { + It "Should handle New-Item failures gracefully" { + Mock Test-Path { $false } + Mock New-Item { throw "Access denied" } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' } | Should -Throw "Access denied" + } + + It "Should handle Test-Path failures gracefully" { + Mock Test-Path { throw "Registry key not accessible" } + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' } | Should -Throw "Registry key not accessible" + } + } + + Context "Integration with Registry Provider" { + It "Should build correct registry paths for HKLM" { + Mock Test-Path { $false } + Mock New-Item { } + + Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\TestPath' + + Should -Invoke Test-Path -ParameterFilter { $Path -like 'HKLM:*' } + Should -Invoke New-Item -ParameterFilter { $Path -like 'HKLM:*' } + } + + It "Should build correct registry paths for HKCU" { + Mock Test-Path { $false } + Mock New-Item { } + + Invoke-EnsureRegistryPath -Root 'HKCU' -Path 'Software\TestPath' + + Should -Invoke Test-Path -ParameterFilter { $Path -like 'HKCU:*' } + Should -Invoke New-Item -ParameterFilter { $Path -like 'HKCU:*' } + } + } +} diff --git a/tests/common/Registry/Invoke-OnEachUserHive.Tests.ps1 b/tests/common/Registry/Invoke-OnEachUserHive.Tests.ps1 new file mode 100644 index 00000000..afcdc577 --- /dev/null +++ b/tests/common/Registry/Invoke-OnEachUserHive.Tests.ps1 @@ -0,0 +1,282 @@ +Describe "Invoke-OnEachUserHive Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock dependencies for cross-platform testing + Mock Get-AllSIDs { + @( + [PSCustomObject]@{ SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001'; UserHive = 'C:\Users\User1\ntuser.dat'; Username = 'User1' }, + [PSCustomObject]@{ SID = 'S-1-5-21-1234567890-1234567890-1234567890-1002'; UserHive = 'C:\Users\User2\ntuser.dat'; Username = 'User2' } + ) + } + Mock Get-LoadedUserHives { + @( + [PSCustomObject]@{ SID = 'S-1-5-21-1234567890-1234567890-1234567890-1001' } + ) + } + Mock Get-UnloadedUserHives { + param($LoadedHives, $ProfileList) + @( + [PSCustomObject]@{ SID = 'S-1-5-21-1234567890-1234567890-1234567890-1002'; UserHive = 'C:\Users\User2\ntuser.dat'; Username = 'User2' } + ) + } + Mock reg { } + Mock Test-Path { $true } + Mock Invoke-Verbose { } + Mock Invoke-Debug { } + Mock Invoke-Warn { } + } + + Context "Basic Functionality" { + It "Should execute script block for each user hive" { + $ExecutionCount = 0 + $ScriptBlock = { param($Hive) $script:ExecutionCount++ } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + $ExecutionCount | Should -Be 2 # Should execute for both users + } + + It "Should pass hive information to script block" { + $ReceivedHives = @() + $ScriptBlock = { param($Hive) $script:ReceivedHives += $Hive } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + $ReceivedHives.Count | Should -Be 2 + $ReceivedHives[0].Username | Should -Be 'User1' + $ReceivedHives[1].Username | Should -Be 'User2' + } + + It "Should handle loaded hives without loading/unloading" { + $LoadedHiveProcessed = $false + $ScriptBlock = { + param($Hive) + if ($Hive.SID -eq 'S-1-5-21-1234567890-1234567890-1234567890-1001') { + $script:LoadedHiveProcessed = $true + } + } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + $LoadedHiveProcessed | Should -Be $true + # Should not call reg load for already loaded hives + Should -Invoke reg -Times 0 -ParameterFilter { $args[0] -eq 'load' -and $args[1] -like '*1001' } + } + + It "Should load and unload unloaded hives" { + Mock Test-Path { $true } # Mock successful hive loading + + $UnloadedHiveProcessed = $false + $ScriptBlock = { + param($Hive) + if ($Hive.SID -eq 'S-1-5-21-1234567890-1234567890-1234567890-1002') { + $script:UnloadedHiveProcessed = $true + } + } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + $UnloadedHiveProcessed | Should -Be $true + # Should call reg load and unload for unloaded hives + Should -Invoke reg -Times 1 -ParameterFilter { $args[0] -eq 'load' } + Should -Invoke reg -Times 1 -ParameterFilter { $args[0] -eq 'unload' } + } + } + + Context "Error Handling" { + It "Should handle hive loading failures gracefully" { + Mock Test-Path { $false } # Simulate hive loading failure + Mock Invoke-Warn { } + + $ScriptBlock = { param($Hive) } + + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Not -Throw + + # Should warn about failed hive loading + Should -Invoke Invoke-Warn -Times 1 + } + + It 'Should continue processing other hives when one fails' { + Mock Test-Path { + # Fail for the second hive, succeed for others + param($Path) + $Path -notlike '*1002' + } + Mock Invoke-Warn { } + + $ProcessedCount = 0 + $ScriptBlock = { param($Hive) $script:ProcessedCount++ } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + # Should still process the first (loaded) hive + $ProcessedCount | Should -Be 1 + Should -Invoke Invoke-Warn -Times 1 + } + + It 'Should handle script block exceptions gracefully' { + $ScriptBlock = { param($Hive) throw 'Script block error' } + + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Throw 'Script block error' + } + + It 'Should always unload hives in finally block even on error' { + Mock Test-Path { $true } + + $ScriptBlock = { param($Hive) + if ($Hive.SID -eq 'S-1-5-21-1234567890-1234567890-1234567890-1002') { + throw 'Processing error' + } + } + + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Throw 'Processing error' + + # Should still call unload even though error occurred + Should -Invoke reg -Times 1 -ParameterFilter { $args[0] -eq 'unload' } + } + } + + Context 'Registry Operations' { + It 'Should call reg load with correct parameters' { + Mock Test-Path { $true } + + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + Should -Invoke reg -Times 1 -ParameterFilter { + $args[0] -eq 'load' -and + $args[1] -eq 'HKU\S-1-5-21-1234567890-1234567890-1234567890-1002' -and + $args[2] -eq 'C:\Users\User2\ntuser.dat' + } + } + + It 'Should call reg unload with correct parameters' { + Mock Test-Path { $true } + + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + Should -Invoke reg -Times 1 -ParameterFilter { + $args[0] -eq 'unload' -and + $args[1] -eq 'HKU\S-1-5-21-1234567890-1234567890-1234567890-1002' + } + } + + It 'Should call garbage collection before unloading' { + Mock Test-Path { $true } + Mock -CommandName 'Invoke-Expression' -MockWith { } -ParameterFilter { $Command -eq '[GC]::Collect()' } + + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + # Note: [GC]::Collect() is called directly, not via Invoke-Expression, so this test verifies the concept + Should -Invoke reg -Times 1 -ParameterFilter { $args[0] -eq 'unload' } + } + } + + Context 'Hive Detection Logic' { + It 'Should correctly identify loaded vs unloaded hives' { + $LoadedHiveCount = 0 + $UnloadedHiveCount = 0 + + # Override mocks to track which hives are processed as loaded vs unloaded + Mock reg { + if ($args[0] -eq 'load') { $script:UnloadedHiveCount++ } + if ($args[0] -eq 'unload') { $script:UnloadedHiveCount++ } + } + + $ScriptBlock = { + param($Hive) + if ($Hive.SID -eq 'S-1-5-21-1234567890-1234567890-1234567890-1001') { + $script:LoadedHiveCount++ + } + } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + $LoadedHiveCount | Should -Be 1 # One loaded hive processed + # One unloaded hive should have been loaded and unloaded + Should -Invoke reg -Times 2 # load + unload calls + } + + It 'Should handle empty hive lists gracefully' { + Mock Get-AllSIDs { @() } + Mock Get-LoadedUserHives { @() } + Mock Get-UnloadedUserHives { @() } + + $ExecutionCount = 0 + $ScriptBlock = { param($Hive) $script:ExecutionCount++ } + + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Not -Throw + + $ExecutionCount | Should -Be 0 + } + } + + Context 'Parameter Validation' { + It 'Should require ScriptBlock parameter' { + { Invoke-OnEachUserHive } | Should -Throw + } + + It 'Should accept valid script blocks' { + $ScriptBlock = { param($Hive) Write-Output "Processing $($Hive.Username)" } + + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Not -Throw + } + } + + Context 'Debug and Verbose Output' { + It 'Should provide debug output for hive operations' { + Mock Test-Path { $true } + + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + # Should call debug logging functions + Should -Invoke Invoke-Debug -AtLeast 1 + } + + It "Should provide verbose output for hive processing" { + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + # Should call verbose logging + Should -Invoke Invoke-Verbose -AtLeast 1 + } + } + + Context "Integration with Helper Functions" { + It "Should call Get-AllSIDs to get profile list" { + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + Should -Invoke Get-AllSIDs -Times 1 + } + + It "Should call Get-LoadedUserHives to get loaded hives" { + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + Should -Invoke Get-LoadedUserHives -Times 1 + } + + It "Should call Get-UnloadedUserHives with correct parameters" { + $ScriptBlock = { param($Hive) } + + Invoke-OnEachUserHive -ScriptBlock $ScriptBlock + + Should -Invoke Get-UnloadedUserHives -Times 1 -ParameterFilter { + $LoadedHives -ne $null -and $ProfileList -ne $null + } + } + } +} diff --git a/tests/common/Registry/Registry.Tests.ps1 b/tests/common/Registry/Registry.Tests.ps1 new file mode 100644 index 00000000..0dd86309 --- /dev/null +++ b/tests/common/Registry/Registry.Tests.ps1 @@ -0,0 +1,145 @@ +Describe "Registry Module Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules with force to ensure clean state + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + } + + Context "Module Import" { + It "Should import Registry module successfully" { + Get-Module -Name Registry* | Should -Not -BeNullOrEmpty + } + + It "Should export expected functions" { + $ExportedFunctions = (Get-Module -Name Registry*).ExportedFunctions.Keys + $ExportedFunctions | Should -Contain 'Invoke-EnsureRegistryPath' + $ExportedFunctions | Should -Contain 'Test-RegistryKey' + $ExportedFunctions | Should -Contain 'Get-RegistryKey' + $ExportedFunctions | Should -Contain 'Set-RegistryKey' + $ExportedFunctions | Should -Contain 'Remove-RegistryKey' + $ExportedFunctions | Should -Contain 'Invoke-OnEachUserHive' + } + } + + Context "Test-RegistryKey Basic Tests" { + BeforeEach { + # Mock dependencies for cross-platform testing + Mock Test-Path { $true } -ModuleName Registry + Mock Get-ItemProperty { + [PSCustomObject]@{ TestKey = 'TestValue' } + } -ModuleName Registry + } + + It "Should return True when registry key exists" -Skip:($IsLinux -or $IsMacOS) { + # Skip on non-Windows platforms as this requires actual registry access + Mock Test-Path { $true } -ModuleName Registry + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'TestValue' } } -ModuleName Registry + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + $Result | Should -Be $true + } + + It "Should return False when registry path does not exist" -Skip:($IsLinux -or $IsMacOS) { + Mock Test-Path { $false } -ModuleName Registry + + $Result = Test-RegistryKey -Path 'HKLM:\Software\NonExistent' -Key 'TestKey' + $Result | Should -Be $false + } + + It "Should accept mandatory parameters" { + { Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + } + } + + Context "Get-RegistryKey Basic Tests" { + It "Should require Path and Key parameters" { + { Get-RegistryKey } | Should -Throw + } + + It "Should accept valid parameters without throwing" { + Mock Test-RegistryKey { $false } -ModuleName Registry + + { Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + } + + It "Should return null when Test-RegistryKey returns false" { + Mock Test-RegistryKey { $false } -ModuleName Registry + + $Result = Get-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + $Result | Should -Be $null + } + } + + Context "Set-RegistryKey Basic Tests" { + It "Should require all mandatory parameters" { + { Set-RegistryKey } | Should -Throw + { Set-RegistryKey -Path 'HKLM:\Software\Test' } | Should -Throw + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' } | Should -Throw + } + + It "Should accept all required parameters" { + Mock Invoke-EnsureRegistryPath { } -ModuleName Registry + Mock Set-ItemProperty { } -ModuleName Registry + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Not -Throw + } + } + + Context "Remove-RegistryKey Basic Tests" { + It "Should require Path and Key parameters" { + { Remove-RegistryKey } | Should -Throw + { Remove-RegistryKey -Path 'HKLM:\Software\Test' } | Should -Throw + } + + It "Should accept required parameters" { + Mock Test-RegistryKey { $false } -ModuleName Registry + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + } + } + + Context "Invoke-EnsureRegistryPath Basic Tests" { + It "Should require Root and Path parameters" { + { Invoke-EnsureRegistryPath } | Should -Throw + { Invoke-EnsureRegistryPath -Root 'HKLM' } | Should -Throw + } + + It "Should accept valid Root values" { + Mock Test-Path { $true } -ModuleName Registry + + { Invoke-EnsureRegistryPath -Root 'HKLM' -Path 'Software\Test' } | Should -Not -Throw + { Invoke-EnsureRegistryPath -Root 'HKCU' -Path 'Software\Test' } | Should -Not -Throw + } + + It "Should reject invalid Root values" { + { Invoke-EnsureRegistryPath -Root 'INVALID' -Path 'Software\Test' } | Should -Throw + } + } + + Context "Invoke-OnEachUserHive Basic Tests" { + It "Should require ScriptBlock parameter" { + { Invoke-OnEachUserHive } | Should -Throw + } + + It "Should accept ScriptBlock parameter" { + # Mock all the helper functions to avoid Windows-specific operations + Mock Get-AllSIDs { @() } -ModuleName Registry + Mock Get-LoadedUserHives { @() } -ModuleName Registry + Mock Get-UnloadedUserHives { @() } -ModuleName Registry + + $ScriptBlock = { param($Hive) } + { Invoke-OnEachUserHive -ScriptBlock $ScriptBlock } | Should -Not -Throw + } + } + + Context "Cross-Platform Considerations" { + It "Should handle non-Windows platforms gracefully" { + if ($IsLinux -or $IsMacOS) { + # On non-Windows platforms, registry operations should be mockable + Mock Test-Path { $false } -ModuleName Registry + + { Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + } + } + } +} diff --git a/tests/common/Registry/Remove-RegistryKey.Tests.ps1 b/tests/common/Registry/Remove-RegistryKey.Tests.ps1 new file mode 100644 index 00000000..db68f3e5 --- /dev/null +++ b/tests/common/Registry/Remove-RegistryKey.Tests.ps1 @@ -0,0 +1,201 @@ +Describe "Remove-RegistryKey Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock dependencies + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + } + + Context "Basic Functionality" { + It "Should remove registry key when it exists" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + + Should -Invoke Test-RegistryKey -Exactly 1 -Scope It + Should -Invoke Remove-ItemProperty -Exactly 1 -Scope It + } + + It "Should not attempt to remove when key does not exist" { + Mock Test-RegistryKey { $false } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'NonExistentKey' } | Should -Not -Throw + + Should -Invoke Test-RegistryKey -Exactly 1 -Scope It + Should -Invoke Remove-ItemProperty -Exactly 0 -Scope It + } + + It "Should check registry key existence before removal" { + Mock Test-RegistryKey { $false } + Mock Remove-ItemProperty { } + + Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + Should -Invoke Test-RegistryKey -ParameterFilter { + $Path -eq 'HKLM:\Software\Test' -and $Key -eq 'TestKey' + } + } + } + + Context "ShouldProcess Support" { + It "Should support WhatIf parameter" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -WhatIf } | Should -Not -Throw + + # When WhatIf is used, Remove-ItemProperty should not be called + Should -Invoke Remove-ItemProperty -Exactly 0 -Scope It + } + + It "Should support Confirm parameter" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Confirm:$false } | Should -Not -Throw + + Should -Invoke Remove-ItemProperty -Exactly 1 -Scope It + } + } + + Context "Parameter Validation" { + It "Should require Path parameter" { + { Remove-RegistryKey -Key 'TestKey' } | Should -Throw + } + + It "Should require Key parameter" { + { Remove-RegistryKey -Path 'HKLM:\Software\Test' } | Should -Throw + } + + It "Should handle various registry path formats" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + { Remove-RegistryKey -Path 'HKCU:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + } + } + + Context "Error Handling" { + It "Should handle Test-RegistryKey failures" { + Mock Test-RegistryKey { throw "Registry access error" } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw "Registry access error" + } + + It "Should handle Remove-ItemProperty failures" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { throw "Access denied" } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw "Access denied" + } + + It "Should handle invalid registry paths" { + Mock Test-RegistryKey { throw "Invalid path" } + + { Remove-RegistryKey -Path 'InvalidPath' -Key 'TestKey' } | Should -Throw "Invalid path" + } + } + + Context "Verbose Output" { + It "Should provide verbose output when key does not exist" { + Mock Test-RegistryKey { $false } + Mock Remove-ItemProperty { } + + $VerboseOutput = Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'NonExistentKey' -Verbose 4>&1 + + # Should indicate that the key does not exist (assuming the function outputs this) + # This is based on the source code showing a Invoke-Verbose call + } + + It "Should provide verbose output when removing key" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + $VerboseOutput = Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Verbose 4>&1 + + # Should indicate that the key is being removed + } + } + + Context "Integration Tests" { + It "Should call functions in correct order" { + $CallOrder = @() + Mock Test-RegistryKey { + $script:CallOrder += 'TestKey' + return $true + } + Mock Remove-ItemProperty { $script:CallOrder += 'RemoveProperty' } + + Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $CallOrder[0] | Should -Be 'TestKey' + $CallOrder[1] | Should -Be 'RemoveProperty' + } + + It "Should pass correct parameters to Remove-ItemProperty" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'MyTestKey' + + Should -Invoke Remove-ItemProperty -ParameterFilter { + $Path -eq 'HKLM:\Software\Test' -and $Name -eq 'MyTestKey' + } + } + } + + Context "Edge Cases" { + It "Should handle special characters in key names" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'Special-Key_Name.123' } | Should -Not -Throw + + Should -Invoke Remove-ItemProperty -ParameterFilter { $Name -eq 'Special-Key_Name.123' } + } + + It "Should handle complex registry paths" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Company\Product\Version\Settings' -Key 'TestKey' } | Should -Not -Throw + + Should -Invoke Test-RegistryKey -ParameterFilter { + $Path -eq 'HKLM:\Software\Company\Product\Version\Settings' + } + } + + It "Should handle empty or whitespace key names" { + Mock Test-RegistryKey { $false } + + # Empty key name should be caught by parameter validation + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key '' } | Should -Throw + } + } + + Context "Registry Hive Support" { + It "Should work with HKLM registry hive" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + + Should -Invoke Test-RegistryKey -ParameterFilter { $Path -like 'HKLM:*' } + } + + It "Should work with HKCU registry hive" { + Mock Test-RegistryKey { $true } + Mock Remove-ItemProperty { } + + { Remove-RegistryKey -Path 'HKCU:\Software\Test' -Key 'TestKey' } | Should -Not -Throw + + Should -Invoke Test-RegistryKey -ParameterFilter { $Path -like 'HKCU:*' } + } + } +} diff --git a/tests/common/Registry/Set-RegistryKey.Tests.ps1 b/tests/common/Registry/Set-RegistryKey.Tests.ps1 new file mode 100644 index 00000000..fbe7bc20 --- /dev/null +++ b/tests/common/Registry/Set-RegistryKey.Tests.ps1 @@ -0,0 +1,277 @@ +Describe "Set-RegistryKey Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock dependencies for cross-platform testing + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + } + + Context "Basic Functionality" { + It "Should set registry key with string value" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Not -Throw + + Should -Invoke Invoke-EnsureRegistryPath -Exactly 1 -Scope It + Should -Invoke Set-ItemProperty -Exactly 1 -Scope It + } + + It "Should set registry key with DWord value" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '42' -Kind 'DWord' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'DWord' } -Exactly 1 -Scope It + } + + It "Should set registry key with Binary value" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'BinaryData' -Kind 'Binary' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'Binary' } -Exactly 1 -Scope It + } + + It "Should ensure registry path exists before setting value" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKLM:\Software\TestPath\SubPath' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + Should -Invoke Invoke-EnsureRegistryPath -ParameterFilter { + $Root -eq 'HKLM' -and $Path -eq 'Software\TestPath\SubPath' + } -Exactly 1 -Scope It + } + } + + Context "Registry Value Types" { + It "Should support String registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'StringValue' -Kind 'String' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'String' } + } + + It "Should support DWord registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '123' -Kind 'DWord' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'DWord' } + } + + It "Should support QWord registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '9223372036854775807' -Kind 'QWord' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'QWord' } + } + + It "Should support Binary registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'BinaryData' -Kind 'Binary' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'Binary' } + } + + It "Should support ExpandString registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '%SystemRoot%\System32' -Kind 'ExpandString' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'ExpandString' } + } + + It "Should support MultiString registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'Value1;Value2;Value3' -Kind 'MultiString' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'MultiString' } + } + + It "Should support None registry type" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '' -Kind 'None' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Type -eq 'None' } + } + } + + Context "Path Processing" { + It "Should extract root correctly from HKLM path" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + Should -Invoke Invoke-EnsureRegistryPath -ParameterFilter { $Root -eq 'HKLM' } + } + + It "Should extract root correctly from HKCU path" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKCU:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + Should -Invoke Invoke-EnsureRegistryPath -ParameterFilter { $Root -eq 'HKCU' } + } + + It "Should extract path correctly by removing root prefix" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKLM:\Software\Microsoft\Windows' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + Should -Invoke Invoke-EnsureRegistryPath -ParameterFilter { $Path -eq 'Software\Microsoft\Windows' } + } + + It "Should handle complex nested paths" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKLM:\Software\Company\Product\Version\Settings' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + Should -Invoke Invoke-EnsureRegistryPath -ParameterFilter { + $Root -eq 'HKLM' -and $Path -eq 'Software\Company\Product\Version\Settings' + } + } + } + + Context "ShouldProcess Support" { + It "Should support WhatIf parameter" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' -WhatIf } | Should -Not -Throw + + # When WhatIf is used, Set-ItemProperty should not be called + Should -Invoke Set-ItemProperty -Exactly 0 -Scope It + } + + It "Should support Confirm parameter" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' -Confirm:$false } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -Exactly 1 -Scope It + } + } + + Context "Parameter Validation" { + It "Should require Path parameter" { + { Set-RegistryKey -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Throw + } + + It "Should require Key parameter" { + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Value 'TestValue' -Kind 'String' } | Should -Throw + } + + It "Should require Value parameter" { + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Kind 'String' } | Should -Throw + } + + It "Should require Kind parameter" { + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' } | Should -Throw + } + + It "Should validate Kind parameter values" { + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'InvalidType' } | Should -Throw + } + } + + Context "Error Handling" { + It "Should handle Invoke-EnsureRegistryPath failures" { + Mock Invoke-EnsureRegistryPath { throw "Access denied" } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Throw "Access denied" + } + + It "Should handle Set-ItemProperty failures" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { throw "Registry write error" } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Throw "Registry write error" + } + + It "Should handle invalid registry paths" { + Mock Invoke-EnsureRegistryPath { throw "Invalid path" } + + { Set-RegistryKey -Path 'InvalidPath' -Key 'TestKey' -Value 'TestValue' -Kind 'String' } | Should -Throw "Invalid path" + } + } + + Context "Integration Tests" { + It "Should call functions in correct order" { + $CallOrder = @() + Mock Invoke-EnsureRegistryPath { $script:CallOrder += 'EnsurePath' } + Mock Set-ItemProperty { $script:CallOrder += 'SetProperty' } + + Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'TestValue' -Kind 'String' + + $CallOrder[0] | Should -Be 'EnsurePath' + $CallOrder[1] | Should -Be 'SetProperty' + } + + It "Should pass correct parameters to Set-ItemProperty" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'MyKey' -Value 'MyValue' -Kind 'String' + + Should -Invoke Set-ItemProperty -ParameterFilter { + $Path -eq 'HKLM:\Software\Test' -and + $Name -eq 'MyKey' -and + $Value -eq 'MyValue' -and + $Type -eq 'String' + } + } + } + + Context "Edge Cases" { + It "Should handle empty string values" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value '' -Kind 'String' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Value -eq '' } + } + + It "Should handle special characters in key names" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'Special-Key_Name.123' -Value 'TestValue' -Kind 'String' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Name -eq 'Special-Key_Name.123' } + } + + It "Should handle special characters in values" { + Mock Invoke-EnsureRegistryPath { } + Mock Set-ItemProperty { } + + { Set-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' -Value 'Value with spaces and $pecial chars!' -Kind 'String' } | Should -Not -Throw + + Should -Invoke Set-ItemProperty -ParameterFilter { $Value -eq 'Value with spaces and $pecial chars!' } + } + } +} diff --git a/tests/common/Registry/Test-RegistryKey.Tests.ps1 b/tests/common/Registry/Test-RegistryKey.Tests.ps1 new file mode 100644 index 00000000..365486f4 --- /dev/null +++ b/tests/common/Registry/Test-RegistryKey.Tests.ps1 @@ -0,0 +1,145 @@ +Describe "Test-RegistryKey Tests" -Skip:(-not $IsWindows) { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/Registry.psm1" -Force + + # Mock dependencies for cross-platform testing + Mock Test-Path { $true } + Mock Get-ItemProperty { + [PSCustomObject]@{ + TestKey = 'TestValue' + PSPath = 'TestRegistry::HKLM\Software\Test' + } + } + } + + Context "Basic Functionality" { + It "Should return True when registry key exists and has property" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'TestValue' } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + Should -Invoke Test-Path -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 1 -Scope It + } + + It "Should return False when registry path does not exist" { + Mock Test-Path { $false } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\NonExistent' -Key 'TestKey' + + $Result | Should -Be $false + Should -Invoke Test-Path -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 0 -Scope It + } + + It "Should return False when registry key does not exist" { + Mock Test-Path { $true } + Mock Get-ItemProperty { $null } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'NonExistentKey' + + $Result | Should -Be $false + Should -Invoke Test-Path -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 1 -Scope It + } + + It "Should return False when Get-ItemProperty throws error" { + Mock Test-Path { $true } + Mock Get-ItemProperty { throw "Property does not exist" } -ParameterFilter { $ErrorAction -eq 'SilentlyContinue' } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $false + Should -Invoke Test-Path -Exactly 1 -Scope It + Should -Invoke Get-ItemProperty -Exactly 1 -Scope It + } + } + + Context "Parameter Validation" { + It "Should require Path parameter" { + { Test-RegistryKey -Key 'TestKey' } | Should -Throw + } + + It "Should require Key parameter" { + { Test-RegistryKey -Path 'HKLM:\Software\Test' } | Should -Throw + } + + It "Should handle various registry paths" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'TestValue' } } + + Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' | Should -Be $true + Test-RegistryKey -Path 'HKCU:\Software\Test' -Key 'TestKey' | Should -Be $true + Test-RegistryKey -Path 'HKEY_LOCAL_MACHINE\Software\Test' -Key 'TestKey' | Should -Be $true + } + } + + Context "Error Handling" { + It "Should handle Test-Path exceptions" { + Mock Test-Path { throw "Access denied" } + + { Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' } | Should -Throw "Access denied" + } + + It "Should handle invalid registry paths" { + Mock Test-Path { $false } + + $Result = Test-RegistryKey -Path 'InvalidPath' -Key 'TestKey' + + $Result | Should -Be $false + } + } + + Context "Edge Cases" { + It "Should handle empty string values in registry" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = '' } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + } + + It "Should handle null values in registry" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = $null } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + } + + It "Should handle zero values in registry" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 0 } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + } + + It "Should handle boolean values in registry" { + Mock Test-Path { $true } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = $false } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + } + } + + Context "PathType Validation" { + It "Should validate path as Container" { + Mock Test-Path { $true } -ParameterFilter { $PathType -eq 'Container' } + Mock Get-ItemProperty { [PSCustomObject]@{ TestKey = 'TestValue' } } + + $Result = Test-RegistryKey -Path 'HKLM:\Software\Test' -Key 'TestKey' + + $Result | Should -Be $true + Should -Invoke Test-Path -ParameterFilter { $PathType -eq 'Container' } + } + } +} diff --git a/tests/common/Temp/Get-NamedTempFolder.Tests.ps1 b/tests/common/Temp/Get-NamedTempFolder.Tests.ps1 new file mode 100644 index 00000000..a1e3c91b --- /dev/null +++ b/tests/common/Temp/Get-NamedTempFolder.Tests.ps1 @@ -0,0 +1,116 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Temp.psm1" } + +Describe 'Get-NamedTempFolder Tests' { + BeforeAll { + $TestTempPath = [System.IO.Path]::GetTempPath() + } + + AfterEach { + # Clean up any test folders created + Get-ChildItem -Path $TestTempPath -Directory | Where-Object { $_.Name -like 'PesterTest*' } | Remove-Item -Recurse -Force + } + + Context 'Basic Functionality' { + It 'Should create a new folder with the specified name' { + $FolderName = 'PesterTestFolder' + $Result = Get-NamedTempFolder -Name $FolderName + + $Result | Should -Be (Join-Path $TestTempPath $FolderName) + Test-Path $Result -PathType Container | Should -Be $true + } + + It 'Should return existing folder if it already exists' { + $FolderName = 'PesterTestExisting' + $ExpectedPath = Join-Path $TestTempPath $FolderName + + # Create the folder first + New-Item -ItemType Directory -Path $ExpectedPath -Force | Out-Null + + $Result = Get-NamedTempFolder -Name $FolderName + + $Result | Should -Be $ExpectedPath + Test-Path $Result -PathType Container | Should -Be $true + } + + It 'Should handle folder names with special characters' { + $FolderName = 'PesterTest-Folder_123' + $Result = Get-NamedTempFolder -Name $FolderName + + $Result | Should -Be (Join-Path $TestTempPath $FolderName) + Test-Path $Result -PathType Container | Should -Be $true + } + } + + Context 'ForceEmpty Parameter' { + It 'Should empty existing folder when ForceEmpty is specified' { + $FolderName = 'PesterTestForceEmpty' + $FolderPath = Join-Path $TestTempPath $FolderName + + # Create folder with some content + New-Item -ItemType Directory -Path $FolderPath -Force | Out-Null + $TestFile = Join-Path $FolderPath 'testfile.txt' + 'test content' | Out-File -FilePath $TestFile + + # Verify file exists + Test-Path $TestFile | Should -Be $true + + $Result = Get-NamedTempFolder -Name $FolderName -ForceEmpty + + $Result | Should -Be $FolderPath + Test-Path $Result -PathType Container | Should -Be $true + Test-Path $TestFile | Should -Be $false + } + + It 'Should create folder if it does not exist when ForceEmpty is specified' { + $FolderName = 'PesterTestForceEmptyNew' + $ExpectedPath = Join-Path $TestTempPath $FolderName + + Test-Path $ExpectedPath | Should -Be $false + + $Result = Get-NamedTempFolder -Name $FolderName -ForceEmpty + + $Result | Should -Be $ExpectedPath + Test-Path $Result -PathType Container | Should -Be $true + } + + It 'Should handle nested folders when ForceEmpty is specified' { + $FolderName = 'PesterTestNested' + $FolderPath = Join-Path $TestTempPath $FolderName + + # Create folder with nested structure + New-Item -ItemType Directory -Path $FolderPath -Force | Out-Null + $NestedFolder = Join-Path $FolderPath 'nested' + New-Item -ItemType Directory -Path $NestedFolder -Force | Out-Null + $TestFile = Join-Path $NestedFolder 'testfile.txt' + 'test content' | Out-File -FilePath $TestFile + + $Result = Get-NamedTempFolder -Name $FolderName -ForceEmpty + + $Result | Should -Be $FolderPath + Test-Path $Result -PathType Container | Should -Be $true + Test-Path $NestedFolder | Should -Be $false + Test-Path $TestFile | Should -Be $false + } + } + + Context 'Error Handling' { + It 'Should handle empty folder name' { + { Get-NamedTempFolder -Name '' } | Should -Throw + } + + It 'Should handle null folder name' { + { Get-NamedTempFolder -Name $null } | Should -Throw + } + + It 'Should handle folder names with trailing whitespace' { + # Trailing whitespace gets trimmed by the filesystem + $FolderName = 'PesterTestWhitespace ' + $Result = Get-NamedTempFolder -Name $FolderName + + # The result should be the expected path, but filesystem trims trailing space + $Result | Should -Be (Join-Path $TestTempPath $FolderName) + # The folder should exist (filesystem trims the trailing spaces) + Test-Path (Join-Path $TestTempPath 'PesterTestWhitespace') -PathType Container | Should -Be $true + } + } +} \ No newline at end of file diff --git a/tests/common/Temp/Get-UniqueTempFolder.Tests.ps1 b/tests/common/Temp/Get-UniqueTempFolder.Tests.ps1 new file mode 100644 index 00000000..ab57df5e --- /dev/null +++ b/tests/common/Temp/Get-UniqueTempFolder.Tests.ps1 @@ -0,0 +1,27 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Temp.psm1" } + +Describe 'Get-UniqueTempFolder Tests' { + Context 'Basic Functionality' { + It 'Should create a unique folder in temp directory' { + $Result = Get-UniqueTempFolder + + $Result | Should -Not -BeNullOrEmpty + Test-Path $Result -PathType Container | Should -Be $true + $Result | Should -BeLike "$([System.IO.Path]::GetTempPath())*" + } + + It 'Should create different folders on multiple calls' { + $Folder1 = Get-UniqueTempFolder + $Folder2 = Get-UniqueTempFolder + + $Folder1 | Should -Not -Be $Folder2 + } + + It 'Should create empty folders' { + $Result = Get-UniqueTempFolder + + $ChildItems = Get-ChildItem -Path $Result + $ChildItems | Should -BeNullOrEmpty + } + } +} diff --git a/tests/common/Temp/Invoke-WithinEphemeral.Tests.ps1 b/tests/common/Temp/Invoke-WithinEphemeral.Tests.ps1 new file mode 100644 index 00000000..5431eb1e --- /dev/null +++ b/tests/common/Temp/Invoke-WithinEphemeral.Tests.ps1 @@ -0,0 +1,85 @@ +BeforeDiscovery { Import-Module "$PSScriptRoot/../../../src/common/Temp.psm1" } + +Describe 'Invoke-WithinEphemeral Tests' { + BeforeEach { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', $null)] + $OriginalLocation = (Get-Location).Path + } + + Context 'Basic Functionality' { + It 'Should execute script block in temporary folder' { + $Script:TempFolderPath = $null + + Invoke-WithinEphemeral { + $Script:TempFolderPath = (Get-Location).Path + } + + $TempFolderPath | Should -Not -BeNullOrEmpty + $TempFolderPath | Should -Not -Be $OriginalLocation + } + + It 'Should return to original location after execution' { + Invoke-WithinEphemeral { + 'test' | Out-File 'testfile.txt' + } + + (Get-Location).Path | Should -Be $OriginalLocation + } + + It 'Should clean up temporary folder after execution' { + $Script:TempFolderPath = $null + + Invoke-WithinEphemeral { + $Script:TempFolderPath = (Get-Location).Path + 'test content' | Out-File 'testfile.txt' + New-Item -ItemType Directory -Name 'subfolder' + } + + Test-Path $Script:TempFolderPath | Should -Be $false + } + } + + Context 'Error Handling' { + It 'Should clean up temporary folder even if script block throws an error' { + $Script:TempFolderPath = $null + + { Invoke-WithinEphemeral { + $Script:TempFolderPath = (Get-Location).Path + 'test content' | Out-File 'testfile.txt' + throw 'Test error' + } } | Should -Throw 'Test error' + + Test-Path $Script:TempFolderPath | Should -Be $false + } + + It 'Should return to original location even if script block throws an error' { + { Invoke-WithinEphemeral { + throw 'Test error' + } } | Should -Throw + + (Get-Location).Path | Should -Be $OriginalLocation + } + } + + Context 'Location Management' { + It 'Should return to original location after Push-Location and Pop-Location Usage' { + $Script:LocationStack = @() + + Invoke-WithinEphemeral { + if ($env:TEMP) { + Push-Location $env:TEMP + } else { + Push-Location '/tmp' + } + } + + (Get-Location).Path | Should -Be $OriginalLocation + + Invoke-WithinEphemeral { + Pop-Location + } + + (Get-Location).Path | Should -Be $OriginalLocation + } + } +} diff --git a/tests/common/UsersAndAccounts/UsersAndAccounts.Tests.ps1 b/tests/common/UsersAndAccounts/UsersAndAccounts.Tests.ps1 new file mode 100644 index 00000000..0bf2a6ca --- /dev/null +++ b/tests/common/UsersAndAccounts/UsersAndAccounts.Tests.ps1 @@ -0,0 +1,312 @@ +Describe "UsersAndAccounts Module Tests" { + BeforeAll { + # Import required modules + Import-Module "$PSScriptRoot/../../../src/common/UsersAndAccounts.psm1" -Force + + # Mock ADSI objects for cross-platform testing + $MockGroup = [PSCustomObject]@{ + Name = 'Administrators' + SchemaClassName = 'Group' + Path = 'WinNT://COMPUTER/Administrators,group' + PSChildName = 'Administrators' + } + + $MockUser = [PSCustomObject]@{ + Name = 'TestUser' + SchemaClassName = 'User' + Path = 'WinNT://COMPUTER/TestUser,user' + PSChildName = 'TestUser' + } + + # Mock ADSI constructor + Mock -CommandName 'Invoke-Expression' -MockWith { + param($Command) + if ($Command -like '*WinNT://*,group*') { + return $MockGroup + } elseif ($Command -like '*WinNT://*,user*') { + return $MockUser + } elseif ($Command -like '*WinNT://*' -and $Command -notlike '*,*') { + # Mock for container operations + return [PSCustomObject]@{ + Children = @($MockGroup, $MockUser) + } + } + return $null + } + } + + Context "Module Import" { + It "Should import UsersAndAccounts module successfully" { + Get-Module -Name UsersAndAccounts* | Should -Not -BeNullOrEmpty + } + + It "Should export expected functions" { + $ExportedFunctions = (Get-Module -Name UsersAndAccounts*).ExportedFunctions.Keys + $ExportedFunctions | Should -Contain 'Get-User' + $ExportedFunctions | Should -Contain 'Get-UserGroups' + $ExportedFunctions | Should -Contain 'Get-Group' + $ExportedFunctions | Should -Contain 'Get-MembersOfGroup' + $ExportedFunctions | Should -Contain 'Test-MemberOfGroup' + $ExportedFunctions | Should -Contain 'Add-MemberToGroup' + $ExportedFunctions | Should -Contain 'Remove-MemberFromGroup' + $ExportedFunctions | Should -Contain 'Format-ADSIUser' + } + } + + Context "Get-Group Tests" { + BeforeEach { + # Reset cached groups for each test + if (Get-Variable -Name 'Script:InitialisedAllGroups' -Scope Script -ErrorAction SilentlyContinue) { + Set-Variable -Name 'Script:InitialisedAllGroups' -Value $false -Scope Script + } + if (Get-Variable -Name 'Script:CachedGroups' -Scope Script -ErrorAction SilentlyContinue) { + Set-Variable -Name 'Script:CachedGroups' -Value @{} -Scope Script + } + } + + It "Should accept Name parameter" { + { Get-Group -Name 'Administrators' } | Should -Not -Throw + } + + It "Should accept no parameters to get all groups" { + { Get-Group } | Should -Not -Throw + } + + It "Should handle empty or null group names" { + { Get-Group -Name '' } | Should -Not -Throw + { Get-Group -Name $null } | Should -Not -Throw + } + } + + Context "Get-User Tests" { + BeforeEach { + # Reset cached users for each test + if (Get-Variable -Name 'Script:InitialisedAllUsers' -Scope Script -ErrorAction SilentlyContinue) { + Set-Variable -Name 'Script:InitialisedAllUsers' -Value $false -Scope Script + } + if (Get-Variable -Name 'Script:CachedUsers' -Scope Script -ErrorAction SilentlyContinue) { + Set-Variable -Name 'Script:CachedUsers' -Value @{} -Scope Script + } + } + + It "Should accept Name parameter" { + { Get-User -Name 'TestUser' } | Should -Not -Throw + } + + It "Should accept no parameters to get all users" { + { Get-User } | Should -Not -Throw + } + + It "Should handle empty or null user names" { + { Get-User -Name '' } | Should -Not -Throw + { Get-User -Name $null } | Should -Not -Throw + } + } + + Context "Test-MemberOfGroup Tests" { + It "Should require Group parameter" { + { Test-MemberOfGroup -User 'TestUser' } | Should -Throw + } + + It "Should require User parameter" { + { Test-MemberOfGroup -Group 'Administrators' } | Should -Throw + } + + It "Should accept Group and User parameters" { + Mock Get-GroupByInputOrName { $MockGroup } -ModuleName UsersAndAccounts + Mock Get-UserByInputOrName { $MockUser } -ModuleName UsersAndAccounts + + # Mock the ADSI Invoke method + Add-Member -InputObject $MockGroup -MemberType ScriptMethod -Name 'Invoke' -Value { + param($Method, $Path) + if ($Method -eq 'IsMember') { return $true } + return $null + } -Force + + { Test-MemberOfGroup -Group $MockGroup -User $MockUser } | Should -Not -Throw + } + } + + Context "Add-MemberToGroup Tests" { + It "Should require Group parameter" { + { Add-MemberToGroup -User 'TestUser' } | Should -Throw + } + + It "Should require User parameter" { + { Add-MemberToGroup -Group 'Administrators' } | Should -Throw + } + + It "Should accept Group and User parameters" { + Mock Get-GroupByInputOrName { $MockGroup } -ModuleName UsersAndAccounts + Mock Get-UserByInputOrName { $MockUser } -ModuleName UsersAndAccounts + Mock Test-MemberOfGroup { $false } -ModuleName UsersAndAccounts + + # Mock the ADSI Invoke method for Add + Add-Member -InputObject $MockGroup -MemberType ScriptMethod -Name 'Invoke' -Value { + param($Method, $Path) + if ($Method -eq 'Add') { return $null } + return $null + } -Force + + { Add-MemberToGroup -Group $MockGroup -User $MockUser } | Should -Not -Throw + } + } + + Context "Remove-MemberFromGroup Tests" { + It "Should require Group parameter" { + { Remove-MemberFromGroup -Member 'TestUser' } | Should -Throw + } + + It "Should require Member parameter" { + { Remove-MemberFromGroup -Group 'Administrators' } | Should -Throw + } + + It "Should accept Group and Member parameters" { + Mock Get-GroupByInputOrName { $MockGroup } -ModuleName UsersAndAccounts + Mock Get-UserByInputOrName { $MockUser } -ModuleName UsersAndAccounts + Mock Test-MemberOfGroup { $true } -ModuleName UsersAndAccounts + + # Mock the ADSI Invoke method for Remove + Add-Member -InputObject $MockGroup -MemberType ScriptMethod -Name 'Invoke' -Value { + param($Method, $Path) + if ($Method -eq 'Remove') { return $null } + return $null + } -Force + + { Remove-MemberFromGroup -Group $MockGroup -Member $MockUser } | Should -Not -Throw + } + } + + Context "Get-MembersOfGroup Tests" { + It "Should require Group parameter" { + { Get-MembersOfGroup } | Should -Throw + } + + It "Should accept Group parameter" { + Mock Get-GroupByInputOrName { $MockGroup } -ModuleName UsersAndAccounts + + # Mock the ADSI Invoke method for Members + Add-Member -InputObject $MockGroup -MemberType ScriptMethod -Name 'Invoke' -Value { + param($Method) + if ($Method -eq 'Members') { + return @($MockUser) + } + return $null + } -Force + + { Get-MembersOfGroup -Group $MockGroup } | Should -Not -Throw + } + } + + Context "Get-UserGroups Tests" { + It "Should require User parameter" { + { Get-UserGroups } | Should -Throw + } + + It "Should accept User parameter" { + Mock Get-UserByInputOrName { $MockUser } -ModuleName UsersAndAccounts + Mock Get-WmiObject { + @([PSCustomObject]@{ GroupComponent = 'Win32_Group.Domain="COMPUTER",Name="Administrators"' }) + } -ModuleName UsersAndAccounts + + { Get-UserGroups -User $MockUser } | Should -Not -Throw + } + } + + Context "Format-ADSIUser Tests" { + It "Should require User parameter" { + { Format-ADSIUser } | Should -Throw + } + + It "Should accept User parameter with correct schema" { + $ValidUser = [PSCustomObject]@{ + SchemaClassName = 'User' + Path = 'WinNT://COMPUTER/TestUser' + } + + { Format-ADSIUser -User $ValidUser } | Should -Not -Throw + } + + It "Should handle array of users" { + $Users = @( + [PSCustomObject]@{ SchemaClassName = 'User'; Path = 'WinNT://COMPUTER/User1' }, + [PSCustomObject]@{ SchemaClassName = 'User'; Path = 'WinNT://COMPUTER/User2' } + ) + + { Format-ADSIUser -User $Users } | Should -Not -Throw + } + + It "Should validate SchemaClassName is User" { + $InvalidUser = [PSCustomObject]@{ + SchemaClassName = 'Group' + Path = 'WinNT://COMPUTER/SomeGroup' + } + + { Format-ADSIUser -User $InvalidUser } | Should -Throw + } + } + + Context "Cross-Platform Considerations" { + It "Should handle non-Windows platforms gracefully" { + if ($IsLinux -or $IsMacOS) { + # On non-Windows platforms, ADSI operations should be mockable + { Get-Group -Name 'TestGroup' } | Should -Not -Throw + { Get-User -Name 'TestUser' } | Should -Not -Throw + } + } + + It "Should handle ADSI constructor failures" { + # Mock ADSI constructor to fail + Mock -CommandName 'Invoke-Expression' -MockWith { + throw "ADSI not available" + } + + # Functions should handle this gracefully (or throw appropriate errors) + { Get-Group -Name 'TestGroup' } | Should -Throw + } + } + + Context "Caching Behavior" { + It "Should cache group results" { + # First call should initialize cache + Get-Group -Name 'Administrators' + + # Second call should use cache + Get-Group -Name 'Administrators' + + # This is difficult to test without access to internal variables + # but at least verify it doesn't throw + $true | Should -Be $true + } + + It "Should cache user results" { + # First call should initialize cache + Get-User -Name 'TestUser' + + # Second call should use cache + Get-User -Name 'TestUser' + + # This is difficult to test without access to internal variables + # but at least verify it doesn't throw + $true | Should -Be $true + } + } + + Context "Error Handling" { + It "Should handle missing groups gracefully" -Skip:($IsLinux -or $IsMacOS) { + { Get-Group -Name 'NonExistentGroup' } | Should -Not -Throw + } + + It "Should handle missing users gracefully" -Skip:($IsLinux -or $IsMacOS) { + { Get-User -Name 'NonExistentUser' } | Should -Not -Throw + } + + It "Should handle ADSI exceptions" { + Mock -CommandName 'Invoke-Expression' -MockWith { + throw "ADSI access denied" + } + + { Get-Group -Name 'TestGroup' } | Should -Throw + } + } +} \ No newline at end of file diff --git a/tests/common/Windows/Get-LastSyncTime.Tests.ps1 b/tests/common/Windows/Get-LastSyncTime.Tests.ps1 new file mode 100644 index 00000000..4f8f6026 --- /dev/null +++ b/tests/common/Windows/Get-LastSyncTime.Tests.ps1 @@ -0,0 +1,18 @@ +BeforeDiscovery { + Import-Module "$PSScriptRoot/../../../src/common/Windows.psm1" +} + +Describe 'Get-LastSyncTime Tests' -Skip:($IsWindows -eq $false) -Fixture { + Context 'Basic Functionality' { + It 'Should return a DateTime object' { + $Result = Get-LastSyncTime + + $Result | Should -BeOfType [DateTime] + } + + It 'Should parse valid w32tm output correctly' { + $Result = Get-LastSyncTime + $Result.Year | Should -Not -Be 1970 + } + } +} diff --git a/tests/common/Windows/Sync-Time.Tests.ps1 b/tests/common/Windows/Sync-Time.Tests.ps1 new file mode 100644 index 00000000..268cd456 --- /dev/null +++ b/tests/common/Windows/Sync-Time.Tests.ps1 @@ -0,0 +1,205 @@ +BeforeDiscovery { + Import-Module "$PSScriptRoot/../../../src/common/Windows.psm1" +} + +Describe 'Sync-Time Tests' { + BeforeAll { + # Mock all external dependencies for cross-platform testing + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + Mock Get-LastSyncTime { return (Get-Date).AddHours(-1) } -ModuleName Windows + } + + BeforeEach { + # Reset mocks for each test to ensure clean state + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + Mock Get-LastSyncTime { return (Get-Date).AddHours(-1) } -ModuleName Windows + } + + Context 'Basic Functionality' { + It 'Should return a Boolean value' { + # Mock Get-LastSyncTime to return a recent time (no sync needed) + Mock Get-LastSyncTime { return (Get-Date).AddHours(-1) } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -BeOfType [Boolean] + } + + It 'Should return False when last sync time is within threshold' { + # Mock Get-LastSyncTime to return a recent time (within default 7 days) + Mock Get-LastSyncTime { return (Get-Date).AddDays(-2) } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -Be $false + } + + It 'Should return True and trigger resync when last sync time exceeds threshold' { + # Mock Get-LastSyncTime to return an old time (beyond default 7 days) + Mock Get-LastSyncTime { return (Get-Date).AddDays(-10) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -Be $true + + # Verify w32tm was called with correct parameters + Should -Invoke w32tm -ModuleName Windows -ParameterFilter { $args -contains '/resync' -and $args -contains '/force' } + } + + It 'Should respect custom threshold parameter' { + # Mock Get-LastSyncTime to return a time 2 days ago + Mock Get-LastSyncTime { return (Get-Date).AddDays(-2) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + # Set threshold to 1 day - should trigger resync + $CustomThreshold = New-TimeSpan -Days 1 + $Result = Sync-Time -Threshold $CustomThreshold + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows -ParameterFilter { $args -contains '/resync' -and $args -contains '/force' } + } + + It 'Should handle different threshold units' { + # Mock Get-LastSyncTime to return a time 3 hours ago + Mock Get-LastSyncTime { return (Get-Date).AddHours(-3) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + # Test with hours threshold + $HoursThreshold = New-TimeSpan -Hours 2 + $Result = Sync-Time -Threshold $HoursThreshold + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows -ParameterFilter { $args -contains '/resync' -and $args -contains '/force' } + } + } + + Context 'Default Parameters' { + It 'Should use 7 days as default threshold' { + # Mock Get-LastSyncTime to return exactly 7 days ago + Mock Get-LastSyncTime { return (Get-Date).AddDays(-7).AddMinutes(-1) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows + } + + It 'Should not sync when exactly at threshold' { + # Mock Get-LastSyncTime to return exactly 7 days ago + Mock Get-LastSyncTime { return (Get-Date).AddDays(-7) } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -Be $false + } + } + + Context 'Edge Cases' { + It 'Should handle future last sync time' { + # Mock Get-LastSyncTime to return a future time (system clock skew) + Mock Get-LastSyncTime { return (Get-Date).AddDays(1) } -ModuleName Windows + + $Result = Sync-Time + + # Should not sync when last sync is in the future + $Result | Should -Be $false + } + + It 'Should handle Unix epoch last sync time' { + # Mock Get-LastSyncTime to return Unix epoch (never synced) + Mock Get-LastSyncTime { return (Get-Date -Year 1970 -Month 1 -Day 1) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + $Result = Sync-Time + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows + } + + It 'Should handle very large threshold values' { + # Mock Get-LastSyncTime to return a very old time + Mock Get-LastSyncTime { return (Get-Date).AddDays(-365) } -ModuleName Windows + + # Set a very large threshold (2 years) + $LargeThreshold = New-TimeSpan -Days 730 + $Result = Sync-Time -Threshold $LargeThreshold + + $Result | Should -Be $false + } + + It 'Should handle very small threshold values' { + # Mock Get-LastSyncTime to return a time 5 minutes ago + Mock Get-LastSyncTime { return (Get-Date).AddMinutes(-5) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + # Set a very small threshold (1 minute) + $SmallThreshold = New-TimeSpan -Minutes 1 + $Result = Sync-Time -Threshold $SmallThreshold + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows + } + } + + Context 'Error Handling' { + It 'Should handle w32tm resync command failure' { + # Mock Get-LastSyncTime to return an old time + Mock Get-LastSyncTime { return (Get-Date).AddDays(-10) } -ModuleName Windows + Mock w32tm { throw 'Access denied' } -ModuleName Windows + + # Should still return true even if w32tm fails (indicates sync was attempted) + $Result = Sync-Time + + $Result | Should -Be $true + Should -Invoke w32tm -ModuleName Windows + } + + It 'Should handle Get-LastSyncTime returning null' { + Mock Get-LastSyncTime { return $null } -ModuleName Windows + + # This test depends on how the function handles null from Get-LastSyncTime + # If it throws, we catch it; if it handles gracefully, we verify behavior + { $Result = Sync-Time } | Should -Not -Throw + } + } + + Context 'Parameter Validation' { + It 'Should validate threshold parameter is not null or empty' { + { Sync-Time -Threshold $null } | Should -Throw + } + + It 'Should accept zero timespan threshold' { + # Mock Get-LastSyncTime to return current time + Mock Get-LastSyncTime { return (Get-Date) } -ModuleName Windows + + $ZeroThreshold = New-TimeSpan -Seconds 0 + $Result = Sync-Time -Threshold $ZeroThreshold + + $Result | Should -Be $false + } + + It 'Should accept negative timespan threshold' { + # Mock Get-LastSyncTime to return current time + Mock Get-LastSyncTime { return (Get-Date) } -ModuleName Windows + Mock w32tm { return 'Sending resync command to local computer' } -ModuleName Windows + + $NegativeThreshold = New-TimeSpan -Days -1 + $Result = Sync-Time -Threshold $NegativeThreshold + + # With negative threshold, any sync time should trigger resync + $Result | Should -Be $true + } + } + + Context 'Integration with Get-LastSyncTime' { + It 'Should call Get-LastSyncTime to determine sync status' { + Mock Get-LastSyncTime { return (Get-Date).AddDays(-2) } -ModuleName Windows + + $Result = Sync-Time + + Should -Invoke Get-LastSyncTime -ModuleName Windows -Exactly 1 + } + } +}