package api import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/rcourtman/pulse-go-rewrite/internal/config" ) func TestHandleSetupScriptRejectsUnsafeAuthToken(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://example.com&setup_token=$(touch%20/tmp/pwned)", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 bad request for unsafe auth token, got %d (%s)", rr.Code, rr.Body.String()) } } func TestHandleSetupScriptRejectsUnsafePulseURL(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://example.com&pulse_url=http://example.com%5C%0Aecho%20oops", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 bad request for unsafe pulse_url, got %d (%s)", rr.Code, rr.Body.String()) } } func TestPVESetupScriptArgumentAlignment(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) // Use sentinel values to verify fmt.Sprintf argument alignment req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() // Critical alignment checks to prevent fmt.Sprintf argument mismatch bugs // After refactor: script uses bash variables ($PULSE_URL, $TOKEN_NAME) instead of fmt.Sprintf substitutions tests := []struct { name string contains string desc string }{ { name: "token_id_uses_tokenname", contains: `Token ID: $PULSE_TOKEN_ID`, desc: "Token ID should use $PULSE_TOKEN_ID bash variable", }, { name: "bash_variables_defined", contains: `PULSE_URL="http://sentinel-url:7656"`, desc: "Bash variable PULSE_URL should be defined at top of script", }, { name: "token_name_variable_defined", contains: `TOKEN_NAME="pulse-sentinel-url"`, desc: "Bash variable TOKEN_NAME should use deterministic Pulse token naming", }, { name: "register_json_marks_script_source", contains: `"source":"script"`, desc: "Auto-register payload should mark script source so later canonical reruns can reuse confirmed script-created tokens", }, { name: "register_json_uses_secure_contract", contains: `"tokenValue":"'"$TOKEN_VALUE"'"`, desc: "Generated setup scripts should submit locally created token completion through the canonical /api/auto-register payload", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if !containsString(script, tt.contains) { t.Errorf("%s\nExpected to find: %s\nIn generated script (first 500 chars):\n%s", tt.desc, tt.contains, truncate(script, 500)) } }) } // Additional check: ensure authToken doesn't appear in --pulse-server flags if containsString(script, "--pulse-server deadbeef") { t.Error("BUG: authToken appearing in --pulse-server URL (argument misalignment)") } } func TestHandleSetupScriptUsesCanonicalShellDownloadHeaders(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } if got := rr.Header().Get("Content-Type"); got != "text/x-shellscript; charset=utf-8" { t.Fatalf("Content-Type = %q, want %q", got, "text/x-shellscript; charset=utf-8") } if got := rr.Header().Get("Content-Disposition"); got != "attachment; filename=\"pulse-setup-pve.sh\"" { t.Fatalf("Content-Disposition = %q, want %q", got, "attachment; filename=\"pulse-setup-pve.sh\"") } if body := strings.TrimSpace(rr.Body.String()); body == "" { t.Fatal("expected non-empty setup script body") } } func TestPVESetupScriptRestrictsTemperatureMonitoringToPulseSensorWrapper(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() required := []string{ `PULSE_SENSORS_WRAPPER="/usr/local/sbin/pulse-sensors"`, `install_pulse_sensors_wrapper() {`, `def block_device_targets():`, `["lsblk", "-J", "-d", "-o", "NAME,TYPE,TRAN,MODEL,VENDOR,SUBSYSTEMS"]`, `def union_smart_targets(scan_targets, block_devices):`, `def smart_probe_attempts(device, device_type):`, `def inferred_smart_device_types(device):`, `for dtype in inferred_smart_device_types(device):`, `for attempt_index, (attempt_device, attempt_type) in enumerate(attempts):`, `attempt_index == len(attempts) - 1`, `"smart": collect_smart(),`, `SSH_SENSORS_KEY_ENTRY="command=\"$PULSE_SENSORS_WRAPPER\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty $SSH_SENSORS_PUBLIC_KEY # pulse-sensors"`, } for _, want := range required { if !containsString(script, want) { t.Fatalf("expected setup script to contain %q, got:\n%s", want, truncate(script, 1800)) } } if containsString(script, `SSH_SENSORS_KEY_ENTRY="command=\"sensors -j\"`) || containsString(script, `SSH_SENSORS_KEY_ENTRY="command="sensors -j",`) { t.Fatalf("unexpected raw sensors forced-command key entry in generated script:\n%s", truncate(script, 1800)) } } func TestPVESetupScriptPreservesAuthorizedKeysSymlink(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() required := []string{ `resolve_authorized_keys_path() {`, `if [ -L "$auth_keys" ]; then`, `resolved="$(readlink -f "$auth_keys" 2>/dev/null || true)"`, `install_authorized_keys_file() {`, `chmod --reference="$auth_keys" "$tmp_auth_keys" 2>/dev/null || chmod 600 "$tmp_auth_keys"`, `if mv -f "$tmp_auth_keys" "$auth_keys" 2>/dev/null; then`, `if cp -f "$tmp_auth_keys" "$auth_keys" 2>/dev/null; then`, `grep -vF '# pulse-' "$AUTH_KEYS" > "$TMP_AUTH_KEYS" 2>/dev/null`, `grep -vF "# pulse-" "$AUTH_KEYS" > "$TMP_AUTH_KEYS" 2>/dev/null`, `printf '%s\n' "$SSH_SENSORS_KEY_ENTRY" >> "$TMP_AUTH_KEYS"`, `install_authorized_keys_file "$TMP_AUTH_KEYS" "$AUTH_KEYS"`, } for _, want := range required { if !containsString(script, want) { t.Fatalf("expected setup script to contain %q, got:\n%s", want, truncate(script, 1200)) } } if got := strings.Count(script, `AUTH_KEYS="$(resolve_authorized_keys_path)"`); got < 2 { t.Fatalf("expected setup script install and uninstall paths to resolve authorized_keys symlinks, count=%d\n%s", got, truncate(script, 1200)) } forbidden := []string{ `grep -vF '# pulse-managed-key'`, `/root/.ssh/authorized_keys.tmp`, `mv /root/.ssh/authorized_keys.tmp /root/.ssh/authorized_keys`, `mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys`, `echo "$SSH_SENSORS_KEY_ENTRY" >> /root/.ssh/authorized_keys`, `chmod --reference=/root/.ssh/authorized_keys`, `chown --reference=/root/.ssh/authorized_keys`, } for _, stale := range forbidden { if containsString(script, stale) { t.Fatalf("setup script preserved stale authorized_keys handling %q:\n%s", stale, truncate(script, 1200)) } } } func TestHandleSetupScriptRejectsPBSBackupPerms(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs.example:8007&pulse_url=https://pulse.example.com:7655&backup_perms=true", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 bad request, got %d (%s)", rr.Code, rr.Body.String()) } if got := strings.TrimSpace(rr.Body.String()); got != "backup_perms is only supported for type 'pve'" { t.Fatalf("body = %q, want canonical backup-perms guidance", got) } } func TestPVESetupScript_ConfiguresPulseMonitorRoleSafely(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() // Privileges must be comma-separated for pveum, and generated tokens must stay privilege-separated. wantSnippets := []string{ `extract_pve_token_value()`, `create_pve_token()`, `pveum user token add pulse-monitor@pve "$TOKEN_NAME" --privsep 1 --output-format json`, `pveum user token add pulse-monitor@pve "$TOKEN_NAME" --privsep 1 2>&1`, `TOKEN_VALUE=$(extract_pve_token_value "$TOKEN_OUTPUT")`, `pveum aclmod / -user pulse-monitor@pve -role PVEAuditor`, `pveum aclmod / -token "$PULSE_TOKEN_ID" -role PVEAuditor`, `PRIV_STRING="$(IFS=,; echo "${EXTRA_PRIVS[*]}")"`, `pveum role modify PulseMonitor -privs "$PRIV_STRING" 2>/dev/null || pveum role add PulseMonitor -privs "$PRIV_STRING" 2>/dev/null`, `pveum aclmod / -user pulse-monitor@pve -role PulseMonitor`, `pveum aclmod / -token "$PULSE_TOKEN_ID" -role PulseMonitor`, } for _, snippet := range wantSnippets { if !containsString(script, snippet) { t.Fatalf("expected generated script to contain:\n%s\n\nGot (first 500 chars):\n%s", snippet, truncate(script, 500)) } } if containsString(script, `--privsep 0`) { t.Fatalf("expected generated PVE token to remain privilege-separated, got: %s", truncate(script, 900)) } } func TestPVESetupScript_HardensTokenValueExtraction(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() wantSnippets := []string{ `extract_json_string_field()`, `sed -n 's/.*"'"$field_name"'"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'`, `awk -F'[|│]'`, `unknown option|unknown command|no such option|unable to parse option|output-format`, } for _, snippet := range wantSnippets { if !containsString(script, snippet) { t.Fatalf("expected generated script to contain:\n%s\n\nGot (first 1200 chars):\n%s", snippet, truncate(script, 1200)) } } jsonCreate := strings.Index(script, `pveum user token add pulse-monitor@pve "$TOKEN_NAME" --privsep 1 --output-format json`) legacyCreate := strings.Index(script, `pveum user token add pulse-monitor@pve "$TOKEN_NAME" --privsep 1 2>&1`) if jsonCreate < 0 || legacyCreate < 0 || jsonCreate > legacyCreate { t.Fatalf("generated script must try JSON token output before legacy output fallback") } if containsString(script, `grep "│ value" | awk -F'│'`) { t.Fatalf("expected hardened token parser to replace the old grep/awk single-shape extraction") } } func TestPVESetupScript_PrefersGuestAgentPrivileges(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() wantSnippets := []string{ `local HAS_VM_GUEST_AGENT_FILE_READ=false`, `pveum role add TestGuestAgentFileRead -privs VM.GuestAgent.FileRead 2>/dev/null`, `if [ "$HAS_VM_GUEST_AGENT_AUDIT" = true ]; then`, `EXTRA_PRIVS+=("VM.GuestAgent.Audit")`, `if [ "$HAS_VM_GUEST_AGENT_FILE_READ" = true ]; then`, `EXTRA_PRIVS+=("VM.GuestAgent.FileRead")`, `elif [ "$HAS_VM_MONITOR" = true ]; then`, `EXTRA_PRIVS+=("VM.Monitor")`, } for _, snippet := range wantSnippets { if !containsString(script, snippet) { t.Fatalf("expected generated script to contain:\n%s\n\nGot (first 900 chars):\n%s", snippet, truncate(script, 900)) } } guestBranch := strings.Index(script, `if [ "$HAS_VM_GUEST_AGENT_AUDIT" = true ]; then`) monitorBranch := strings.Index(script, `elif [ "$HAS_VM_MONITOR" = true ]; then`) if guestBranch < 0 || monitorBranch < 0 || guestBranch > monitorBranch { t.Fatalf("generated script must prefer VM.GuestAgent.* privileges before VM.Monitor") } } func TestPVESetupScript_UsesFailFastRetryGuidance(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `PULSE_BOOTSTRAP_COMMAND_WITH_ENV='curl -fsSL '"'"'http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve'"'"' | `) { t.Fatalf("expected canonical bootstrap command owner in setup script, got: %s", truncate(script, 700)) } if !containsString(script, `echo " $PULSE_BOOTSTRAP_COMMAND_WITH_ENV"`) { t.Fatalf("expected canonical bootstrap-command retry guidance in setup script, got: %s", truncate(script, 700)) } if !containsString(script, `SETUP_SCRIPT_URL="http://sentinel-url:7656/api/setup-script?host=http%3A%2F%2Fsentinel-host%3A8006&pulse_url=http%3A%2F%2Fsentinel-url%3A7656&type=pve"`) { t.Fatalf("expected canonical encoded retry URL in setup script, got: %s", truncate(script, 700)) } if !containsString(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-`) { t.Fatalf("expected canonical embedded setup token initialization before retry guidance, got: %s", truncate(script, 700)) } if !containsString(script, `echo "Root privileges required. Run as root (su -) and retry."`) { t.Fatalf("expected canonical root requirement guidance in setup script, got: %s", truncate(script, 700)) } if !containsString(script, `echo "This setup flow must run on the Proxmox host so Pulse can create"`) { t.Fatalf("expected canonical off-host rerun guidance in setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " curl -sSL \"$SETUP_SCRIPT_URL\" | bash"`) || containsString(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pve&host=YOUR_PVE_URL&pulse_url=$PULSE_URL\" | bash"`) { t.Fatalf("expected stale non-fail-fast guidance to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo "Manual setup steps:"`) || containsString(script, `echo " 2. In Pulse: Settings → Nodes → Add Node (enter token from above)"`) { t.Fatalf("expected stale off-host manual token flow to be removed from setup script, got: %s", truncate(script, 900)) } } func TestPVESetupScript_PreservesEncodedRerunURLAndBackupPerms(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https%3A%2F%2F%5B2001%3Adb8%3A%3A1%5D%3A8006&pulse_url=https%3A%2F%2Fpulse.example.com%3A7656&backup_perms=true&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() expectedSetupScriptURL := `SETUP_SCRIPT_URL="https://pulse.example.com:7656/api/setup-script?backup_perms=true&host=https%3A%2F%2F%5B2001%3Adb8%3A%3A1%5D%3A8006&pulse_url=https%3A%2F%2Fpulse.example.com%3A7656&type=pve"` if !containsString(script, expectedSetupScriptURL) { t.Fatalf("expected encoded setup rerun URL with preserved backup permissions, got: %s", truncate(script, 900)) } if !containsString(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}"`) { t.Fatalf("expected canonical embedded setup token initialization before PVE rerun guidance, got: %s", truncate(script, 900)) } if !containsString(script, `pveum aclmod /storage -user pulse-monitor@pve -role PVEDatastoreAdmin`) { t.Fatalf("expected backup permission role setup to remain enabled, got: %s", truncate(script, 900)) } if !containsString(script, `pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin`) { t.Fatalf("expected backup permission role setup to apply to the privilege-separated token, got: %s", truncate(script, 900)) } } func TestPVESetupScript_RemovesDiscoveredOldTokensFromBothUsers(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `done <<< "$OLD_TOKENS_PVE"`) { t.Fatalf("expected PVE cleanup loop to iterate discovered pve tokens, got: %s", truncate(script, 900)) } if !containsString(script, `done <<< "$OLD_TOKENS_PAM"`) { t.Fatalf("expected PVE cleanup loop to iterate discovered pam tokens, got: %s", truncate(script, 900)) } if containsString(script, `done <<< "$OLD_TOKENS"`) { t.Fatalf("expected stale undefined old-token cleanup variable to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `pveum user token remove pulse-monitor@pve "$TOKEN"`) { t.Fatalf("expected PVE cleanup to remove pve user tokens, got: %s", truncate(script, 900)) } if !containsString(script, `pveum user token remove pulse-monitor@pam "$TOKEN"`) { t.Fatalf("expected PVE cleanup to remove pam user tokens, got: %s", truncate(script, 900)) } } func TestPVESetupScript_IncludesNonDestructiveSetupDoctor(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&backup_perms=true", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() required := []string{ `echo " [2] Audit/Repair - Check and repair Pulse-managed Proxmox setup"`, `MAIN_ACTION="${PULSE_SETUP_ACTION:-}"`, `run_pve_setup_doctor() {`, `echo "This mode does not rotate the API token."`, `echo " Expire: $(pulse_pve_token_expire_label)"`, `pveum aclmod / -user pulse-monitor@pve -role PVEAuditor`, `if pulse_pve_token_exists; then`, `pveum aclmod /storage -token "$PULSE_TOKEN_ID" -role PVEDatastoreAdmin`, `configure_pve_pulse_monitor_role "$token_exists"`, `Choose Install/Configure to rotate the token and let Pulse re-register it.`, } for _, snippet := range required { if !containsString(script, snippet) { t.Fatalf("expected setup doctor snippet %q, got: %s", snippet, truncate(script, 2200)) } } } func TestPVESetupScript_RemovesMatchingPulseConnectionBeforeLocalCleanup(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655&setup_token=deadbeefdeadbeefdeadbeefdeadbeef", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `echo " • Removing Pulse connection from server..."`) { t.Fatalf("expected teardown script to announce Pulse-side removal, got: %s", truncate(script, 900)) } if !containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-unregister"`) { t.Fatalf("expected teardown script to use canonical /api/auto-unregister endpoint, got: %s", truncate(script, 900)) } if !containsString(script, `"tokenId":"'"$PULSE_TOKEN_ID"'"`) { t.Fatalf("expected teardown payload to bind the canonical token id, got: %s", truncate(script, 900)) } } func TestPVESetupScript_UsesCanonicalTokenPrefixForCleanupDiscovery(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `TOKEN_MATCH_PREFIX="pulse-pulse-example-com"`) { t.Fatalf("expected cleanup discovery to reuse canonical token prefix, got: %s", truncate(script, 900)) } if !containsString(script, `grep -E "^${TOKEN_MATCH_PREFIX}(-[0-9]+)?$"`) { t.Fatalf("expected cleanup discovery to match canonical token prefix and legacy suffixes, got: %s", truncate(script, 900)) } if containsString(script, `PULSE_IP_PATTERN=`) { t.Fatalf("expected stale ip-pattern token discovery to be removed, got: %s", truncate(script, 900)) } } func TestPBSSetupScript_UsesCanonicalTokenPrefixForCleanupDiscovery(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs-node.example.internal:8007&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `TOKEN_MATCH_PREFIX="pulse-pulse-example-com"`) { t.Fatalf("expected PBS cleanup discovery to reuse canonical token prefix, got: %s", truncate(script, 900)) } if !containsString(script, `grep -oE "${TOKEN_MATCH_PREFIX}(-[0-9]+)?" | sort -u || true`) { t.Fatalf("expected PBS cleanup discovery to match canonical token prefix and legacy suffixes, got: %s", truncate(script, 900)) } if containsString(script, `PULSE_IP_PATTERN=`) { t.Fatalf("expected stale PBS ip-pattern token discovery to be removed, got: %s", truncate(script, 900)) } } func TestSetupScripts_UseExactTokenMatchForRotationDetection(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) pveReq := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) pveRR := httptest.NewRecorder() handlers.HandleSetupScript(pveRR, pveReq) if pveRR.Code != http.StatusOK { t.Fatalf("expected 200 OK for PVE, got %d (%s)", pveRR.Code, pveRR.Body.String()) } pveScript := pveRR.Body.String() if !containsString(pveScript, `pulse_pve_token_exists() {`) || !containsString(pveScript, `grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) || !containsString(pveScript, `if pulse_pve_token_exists; then`) { t.Fatalf("expected PVE token rotation detection to use exact token-name matching, got: %s", truncate(pveScript, 900)) } if containsString(pveScript, `grep -q "$TOKEN_NAME"`) { t.Fatalf("expected stale broad PVE token detection to be removed, got: %s", truncate(pveScript, 900)) } pbsReq := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs-node.example.internal:8007&pulse_url=https://pulse.example.com:7655", nil, ) pbsRR := httptest.NewRecorder() handlers.HandleSetupScript(pbsRR, pbsReq) if pbsRR.Code != http.StatusOK { t.Fatalf("expected 200 OK for PBS, got %d (%s)", pbsRR.Code, pbsRR.Body.String()) } pbsScript := pbsRR.Body.String() if !containsString(pbsScript, `awk '{print $1}' | grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) { t.Fatalf("expected PBS token rotation detection to use exact token-name matching, got: %s", truncate(pbsScript, 900)) } if containsString(pbsScript, `grep -q "$TOKEN_NAME"`) { t.Fatalf("expected stale broad PBS token detection to be removed, got: %s", truncate(pbsScript, 900)) } } func TestPBSSetupScript_ShowsTokenCopyBannerOnlyAfterSuccessfulCreation(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs-node.example.internal:8007&pulse_url=https://pulse.example.com:7655", nil, ) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() tokenCreateIndex := strings.Index(script, `TOKEN_CREATE_RC=$?`) bannerIndex := strings.Index(script, `echo "IMPORTANT: Copy the token value below - it's only shown once!"`) successBranchIndex := strings.Index(script, "else\n TOKEN_CREATED=true") if tokenCreateIndex == -1 || bannerIndex == -1 || successBranchIndex == -1 { t.Fatalf("expected PBS token creation flow markers to exist, got: %s", truncate(script, 1200)) } if bannerIndex < tokenCreateIndex { t.Fatalf("expected PBS token-copy banner to appear after token creation runs, got: %s", truncate(script, 1200)) } if bannerIndex < successBranchIndex { t.Fatalf("expected PBS token-copy banner to be inside the successful token-create branch, got: %s", truncate(script, 1200)) } } func TestPVESetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("expected fail-fast auto-register transport in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `"source":"script"`) { t.Fatalf("expected PVE setup script to use canonical /api/auto-register source marker, got: %s", truncate(script, 900)) } if !containsString(script, `REGISTER_RC=$?`) { t.Fatalf("expected explicit auto-register curl exit-code handling in setup script, got: %s", truncate(script, 900)) } if containsString(script, `curl -s -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("expected stale non-fail-fast auto-register transport to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'`) { t.Fatalf("expected secure success detection in setup script, got: %s", truncate(script, 900)) } if containsString(script, `grep -q "success"`) { t.Fatalf("expected broad success substring detection to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `echo "The provided Pulse setup token was invalid or expired"`) { t.Fatalf("expected invalid setup-token guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) { t.Fatalf("expected fresh setup-token rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `SETUP_TOKEN_INVALID=true`) { t.Fatalf("expected PVE auth-failure state tracking in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `elif [ "$SETUP_TOKEN_INVALID" = true ]; then`) { t.Fatalf("expected PVE auth-failure completion branch in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse setup token authentication failed."`) { t.Fatalf("expected PVE auth-failure completion guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("expected canonical PVE auto-register failure continuation guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) { t.Fatalf("expected PVE manual footer to stay disabled on auth failure, got: %s", truncate(script, 900)) } if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) { t.Fatalf("expected canonical PVE auto-register failure summary guidance in setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "To enable auto-registration, add your API token to the setup URL"`) { t.Fatalf("expected stale API-token auth guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) { t.Fatalf("expected stale split setup-token auth guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "The provided API token was invalid"`) { t.Fatalf("expected stale invalid API-token guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "📝 For manual setup:"`) { t.Fatalf("expected stale PVE numbered manual-setup fallback to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " 2. Add this node manually in Pulse Settings"`) { t.Fatalf("expected stale PVE auto-register failure guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) { t.Fatalf("expected stale PVE auto-register failure summary to be removed from setup script, got: %s", truncate(script, 900)) } if !containsString(script, `if [ "$AUTO_REG_SUCCESS" = true ]; then`) { t.Fatalf("expected conditional completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Successfully registered with Pulse monitoring."`) { t.Fatalf("expected canonical PVE success messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup completed."`) { t.Fatalf("expected manual completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup failed."`) { t.Fatalf("expected token-create failure completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup could not be completed."`) { t.Fatalf("expected token-extract failure completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Fix the token creation error above and rerun this script on the node."`) { t.Fatalf("expected token-create failure immediate rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Resolve the token creation error shown above and rerun this script on the node."`) { t.Fatalf("expected token-create failure rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Resolve the token output issue above and rerun this script on the node."`) { t.Fatalf("expected token-extract failure rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Resolve the token output issue shown above and rerun this script on the node."`) { t.Fatalf("expected token-extract completion rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Token Value: [See token output above]"`) { t.Fatalf("expected canonical PVE token placeholder guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Finish registration in Pulse using the manual setup details below."`) { t.Fatalf("expected truthful manual registration guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Add this server to Pulse with:"`) { t.Fatalf("expected canonical PVE manual-add heading in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("expected canonical PVE manual registration continuation guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Host URL: $SERVER_HOST"`) { t.Fatalf("expected canonical PVE host continuity in manual setup instructions, got: %s", truncate(script, 900)) } if containsString(script, `echo "Manual setup instructions:"`) { t.Fatalf("expected stale PVE manual setup heading to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo " Host URL: YOUR_PROXMOX_HOST:8006"`) { t.Fatalf("expected stale placeholder PVE host guidance to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo " Token Value: [See above]"`) { t.Fatalf("expected stale PVE short token placeholder guidance to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo "Node registered successfully"`) || containsString(script, `echo "Node successfully registered with Pulse monitoring."`) { t.Fatalf("expected stale PVE success copy to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo "Manual registration may be required."`) { t.Fatalf("expected stale PVE manual-registration token-failure guidance to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `if [ "$TOKEN_READY" = true ]; then`) { t.Fatalf("expected PVE manual footer to be gated on usable token extraction, got: %s", truncate(script, 1100)) } if !containsString(script, `if [ "$TOKEN_READY" = true ]; then if smoke_test_pve_token; then attempt_auto_registration else AUTO_REG_SUCCESS=false fi else AUTO_REG_SUCCESS=false fi`) { t.Fatalf("expected PVE auto-registration to be skipped when no usable token or smoke-tested token is ready, got: %s", truncate(script, 1400)) } if containsString(script, `elif [ "$TOKEN_READY" != true ]; then echo "Pulse monitoring token setup completed."`) { t.Fatalf("expected PVE token-extract failure to avoid completed token-setup messaging, got: %s", truncate(script, 1400)) } } func TestPVESetupScriptSmokeTestsCreatedTokenBeforeAutoRegistration(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=http://sentinel-host:8006&pulse_url=http://sentinel-url:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() for _, needle := range []string{ `smoke_test_pve_token() {`, `curl -kfsS --retry 2 --retry-delay 1`, `Authorization: PVEAPIToken=$PULSE_TOKEN_ID=$TOKEN_VALUE`, `${HOST_URL%/}/api2/json/nodes`, `TOKEN_READY=false`, `if smoke_test_pve_token; then`, } { if !containsString(script, needle) { t.Fatalf("expected PVE setup script token smoke-check contract %q, got: %s", needle, truncate(script, 1200)) } } permissionsIdx := strings.Index(script, `configure_pve_pulse_monitor_role "$TOKEN_CREATED"`) smokeCallIdx := strings.Index(script, `if smoke_test_pve_token; then`) registerCallIdx := -1 if smokeCallIdx >= 0 { registerCallIdx = strings.Index(script[smokeCallIdx:], `attempt_auto_registration`) if registerCallIdx >= 0 { registerCallIdx += smokeCallIdx } } if permissionsIdx < 0 || smokeCallIdx < 0 || registerCallIdx < 0 || !(permissionsIdx < smokeCallIdx && smokeCallIdx < registerCallIdx) { t.Fatalf("expected PVE setup script to smoke-test token after permissions and before auto-registration (perms=%d smoke=%d register=%d)", permissionsIdx, smokeCallIdx, registerCallIdx) } } func containsString(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (findSubstring(s, substr) >= 0)) } func findSubstring(s, substr string) int { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return i } } return -1 } func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." } func TestHandleSetupScript_MethodNotAllowed(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) for _, method := range []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch} { req := httptest.NewRequest(method, "/api/setup-script?type=pve", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusMethodNotAllowed { t.Errorf("%s: expected 405 Method Not Allowed, got %d", method, rr.Code) } } } func TestHandleSetupScript_MissingTypeParameter(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) // No type parameter req := httptest.NewRequest(http.MethodGet, "/api/setup-script?host=https://example.com", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for missing type, got %d", rr.Code) } } func TestHandleSetupScript_MissingHostParameter(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for missing host, got %d (%s)", rr.Code, rr.Body.String()) } if got := strings.TrimSpace(rr.Body.String()); got != "Missing required parameter: host" { t.Fatalf("expected canonical missing host guidance, got %q", got) } } func TestHandleSetupScript_RejectsUnknownType(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pmg&host=https://example.com:8006&pulse_url=https://pulse.example.com:7655", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for unknown type, got %d (%s)", rr.Code, rr.Body.String()) } if got := strings.TrimSpace(rr.Body.String()); got != "type must be 'pve' or 'pbs'" { t.Fatalf("expected canonical type guidance, got %q", got) } } func TestHandleSetupScript_MissingPulseURLParameter(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://example.com:8006", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for missing pulse_url, got %d (%s)", rr.Code, rr.Body.String()) } if got := strings.TrimSpace(rr.Body.String()); got != "Missing required parameter: pulse_url" { t.Fatalf("expected canonical missing pulse_url guidance, got %q", got) } } func TestHandleSetupScript_NormalizesCanonicalHost(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://pve-node.example.internal&pulse_url=https://pulse.example.com:7655", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `SERVER_HOST="https://pve-node.example.internal:8006"`) { t.Fatalf("expected setup script to normalize host with default PVE port, got: %s", truncate(script, 500)) } if !containsString(script, `HOST_URL="$SERVER_HOST"`) { t.Fatalf("expected setup script to preserve normalized host through HOST_URL, got: %s", truncate(script, 500)) } if !containsString(script, `SETUP_SCRIPT_URL="https://pulse.example.com:7655/api/setup-script?host=https%3A%2F%2Fpve-node.example.internal%3A8006&pulse_url=https%3A%2F%2Fpulse.example.com%3A7655&type=pve"`) { t.Fatalf("expected rerun URL to preserve normalized host identity, got: %s", truncate(script, 700)) } } func TestHandleSetupScript_InvalidHostParameter(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) // Host with shell injection attempt req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://example.com%5C%0Aecho%20pwned", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for invalid host, got %d (%s)", rr.Code, rr.Body.String()) } } func TestHandleSetupScript_RejectsShellExpansionHost(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pve&host=https://example$(id).com:8006", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request for shell-expansion host, got %d (%s)", rr.Code, rr.Body.String()) } } func TestHandleSetupScript_PBSTypeGeneratesScript(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://192.168.0.10:8007&pulse_url=http://pulse.local:7656", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK for PBS type, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() // Verify PBS-specific content tests := []struct { name string contains string desc string }{ { name: "pbs_header", contains: "Pulse Monitoring Setup for PBS", desc: "Should have PBS-specific header", }, { name: "proxmox_backup_manager_check", contains: "proxmox-backup-manager", desc: "Should check for proxmox-backup-manager command", }, { name: "pbs_user_realm", contains: "pulse-monitor@pbs", desc: "Should use @pbs realm for user", }, { name: "pbs_acl_update", contains: "proxmox-backup-manager acl update", desc: "Should set PBS ACLs", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if !containsString(script, tt.contains) { t.Errorf("%s\nExpected to find: %s", tt.desc, tt.contains) } }) } wantACLs := []string{ `proxmox-backup-manager acl update / Audit --auth-id pulse-monitor@pbs`, `proxmox-backup-manager acl update / Audit --auth-id "$PULSE_TOKEN_ID"`, } for _, snippet := range wantACLs { if !containsString(script, snippet) { t.Fatalf("expected generated PBS script to contain exact Audit ACL:\n%s\n\nGot (first 900 chars):\n%s", snippet, truncate(script, 900)) } } for _, forbidden := range []string{ `Admin --auth-id pulse-monitor@pbs`, `Admin --auth-id "$PULSE_TOKEN_ID"`, `DatastoreAdmin`, `PVEDatastoreAdmin`, } { if containsString(script, forbidden) { t.Fatalf("expected generated PBS setup to avoid admin ACL %q, got: %s", forbidden, truncate(script, 900)) } } // Verify PBS script does NOT contain PVE-specific content if containsString(script, "pveum user add") { t.Error("PBS script should not contain PVE commands like 'pveum user add'") } } func TestPBSSetupScript_UsesFailFastRetryGuidance(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://192.168.0.10:8007&pulse_url=http://pulse.local:7656", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `echo "Root privileges required. Run as root (su -) and retry."`) { t.Fatalf("expected canonical PBS root requirement guidance in setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pbs&host=YOUR_PBS_URL&pulse_url=$PULSE_URL\" | bash"`) { t.Fatalf("expected stale PBS non-fail-fast guidance to be removed, got: %s", truncate(script, 900)) } if containsString(script, `echo "Please run this script as root"`) { t.Fatalf("expected stale PBS root-only setup script guidance to be removed, got: %s", truncate(script, 900)) } } func TestPBSSetupScript_FailsClosedOnAutoRegisterSuccessDetection(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://192.168.0.10:8007&pulse_url=http://pulse.local:7656", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() if !containsString(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("expected fail-fast PBS auto-register transport in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `"source":"script"`) { t.Fatalf("expected PBS setup script to use canonical /api/auto-register source marker, got: %s", truncate(script, 900)) } if !containsString(script, `PULSE_URL="http://pulse.local:7656"`) { t.Fatalf("expected PBS auto-registration to target the Pulse base URL, got: %s", truncate(script, 1200)) } if containsString(script, `PULSE_URL="http://pulse.local:7656/api/setup-script?`) { t.Fatalf("expected PBS auto-registration URL not to reuse the setup-script download URL, got: %s", truncate(script, 1200)) } if containsString(script, `"source": "script",`) || containsString(script, `"source":"script",`) { t.Fatalf("expected PBS auto-register JSON not to preserve a trailing comma after source, got: %s", truncate(script, 1200)) } if !containsString(script, `REGISTER_RC=$?`) { t.Fatalf("expected explicit PBS auto-register curl exit-code handling in setup script, got: %s", truncate(script, 900)) } if containsString(script, `curl -s -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("expected stale non-fail-fast PBS auto-register transport to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'`) { t.Fatalf("expected secure PBS success detection in setup script, got: %s", truncate(script, 900)) } if containsString(script, `grep -q "success"`) { t.Fatalf("expected broad PBS success substring detection to be removed, got: %s", truncate(script, 900)) } if !containsString(script, `echo "⚠️ Auto-registration skipped: token value unavailable"`) { t.Fatalf("expected fail-closed PBS token-value-unavailable guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "⚠️ Auto-registration skipped: no setup token provided"`) { t.Fatalf("expected truthful PBS setup-token-skip guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-}"`) { t.Fatalf("expected canonical PBS setup token initialization before rerun guidance, got: %s", truncate(script, 900)) } if containsString(script, `echo "⚠️ Auto-registration skipped: no setup token provided" AUTO_REG_SUCCESS=false REGISTER_RESPONSE="" REGISTER_RC=1`) { t.Fatalf("expected skipped PBS setup-token path to avoid forcing fake request-failure state, got: %s", truncate(script, 900)) } if containsString(script, `echo "⚠️ Auto-registration skipped: token value unavailable" AUTO_REG_SUCCESS=false REGISTER_RESPONSE="" REGISTER_RC=1`) { t.Fatalf("expected skipped PBS token-value path to avoid forcing fake request-failure state, got: %s", truncate(script, 900)) } if !containsString(script, `if [ "$REGISTER_ATTEMPTED" != true ]; then`) { t.Fatalf("expected PBS auto-register reporting to distinguish skipped from attempted requests, got: %s", truncate(script, 900)) } if !containsString(script, `echo "The provided Pulse setup token was invalid or expired"`) { t.Fatalf("expected invalid PBS setup-token guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) { t.Fatalf("expected fresh PBS setup-token rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `SETUP_TOKEN_INVALID=true`) { t.Fatalf("expected PBS auth-failure state tracking in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `elif [ "$SETUP_TOKEN_INVALID" = true ]; then`) { t.Fatalf("expected PBS auth-failure completion branch in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse setup token authentication failed."`) { t.Fatalf("expected PBS auth-failure completion guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("expected canonical PBS auto-register failure continuation guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) { t.Fatalf("expected PBS manual footer to stay disabled on auth failure, got: %s", truncate(script, 900)) } if strings.Count(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) < 2 { t.Fatalf("expected PBS request-failure and response-failure branches to share canonical manual continuation guidance, got: %s", truncate(script, 900)) } if !containsString(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) { t.Fatalf("expected canonical PBS auto-register failure summary guidance in setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "To enable auto-registration, add your API token to the setup URL"`) { t.Fatalf("expected stale PBS API-token auth guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) { t.Fatalf("expected stale split PBS setup-token auth guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "The provided API token was invalid"`) { t.Fatalf("expected stale invalid PBS API-token guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "📝 For manual setup:"`) { t.Fatalf("expected stale PBS numbered manual-setup fallback to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " 2. Add this node manually in Pulse Settings"`) { t.Fatalf("expected stale PBS auto-register failure guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) { t.Fatalf("expected stale PBS auto-register failure summary to be removed from setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Successfully registered with Pulse monitoring."`) { t.Fatalf("expected canonical PBS success completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup completed."`) { t.Fatalf("expected PBS manual completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup failed."`) { t.Fatalf("expected PBS token-create failure completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Pulse monitoring token setup could not be completed."`) { t.Fatalf("expected PBS token-extract failure completion messaging in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Fix the token creation error above and rerun this script on the node."`) { t.Fatalf("expected PBS token-create failure immediate rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Resolve the token creation error shown above and rerun this script on the node."`) { t.Fatalf("expected PBS token-create failure rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Resolve the token output issue above and rerun this script on the node."`) { t.Fatalf("expected PBS token-extract failure rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Resolve the token output issue shown above and rerun this script on the node."`) { t.Fatalf("expected PBS token-extract completion rerun guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Token Value: [See token output above]"`) { t.Fatalf("expected canonical PBS token placeholder guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Finish registration in Pulse using the manual setup details below."`) { t.Fatalf("expected truthful PBS manual registration guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("expected canonical PBS manual registration continuation guidance in setup script, got: %s", truncate(script, 900)) } if !containsString(script, `echo " Host URL: $HOST_URL"`) { t.Fatalf("expected canonical PBS host continuity in manual setup instructions, got: %s", truncate(script, 900)) } hostURLIdx := strings.Index(script, `HOST_URL="https://192.168.0.10:8007"`) authPromptIdx := strings.Index(script, `if [ -z "$PULSE_SETUP_TOKEN" ]; then`) if hostURLIdx == -1 || authPromptIdx == -1 || hostURLIdx > authPromptIdx { t.Fatalf("expected PBS host URL continuity to be established before auth-token gating, got: %s", truncate(script, 1200)) } tokenCreateFailureIdx := strings.Index(script, `if [ "$TOKEN_CREATE_RC" -ne 0 ]; then`) if hostURLIdx == -1 || tokenCreateFailureIdx == -1 || hostURLIdx > tokenCreateFailureIdx { t.Fatalf("expected PBS host URL continuity to be established before token-create failure fallback, got: %s", truncate(script, 1200)) } if !containsString(script, `if [ "$TOKEN_READY" = true ]; then`) { t.Fatalf("expected PBS manual footer to be gated on usable token extraction, got: %s", truncate(script, 1100)) } if containsString(script, `PULSE_REG_TOKEN=your-token ./setup.sh`) { t.Fatalf("expected stale PBS rerun token guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "Manual registration may be required."`) { t.Fatalf("expected stale PBS manual-registration token-failure guidance to be removed, got: %s", truncate(script, 900)) } if containsString(script, `Generate a registration token in Pulse Settings → Security`) { t.Fatalf("expected stale PBS registration-token guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " Token Value: [See above]"`) || containsString(script, `echo " Token Value: [Check the output above for the token or instructions]"`) { t.Fatalf("expected stale PBS token placeholder guidance to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo "✅ Successfully registered with Pulse!"`) || containsString(script, `echo "Server successfully registered with Pulse monitoring."`) { t.Fatalf("expected stale PBS success copy to be removed from setup script, got: %s", truncate(script, 900)) } if containsString(script, `echo " Host URL: https://$SERVER_IP:8007"`) { t.Fatalf("expected stale PBS runtime-IP host guidance to be removed from setup script, got: %s", truncate(script, 900)) } } func TestHandleSetupScript_PBSWithAuthToken(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) // Valid auth token (64 hex chars) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://192.168.0.10:8007&pulse_url=http://pulse.local:7656&setup_token=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() // Verify auth token handling in PBS script if !containsString(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}"`) { t.Error("PBS script should define canonical PULSE_SETUP_TOKEN bootstrap variable") } } func TestHandleSetupScript_PBSRejectsMissingHost(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusBadRequest { t.Fatalf("expected 400 Bad Request, got %d (%s)", rr.Code, rr.Body.String()) } if got := strings.TrimSpace(rr.Body.String()); got != "Missing required parameter: host" { t.Fatalf("expected canonical missing host guidance, got %q", got) } } func TestPBSSetupScript_OnlyShowsAttemptBannerOnRealRequestPath(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) req := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs-node.example.internal:8007&pulse_url=https://pulse.example.com:7655", nil) rr := httptest.NewRecorder() handlers.HandleSetupScript(rr, req) if rr.Code != http.StatusOK { t.Fatalf("expected 200 OK, got %d (%s)", rr.Code, rr.Body.String()) } script := rr.Body.String() attemptBannerIndex := strings.Index(script, `echo "🔄 Attempting auto-registration with Pulse..."`) authTokenGateIndex := strings.Index(script, `if [ -n "$PULSE_SETUP_TOKEN" ]; then`) tokenSkipIndex := strings.Index(script, `echo "⚠️ Auto-registration skipped: token value unavailable"`) if attemptBannerIndex == -1 || authTokenGateIndex == -1 || tokenSkipIndex == -1 { t.Fatalf("expected PBS auto-registration truth markers, got: %s", truncate(script, 900)) } if attemptBannerIndex < authTokenGateIndex { t.Fatalf("expected PBS auto-registration banner to stay inside the real request path, got: %s", truncate(script, 900)) } if attemptBannerIndex < tokenSkipIndex { t.Fatalf("expected PBS auto-registration banner to stay after token-unavailable skip handling, got: %s", truncate(script, 900)) } }