Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a220f57
adding some basic tests and linting
byteskeptical Feb 2, 2026
8f62a04
reworking test suite, adding lint rule exception for ConvertTo-Secure…
byteskeptical Feb 2, 2026
1f051a8
adding input validation for -v, removing -v from other input validati…
byteskeptical Feb 2, 2026
ac43fce
Not sure why Pester doesn't let you pass the -Path flag with the -Con…
byteskeptical Feb 2, 2026
a0b5873
switching shells to pwsh, adding missing stubs for missing Keeper fun…
byteskeptical Feb 2, 2026
00a9db7
still trying to get workflow to reflect test result state, using Keep…
byteskeptical Feb 2, 2026
e3a9862
removing unecessary stubs and mocks for Import and Install module fun…
byteskeptical Feb 2, 2026
23bf6a9
combining Keeper tests, trying to setup local vault to remove more mo…
byteskeptical Feb 2, 2026
6fd805e
too much forcing
byteskeptical Feb 2, 2026
d7d3076
forgot to define secretName
byteskeptical Feb 3, 2026
dcbfc57
removing unsupported list entry in keeperSecret
byteskeptical Feb 3, 2026
3c002f8
ditching the nested hashtable for Files as that isn't supported by Se…
byteskeptical Feb 4, 2026
4862b5f
switch to using the BeforeAll vault password used to create the real …
byteskeptical Feb 4, 2026
9bb5739
. instead of : for :VAULT, duh
byteskeptical Feb 5, 2026
78d34b7
setting environment variable for vault password
byteskeptical Feb 7, 2026
3a3db14
using pass through on Pester call in actions workflow, letting vault …
byteskeptical Feb 17, 2026
21a013c
need to set PassThru in the config duh, just using the variable dire…
byteskeptical Feb 17, 2026
147b24f
subset of Run object PassThru is
byteskeptical Feb 17, 2026
939a6fa
trying to set the vault environment variable in the context block of …
byteskeptical Feb 17, 2026
c29bd26
need a Mock for the Files logic which passes more arguments than Secr…
byteskeptical Feb 17, 2026
70f792c
parameter name for Mock should have been Name
byteskeptical Feb 17, 2026
ab392d3
fixing Get-Secret Mock and flattening nested Files hashtable in keepe…
byteskeptical Feb 18, 2026
b677be3
adding the overwrite of VAULT environment variable back in
byteskeptical Feb 18, 2026
52a068f
somehow the module-qualified calls were causing infinite recursion lo…
byteskeptical Feb 18, 2026
461de4c
updating Get-Secret Mock once more, unify File field contents into ke…
byteskeptical Feb 20, 2026
0d73a03
Switching Files to String[] and converting from json after Set-Secret…
byteskeptical Feb 20, 2026
b6527f0
using AsPlainText as a differential switch for what to return
byteskeptical Feb 20, 2026
6492211
missed an s
byteskeptical Feb 20, 2026
b7a3df0
mis-spelled twice
byteskeptical Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Test

on:
push:
branches: [root]
pull_request:
branches: [root]
workflow_dispatch:

defaults:
run:
shell: pwsh

jobs:
test:
name: Validate
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Install
run: |
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module -Name Pester -Scope CurrentUser
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser

- name: Lint
run: |
$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.' -ErrorAction Stop
} else {
Write-Host 'PSScriptAnalyzer passed.'
}

- name: Test
run: |
$config = New-PesterConfiguration
$config.Output.Verbosity = 'Detailed'
$config.Run.PassThru = $true
$config.Run.Exit = $true
$config.Run.Path = '.\tests'
$config.Should.ErrorAction = 'Continue'
$config.TestResult.Enabled = $true

Invoke-Pester -Configuration $config
157 changes: 157 additions & 0 deletions tests/rcgmsa.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
BeforeAll {
$keeperSecret = @{
API_KEY = '06ed1705-a2d5-4d16-b3b2-1a2814e7ef67'
DB_PASS = 'SuperSecretPass'
Files = '["license.key"]'
Keys = 'Files'
'license.key' = [System.Text.Encoding]::UTF8.GetBytes('RealFileContent')
}
$scriptPath = "$PSScriptRoot/../rcgmsa.ps1"
$secretName = '9vb_wew-d6_AmgUNmIO6Ez'
$setupPath = "$PSScriptRoot/../vault.ps1"
$vaultName = 'devops'
$vaultPassword = 'VaultPassword123'

function Get-Credential {
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$UserName,

[Parameter(Mandatory=$false)]
[string]$Message
)
$securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force
return [PSCredential]::new($UserName, $securePass)
}

$credFile = [System.IO.Path]::GetTempFileName()
. $setupPath -Path $credFile -Vault $vaultName

Remove-Item Function:\Get-Credential -ErrorAction Stop

[Environment]::SetEnvironmentVariable(
"VAULT",
$vaultPassword,
[System.EnvironmentVariableTarget]::User
)

$securePass = ConvertTo-SecureString $vaultPassword -AsPlainText -Force
Unlock-SecretStore -Password $securePass

Set-Secret -Name $secretName -Secret $keeperSecret -Vault $vaultName
$keeperSecret.Files = @($keeperSecret.Files.Trim('[]').Split(',').Trim())
}

Describe 'Integration Tests' {

Context 'Input Validation' {
It 'Should accept valid hostnames or IPs' {
{ & $scriptPath -Command 'Get-Date' -Computers 'localhost','127.0.0.1' -User 'svc_account$' } | Should -Not -Throw
}

It 'Should reject invalid characters in computer names' {
$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' {
$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 6>&1
$output | Should -Match '^Version: \d+\.\d+\.\d+$'
}
}

Context 'Keeper Vault Integration' {
BeforeAll {
Mock Get-Secret {
[CmdletBinding()]
param(
[Parameter(Position=0)]$Name,
[Parameter(Position=1)]$FieldID,
$Vault,
[switch]$AsPlainText
)

if ($AsPlainText) {
return $keeperSecret
}

return $keeperSecret[$FieldID]
}
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 {}
}

It 'Should retrieve secrets and process files when -Keeper is used' {
& $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

[Environment]::SetEnvironmentVariable('KEEPER_API_KEY', $null, 'User')

Assert-MockCalled Set-Content -ParameterFilter {
$Path -match 'license.key'
} -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 $secretName

$sbContent | Should -Match 'KEEPER_'
$sbContent | Should -Match '\[Environment\]::SetEnvironmentVariable'
}
}

Context 'Logic Branching' {
BeforeAll {
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'

Assert-MockCalled Invoke-Command -Times 1
}

It 'Should retry with -IncludePortInSPN if a specific SPN error occurs' {
Mock Invoke-Command -ParameterFilter {
-not ($SessionOption.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 -MockWith { return 'Retry Successful' }

& $scriptPath -Command 'hostname' -Computers 'localhost' -User 'gmsa$'

Assert-MockCalled Invoke-Command -Times 2
Assert-MockCalled Invoke-Command -ParameterFilter {
$SessionOption.IncludePortInSPN -eq $true
} -Times 1
}
}
}
14 changes: 7 additions & 7 deletions vault.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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!!!"
Loading