Skip to content

Commit 3e3d5ca

Browse files
test: expand Jwt test suite to production-level coverage (#40)
## Summary Targets `feat/13-implement-jwt-module` and adds production-level edge-case coverage to the JWT v2 integration suite. While adding tests, it also fixes a regression where `New-JwtSigningKey -Algorithm HS*` returned `[object[]]` instead of `[byte[]]`, breaking `New-Jwt -GenerateKey` for HMAC algorithms. ## What changed ### Tests (`tests/Integration.Jwt.Tests.ps1`) Added a new `Production-level edge cases` context covering: - `Test-Jwt -Detailed` reports failed signature and failed claim checks. - `New-Jwt -GenerateKey` produces valid tokens for HS256, RS256, and ES256. - `ConvertFrom-Jwt` accepts a `SecureString` token. - `Test-Jwt` returns `$false` for an empty signature segment on signed algorithms. - `New-Jwt` parameter validation rejects non-hashtable payloads. - `Test-Jwt` parameter validation rejects `$null` tokens. - Verbose output does not leak payload secrets or key material. ### Bug fix (`src/functions/public/Keys/New-JwtSigningKey.ps1`) PowerShell unwraps `[byte[]]` to `[object[]]` when returned through an untyped variable. The HS* branch now returns `,$bytes` so the byte-array type is preserved, allowing `New-Jwt -Algorithm HS256 -GenerateKey` to sign and verify correctly. ### CI Bumped the reusable workflow pin to Process-PSModule v6.1.15 while preserving the explicit `TestData` mapping required by the reusable workflow's secrets interface. ## Verification ```powershell Import-Module Pester -RequiredVersion 6.0.1 -Force $config = New-PesterConfiguration $config.Run.Path = 'tests' Invoke-Pester -Configuration $config ``` Result: **123 passed, 0 failed** (1 skipped: optional Azure Key Vault test). ## Related Contributes to #26 (JWT v2 overhaul). --- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 932350e commit 3e3d5ca

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

.github/workflows/Process-PSModule.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ permissions:
2727

2828
jobs:
2929
Process-PSModule:
30-
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13
30+
uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@688896dc3ef70fb35bd74ae5328e76d5e57fe08a # v6.1.15
3131
secrets:
3232
APIKey: ${{ secrets.APIKey }}
3333
TestData: >-

src/functions/public/Keys/New-JwtSigningKey.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function New-JwtSigningKey {
8686
}
8787
$bytes = [byte[]]::new($keyLength)
8888
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
89-
$key = $bytes
89+
return , $bytes
9090
}
9191
'^(RS|PS)' {
9292
$rsa = [System.Security.Cryptography.RSA]::Create()

tests/Integration.Jwt.Tests.ps1

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,5 +671,81 @@ Describe 'Jwt module' {
671671
}
672672
}
673673
}
674+
675+
Context 'Production-level edge cases' {
676+
BeforeAll {
677+
$script:secret = 'a-string-secret-at-least-256-bits-long'
678+
$script:goodJwt = New-Jwt -Payload @{ sub = 'joe' } -Algorithm HS256 -Key $script:secret
679+
}
680+
681+
It 'Test-Jwt -Detailed reports the failed check when the signature is invalid' {
682+
$compact = $script:goodJwt.ToString()
683+
$parts = $compact.Split('.')
684+
$parts[2] = ConvertTo-Base64UrlString ([byte[]](1..32))
685+
$tampered = $parts -join '.'
686+
$result = Test-Jwt -Token $tampered -Key $script:secret -RequireExpiration $false -Detailed
687+
688+
$result.Valid | Should -BeFalse
689+
$result.SignatureValidated | Should -BeFalse
690+
($result.Checks | Where-Object Name -EQ 'Signature').Passed | Should -BeFalse
691+
}
692+
693+
It 'Test-Jwt -Detailed reports the failed claim check' {
694+
$nowSec = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
695+
$expired = New-Jwt -Payload @{ sub = 'joe'; exp = $nowSec - 60 } -Algorithm HS256 -Key $script:secret
696+
$result = Test-Jwt -Token $expired -Key $script:secret -Detailed
697+
698+
$result.Valid | Should -BeFalse
699+
($result.Checks | Where-Object Name -EQ 'Expiration').Passed | Should -BeFalse
700+
}
701+
702+
It 'New-Jwt -GenerateKey produces valid tokens for all algorithm families' -ForEach @(
703+
@{ Alg = 'HS256' },
704+
@{ Alg = 'RS256' },
705+
@{ Alg = 'ES256' }
706+
) {
707+
$jwt = New-Jwt -Payload @{ sub = 'joe' } -Algorithm $Alg -GenerateKey
708+
709+
$jwt | Should -BeOfType [Jwt]
710+
$jwt.Header.alg | Should -Be $Alg
711+
$jwt.ToString().Split('.').Count | Should -Be 3
712+
$jwt.Signature | Should -Not -BeNullOrEmpty
713+
}
714+
715+
It 'ConvertFrom-Jwt accepts a SecureString token' {
716+
$compact = $script:goodJwt.ToString()
717+
$secure = ConvertTo-SecureString $compact -AsPlainText -Force
718+
$parsed = ConvertFrom-Jwt -Token $secure
719+
720+
$parsed.Payload.sub | Should -Be 'joe'
721+
}
722+
723+
It 'Test-Jwt returns false when the signature segment is empty for a signed algorithm' {
724+
$compact = $script:goodJwt.ToString()
725+
$parts = $compact.Split('.')
726+
$emptySig = "$($parts[0]).$($parts[1])."
727+
728+
Test-Jwt -Token $emptySig -Key $script:secret -RequireExpiration $false | Should -BeFalse
729+
}
730+
731+
It 'New-Jwt parameter validation rejects a non-hashtable payload' {
732+
{ New-Jwt -Payload 'not-a-hashtable' -Algorithm HS256 -Key $script:secret } |
733+
Should -Throw
734+
}
735+
736+
It 'Test-Jwt parameter validation rejects a null token' {
737+
{ Test-Jwt -Token $null -Key $script:secret -RequireExpiration $false } | Should -Throw
738+
}
739+
740+
It 'Verbose output does not include the payload or key material' {
741+
$payload = @{ sub = 'joe'; secret = 'do-not-leak' }
742+
$verbose = & { New-Jwt -Payload $payload -Algorithm HS256 -Key $script:secret -Verbose } 4>&1 |
743+
Where-Object { $_.GetType().Name -eq 'VerboseRecord' } |
744+
Out-String
745+
746+
$verbose | Should -Not -Match 'do-not-leak'
747+
$verbose | Should -Not -Match ([regex]::Escape($script:secret))
748+
}
749+
}
674750
}
675751

0 commit comments

Comments
 (0)