[CmdletBinding()] param( [string]$PostgresBin = 'D:\pgsql17\bin' ) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $pgStarted = $false $server = $null $testRoot = Join-Path ([IO.Path]::GetTempPath()) ('yovision-bell-132-' + [guid]::NewGuid().ToString('N')) $pgData = Join-Path $testRoot 'postgres' $pgLog = Join-Path $testRoot 'postgres.log' $pgCtlOut = Join-Path $testRoot 'pg-ctl.out.log' $pgCtlErr = Join-Path $testRoot 'pg-ctl.err.log' $serverOut = Join-Path $testRoot 'bell.out.log' $serverErr = Join-Path $testRoot 'bell.err.log' $serverExe = Join-Path $testRoot 'bell-server.exe' $serverRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path function Get-FreeTcpPort { $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) try { $listener.Start() return ([Net.IPEndPoint]$listener.LocalEndpoint).Port } finally { $listener.Stop() } } function Wait-Tcp([int]$Port, [bool]$Open, [int]$Attempts = 120) { for ($attempt = 0; $attempt -lt $Attempts; $attempt++) { $client = [Net.Sockets.TcpClient]::new() try { $connected = $client.ConnectAsync('127.0.0.1', $Port).Wait(250) -and $client.Connected } catch { $connected = $false } finally { $client.Dispose() } if ($connected -eq $Open) { return } Start-Sleep -Milliseconds 250 } throw "TCP port $Port did not reach open=$Open" } function Wait-Health([string]$BaseUrl) { for ($attempt = 0; $attempt -lt 100; $attempt++) { try { $health = Invoke-RestMethod -Uri "$BaseUrl/healthz" -TimeoutSec 2 -NoProxy if ($health.status -eq 'ok' -and $health.service -eq 'bell') { return } } catch {} Start-Sleep -Milliseconds 300 } throw 'Bell health endpoint did not become ready' } New-Item -ItemType Directory -Path $testRoot | Out-Null $pgPort = Get-FreeTcpPort $bellPort = Get-FreeTcpPort $baseUrl = "http://127.0.0.1:$bellPort" $database = 'bell_132' try { foreach ($required in @('initdb.exe', 'pg_ctl.exe', 'createdb.exe')) { $path = Join-Path $PostgresBin $required if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Missing PostgreSQL tool: $path" } } & (Join-Path $PostgresBin 'initdb.exe') -D $pgData -U postgres -A trust --encoding=UTF8 --no-locale | Out-Null if ($LASTEXITCODE -ne 0) { throw 'isolated PostgreSQL initdb failed' } $pgArguments = "-D `"$pgData`" -l `"$pgLog`" -o `"-p $pgPort -h 127.0.0.1`" start" Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList $pgArguments -RedirectStandardOutput $pgCtlOut -RedirectStandardError $pgCtlErr -WindowStyle Hidden | Out-Null Wait-Tcp -Port $pgPort -Open $true $pgStarted = $true & (Join-Path $PostgresBin 'createdb.exe') -h 127.0.0.1 -p $pgPort -U postgres $database if ($LASTEXITCODE -ne 0) { throw 'isolated Bell database creation failed' } $env:GOTOOLCHAIN = 'go1.26.5' $env:BELL_DATABASE_URL = "host=127.0.0.1 port=$pgPort user=postgres dbname=$database sslmode=disable" $env:BELL_RULE_ALERT_TEST_DATABASE_URL = $env:BELL_DATABASE_URL $env:BELL_JWT_SECRET = [guid]::NewGuid().ToString('N') + [guid]::NewGuid().ToString('N') $env:BELL_BOOTSTRAP_USERNAME = 'bell_132_admin' $env:BELL_BOOTSTRAP_PASSWORD = [guid]::NewGuid().ToString('N') $env:BELL_RULE_ALERT_OPERATOR_PASSWORD = [guid]::NewGuid().ToString('N') $env:BELL_HOST = '127.0.0.1' $env:BELL_PORT = $bellPort.ToString() Push-Location $serverRoot try { go run . migrate -c config/settings.demo.yml *> (Join-Path $testRoot 'migrate.log') if ($LASTEXITCODE -ne 0) { throw "Bell migration failed; see $(Join-Path $testRoot 'migrate.log')" } go test ./tests/bell_rule_alert -count=1 -v if ($LASTEXITCODE -ne 0) { throw 'Bell rule-alert integration test failed' } go build -o $serverExe . if ($LASTEXITCODE -ne 0) { throw 'Bell build failed' } } finally { Pop-Location } $server = Start-Process -FilePath $serverExe -ArgumentList @('server', '-c', 'config/settings.demo.yml') -WorkingDirectory $serverRoot -RedirectStandardOutput $serverOut -RedirectStandardError $serverErr -WindowStyle Hidden -PassThru Wait-Health $baseUrl $unauthorized = Invoke-RestMethod -Uri "$baseUrl/api/v1/bell/rules" -TimeoutSec 5 -NoProxy if ([int]$unauthorized.code -ne 401) { throw "unauthenticated rule list returned code $($unauthorized.code)" } $adminLoginBody = @{ username = $env:BELL_BOOTSTRAP_USERNAME; password = $env:BELL_BOOTSTRAP_PASSWORD; code = '0'; uuid = '0' } | ConvertTo-Json -Compress $adminLogin = Invoke-RestMethod -Method Post -Uri "$baseUrl/api/v1/login" -ContentType 'application/json' -Body $adminLoginBody -TimeoutSec 5 -NoProxy if ([int]$adminLogin.code -ne 200) { throw 'Bell administrator login failed' } $adminHeaders = @{ Authorization = "Bearer $($adminLogin.token)" } $adminRule = @{ code = 'http-admin'; name = '管理员 HTTP 规则'; eventType = $null; minimumSeverity = 'high'; locationContains = $null } | ConvertTo-Json -Compress $adminWrite = Invoke-RestMethod -Method Post -Uri "$baseUrl/api/v1/bell/rules" -Headers $adminHeaders -ContentType 'application/json; charset=utf-8' -Body $adminRule -TimeoutSec 5 -NoProxy if ([int]$adminWrite.code -ne 200 -or [int]$adminWrite.data.version -ne 1) { throw 'administrator rule create failed' } $operatorLoginBody = @{ username = 'bell_132_operator'; password = $env:BELL_RULE_ALERT_OPERATOR_PASSWORD; code = '0'; uuid = '0' } | ConvertTo-Json -Compress $operatorLogin = Invoke-RestMethod -Method Post -Uri "$baseUrl/api/v1/login" -ContentType 'application/json' -Body $operatorLoginBody -TimeoutSec 5 -NoProxy if ([int]$operatorLogin.code -ne 200) { throw 'Bell operator login failed' } $operatorHeaders = @{ Authorization = "Bearer $($operatorLogin.token)" } foreach ($path in @('/api/v1/bell/rules','/api/v1/bell/events','/api/v1/bell/alerts')) { $read = Invoke-RestMethod -Uri "$baseUrl$path" -Headers $operatorHeaders -TimeoutSec 5 -NoProxy if ([int]$read.code -ne 200) { throw "operator read $path returned code $($read.code)" } } $operatorWrite = Invoke-RestMethod -Method Post -Uri "$baseUrl/api/v1/bell/rules" -Headers $operatorHeaders -ContentType 'application/json' -Body $adminRule -TimeoutSec 5 -NoProxy if ([int]$operatorWrite.code -ne 403) { throw "operator rule write returned code $($operatorWrite.code)" } $menu = Invoke-RestMethod -Uri "$baseUrl/api/v1/menurole" -Headers $operatorHeaders -TimeoutSec 5 -NoProxy $menuJson = $menu.data | ConvertTo-Json -Depth 20 -Compress foreach ($title in @('预警中心','预警管理','事件查询','规则配置')) { if (-not $menuJson.Contains($title)) { throw "operator menu is missing $title" } } if ($menuJson.Contains('系统管理') -or $menuJson.Contains('开发工具')) { throw 'operator menu exposed unrelated GoAdmin modules' } Write-Output 'BELL_132_HTTP unauthenticated=401 admin_write=200 operator_reads=200 operator_write=403 minimal_menu=true' } finally { if ($null -ne $server -and -not $server.HasExited) { Stop-Process -Id $server.Id -Force $server.WaitForExit(5000) | Out-Null } if ($pgStarted) { & (Join-Path $PostgresBin 'pg_ctl.exe') -D $pgData -m fast stop *> (Join-Path $testRoot 'pg-stop.log') } foreach ($name in @('BELL_DATABASE_URL','BELL_RULE_ALERT_TEST_DATABASE_URL','BELL_RULE_ALERT_OPERATOR_PASSWORD','BELL_JWT_SECRET','BELL_BOOTSTRAP_USERNAME','BELL_BOOTSTRAP_PASSWORD','BELL_HOST','BELL_PORT')) { Remove-Item "Env:$name" -ErrorAction SilentlyContinue } Write-Verbose "Bell #132 temporary artifacts: $testRoot" }