Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $senseRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) . (Join-Path $senseRoot 'scripts\runtime\sense-common.ps1') $script:passed = 0 function Assert-True([bool]$Condition, [string]$Message) { if (-not $Condition) { throw "ASSERT FAILED: $Message" } $script:passed++ } function Assert-Equal($Expected, $Actual, [string]$Message) { if ($Expected -cne $Actual) { throw "ASSERT FAILED: $Message; expected [$Expected], got [$Actual]" } $script:passed++ } function Assert-Throws([scriptblock]$Action, [string]$Pattern, [string]$Message) { try { & $Action; throw "ASSERT FAILED: $Message; no error was raised" } catch { if ($_.Exception.Message -notmatch $Pattern) { throw "ASSERT FAILED: $Message; unexpected error: $($_.Exception.Message)" } } $script:passed++ } $temporary = Join-Path ([IO.Path]::GetTempPath()) ("sense-package-tests-" + [guid]::NewGuid().ToString('N')) $listener = $null $oldValues = @{} foreach ($name in $script:SenseAllowedEnvironment) { $oldValues[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') [Environment]::SetEnvironmentVariable($name, $null, 'Process') } try { New-Item -ItemType Directory -Path (Join-Path $temporary 'config'), (Join-Path $temporary 'web'), (Join-Path $temporary 'web\js'), (Join-Path $temporary 'bin') | Out-Null $webIndex = '
' [IO.File]::WriteAllText((Join-Path $temporary 'web\index.html'), $webIndex, (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $temporary 'web\js\runtime.fixture.js'), 'fixture', (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $temporary 'bin\mediamtx.exe'), 'fixture', (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $temporary 'config\mediamtx.yml'), 'api: true', (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $temporary 'config\mediamtx-shards.json'), '[]', (New-Object Text.UTF8Encoding($false))) $webAssetAudit = Join-Path $senseRoot 'scripts\build\assert-web-assets.ps1' & $webAssetAudit -WebRoot (Join-Path $temporary 'web') Assert-True $true 'web asset audit must accept existing local references' Remove-Item -LiteralPath (Join-Path $temporary 'web\js\runtime.fixture.js') Assert-Throws { & $webAssetAudit -WebRoot (Join-Path $temporary 'web') } 'missing local asset' 'web asset audit must reject missing runtime files' [IO.File]::WriteAllText((Join-Path $temporary 'web\js\runtime.fixture.js'), 'fixture', (New-Object Text.UTF8Encoding($false))) $listener = New-Object Net.Sockets.TcpListener([Net.IPAddress]::Loopback, 0) $listener.Start() $dbPort = ([Net.IPEndPoint]$listener.LocalEndpoint).Port $marker = Join-Path $temporary 'must-not-exist.txt' $envText = @( 'SENSE_HOST=127.0.0.1', 'SENSE_PORT=18070', "SENSE_DATABASE_URL=host=127.0.0.1 port=$dbPort user=sense password=p#&;=x dbname=sense sslmode=disable", "SENSE_JWT_SECRET=`$(Set-Content -LiteralPath '$marker' hacked)-literal-secret-1234567890", 'SENSE_MEDIAMTX_MODE=managed', 'SENSE_MEDIAMTX_BINARY=bin\mediamtx.exe', 'SENSE_MEDIAMTX_CONFIG=config\mediamtx.yml', 'SENSE_MEDIAMTX_API=http://127.0.0.1:9997', 'SENSE_MEDIAMTX_SHARDS_FILE=config\mediamtx-shards.json', 'SENSE_WEB_ROOT=web' ) -join "`n" [IO.File]::WriteAllText((Join-Path $temporary 'config\sense.env'), $envText, (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $temporary 'config\sense.demo.env'), $envText.Replace('SENSE_DATABASE_URL=', 'SENSE_DEMO_DATABASE_URL=').Replace('dbname=sense ', 'dbname=sense_demo '), (New-Object Text.UTF8Encoding($false))) [Environment]::SetEnvironmentVariable('SENSE_PORT', '18071', 'Process') $state = Initialize-SenseRuntime -PackageRoot $temporary -Mode production Assert-Equal 18071 $state.Port 'non-empty process environment must override sense.env' Assert-True (-not (Test-Path -LiteralPath $marker)) 'sense.env content must never execute' $generatedSettings = Get-Content -LiteralPath $state.SettingsPath -Raw Assert-True $generatedSettings.Contains('timeout: 2592000') 'generated runtime settings must keep login valid for 30 days' Assert-True ((Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL').Contains('p#&;=x')) 'special characters must survive env parsing' Assert-True ($generatedSettings.Contains('password=p#\u0026;=x')) 'special characters must be safely JSON-escaped in YAML' Assert-Equal (Join-Path $temporary 'config\mediamtx-shards.json') (Get-SenseEnvironmentValue -Name 'SENSE_MEDIAMTX_SHARDS_FILE') 'relative shard config path must resolve inside package root' Assert-True (-not ((Get-SenseDatabaseInfo (Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL')).Sanitized.Contains('password='))) 'PostgreSQL tool arguments must not contain password' [Environment]::SetEnvironmentVariable('SENSE_PORT', $null, 'Process') foreach ($name in @('SENSE_DATABASE_URL','SENSE_JWT_SECRET','SENSE_MEDIAMTX_MODE','SENSE_MEDIAMTX_BINARY','SENSE_MEDIAMTX_CONFIG','SENSE_MEDIAMTX_API','SENSE_WEB_ROOT')) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') } $demo = Initialize-SenseRuntime -PackageRoot $temporary -Mode demo Assert-Equal 'sense_demo' $demo.Database.Database 'demo must use its dedicated database variable' $bad = Join-Path $temporary 'config\bad.env' [IO.File]::WriteAllText($bad, 'SENSE_UNKNOWN=value', (New-Object Text.UTF8Encoding($false))) Assert-Throws { Import-SenseEnvironment -Path $bad } 'Unsupported Sense configuration key' 'unknown keys must be rejected' [IO.File]::WriteAllText((Join-Path $temporary 'config\sense.demo.env'), $envText.Replace('SENSE_DATABASE_URL=', 'SENSE_DEMO_DATABASE_URL='), (New-Object Text.UTF8Encoding($false))) foreach ($name in $script:SenseAllowedEnvironment) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') } Assert-Throws { Initialize-SenseRuntime -PackageRoot $temporary -Mode demo } 'database name containing demo or test' 'demo must reject production database names' foreach ($file in Get-ChildItem -LiteralPath (Join-Path $senseRoot 'scripts') -Recurse -Filter '*.ps1') { [void][scriptblock]::Create((Get-Content -LiteralPath $file.FullName -Raw)) $script:passed++ } Write-Host "Sense package tests passed: $script:passed assertions." } finally { if ($listener) { $listener.Stop() } foreach ($name in $script:SenseAllowedEnvironment) { [Environment]::SetEnvironmentVariable($name, $oldValues[$name], 'Process') } if (Test-Path -LiteralPath $temporary) { $resolved = [IO.Path]::GetFullPath($temporary) if (-not $resolved.StartsWith([IO.Path]::GetTempPath(), [StringComparison]::OrdinalIgnoreCase)) { throw "Unsafe temporary test path: $resolved" } Remove-Item -LiteralPath $resolved -Recurse -Force } }