$ErrorActionPreference='Stop' . (Join-Path $PSScriptRoot 'Catalog.ps1') $script:Checks=0 function Assert-Panel($Condition,[string]$Message) { if (-not $Condition) { throw $Message }; $script:Checks++ } function Expect-Rejection($Action,[string]$Message) { $rejected=$false try { & $Action | Out-Null } catch { $rejected=$true } Assert-Panel $rejected $Message } function New-Fixture([string]$Role,[int]$Port,[string]$Address='192.0.2.10') { return [pscustomobject]@{Role=$Role; Label=('Server '+$Role); HostName=$Address; UserName='operator'; PortNumber=64646; LocalPort=$Port; RemotePort=2053; Fingerprint=('SHA256:'+('A'*43)); PanelPath='/private-panel/'} } $ru=New-Fixture 'RU' 18053 $de=New-Fixture 'DE' 18054 'server.example.org' $dk=New-Fixture 'DK' 18055 '2001:db8::10' $catalog=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($ru,$de,$dk)}) Assert-Panel ($catalog.Sessions.Count -eq 3) 'Three initial servers must load.' $chosen=@(Select-PanelSessions $catalog @('KAgent-DK-panel')) Assert-Panel ($chosen.Count -eq 1 -and $chosen[0].Role -ceq 'DK') 'Installing DK must select only DK.' $chosen=@(Select-PanelSessions $catalog @('KAgent-DE-panel','KAgent-RU-panel')) Assert-Panel (($chosen.Role -join ',') -ceq 'DE,RU') 'Multiple selected servers must be preserved in requested order.' Expect-Rejection { Select-PanelSessions $catalog @() } 'Empty selection must not install anything.' Expect-Rejection { Select-PanelSessions $catalog @('KAgent-FI-panel') } 'Unknown selection must be rejected.' Expect-Rejection { Select-PanelSessions $catalog @('KAgent-DK-panel','KAgent-DK-panel') } 'Duplicate selection must be rejected.' $fi=New-Fixture 'FI2' 18056 $larger=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($catalog.Sessions)+@($fi)}) Assert-Panel ($larger.Sessions.Count -eq 4 -and $larger.Sessions[3].Role -ceq 'FI2') 'A new country must work without changing installer code.' $legacy=[pscustomobject]@{Sessions=@([pscustomobject]@{Role='DE';Name='KAgent-DE-panel';HostName='198.51.100.20';UserName='operator';PortNumber=22;LocalPort=18054;RemotePort=2053;Fingerprint=('SHA256:'+('A'*43))})} $normalized=ConvertTo-PanelCatalog $legacy Assert-Panel ($normalized.Sessions[0].PanelPath -ceq '/' -and $normalized.Sessions[0].Label -ceq 'DE') 'Legacy RU/DE catalog compatibility failed.' $empty=ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@()}) Assert-Panel ($empty.Sessions.Count -eq 0) 'Clean install must support an empty catalog.' Expect-Rejection { ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($ru,$ru)}) } 'Duplicate session must be rejected.' $collision=New-Fixture 'OTHER' 18053 Expect-Rejection { ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($ru,$collision)}) } 'Loopback port collision must be rejected.' $caseCollision=New-Fixture 'ru' 18056 Expect-Rejection { ConvertTo-PanelCatalog ([pscustomobject]@{Sessions=@($ru,$caseCollision)}) } 'Case-insensitive Windows session collision must be rejected.' Expect-Rejection { ConvertTo-PanelCatalog ([pscustomobject]@{SchemaVersion=2;Sessions=@($ru)}) } 'Unknown schema must be rejected.' foreach ($pair in @( @('Role','../../owned'), @('Role','DK; Start-Process cmd'), @('Name','Other-session'), @('HostName','https://example.org/path'), @('HostName','server -proxycmd cmd'), @('HostName',''), @('HostName',"server`r`nPort=1"), @('UserName','user;cmd'), @('PortNumber','0'), @('PortNumber','65536'), @('LocalPort','12.5'), @('RemotePort','1;2'), @('Fingerprint',''), @('Fingerprint',('SHA256:'+('A'*43)+' -pw injected')), @('PanelPath','//example.org/'), @('PanelPath','/../secret'), @('PanelPath',"/x`r`nInjected: yes"), @('PanelPath','/panel?password=secret'), @('Label',"line`nnewline") )) { $copy=$ru | ConvertTo-Json | ConvertFrom-Json $copy | Add-Member -NotePropertyName $pair[0] -NotePropertyValue $pair[1] -Force Expect-Rejection { ConvertTo-PanelSession $copy } ('Unsafe input accepted: '+$pair[0]) } $key='C:\Keys & files\my owner.ppk' $plan=Get-PanelRegistryPlan $catalog.Sessions[2] $key Assert-Panel ($plan.Strings.PublicKeyFile -ceq $key) 'Key path with spaces must remain data.' Assert-Panel ($plan.Strings.PortForwardings -ceq 'L127.0.0.1:18055=127.0.0.1:2053') 'Tunnel must bind loopback and target remote loopback.' Assert-Panel ($plan.Numbers.LocalPortAcceptAll -eq 0 -and $plan.Numbers.AgentFwd -eq 0 -and $plan.Numbers.ProxyMethod -eq 0) 'Unsafe forwarding/proxy settings.' Assert-Panel ($plan.Strings.SSHManualHostKeys -ceq $catalog.Sessions[2].Fingerprint) 'Saved session must replace any previous manual host key allowlist.' Assert-Panel ($plan.Arguments -ceq ('-load KAgent-DK-panel -N -hostkey SHA256:'+('A'*43))) 'Shortcut must pin the exact host key.' Assert-Panel ($plan.PanelUrl -ceq 'http://127.0.0.1:18055/private-panel/') 'Browser URL must stay loopback.' Expect-Rejection { Get-PanelRegistryPlan $ru '' } 'Missing key path must be rejected.' $temp=Join-Path ([IO.Path]::GetTempPath()) ('panel-catalog-test-'+[Guid]::NewGuid().ToString('N')) [IO.Directory]::CreateDirectory($temp) | Out-Null try { $file=Join-Path $temp 'connections.json' Write-PanelCatalog $file $larger $read=Read-PanelCatalog $file Assert-Panel (($read.Sessions.Role -join ',') -ceq 'RU,DE,DK,FI2') 'Export/import roundtrip lost a server.' Write-PanelCatalog $file $catalog Assert-Panel ((Read-PanelCatalog $file).Sessions.Count -eq 3) 'Atomic replacement failed.' $before=[IO.File]::ReadAllText($file) Expect-Rejection { Write-PanelCatalog $file ([pscustomobject]@{Sessions=@($collision,$ru)}) } 'Invalid replacement must fail.' Assert-Panel ([IO.File]::ReadAllText($file) -ceq $before) 'Invalid replacement modified the existing catalog.' Assert-Panel (@(Get-ChildItem -LiteralPath $temp -Filter '*.tmp').Count -eq 0) 'Temporary catalog leaked.' $withSecret=$ru | ConvertTo-Json | ConvertFrom-Json $withSecret | Add-Member -NotePropertyName Password -NotePropertyValue 'must-not-be-saved' Write-PanelCatalog $file ([pscustomobject]@{Sessions=@($withSecret)}) Assert-Panel (-not [IO.File]::ReadAllText($file).Contains('must-not-be-saved')) 'Unknown secret field must not be persisted.' } finally { Remove-Item -LiteralPath $temp -Recurse -Force } foreach ($file in Get-ChildItem -LiteralPath $PSScriptRoot -Filter '*.ps1') { $tokens=$null; $parseErrors=$null [System.Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$parseErrors) | Out-Null Assert-Panel ($parseErrors.Count -eq 0) ('PowerShell syntax errors: '+$file.Name) } Write-Output ('PASS: '+$script:Checks+' catalog, selection, safe-forwarding, host-key and persistence checks.')