Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions NctApiClientLibrary/Functions/Test-NctSession.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Function Test-NctSession {
New-Item -Path "$env:USERPROFILE\.nct client library" -ItemType Directory
}

# If text files found in $env:USERPROFILE\.nct client library then print a numbered list for the user to select which to load
$files = Get-ChildItem -Path "$env:USERPROFILE\.nct client library" -Filter "*.txt" | Select-Object -ExpandProperty Name
# If credential files found in $env:USERPROFILE\.nct client library then print a numbered list for the user to select which to load
$files = Get-ChildItem -Path "$env:USERPROFILE\.nct client library" -Filter "*.dat" | Select-Object -ExpandProperty Name
if ($files.Count -gt 0)
{
Write-Host "The following credential files were found in $env:USERPROFILE\.nct client library:"
Expand Down
221 changes: 221 additions & 0 deletions NctApiClientLibrary/Tests/CredentialPersistence.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
Install-Module -Name Pester -Force -SkipPublisherCheck -PassThru -MinimumVersion 5.7.1
Import-Module Pester -PassThru -MinimumVersion 5.7.1

BeforeAll {
. $PSScriptRoot\..\Functions\New-NctApiCredential.ps1
. $PSScriptRoot\..\Functions\Protect-Credential.ps1
}

Describe 'Credential Persistence and File Discovery' {
BeforeAll {
# Create a test directory for credential files
$TestCredentialPath = Join-Path $env:TEMP "nct-test-credentials-$(Get-Random)"
New-Item -Path $TestCredentialPath -ItemType Directory -Force | Out-Null

$TestUsername = "test-user-$([System.Guid]::NewGuid().ToString().Substring(0,8))"
$TestPassword = "TestPassword123!"
}

Context 'File Extension Validation' {
It 'should save persisted credentials with .dat extension' {
# Create a credential file path
$expectedPath = Join-Path $TestCredentialPath "$TestUsername.dat"

# Manually create a credential file to simulate New-NctApiCredential
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($TestPassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($expectedPath, $encryptedBytes)

# Verify the file exists with .dat extension
Test-Path $expectedPath | Should -Be $true

# Verify it's NOT a .txt file
$wrongPath = Join-Path $TestCredentialPath "$TestUsername.txt"
Test-Path $wrongPath | Should -Be $false
}

It 'should find .dat files when discovering credentials' {
# Create multiple test credential files with .dat extension
$user1 = "testuser1"
$user2 = "testuser2"
$path1 = Join-Path $TestCredentialPath "$user1.dat"
$path2 = Join-Path $TestCredentialPath "$user2.dat"

$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($TestPassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($path1, $encryptedBytes)
[System.IO.File]::WriteAllBytes($path2, $encryptedBytes)

# Simulate the credential discovery logic from Test-NctSession
$files = Get-ChildItem -Path $TestCredentialPath -Filter "*.dat" | Select-Object -ExpandProperty Name

$files | Should -Not -BeNullOrEmpty
$files.Count | Should -Be 2
$files | Should -Contain "$user1.dat"
$files | Should -Contain "$user2.dat"
}

It 'should NOT find .txt files when discovering credentials' {
# Create a .txt file (wrong extension)
$wrongUser = "wrongextension"
$wrongPath = Join-Path $TestCredentialPath "$wrongUser.txt"
"dummy content" | Out-File -FilePath $wrongPath

# Create a correct .dat file
$correctUser = "correctextension"
$correctPath = Join-Path $TestCredentialPath "$correctUser.dat"
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($TestPassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($correctPath, $encryptedBytes)

# Simulate the credential discovery logic with CORRECT filter
$datFiles = Get-ChildItem -Path $TestCredentialPath -Filter "*.dat" | Select-Object -ExpandProperty Name

# Should only find .dat files
$datFiles | Should -Contain "$correctUser.dat"
$datFiles | Should -Not -Contain "$wrongUser.txt"

# Simulate the OLD BUGGY logic with .txt filter (regression test)
$txtFiles = Get-ChildItem -Path $TestCredentialPath -Filter "*.txt" | Select-Object -ExpandProperty Name

# Should find .txt but NOT .dat
$txtFiles | Should -Contain "$wrongUser.txt"
$txtFiles | Should -Not -Contain "$correctUser.dat"
}
}

Context 'Credential File Discovery Logic' {
It 'should correctly extract username from .dat filename' {
# Create credential files
$user1 = "admin"
$user2 = "james.anderson"
$path1 = Join-Path $TestCredentialPath "$user1.dat"
$path2 = Join-Path $TestCredentialPath "$user2.dat"

$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($TestPassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($path1, $encryptedBytes)
[System.IO.File]::WriteAllBytes($path2, $encryptedBytes)

# Get files and extract usernames
$files = Get-ChildItem -Path $TestCredentialPath -Filter "*.dat" | Select-Object -ExpandProperty Name

foreach ($file in $files) {
$username = [System.IO.Path]::GetFileNameWithoutExtension($file)

# Verify username extraction works correctly
$username | Should -Match '^[a-zA-Z0-9._-]+$'
$username | Should -Not -Contain '.dat'
}
}

It 'should handle empty credential directory gracefully' {
# Create an empty test directory
$EmptyPath = Join-Path $env:TEMP "nct-empty-$(Get-Random)"
New-Item -Path $EmptyPath -ItemType Directory -Force | Out-Null

try {
# Try to find credential files
$files = Get-ChildItem -Path $EmptyPath -Filter "*.dat" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name

# Should return empty, not error
$files.Count | Should -Be 0
}
finally {
Remove-Item -Path $EmptyPath -Force -ErrorAction SilentlyContinue
}
}
}

Context 'Credential Round-Trip' {
It 'should successfully encrypt and decrypt credentials' {
$originalPassword = "MySecurePassword123!"

# Encrypt
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($originalPassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt

# Verify encrypted data is different from original
$encryptedBytes | Should -Not -Be $passwordBytes
$encryptedBytes.Length | Should -BeGreaterThan 0

# Decrypt
$decryptedBytes = Protect-Credential -Data $encryptedBytes -Action Decrypt
$decryptedPassword = [System.Text.Encoding]::UTF8.GetString($decryptedBytes)

# Verify round-trip
$decryptedPassword | Should -Be $originalPassword
}

It 'should persist and retrieve credential from file' {
$testUser = "roundtrip-user"
$testPass = "RoundTripPassword123!"
$credPath = Join-Path $TestCredentialPath "$testUser.dat"

# Simulate saving credential
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($testPass)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($credPath, $encryptedBytes)

# Verify file was created
Test-Path $credPath | Should -Be $true

# Simulate discovering credential
$files = Get-ChildItem -Path $TestCredentialPath -Filter "*.dat" | Where-Object { $_.Name -eq "$testUser.dat" }
$files | Should -Not -BeNullOrEmpty

# Simulate loading credential
$loadedBytes = [System.IO.File]::ReadAllBytes($credPath)
$decryptedBytes = Protect-Credential -Data $loadedBytes -Action Decrypt
$loadedPassword = [System.Text.Encoding]::UTF8.GetString($decryptedBytes)

# Verify loaded password matches
$loadedPassword | Should -Be $testPass
}
}

Context 'Security Validation' {
It 'should not store passwords in plain text' {
$sensitivePassword = "SuperSecretPassword123!"
$credPath = Join-Path $TestCredentialPath "security-test.dat"

# Encrypt and save
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($sensitivePassword)
$encryptedBytes = Protect-Credential -Data $passwordBytes -Action Encrypt
[System.IO.File]::WriteAllBytes($credPath, $encryptedBytes)

# Read raw file content
$rawContent = [System.IO.File]::ReadAllText($credPath)

# Verify password is not in plain text
$rawContent | Should -Not -Match $sensitivePassword
}

It 'should use Windows DPAPI for encryption' {
$testPass = "TestDPAPI123!"
$passwordBytes = [System.Text.Encoding]::UTF8.GetBytes($testPass)

# Encrypt using Protect-Credential (which uses DPAPI)
$encrypted1 = Protect-Credential -Data $passwordBytes -Action Encrypt
$encrypted2 = Protect-Credential -Data $passwordBytes -Action Encrypt

# DPAPI should produce different encrypted output each time (due to entropy/IV)
# But both should decrypt to the same value
$decrypted1 = Protect-Credential -Data $encrypted1 -Action Decrypt
$decrypted2 = Protect-Credential -Data $encrypted2 -Action Decrypt

$pass1 = [System.Text.Encoding]::UTF8.GetString($decrypted1)
$pass2 = [System.Text.Encoding]::UTF8.GetString($decrypted2)

$pass1 | Should -Be $testPass
$pass2 | Should -Be $testPass
}
}

AfterAll {
# Clean up test directory
if (Test-Path $TestCredentialPath) {
Remove-Item -Path $TestCredentialPath -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
19 changes: 14 additions & 5 deletions NctApiClientLibrary/Tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,20 @@ Naviage to the Tests directory and invoke all tests with `Invoke-Pester`.

Example output:
```
Starting discovery in 2 files.
Discovery found 8 tests in 10.51s.
Starting discovery in 4 files.
Discovery found 30+ tests in 12.5s.
Running tests.
[+] C:\dev\change-tracker\NctApiClientLibrary\Tests\Credentials.Tests.ps1 9.66s (1.78s|327ms)
[+] C:\dev\change-tracker\NctApiClientLibrary\Tests\CredentialPersistence.Tests.ps1 5.43s (2.12s|154ms)
[+] C:\dev\change-tracker\NctApiClientLibrary\Tests\Devices.Tests.ps1 7.21s (4.21s|64ms)
Tests completed in 16.88s
Tests Passed: 8, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
```
[+] C:\dev\change-tracker\NctApiClientLibrary\Tests\SessionCredentialDiscovery.Tests.ps1 6.89s (3.45s|201ms)
Tests completed in 29.19s
Tests Passed: 30, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
```

### Test Files

- **Credentials.Tests.ps1** - Integration tests for adding, retrieving, and deleting database credentials via the API
- **CredentialPersistence.Tests.ps1** - Unit tests for credential file persistence, encryption, and the .dat file extension
- **Devices.Tests.ps1** - Integration tests for proxied device management
- **SessionCredentialDiscovery.Tests.ps1** - Tests for the credential file discovery logic, including regression tests for the file extension bug
Loading