From a220f57ce9c8fb2e6c243bbbcdfb811fd6f884d4 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 03:03:45 +0000 Subject: [PATCH 01/29] adding some basic tests and linting --- .github/workflows/test.yml | 39 +++++++++++++++++++++++++++++++ tests/test_rcgmsa.ps1 | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 tests/test_rcgmsa.ps1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f3986b4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Test + +on: + push: + branches: [root] + pull_request: + branches: [root] + workflow_dispatch: + +defaults: + run: + shell: powershell + +jobs: + test: + name: Validate + runs-on: windows-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install + run: | + Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser + Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + + - name: Lint + run: | + $results = Invoke-ScriptAnalyzer -Path .\rcgmsa.ps1 -Severity Error + if ($results) { + $results | Format-Table + Write-Error "PSScriptAnalyzer found issues. Please fix them." + } else { + Write-Host "PSScriptAnalyzer passed." + } + + - name: Test + run: | + Invoke-Pester -Path .\tests\test_rcgmsa.ps1 -Output Detailed diff --git a/tests/test_rcgmsa.ps1 b/tests/test_rcgmsa.ps1 new file mode 100644 index 0000000..02022c6 --- /dev/null +++ b/tests/test_rcgmsa.ps1 @@ -0,0 +1,48 @@ +BeforeAll { + $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" +} + +Describe "Validation" { + + Context "Parameter Validation" { + It "valid computers" { + { & $scriptPath -Command "echo hi" -Computers "localhost", "192.168.1.1" -User "gmsa$" -v } | Should -Not -Throw + } + + It "invalid computers" { + { & $scriptPath -Command "echo hi" -Computers "invalid_name!" -User "gmsa$" -v } | Should -Throw "Creativity meets catastrophe" + } + } + + Context "execution flow" { + Mock New-Object { + return [PSCredential]::new("User", + (ConvertTo-SecureString "supersecret" -AsPlainText -Force) + ) + } + Mock Invoke-Command { return "Command Success" } + Mock Import-Module {} + Mock Install-Module {} + Mock Get-Secret { return "FakeSecretData" } + Mock Unlock-SecretStore {} + + Mock Get-Command -ParameterFilter { $Name -eq 'Get-Secret' } { return $true } + + It "keeper module" { + & $scriptPath -Command "hostname" -Computers "server1" -User "gmsa$" -Keeper "RecordID" -Vault "devops" + + Assert-MockCalled Import-Module -ParameterFilter { $Name -eq "SecretManagement.Keeper" } -Times 1 + } + + It "script block" { + Mock Invoke-Command { + param($ScriptBlock) + return $ScriptBlock.ToString() + } + + $result = & $scriptPath -Command "Get-Process" -Computers "server1" -User "gmsa$" + + Assert-MockCalled Invoke-Command -Times 1 + } + } +} From 8f62a048287e809ccce3f0ae24595254308f6dcc Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 04:39:23 +0000 Subject: [PATCH 02/29] reworking test suite, adding lint rule exception for ConvertTo-SecureString with plain text as it's a necessary evil in this use case. --- .github/workflows/test.yml | 9 ++- tests/test_rcgmsa.ps1 | 127 +++++++++++++++++++++++++++++-------- 2 files changed, 107 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f3986b4..0c80fd5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,12 +26,15 @@ jobs: - name: Lint run: | - $results = Invoke-ScriptAnalyzer -Path .\rcgmsa.ps1 -Severity Error + $excludedRules = @( + 'PSAvoidUsingConvertToSecureStringWithPlainText' + ) + $results = Invoke-ScriptAnalyzer -ExcludeRule $excludedRules -Path .\rcgmsa.ps1 -Severity Error if ($results) { $results | Format-Table - Write-Error "PSScriptAnalyzer found issues. Please fix them." + Write-Error 'PSScriptAnalyzer found issues. Please fix them.' } else { - Write-Host "PSScriptAnalyzer passed." + Write-Host 'PSScriptAnalyzer passed.' } - name: Test diff --git a/tests/test_rcgmsa.ps1 b/tests/test_rcgmsa.ps1 index 02022c6..b2f3cd4 100644 --- a/tests/test_rcgmsa.ps1 +++ b/tests/test_rcgmsa.ps1 @@ -1,48 +1,123 @@ BeforeAll { + $env:VAULT = 'VaultPassword' + $keeperSecret = @{ + API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' + DB_PASS = 'SuperSecretPass' + Files = @{ + 'license.key' = 'RECORD_ID_FOR_FILE' + } + Keys = @('API_KEY', 'DB_PASS', 'Files') + } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" } -Describe "Validation" { +Describe 'Integration Tests' { + + Context 'Input Validation' { + It 'Should accept valid hostnames or IPs' { + { & $scriptPath -Command 'Get-Date' -Computers 'localhost','10.0.0.1' -User 'svc_account$' -v } | Should -Not -Throw + } - Context "Parameter Validation" { - It "valid computers" { - { & $scriptPath -Command "echo hi" -Computers "localhost", "192.168.1.1" -User "gmsa$" -v } | Should -Not -Throw + It 'Should reject invalid characters in computer names' { + { & $scriptPath -Command 'Get-Date' -Computers 'bad_host!' -User 'svc_account$' -v } | Should -Throw 'Creativity meets catastrophe' } - It "invalid computers" { - { & $scriptPath -Command "echo hi" -Computers "invalid_name!" -User "gmsa$" -v } | Should -Throw "Creativity meets catastrophe" + It 'Should reject invalid characters in orb names' { + { & $scriptPath -Command 'Get-Date' -Computers 'localhost' -User 'svc_account$' -Orbs 'bad_orb!' -v } | Should -Throw "For FQDN's sake" } } - Context "execution flow" { - Mock New-Object { - return [PSCredential]::new("User", - (ConvertTo-SecureString "supersecret" -AsPlainText -Force) - ) + Context 'Keeper Vault Integration' { + BeforeAll { + Mock New-Object { + return [PSCredential]::new('User', + (ConvertTo-SecureString 'pass' -AsPlainText -Force) + ) + } + Mock Import-Module {} + Mock Install-Module {} + Mock Invoke-Command { return 'Remote Execution Successful' } + Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } + Mock New-Item { return 'C:\Mock\Temp' } + Mock Remove-Item {} + Mock Set-Content {} + Mock Unlock-SecretStore {} } - Mock Invoke-Command { return "Command Success" } - Mock Import-Module {} - Mock Install-Module {} - Mock Get-Secret { return "FakeSecretData" } - Mock Unlock-SecretStore {} - Mock Get-Command -ParameterFilter { $Name -eq 'Get-Secret' } { return $true } + It 'Should retrieve secrets and process files when -Keeper is used' { + Mock Get-Secret -MockWith { + if ($args[1] -match 'RECORD_ID_FOR_FILE') { + return [System.Text.Encoding]::UTF8.GetBytes('FileContent') + } + return $keeperSecret + } - It "keeper module" { - & $scriptPath -Command "hostname" -Computers "server1" -User "gmsa$" -Keeper "RecordID" -Vault "devops" + & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper 'RecordID' -Vault 'devops' - Assert-MockCalled Import-Module -ParameterFilter { $Name -eq "SecretManagement.Keeper" } -Times 1 + Assert-MockCalled Unlock-SecretStore -Times 1 + Assert-MockCalled Get-Secret -ParameterFilter { $Name -eq 'RecordID' } -Times 1 + Assert-MockCalled Get-Secret -ParameterFilter { $Name -eq 'RECORD_ID_FOR_FILE' } -Times 1 + Assert-MockCalled Set-Content -ParameterFilter { $Path -match 'license.key' } -Times 1 } + } - It "script block" { - Mock Invoke-Command { - param($ScriptBlock) - return $ScriptBlock.ToString() + Context 'Logic Branching' { + BeforeAll { + Mock New-Object { + return [PSCredential]::new('User', + (ConvertTo-SecureString 'pass' -AsPlainText -Force) + ) } + Mock Import-Module {} + Mock New-Item { return 'C:\Mock\Temp' } + Mock Remove-Item {} + Mock Set-Content {} + } + + It 'Should execute the orbs logic when provided' { + Mock Invoke-Command { return 'Jump Host Success' } + + & $scriptPath -Command 'whoami' -Computers 'target1' -User 'gmsa$' -Orbs 'jump1' - $result = & $scriptPath -Command "Get-Process" -Computers "server1" -User "gmsa$" - Assert-MockCalled Invoke-Command -Times 1 } + + It 'Should retry with -IncludePortInSPN if a specific SPN error occurs' { + Mock Invoke-Command -ParameterFilter { -not $IncludePortInSPN } -MockWith { + $err = [System.Management.Automation.ErrorRecord]::new( + [Exception]::new('SPN Error'), + '-2144108387,PSSessionStateBroken', + [System.Management.Automation.ErrorCategory]::OpenError, + $null + ) + throw $err + } + + Mock Invoke-Command -ParameterFilter { $IncludePortInSPN } -MockWith { return 'Retry Successful' } + + $result = & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' + + Assert-MockCalled Invoke-Command -Times 2 + $result | Should -Be 'Retry Successful' + } + } + + Context 'Environment Variable Injection' { + It 'Should inject KEEPER_ variables into the scriptblock' { + Mock Get-Secret -Return $keeperSecret + Mock Invoke-Command -MockWith { + param($ScriptBlock) + return $ScriptBlock.ToString() + } + Mock New-Item { return 'C:\Mock\Temp' } + Mock Remove-Item {} + Mock Set-Content {} + Mock Unlock-SecretStore {} + + $sbContent = & $scriptPath -Command 'echo hi' -Computers 'server1' -User 'gmsa$' -Keeper 'rec1' + + $sbContent | Should -Match 'KEEPER_' + $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' + } } } From 1f051a809bf4c85955e8e6a111a0db3d86152d9c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:28:02 +0000 Subject: [PATCH 03/29] adding input validation for -v, removing -v from other input validation tests and being hyper specific about the expected err for computers & orbs name validation, adding stub for Install-Module which should exist but doesn't for some reason, updating workflow to reflect the status of the test outcomes. --- .github/workflows/test.yml | 7 ++++++- tests/test_rcgmsa.ps1 | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c80fd5..e029d11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,6 +30,7 @@ jobs: 'PSAvoidUsingConvertToSecureStringWithPlainText' ) $results = Invoke-ScriptAnalyzer -ExcludeRule $excludedRules -Path .\rcgmsa.ps1 -Severity Error + if ($results) { $results | Format-Table Write-Error 'PSScriptAnalyzer found issues. Please fix them.' @@ -39,4 +40,8 @@ jobs: - name: Test run: | - Invoke-Pester -Path .\tests\test_rcgmsa.ps1 -Output Detailed + $config = New-PesterConfiguration + $config.Run.Exit = $true + $config.Output.Verbosity = 'Detailed' + + Invoke-Pester -Configuration $config -Path .\tests\test_rcgmsa.ps1 diff --git a/tests/test_rcgmsa.ps1 b/tests/test_rcgmsa.ps1 index b2f3cd4..f428e7e 100644 --- a/tests/test_rcgmsa.ps1 +++ b/tests/test_rcgmsa.ps1 @@ -15,20 +15,28 @@ Describe 'Integration Tests' { Context 'Input Validation' { It 'Should accept valid hostnames or IPs' { - { & $scriptPath -Command 'Get-Date' -Computers 'localhost','10.0.0.1' -User 'svc_account$' -v } | Should -Not -Throw + { & $scriptPath -Command 'Get-Date' -Computers 'localhost','10.0.0.1' -User 'svc_account$' } | Should -Not -Throw } It 'Should reject invalid characters in computer names' { - { & $scriptPath -Command 'Get-Date' -Computers 'bad_host!' -User 'svc_account$' -v } | Should -Throw 'Creativity meets catastrophe' + $expectedErr = "Cannot validate argument on parameter 'Computers'. Creativity meets catastrophe, invalid computer name: bad_host!" + { & $scriptPath -Command 'Get-Date' -Computers 'bad_host!' -User 'svc_account$' } | Should -Throw $expectedErr } It 'Should reject invalid characters in orb names' { - { & $scriptPath -Command 'Get-Date' -Computers 'localhost' -User 'svc_account$' -Orbs 'bad_orb!' -v } | Should -Throw "For FQDN's sake" + $expectedErr = "Cannot validate argument on parameter 'Orbs'. For FQDN's sake, invalid computer name: bad_orb!" + { & $scriptPath -Command 'Get-Date' -Computers 'localhost' -User 'svc_account$' -Orbs 'bad_orb!' } | Should -Throw $expectedErr + } + + It 'Should output a semantic version number' { + $output = & $scriptPath -v + $output | Should -Match '^Version: \d+\.\d+\.\d+$' } } Context 'Keeper Vault Integration' { BeforeAll { + function Install-Module {} Mock New-Object { return [PSCredential]::new('User', (ConvertTo-SecureString 'pass' -AsPlainText -Force) @@ -83,7 +91,7 @@ Describe 'Integration Tests' { } It 'Should retry with -IncludePortInSPN if a specific SPN error occurs' { - Mock Invoke-Command -ParameterFilter { -not $IncludePortInSPN } -MockWith { + Mock Invoke-Command -ParameterFilter { -not ($SessionOption.IncludePortInSPN) } -MockWith { $err = [System.Management.Automation.ErrorRecord]::new( [Exception]::new('SPN Error'), '-2144108387,PSSessionStateBroken', @@ -93,7 +101,7 @@ Describe 'Integration Tests' { throw $err } - Mock Invoke-Command -ParameterFilter { $IncludePortInSPN } -MockWith { return 'Retry Successful' } + Mock Invoke-Command -ParameterFilter { $SessionOption.IncludePortInSPN -eq $true } -MockWith { return 'Retry Successful' } $result = & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' @@ -104,7 +112,7 @@ Describe 'Integration Tests' { Context 'Environment Variable Injection' { It 'Should inject KEEPER_ variables into the scriptblock' { - Mock Get-Secret -Return $keeperSecret + Mock Get-Secret -MockWith { return $keeperSecret } Mock Invoke-Command -MockWith { param($ScriptBlock) return $ScriptBlock.ToString() From ac43fce4ffc717152037473f11c8e6a9a2cef0c4 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 05:38:29 +0000 Subject: [PATCH 04/29] Not sure why Pester doesn't let you pass the -Path flag with the -Configuration flag but here we are. --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e029d11..da3c949 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,8 @@ jobs: - name: Test run: | $config = New-PesterConfiguration - $config.Run.Exit = $true $config.Output.Verbosity = 'Detailed' + $config.Run.Exit = $true + $config.Run.Path = .\tests\test_rcgmsa.ps1 - Invoke-Pester -Configuration $config -Path .\tests\test_rcgmsa.ps1 + Invoke-Pester -Configuration $config From a0b58735d3c5dbd5d3e61b261f3c3bcc047ecd04 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 06:46:03 +0000 Subject: [PATCH 05/29] switching shells to pwsh, adding missing stubs for missing Keeper functions, attempting to grab correct command output to validate certain test outcomes. --- .github/workflows/test.yml | 4 ++-- tests/{test_rcgmsa.ps1 => rcgmsa.Tests.ps1} | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) rename tests/{test_rcgmsa.ps1 => rcgmsa.Tests.ps1} (92%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da3c949..32da34f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ on: defaults: run: - shell: powershell + shell: pwsh jobs: test: @@ -43,6 +43,6 @@ jobs: $config = New-PesterConfiguration $config.Output.Verbosity = 'Detailed' $config.Run.Exit = $true - $config.Run.Path = .\tests\test_rcgmsa.ps1 + $config.Run.Path = .\tests\rcgmsa.Tests.ps1 Invoke-Pester -Configuration $config diff --git a/tests/test_rcgmsa.ps1 b/tests/rcgmsa.Tests.ps1 similarity index 92% rename from tests/test_rcgmsa.ps1 rename to tests/rcgmsa.Tests.ps1 index f428e7e..5928d3e 100644 --- a/tests/test_rcgmsa.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -1,4 +1,8 @@ BeforeAll { + function Get-Secret {} + function Install-Module {} + function Unlock-SecretStore {} + $env:VAULT = 'VaultPassword' $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' @@ -15,7 +19,8 @@ Describe 'Integration Tests' { Context 'Input Validation' { It 'Should accept valid hostnames or IPs' { - { & $scriptPath -Command 'Get-Date' -Computers 'localhost','10.0.0.1' -User 'svc_account$' } | Should -Not -Throw + $expectedErr = "Connecting to remote server * failed with the following error message : The WinRM client cannot process the request.*" + { & $scriptPath -Command 'Get-Date' -Computers 'localhost','127.0.0.1' -User 'svc_account$' } | Should -Throw $expectedErr } It 'Should reject invalid characters in computer names' { @@ -29,7 +34,7 @@ Describe 'Integration Tests' { } It 'Should output a semantic version number' { - $output = & $scriptPath -v + $output = & $scriptPath -v 6>&1 $output | Should -Match '^Version: \d+\.\d+\.\d+$' } } @@ -103,7 +108,7 @@ Describe 'Integration Tests' { Mock Invoke-Command -ParameterFilter { $SessionOption.IncludePortInSPN -eq $true } -MockWith { return 'Retry Successful' } - $result = & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' + $result = & $scriptPath -Command 'hostname' -Computers 'localhost' -User 'gmsa$' 6>&1 Assert-MockCalled Invoke-Command -Times 2 $result | Should -Be 'Retry Successful' @@ -122,7 +127,7 @@ Describe 'Integration Tests' { Mock Set-Content {} Mock Unlock-SecretStore {} - $sbContent = & $scriptPath -Command 'echo hi' -Computers 'server1' -User 'gmsa$' -Keeper 'rec1' + $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper 'rec1' $sbContent | Should -Match 'KEEPER_' $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' From 00a9db7946920f1ebe0ae4506ceb0f1101e77e61 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:41:57 +0000 Subject: [PATCH 06/29] still trying to get workflow to reflect test result state, using Keeper input that will pass param validation, fix for SPN test, a little bit of clean-up. --- .github/workflows/test.yml | 6 +++++- tests/rcgmsa.Tests.ps1 | 44 +++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 32da34f..8529622 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,4 +45,8 @@ jobs: $config.Run.Exit = $true $config.Run.Path = .\tests\rcgmsa.Tests.ps1 - Invoke-Pester -Configuration $config + $result = Invoke-Pester -Configuration $config + + if ($result.FailedCount -gt 0) { + exit 1 + } diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 5928d3e..69ac7fd 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -19,8 +19,7 @@ Describe 'Integration Tests' { Context 'Input Validation' { It 'Should accept valid hostnames or IPs' { - $expectedErr = "Connecting to remote server * failed with the following error message : The WinRM client cannot process the request.*" - { & $scriptPath -Command 'Get-Date' -Computers 'localhost','127.0.0.1' -User 'svc_account$' } | Should -Throw $expectedErr + { & $scriptPath -Command 'Get-Date' -Computers 'localhost','127.0.0.1' -User 'svc_account$' } | Should -Not -Throw } It 'Should reject invalid characters in computer names' { @@ -41,10 +40,10 @@ Describe 'Integration Tests' { Context 'Keeper Vault Integration' { BeforeAll { - function Install-Module {} Mock New-Object { - return [PSCredential]::new('User', - (ConvertTo-SecureString 'pass' -AsPlainText -Force) + return [PSCredential]::new( + 'User', + (ConvertTo-SecureString 'pass' -AsPlainText -Force) ) } Mock Import-Module {} @@ -65,20 +64,27 @@ Describe 'Integration Tests' { return $keeperSecret } - & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper 'RecordID' -Vault 'devops' - + & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' -Vault 'devops' + + Assert-MockCalled Get-Secret -ParameterFilter { + $Name -eq 'RecordID' + } -Times 1 + Assert-MockCalled Get-Secret -ParameterFilter { + $Name -eq 'RECORD_ID_FOR_FILE' + } -Times 1 + Assert-MockCalled Set-Content -ParameterFilter { + $Path -match 'license.key' + } -Times 1 Assert-MockCalled Unlock-SecretStore -Times 1 - Assert-MockCalled Get-Secret -ParameterFilter { $Name -eq 'RecordID' } -Times 1 - Assert-MockCalled Get-Secret -ParameterFilter { $Name -eq 'RECORD_ID_FOR_FILE' } -Times 1 - Assert-MockCalled Set-Content -ParameterFilter { $Path -match 'license.key' } -Times 1 } } Context 'Logic Branching' { BeforeAll { Mock New-Object { - return [PSCredential]::new('User', - (ConvertTo-SecureString 'pass' -AsPlainText -Force) + return [PSCredential]::new( + 'User', + (ConvertTo-SecureString 'pass' -AsPlainText -Force) ) } Mock Import-Module {} @@ -96,7 +102,9 @@ Describe 'Integration Tests' { } It 'Should retry with -IncludePortInSPN if a specific SPN error occurs' { - Mock Invoke-Command -ParameterFilter { -not ($SessionOption.IncludePortInSPN) } -MockWith { + Mock Invoke-Command -ParameterFilter { + -not ($SessionOption.IncludePortInSPN) + } -MockWith { $err = [System.Management.Automation.ErrorRecord]::new( [Exception]::new('SPN Error'), '-2144108387,PSSessionStateBroken', @@ -106,12 +114,14 @@ Describe 'Integration Tests' { throw $err } - Mock Invoke-Command -ParameterFilter { $SessionOption.IncludePortInSPN -eq $true } -MockWith { return 'Retry Successful' } + Mock Invoke-Command -MockWith { return 'Retry Successful' } - $result = & $scriptPath -Command 'hostname' -Computers 'localhost' -User 'gmsa$' 6>&1 + & $scriptPath -Command 'hostname' -Computers 'localhost' -User 'gmsa$' Assert-MockCalled Invoke-Command -Times 2 - $result | Should -Be 'Retry Successful' + Assert-MockCalled Invoke-Command -ParameterFilter { + $SessionOption.IncludePortInSPN -eq $true + } -Times 1 } } @@ -127,7 +137,7 @@ Describe 'Integration Tests' { Mock Set-Content {} Mock Unlock-SecretStore {} - $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper 'rec1' + $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' $sbContent | Should -Match 'KEEPER_' $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' From e3a986216e2b1e3c50b439018e6e9c124cef35c8 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 07:58:55 +0000 Subject: [PATCH 07/29] removing unecessary stubs and mocks for Import and Install module function calls in test file --- tests/rcgmsa.Tests.ps1 | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 69ac7fd..adcbcce 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -1,8 +1,4 @@ BeforeAll { - function Get-Secret {} - function Install-Module {} - function Unlock-SecretStore {} - $env:VAULT = 'VaultPassword' $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' @@ -46,8 +42,6 @@ Describe 'Integration Tests' { (ConvertTo-SecureString 'pass' -AsPlainText -Force) ) } - Mock Import-Module {} - Mock Install-Module {} Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } Mock New-Item { return 'C:\Mock\Temp' } @@ -87,7 +81,6 @@ Describe 'Integration Tests' { (ConvertTo-SecureString 'pass' -AsPlainText -Force) ) } - Mock Import-Module {} Mock New-Item { return 'C:\Mock\Temp' } Mock Remove-Item {} Mock Set-Content {} From 23bf6a9f033aaffc575f506ac43285948c3af6ff Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 18:21:59 +0000 Subject: [PATCH 08/29] combining Keeper tests, trying to setup local vault to remove more mocks, including vault file for setup validation --- tests/rcgmsa.Tests.ps1 | 86 +++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index adcbcce..807932f 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -4,11 +4,42 @@ BeforeAll { API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' Files = @{ - 'license.key' = 'RECORD_ID_FOR_FILE' + 'license.key' = 'RECORD_ID_LICENSE' + 'config.json' = 'RECORD_ID_CONFIG' } Keys = @('API_KEY', 'DB_PASS', 'Files') } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" + $setupPath = "$PSScriptRoot/../vault.ps1" + $vaultName = 'devops' + + function Get-Credential { + [CmdletBinding()] + param( + [Parameter(Mandatory=$false)] + [string]$UserName, + + [Parameter(Mandatory=$false)] + [string]$Message + ) + $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force + return [PSCredential]::new($UserName, $securePass) + } + + $credFile = [System.IO.Path]::GetTempFileName() + . $setupPath -Path $credFile -Vault $vaultName + + Remove-Item Function:\Get-Credential -ErrorAction Stop + + $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force + Unlock-SecretStore -Password $securePass -Force + + $configBytes = [System.Text.Encoding]::UTF8.GetBytes('ConfigContent') + $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') + + Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName + Set-Secret -Name 'RECORD_ID_CONFIG' -Secret $configBytes -Vault $vaultName + Set-Secret -Name 'RECORD_ID_LICENSE' -Secret $fileBytes -Vault $vaultName } Describe 'Integration Tests' { @@ -47,29 +78,35 @@ Describe 'Integration Tests' { Mock New-Item { return 'C:\Mock\Temp' } Mock Remove-Item {} Mock Set-Content {} - Mock Unlock-SecretStore {} } It 'Should retrieve secrets and process files when -Keeper is used' { - Mock Get-Secret -MockWith { - if ($args[1] -match 'RECORD_ID_FOR_FILE') { - return [System.Text.Encoding]::UTF8.GetBytes('FileContent') - } - return $keeperSecret - } - & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' -Vault 'devops' - Assert-MockCalled Get-Secret -ParameterFilter { - $Name -eq 'RecordID' - } -Times 1 - Assert-MockCalled Get-Secret -ParameterFilter { - $Name -eq 'RECORD_ID_FOR_FILE' + $api_key = [Environment]::GetEnvironmentVariable('KEEPER_API_KEY', 'User') + $api_key | Should -Be $keeperSecret.API_KEY + + [Environment]::SetEnvironmentVariable('KEEPER_API_KEY', $null, 'User') + + Assert-MockCalled Set-Content -ParameterFilter { + $Path -match 'config.json' } -Times 1 + Assert-MockCalled Set-Content -ParameterFilter { $Path -match 'license.key' } -Times 1 - Assert-MockCalled Unlock-SecretStore -Times 1 + } + + It 'Should inject KEEPER_ variables into the scriptblock' { + Mock Invoke-Command -MockWith { + param($ScriptBlock) + return $ScriptBlock.ToString() + } + + $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' + + $sbContent | Should -Match 'KEEPER_' + $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' } } @@ -117,23 +154,4 @@ Describe 'Integration Tests' { } -Times 1 } } - - Context 'Environment Variable Injection' { - It 'Should inject KEEPER_ variables into the scriptblock' { - Mock Get-Secret -MockWith { return $keeperSecret } - Mock Invoke-Command -MockWith { - param($ScriptBlock) - return $ScriptBlock.ToString() - } - Mock New-Item { return 'C:\Mock\Temp' } - Mock Remove-Item {} - Mock Set-Content {} - Mock Unlock-SecretStore {} - - $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' - - $sbContent | Should -Match 'KEEPER_' - $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' - } - } } From 6fd805ec675601baf418029ef13a3f1f976c2132 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:16:23 +0000 Subject: [PATCH 09/29] too much forcing --- tests/rcgmsa.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 807932f..c1bd3cc 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -32,7 +32,7 @@ BeforeAll { Remove-Item Function:\Get-Credential -ErrorAction Stop $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force - Unlock-SecretStore -Password $securePass -Force + Unlock-SecretStore -Password $securePass $configBytes = [System.Text.Encoding]::UTF8.GetBytes('ConfigContent') $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') From d7d30760a5b77c24286b938469d7d1939b3a28a6 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:02:08 +0000 Subject: [PATCH 10/29] forgot to define secretName --- tests/rcgmsa.Tests.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index c1bd3cc..abaf2f3 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -9,6 +9,7 @@ BeforeAll { } Keys = @('API_KEY', 'DB_PASS', 'Files') } + $secretName = '9vb_wew-d6_AmgUNmIO6Ez' $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" $setupPath = "$PSScriptRoot/../vault.ps1" $vaultName = 'devops' @@ -81,7 +82,7 @@ Describe 'Integration Tests' { } It 'Should retrieve secrets and process files when -Keeper is used' { - & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' -Vault 'devops' + & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper $secretName -Vault 'devops' $api_key = [Environment]::GetEnvironmentVariable('KEEPER_API_KEY', 'User') $api_key | Should -Be $keeperSecret.API_KEY @@ -103,7 +104,7 @@ Describe 'Integration Tests' { return $ScriptBlock.ToString() } - $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper '9vb_wew-d6_AmgUNmIO6Ez' + $sbContent = & $scriptPath -Command 'echo hi' -Computers 'localhost' -User 'gmsa$' -Keeper $secretName $sbContent | Should -Match 'KEEPER_' $sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable' From dcbfc571bbc68e0d9e337ab7ae70c61c783ac636 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:32:00 +0000 Subject: [PATCH 11/29] removing unsupported list entry in keeperSecret --- tests/rcgmsa.Tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index abaf2f3..2c67033 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -7,7 +7,6 @@ BeforeAll { 'license.key' = 'RECORD_ID_LICENSE' 'config.json' = 'RECORD_ID_CONFIG' } - Keys = @('API_KEY', 'DB_PASS', 'Files') } $secretName = '9vb_wew-d6_AmgUNmIO6Ez' $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" From 3c002f846544efe8e616b597a41d48418d02ea5f Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:43:49 +0000 Subject: [PATCH 12/29] ditching the nested hashtable for Files as that isn't supported by SecretStore. --- tests/rcgmsa.Tests.ps1 | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 2c67033..0cecde1 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -3,10 +3,7 @@ BeforeAll { $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' - Files = @{ - 'license.key' = 'RECORD_ID_LICENSE' - 'config.json' = 'RECORD_ID_CONFIG' - } + Files = 'license.key' } $secretName = '9vb_wew-d6_AmgUNmIO6Ez' $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" @@ -34,12 +31,10 @@ BeforeAll { $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force Unlock-SecretStore -Password $securePass - $configBytes = [System.Text.Encoding]::UTF8.GetBytes('ConfigContent') $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - Set-Secret -Name 'RECORD_ID_CONFIG' -Secret $configBytes -Vault $vaultName - Set-Secret -Name 'RECORD_ID_LICENSE' -Secret $fileBytes -Vault $vaultName + Set-Secret -Name $keeperSecret.Files -Secret $fileBytes -Vault $vaultName } Describe 'Integration Tests' { @@ -88,10 +83,6 @@ Describe 'Integration Tests' { [Environment]::SetEnvironmentVariable('KEEPER_API_KEY', $null, 'User') - Assert-MockCalled Set-Content -ParameterFilter { - $Path -match 'config.json' - } -Times 1 - Assert-MockCalled Set-Content -ParameterFilter { $Path -match 'license.key' } -Times 1 From 4862b5f1ac0ec0aa0c53b48852d11b68f2ccd245 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:54:55 +0000 Subject: [PATCH 13/29] switch to using the BeforeAll vault password used to create the real vault instead of the mock pass --- tests/rcgmsa.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 0cecde1..ccd13ef 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -65,7 +65,7 @@ Describe 'Integration Tests' { Mock New-Object { return [PSCredential]::new( 'User', - (ConvertTo-SecureString 'pass' -AsPlainText -Force) + (ConvertTo-SecureString $env.VAULT -AsPlainText -Force) ) } Mock Invoke-Command { return 'Remote Execution Successful' } @@ -106,7 +106,7 @@ Describe 'Integration Tests' { Mock New-Object { return [PSCredential]::new( 'User', - (ConvertTo-SecureString 'pass' -AsPlainText -Force) + (ConvertTo-SecureString $env.VAULT -AsPlainText -Force) ) } Mock New-Item { return 'C:\Mock\Temp' } From 9bb5739f73ad92fe4beece5a4ead13dacc0ff635 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Thu, 5 Feb 2026 03:58:13 +0000 Subject: [PATCH 14/29] . instead of : for :VAULT, duh --- tests/rcgmsa.Tests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index ccd13ef..d82c189 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -65,7 +65,7 @@ Describe 'Integration Tests' { Mock New-Object { return [PSCredential]::new( 'User', - (ConvertTo-SecureString $env.VAULT -AsPlainText -Force) + (ConvertTo-SecureString $env:VAULT -AsPlainText -Force) ) } Mock Invoke-Command { return 'Remote Execution Successful' } @@ -106,7 +106,7 @@ Describe 'Integration Tests' { Mock New-Object { return [PSCredential]::new( 'User', - (ConvertTo-SecureString $env.VAULT -AsPlainText -Force) + (ConvertTo-SecureString $env:VAULT -AsPlainText -Force) ) } Mock New-Item { return 'C:\Mock\Temp' } From 78d34b78b2f3e9ba57662c1107ff573c63af2401 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Sat, 7 Feb 2026 23:36:55 +0000 Subject: [PATCH 15/29] setting environment variable for vault password --- tests/rcgmsa.Tests.ps1 | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index d82c189..6bfd073 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -1,14 +1,20 @@ BeforeAll { - $env:VAULT = 'VaultPassword' $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' Files = 'license.key' } - $secretName = '9vb_wew-d6_AmgUNmIO6Ez' $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" + $secretName = '9vb_wew-d6_AmgUNmIO6Ez' $setupPath = "$PSScriptRoot/../vault.ps1" $vaultName = 'devops' + $vaultPassword = 'VaultPassword123' + + [Environment]::SetEnvironmentVariable( + "VAULT", + $vaultPassword, + [System.EnvironmentVariableTarget]::User + ) function Get-Credential { [CmdletBinding()] @@ -62,12 +68,6 @@ Describe 'Integration Tests' { Context 'Keeper Vault Integration' { BeforeAll { - Mock New-Object { - return [PSCredential]::new( - 'User', - (ConvertTo-SecureString $env:VAULT -AsPlainText -Force) - ) - } Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } Mock New-Item { return 'C:\Mock\Temp' } @@ -103,12 +103,6 @@ Describe 'Integration Tests' { Context 'Logic Branching' { BeforeAll { - Mock New-Object { - return [PSCredential]::new( - 'User', - (ConvertTo-SecureString $env:VAULT -AsPlainText -Force) - ) - } Mock New-Item { return 'C:\Mock\Temp' } Mock Remove-Item {} Mock Set-Content {} From 3a3db14aa0d281a6a27bf5957cb8b334e3f42250 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:44:51 +0000 Subject: [PATCH 16/29] using pass through on Pester call in actions workflow, letting vault script set the VAULT environment variable --- .github/workflows/test.yml | 8 ++------ tests/rcgmsa.Tests.ps1 | 9 ++------- vault.ps1 | 14 +++++++------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8529622..d70290c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: if ($results) { $results | Format-Table - Write-Error 'PSScriptAnalyzer found issues. Please fix them.' + Write-Error 'PSScriptAnalyzer found issues. Please fix them.' -ErrorAction Stop } else { Write-Host 'PSScriptAnalyzer passed.' } @@ -45,8 +45,4 @@ jobs: $config.Run.Exit = $true $config.Run.Path = .\tests\rcgmsa.Tests.ps1 - $result = Invoke-Pester -Configuration $config - - if ($result.FailedCount -gt 0) { - exit 1 - } + Invoke-Pester -Configuration $config -Passthru diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 6bfd073..e0db7bb 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -3,6 +3,7 @@ BeforeAll { API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' Files = 'license.key' + Keys = 'Files' } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" $secretName = '9vb_wew-d6_AmgUNmIO6Ez' @@ -10,12 +11,6 @@ BeforeAll { $vaultName = 'devops' $vaultPassword = 'VaultPassword123' - [Environment]::SetEnvironmentVariable( - "VAULT", - $vaultPassword, - [System.EnvironmentVariableTarget]::User - ) - function Get-Credential { [CmdletBinding()] param( @@ -25,7 +20,7 @@ BeforeAll { [Parameter(Mandatory=$false)] [string]$Message ) - $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force + $securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force return [PSCredential]::new($UserName, $securePass) } diff --git a/vault.ps1 b/vault.ps1 index 3570563..17b2ea3 100644 --- a/vault.ps1 +++ b/vault.ps1 @@ -40,24 +40,24 @@ $requiredModules.ForEach{ } Write-Host "Getting secure store password" -$credential = Get-Credential -UserName $vault -$credential.Password | Export-Clixml -Path $path +$credential = Get-Credential -UserName $Vault +$credential.Password | Export-Clixml -Path $Path -$password = Import-CliXml -Path $path +$password = Import-CliXml -Path $Path $parameters = @{ - Name = $vault + Name = $Vault ModuleName = $requiredModules[0] VaultParameters = @{ Confirm = $false Interaction = $null Password = $password - PasswordTimeout = $timeout + PasswordTimeout = $Timeout } DefaultVault = $true } -Write-Host "Registering [$vault] vault" +Write-Host "Registering [$Vault] vault" [Environment]::SetEnvironmentVariable("VAULT", $password, [System.EnvironmentVariableTarget]::User) Register-SecretVault @parameters -Remove-Item -Path $path -Force +Remove-Item -Path $Path -Force Write-Host "All done!!!" From 21a013c0819d44f198a432adb5b4ed324b1234ea Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:57:09 +0000 Subject: [PATCH 17/29] need to set PassThru in the config duh, just using the variable directly instead of the environment variable --- .github/workflows/test.yml | 10 ++++++---- tests/rcgmsa.Tests.ps1 | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d70290c..26ea5d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,8 +21,9 @@ jobs: - name: Install run: | - Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser - Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + Set-PSRepository PSGallery -InstallationPolicy Trusted + Install-Module -Name Pester -Scope CurrentUser + Install-Module -Name PSScriptAnalyzer -Scope CurrentUser - name: Lint run: | @@ -42,7 +43,8 @@ jobs: run: | $config = New-PesterConfiguration $config.Output.Verbosity = 'Detailed' + $config.PassThru = $true $config.Run.Exit = $true - $config.Run.Path = .\tests\rcgmsa.Tests.ps1 + $config.Run.Path = '.\tests' - Invoke-Pester -Configuration $config -Passthru + Invoke-Pester -Configuration $config diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index e0db7bb..044e877 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -29,7 +29,7 @@ BeforeAll { Remove-Item Function:\Get-Credential -ErrorAction Stop - $securePass = ConvertTo-SecureString $env:VAULT -AsPlainText -Force + $securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force Unlock-SecretStore -Password $securePass $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') From 147b24f48541e1c1505f3d7b6c95bef46f2f3f66 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:01:37 +0000 Subject: [PATCH 18/29] subset of Run object PassThru is --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26ea5d4..ef270a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: run: | $config = New-PesterConfiguration $config.Output.Verbosity = 'Detailed' - $config.PassThru = $true + $config.Run.PassThru = $true $config.Run.Exit = $true $config.Run.Path = '.\tests' From 939a6fadc7ed44b12be75310af96e288db8b9c61 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 06:20:50 +0000 Subject: [PATCH 19/29] trying to set the vault environment variable in the context block of the keeper tests --- .github/workflows/test.yml | 1 + tests/rcgmsa.Tests.ps1 | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef270a0..d57b5b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,5 +46,6 @@ jobs: $config.Run.PassThru = $true $config.Run.Exit = $true $config.Run.Path = '.\tests' + $config.Run.Throw = $true Invoke-Pester -Configuration $config diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 044e877..218bbbd 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -63,6 +63,11 @@ Describe 'Integration Tests' { Context 'Keeper Vault Integration' { BeforeAll { + [Environment]::SetEnvironmentVariable( + "VAULT", + $vaultPassword, + [System.EnvironmentVariableTarget]::User + ) Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } Mock New-Item { return 'C:\Mock\Temp' } From c29bd26785f88413ccf65419988394a180fbb6fa Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:19:00 +0000 Subject: [PATCH 20/29] need a Mock for the Files logic which passes more arguments than SecretStore accepts --- tests/rcgmsa.Tests.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 218bbbd..fb074eb 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -68,6 +68,22 @@ Describe 'Integration Tests' { $vaultPassword, [System.EnvironmentVariableTarget]::User ) + + Mock Get-Secret { + [CmdletBinding()] + param( + [Parameter(Position=0)]$RecordID, + [Parameter(Position=1)]$FieldID, + $Vault, + [switch]$AsPlainText + ) + + if ($AsPlainText) { + return $keeperSecret + } + + return $fileBytes + } Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } Mock New-Item { return 'C:\Mock\Temp' } @@ -76,7 +92,7 @@ Describe 'Integration Tests' { } It 'Should retrieve secrets and process files when -Keeper is used' { - & $scriptPath -Command 'hostname' -Computers 'server1' -User 'gmsa$' -Keeper $secretName -Vault 'devops' + & $scriptPath -Command 'hostname' -Computers 'localhost' -User 'gmsa$' -Keeper $secretName -Vault 'devops' $api_key = [Environment]::GetEnvironmentVariable('KEEPER_API_KEY', 'User') $api_key | Should -Be $keeperSecret.API_KEY From 70f792c2b3a11eeeedf388741f0f18fb9ae7d179 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Tue, 17 Feb 2026 07:23:17 +0000 Subject: [PATCH 21/29] parameter name for Mock should have been Name --- tests/rcgmsa.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index fb074eb..1778796 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -72,7 +72,7 @@ Describe 'Integration Tests' { Mock Get-Secret { [CmdletBinding()] param( - [Parameter(Position=0)]$RecordID, + [Parameter(Position=0)]$Name, [Parameter(Position=1)]$FieldID, $Vault, [switch]$AsPlainText From ab392d3d6b07804e2863df7fb76331965271b892 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:19:15 +0000 Subject: [PATCH 22/29] fixing Get-Secret Mock and flattening nested Files hashtable in keeperSecret object --- .github/workflows/test.yml | 1 - tests/rcgmsa.Tests.ps1 | 22 ++++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d57b5b1..ef270a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,6 +46,5 @@ jobs: $config.Run.PassThru = $true $config.Run.Exit = $true $config.Run.Path = '.\tests' - $config.Run.Throw = $true Invoke-Pester -Configuration $config diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 1778796..5e3d4ba 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -2,7 +2,7 @@ BeforeAll { $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' - Files = 'license.key' + Files = '{"license.key": "file-id-123"}' Keys = 'Files' } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" @@ -35,7 +35,7 @@ BeforeAll { $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - Set-Secret -Name $keeperSecret.Files -Secret $fileBytes -Vault $vaultName + Set-Secret -Name 'file-id-123' -Secret $fileBytes -Vault $vaultName } Describe 'Integration Tests' { @@ -63,12 +63,6 @@ Describe 'Integration Tests' { Context 'Keeper Vault Integration' { BeforeAll { - [Environment]::SetEnvironmentVariable( - "VAULT", - $vaultPassword, - [System.EnvironmentVariableTarget]::User - ) - Mock Get-Secret { [CmdletBinding()] param( @@ -78,11 +72,15 @@ Describe 'Integration Tests' { [switch]$AsPlainText ) - if ($AsPlainText) { - return $keeperSecret + if ($FieldID) { + return Microsoft.PowerShell.SecretManagement\Get-Secret -Name $FieldID -Vault $Vault + } + + $result = Microsoft.PowerShell.SecretManagement\Get-Secret @PSBoundParameters + if ($AsPlainText -and $result.ContainsKey('Files')) { + $result.Files = $result.Files | ConvertFrom-Json -AsHashtable } - - return $fileBytes + return $result } Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } From b677be38829be3a4fe7366c47bba47b52c16e3a1 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 18 Feb 2026 05:34:58 +0000 Subject: [PATCH 23/29] adding the overwrite of VAULT environment variable back in --- tests/rcgmsa.Tests.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 5e3d4ba..478b5f8 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -29,6 +29,12 @@ BeforeAll { Remove-Item Function:\Get-Credential -ErrorAction Stop + [Environment]::SetEnvironmentVariable( + "VAULT", + $vaultPassword, + [System.EnvironmentVariableTarget]::User + ) + $securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force Unlock-SecretStore -Password $securePass From 52a068f1a453567557207088b4fdfa1a791b509b Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:00:10 +0000 Subject: [PATCH 24/29] somehow the module-qualified calls were causing infinite recursion loops? --- tests/rcgmsa.Tests.ps1 | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 478b5f8..2a56d6b 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -79,14 +79,11 @@ Describe 'Integration Tests' { ) if ($FieldID) { - return Microsoft.PowerShell.SecretManagement\Get-Secret -Name $FieldID -Vault $Vault + return $fileBytes } - $result = Microsoft.PowerShell.SecretManagement\Get-Secret @PSBoundParameters - if ($AsPlainText -and $result.ContainsKey('Files')) { - $result.Files = $result.Files | ConvertFrom-Json -AsHashtable - } - return $result + $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json -AsHashtable + return $keeperSecret } Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } From 461de4c0d3610b3d4f8bb4a29ce6f3ad711362fe Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 20 Feb 2026 03:04:32 +0000 Subject: [PATCH 25/29] updating Get-Secret Mock once more, unify File field contents into keeperSecret object --- .github/workflows/test.yml | 2 ++ tests/rcgmsa.Tests.ps1 | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef270a0..d2009a3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,5 +46,7 @@ jobs: $config.Run.PassThru = $true $config.Run.Exit = $true $config.Run.Path = '.\tests' + $config.Should.ErrorAction = 'Continue' + $config.TestResult.Enabled = $true Invoke-Pester -Configuration $config diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 2a56d6b..0f6a32b 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -2,8 +2,9 @@ BeforeAll { $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' - Files = '{"license.key": "file-id-123"}' + Files = '{"license.key": "License"}' Keys = 'Files' + License = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" $secretName = '9vb_wew-d6_AmgUNmIO6Ez' @@ -38,10 +39,7 @@ BeforeAll { $securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force Unlock-SecretStore -Password $securePass - $fileBytes = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') - Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - Set-Secret -Name 'file-id-123' -Secret $fileBytes -Vault $vaultName } Describe 'Integration Tests' { @@ -78,11 +76,12 @@ Describe 'Integration Tests' { [switch]$AsPlainText ) + $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json -AsHashtable + if ($FieldID) { - return $fileBytes + return $keeperSecret[$FieldID] } - $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json -AsHashtable return $keeperSecret } Mock Invoke-Command { return 'Remote Execution Successful' } From 0d73a03b56aa6d500de7dfc72ba6e885c9e8bc0a Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 20 Feb 2026 05:44:30 +0000 Subject: [PATCH 26/29] Switching Files to String[] and converting from json after Set-Secret call --- tests/rcgmsa.Tests.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 0f6a32b..120bbab 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -2,7 +2,7 @@ BeforeAll { $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' - Files = '{"license.key": "License"}' + Files = '["License"]' Keys = 'Files' License = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') } @@ -40,6 +40,7 @@ BeforeAll { Unlock-SecretStore -Password $securePass Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName + $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json } Describe 'Integration Tests' { @@ -76,8 +77,6 @@ Describe 'Integration Tests' { [switch]$AsPlainText ) - $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json -AsHashtable - if ($FieldID) { return $keeperSecret[$FieldID] } From b6527f0e70a7c103bab58494be1b2127de78a15f Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:12:20 +0000 Subject: [PATCH 27/29] using AsPlainText as a differential switch for what to return --- tests/rcgmsa.Tests.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index 120bbab..bf82092 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -2,9 +2,9 @@ BeforeAll { $keeperSecret = @{ API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67' DB_PASS = 'SuperSecretPass' - Files = '["License"]' + Files = '["license.key"]' Keys = 'Files' - License = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') + 'license.key' = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent') } $scriptPath = "$PSScriptRoot/../rcgmsa.ps1" $secretName = '9vb_wew-d6_AmgUNmIO6Ez' @@ -40,7 +40,7 @@ BeforeAll { Unlock-SecretStore -Password $securePass Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - $keeperSecret.Files = $keeperSecret.Files | ConvertFrom-Json + $keeperSecret.Files = @($keeperSecrets.Files.Trim('[]').Split(',').Trim()) } Describe 'Integration Tests' { @@ -77,11 +77,11 @@ Describe 'Integration Tests' { [switch]$AsPlainText ) - if ($FieldID) { - return $keeperSecret[$FieldID] + if ($AsPlainText) { + return $keeperSecret } - return $keeperSecret + return $keeperSecret[$FieldID] } Mock Invoke-Command { return 'Remote Execution Successful' } Mock Join-Path { param($Path, $ChildPath) return "$Path\$ChildPath" } From 64922114b2b9d779b9142adeac1930adf95312f0 Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:17:02 +0000 Subject: [PATCH 28/29] missed an s --- tests/rcgmsa.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index bf82092..c65c3ef 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -40,7 +40,7 @@ BeforeAll { Unlock-SecretStore -Password $securePass Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - $keeperSecret.Files = @($keeperSecrets.Files.Trim('[]').Split(',').Trim()) + $keeperSecrets.Files = @($keeperSecrets.Files.Trim('[]').Split(',').Trim()) } Describe 'Integration Tests' { From b7a3df09141519dd4bd78dd22b0f4cd13af82e4c Mon Sep 17 00:00:00 2001 From: byteskeptical <40208858+byteskeptical@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:29:07 +0000 Subject: [PATCH 29/29] mis-spelled twice --- tests/rcgmsa.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rcgmsa.Tests.ps1 b/tests/rcgmsa.Tests.ps1 index c65c3ef..97ba8ee 100644 --- a/tests/rcgmsa.Tests.ps1 +++ b/tests/rcgmsa.Tests.ps1 @@ -40,7 +40,7 @@ BeforeAll { Unlock-SecretStore -Password $securePass Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName - $keeperSecrets.Files = @($keeperSecrets.Files.Trim('[]').Split(',').Trim()) + $keeperSecret.Files = @($keeperSecret.Files.Trim('[]').Split(',').Trim()) } Describe 'Integration Tests' {