Tests/Private/SecureString.Tests.ps1

BeforeDiscovery {
    Import-Module (Join-Path $PSScriptRoot '../../Posh-SecretSharing.psd1') -Force
}

Describe 'Private/SecureString' -Tag Unit {
    InModuleScope 'Posh-SecretSharing' {
        BeforeAll {
            function Get-TestPlainByte {
                param([System.Security.SecureString]$SecureString)
                $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SecureString)
                try {
                    $out = [byte[]]::new($SecureString.Length)
                    for ($i = 0; $i -lt $SecureString.Length; $i++) {
                        $out[$i] = [byte]([System.Runtime.InteropServices.Marshal]::ReadInt16($ptr, $i * 2) -band 0xFF)
                    }
                    return , $out
                } finally {
                    [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($ptr)
                }
            }

            # Builds a SecureString directly rather than via ConvertTo-SecureString, which is
            # unavailable in some Windows PowerShell 5.1 environments (a PSModulePath conflict
            # between the 5.1 and 7 copies of Microsoft.PowerShell.Security, unrelated to this
            # module) - confirmed to fail even outside Pester and outside this module.
            function New-TestSecureString {
                [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
                    'PSUseShouldProcessForStateChangingFunctions', '',
                    Justification = 'Test helper; pure in-memory construction, no external state is changed.')]
                param([string]$PlainText)
                $secure = New-Object System.Security.SecureString
                foreach ($ch in $PlainText.ToCharArray()) {
                    $secure.AppendChar($ch)
                }
                return $secure
            }
        }

        Describe 'ConvertTo-SecretSharingSecureString / ConvertFrom-SecretSharingSecureString' {
            It 'round-trips an arbitrary byte array, including 0x00 and 0xFF' {
                $bytes = [byte[]](0x00, 0x01, 0x7F, 0x80, 0xFF, 0x41, 0x00)
                $secure = ConvertTo-SecretSharingSecureString -Byte $bytes
                $secure.Length | Should -Be $bytes.Count

                $back = ConvertFrom-SecretSharingSecureString -SecureString $secure
                $back | Should -Be $bytes
            }

            It 'round-trips a 32-byte value' {
                $bytes = 1..32 | ForEach-Object { [byte]$_ }
                $secure = ConvertTo-SecretSharingSecureString -Byte $bytes
                (ConvertFrom-SecretSharingSecureString -SecureString $secure) | Should -Be $bytes
            }

            It 'returns a read-only SecureString' {
                $secure = ConvertTo-SecretSharingSecureString -Byte ([byte[]](1, 2, 3))
                $secure.IsReadOnly() | Should -BeTrue
            }
        }

        Describe 'ConvertFrom-SecretSharingPassphraseSecureString' {
            It 'encodes ASCII text as UTF-8 bytes' {
                $secure = New-TestSecureString -PlainText 'hunter2'
                $bytes = ConvertFrom-SecretSharingPassphraseSecureString -SecureString $secure
                $bytes | Should -Be ([System.Text.Encoding]::UTF8.GetBytes('hunter2'))
            }

            It 'encodes non-ASCII text correctly as UTF-8' {
                $secure = New-TestSecureString -PlainText 'caf'
                $secure.AppendChar([char]0xE9)
                $bytes = ConvertFrom-SecretSharingPassphraseSecureString -SecureString $secure
                $bytes | Should -Be ([System.Text.Encoding]::UTF8.GetBytes('caf' + [char]0xE9))
            }

            It 'returns an empty array for an empty SecureString' {
                $secure = New-Object System.Security.SecureString
                (ConvertFrom-SecretSharingPassphraseSecureString -SecureString $secure).Count | Should -Be 0
            }
        }
    }
}