Public/Tests/ModuleSecret.Tests.ps1
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Test values only.')] param() BeforeAll { $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config' $env:TCS_SKIP_UPDATE_CHECK = '1' $env:TCS_TELEMETRY_OPTOUT = '1' $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.core.psd1') -Force } AfterAll { Remove-Module -Name tcs.core -Force -ErrorAction SilentlyContinue } Describe 'Set-ModuleSecret, Get-ModuleSecret and Remove-ModuleSecret' { BeforeAll { $secret = ConvertTo-SecureString -String 'token-123' -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential -ArgumentList 'user@example.com', (ConvertTo-SecureString -String 'p@ss' -AsPlainText -Force) $secretFolder = Join-Path -Path $env:TCS_CONFIG_ROOT -ChildPath 'tcs.secrets/Secrets' } It 'Saves a SecureString encrypted and returns it as a SecureString' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'ApiToken' -SecureString $secret $file = Join-Path $secretFolder 'ApiToken.json' Test-Path $file | Should -BeTrue Get-Content -Path $file -Raw | Should -Not -Match 'token-123' (Get-Content -Path $file -Raw | ConvertFrom-Json).Value | Should -Match '^tcs:v1:' $result = Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'ApiToken' $result | Should -BeOfType [System.Security.SecureString] (New-Object System.Net.NetworkCredential -ArgumentList '', $result).Password | Should -BeExactly 'token-123' } It 'Saves a credential and returns a PSCredential' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Login' -Credential $credential Get-Content -Path (Join-Path $secretFolder 'Login.json') -Raw | Should -Not -Match 'p@ss' $result = Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Login' $result | Should -BeOfType [System.Management.Automation.PSCredential] $result.UserName | Should -Be 'user@example.com' $result.GetNetworkCredential().Password | Should -BeExactly 'p@ss' } It 'Saves an empty secret and a credential without a password' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Empty' -SecureString (New-Object System.Security.SecureString) (Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Empty').Length | Should -Be 0 $noPassword = New-Object System.Management.Automation.PSCredential -ArgumentList 'user', (New-Object System.Security.SecureString) Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'NoPassword' -Credential $noPassword $result = Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'NoPassword' $result.UserName | Should -Be 'user' $result.Password.Length | Should -Be 0 } It 'Keeps secrets separate per module and per name' { Set-ModuleSecret -ModuleName 'tcs.other' -Name 'ApiToken' -SecureString (ConvertTo-SecureString -String 'other' -AsPlainText -Force) (Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'ApiToken' | ForEach-Object { (New-Object System.Net.NetworkCredential -ArgumentList '', $_).Password }) | Should -Be 'token-123' (Get-ModuleSecret -ModuleName 'tcs.other' -Name 'ApiToken' | ForEach-Object { (New-Object System.Net.NetworkCredential -ArgumentList '', $_).Password }) | Should -Be 'other' } It 'Replaces an existing secret' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Replace' -SecureString $secret Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Replace' -Credential $credential Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Replace' | Should -BeOfType [System.Management.Automation.PSCredential] } It 'Writes a non-terminating error for a missing secret' { Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Missing' -ErrorAction SilentlyContinue -ErrorVariable errs | Should -BeNullOrEmpty $errs[0].FullyQualifiedErrorId | Should -Match 'ModuleSecretNotFound' } It 'Writes a non-terminating error when the secret cannot be decrypted' { $null = New-Item -Path $secretFolder -ItemType Directory -Force '{ "Type": "SecureString", "Value": "tcs:v1:aes-key:AAAA" }' | Set-Content -Path (Join-Path $secretFolder 'Broken.json') Get-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Broken' -ErrorAction SilentlyContinue -ErrorVariable errs | Should -BeNullOrEmpty ($errs | Where-Object { $_.FullyQualifiedErrorId -match 'ModuleSecretUnreadable' }) | Should -Not -BeNullOrEmpty } It 'Removes a secret, honouring -WhatIf' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Temp' -SecureString $secret Remove-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Temp' -WhatIf Test-Path (Join-Path $secretFolder 'Temp.json') | Should -BeTrue Remove-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Temp' Test-Path (Join-Path $secretFolder 'Temp.json') | Should -BeFalse { Remove-ModuleSecret -ModuleName 'tcs.secrets' -Name 'Temp' -ErrorAction Stop } | Should -Throw } It 'Does not save anything with -WhatIf' { Set-ModuleSecret -ModuleName 'tcs.secrets' -Name 'WhatIf' -SecureString $secret -WhatIf Test-Path (Join-Path $secretFolder 'WhatIf.json') | Should -BeFalse } It 'Rejects names that are not a single safe folder or file name' -ForEach @( @{ Module = '../x'; Name = 'a' }, @{ Module = 'tcs.secrets'; Name = '../a' }, @{ Module = 'tcs.secrets'; Name = 'a/b' } ) { { Set-ModuleSecret -ModuleName $Module -Name $Name -SecureString $secret } | Should -Throw { Get-ModuleSecret -ModuleName $Module -Name $Name } | Should -Throw { Remove-ModuleSecret -ModuleName $Module -Name $Name } | Should -Throw } } |