mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate.
356 lines
13 KiB
Go
356 lines
13 KiB
Go
package installtests
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestInstallPS1ParsesWithPowerShell(t *testing.T) {
|
|
pwsh, err := exec.LookPath("pwsh")
|
|
if err != nil {
|
|
t.Skip("pwsh not installed")
|
|
}
|
|
|
|
scriptPath := repoFile("scripts", "install.ps1")
|
|
cmd := exec.Command(pwsh,
|
|
"-NoLogo",
|
|
"-NoProfile",
|
|
"-NonInteractive",
|
|
"-Command",
|
|
`$errors = $null; [System.Management.Automation.Language.Parser]::ParseFile($env:PULSE_INSTALL_PS1_PATH, [ref]$null, [ref]$errors) > $null; if ($errors.Count) { $errors | ForEach-Object { Write-Error $_.ToString() }; exit 1 }`,
|
|
)
|
|
cmd.Env = append(os.Environ(), "PULSE_INSTALL_PS1_PATH="+scriptPath)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("install.ps1 failed PowerShell parser check: %v\n%s", err, output)
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1DockerModeDefaultsHostOff(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`if ($EnableDocker -and -not $PSBoundParameters.ContainsKey('EnableHost') -and [string]::IsNullOrWhiteSpace($env:PULSE_ENABLE_HOST)) {`,
|
|
`$EnableHost = $false`,
|
|
`if ($EnableHost) { $ServiceArgs += "--enable-host" } else { $ServiceArgs += "--enable-host=false" }`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing docker/host parity guard: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1AllowsMissingTokenForOptionalAuth(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[string]$TokenFile = $env:PULSE_TOKEN_FILE,`,
|
|
`if (-not [string]::IsNullOrWhiteSpace($Token) -and -not (Test-ValidToken $Token)) {`,
|
|
`if ([string]::IsNullOrWhiteSpace($Token) -and -not [string]::IsNullOrWhiteSpace($TokenFile)) {`,
|
|
`$Token = (Get-Content -Path $resolvedTokenFile -Raw -ErrorAction Stop).Trim()`,
|
|
`function Write-RuntimeTokenFile {`,
|
|
`if (-not [string]::IsNullOrWhiteSpace($Token)) { $ServiceArgs += @("--token-file", "` + "`" + `"$TokenFilePath` + "`" + `"") }`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing optional-token handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1AgentDownloadIsServerVersionAware(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`Invoke-WebRequest -Uri "$Url/api/version"`,
|
|
`$versionInfo = $versionResponse.Content | ConvertFrom-Json`,
|
|
`$ServerVersion = [string]$versionInfo.version`,
|
|
`$escapedServerVersion = [Uri]::EscapeDataString($ServerVersion)`,
|
|
`$DownloadUrl = "$DownloadUrl&serverVersion=$escapedServerVersion"`,
|
|
`downloaded agent version ($DownloadedVersion) does not match Pulse server version ($ServerVersion)`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing version-aware agent download behavior: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1AllowsOptionalAuthUninstallWithoutToken(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`if (-not [string]::IsNullOrWhiteSpace($Url)) {`,
|
|
`$invokeArgs = @{`,
|
|
`if (-not [string]::IsNullOrWhiteSpace($Token)) {`,
|
|
`$invokeArgs.Headers = @{ "X-API-Token" = $Token }`,
|
|
`Invoke-RestMethod @invokeArgs | Out-Null`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing optional-auth uninstall handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1PreservesProxmoxProfileParity(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[bool]$EnableProxmox = $false,`,
|
|
`[string]$ProxmoxType = "",`,
|
|
`if (-not $PSBoundParameters.ContainsKey('EnableProxmox') -and -not [string]::IsNullOrWhiteSpace($env:PULSE_ENABLE_PROXMOX)) {`,
|
|
`$EnableProxmox = Parse-Bool $env:PULSE_ENABLE_PROXMOX $EnableProxmox`,
|
|
`if (-not $PSBoundParameters.ContainsKey('ProxmoxType') -and -not [string]::IsNullOrWhiteSpace($env:PULSE_PROXMOX_TYPE)) {`,
|
|
`$ProxmoxType = $env:PULSE_PROXMOX_TYPE`,
|
|
`if ($EnableProxmox) { $ServiceArgs += "--enable-proxmox" }`,
|
|
`if (-not [string]::IsNullOrWhiteSpace($NormalizedProxmoxType)) { $ServiceArgs += @("--proxmox-type", "` + "`" + `"$NormalizedProxmoxType` + "`" + `"") }`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing proxmox profile parity: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1PreservesCommandExecutionParity(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[bool]$EnableCommands = $false,`,
|
|
`if (-not $PSBoundParameters.ContainsKey('EnableCommands') -and -not [string]::IsNullOrWhiteSpace($env:PULSE_ENABLE_COMMANDS)) {`,
|
|
`$EnableCommands = Parse-Bool $env:PULSE_ENABLE_COMMANDS $EnableCommands`,
|
|
`if ($EnableCommands) { $ServiceArgs += "--enable-commands" }`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing command-execution parity: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1UsesInsecureTlsForRuntimeTransport(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`function Invoke-WithOptionalInsecureTls {`,
|
|
`if ($AllowInsecure -or $null -ne $CustomCaCertificate) {`,
|
|
`if ($AllowInsecure) {`,
|
|
`return Test-CertificateTrustedByCustomCa -Certificate $certificate -CustomCaCertificate $CustomCaCertificate`,
|
|
`if ($Url.ToLowerInvariant().StartsWith("http://") -and -not $Insecure) {`,
|
|
`Plain HTTP Pulse URL detected; enabling insecure mode for persisted agent update checks.`,
|
|
`Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
|
`Invoke-RestMethod @invokeArgs | Out-Null`,
|
|
`$downloadTask = $webClient.DownloadFileTaskAsync($DownloadUrl, $TempPath)`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing insecure runtime transport handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1SupportsDownloadPreflightBeforeAdministratorInstall(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[bool]$PreflightOnly = $false,`,
|
|
`[string]$Output = $env:PULSE_OUTPUT,`,
|
|
`[bool]$NonInteractive = $false`,
|
|
`if (-not $isAdmin -and -not $PreflightOnly) {`,
|
|
`function Write-InstallerEvent {`,
|
|
`function Get-ResponseHeaderValue {`,
|
|
`function Invoke-AgentDownloadPreflight {`,
|
|
`Invoke-WebRequest -Uri $Uri -Method Head -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop`,
|
|
`$checksum = Get-ResponseHeaderValue -Headers $preflightResponse.Headers -Name "X-Checksum-Sha256"`,
|
|
`$downloadPreflightResponse = Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
|
`Invoke-WebRequest -Uri $DownloadUrl -Method Head -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop`,
|
|
`$serverChecksum = Get-ResponseHeaderValue -Headers $downloadPreflightResponse.Headers -Name "X-Checksum-Sha256"`,
|
|
`$downloadMetadata = Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
|
`$downloadChecksum = Get-ResponseHeaderValue -Headers $webClient.ResponseHeaders -Name "X-Checksum-Sha256"`,
|
|
`agent_download_checksum_missing`,
|
|
`agent_download_available`,
|
|
`agent_download_unavailable`,
|
|
`if ($PreflightOnly) {`,
|
|
`Invoke-AgentDownloadPreflight $DownloadUrl`,
|
|
`if (-not $NonInteractive) {`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing download preflight handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1ReadsAgentIdentityFromEnvironment(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[string]$AgentId = $env:PULSE_AGENT_ID,`,
|
|
`[string]$Hostname = $env:PULSE_HOSTNAME`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing agent identity env handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1UsesHostnameLookupForUninstallFallback(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`$lookupHostname = $Hostname`,
|
|
`$lookupHostname = $env:COMPUTERNAME`,
|
|
`Uri = "$Url/api/agents/agent/lookup?hostname=$([System.Uri]::EscapeDataString($lookupHostname))"`,
|
|
`$lookupResult = Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $customCaCertificate -Action {`,
|
|
`Invoke-RestMethod @lookupArgs`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing uninstall hostname lookup fallback: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1PersistsAndRecoversConnectionIdentity(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`$ConnectionStatePath = "$StateDir\connection.env"`,
|
|
`$TokenFilePath = "$StateDir\token"`,
|
|
`Set-Content -Path $ConnectionStatePath -Value ($lines -join "` + "`n" + `") -Encoding UTF8`,
|
|
`$lines += "PULSE_TOKEN_FILE='$TokenFilePath'"`,
|
|
`$Url = Get-ConnectionStateValue "PULSE_URL"`,
|
|
`$Token = Get-ConnectionStateValue "PULSE_TOKEN"`,
|
|
`$savedTokenFile = Get-ConnectionStateValue "PULSE_TOKEN_FILE"`,
|
|
`$AgentId = Get-ConnectionStateValue "PULSE_AGENT_ID"`,
|
|
`$Hostname = Get-ConnectionStateValue "PULSE_HOSTNAME"`,
|
|
`$Insecure = Parse-Bool (Get-ConnectionStateValue "PULSE_INSECURE_SKIP_VERIFY") $Insecure`,
|
|
`$CACertPath = Get-ConnectionStateValue "PULSE_CACERT"`,
|
|
`$lines += "PULSE_INSECURE_SKIP_VERIFY='true'"`,
|
|
`$lines += "PULSE_CACERT='$CACertPath'"`,
|
|
`Write-RuntimeTokenFile`,
|
|
`Save-ConnectionState`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing persisted connection identity handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1SupportsCustomCATransport(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`[string]$CACertPath = $env:PULSE_CACERT,`,
|
|
`function Load-CustomCaCertificate {`,
|
|
`return [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($resolvedPath)`,
|
|
`function Test-CertificateTrustedByCustomCa {`,
|
|
`$chain.ChainPolicy.ExtraStore.Add($CustomCaCertificate)`,
|
|
`Invoke-WithOptionalInsecureTls -AllowInsecure $Insecure -CustomCaCertificate $CustomCaCertificate -Action {`,
|
|
`Show-Error "Invalid CA certificate path. File does not exist.`,
|
|
`Show-Error "Invalid CA certificate file. Provide a PEM, CRT, or CER certificate.`,
|
|
`if (-not [string]::IsNullOrWhiteSpace($CACertPath)) { $ServiceArgs += @("--cacert", "` + "`" + `"$CACertPath` + "`" + `"") }`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing custom CA transport handling: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1ClearsPersistedStateAfterUninstall(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`if (Test-Path $StateDir) {`,
|
|
`Remove-Item $StateDir -Recurse -Force -ErrorAction SilentlyContinue`,
|
|
`Write-Host "Uninstallation complete." -ForegroundColor Green`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing persisted state cleanup after uninstall: %s", needle)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInstallPS1RequiresPinnedSignatureVerificationForReleaseDownloads(t *testing.T) {
|
|
content, err := os.ReadFile(repoFile("scripts", "install.ps1"))
|
|
if err != nil {
|
|
t.Fatalf("read install.ps1: %v", err)
|
|
}
|
|
|
|
script := string(content)
|
|
required := []string{
|
|
`$PinnedInstallerSshPublicKey = "__PULSE_INSTALLER_SSH_PUBLIC_KEY__"`,
|
|
`function Test-HasPinnedInstallerSignatureKey {`,
|
|
`function Invoke-InstallerSignatureVerification {`,
|
|
`Get-Command ssh-keygen.exe -ErrorAction SilentlyContinue`,
|
|
`$serverSshSignature = Get-ResponseHeaderValue -Headers $downloadPreflightResponse.Headers -Name $InstallerSignatureHeaderName`,
|
|
`Show-Error "Server did not provide checksum header; refusing signed install."`,
|
|
`Show-Error "Server did not provide SSH signature header; refusing signed install."`,
|
|
`Invoke-InstallerSignatureVerification -FilePath $TempPath -SignatureHeader $serverSshSignature`,
|
|
`-Y verify -f`,
|
|
}
|
|
for _, needle := range required {
|
|
if !strings.Contains(script, needle) {
|
|
t.Fatalf("install.ps1 missing signed-download verification contract: %s", needle)
|
|
}
|
|
}
|
|
}
|