From c9c4a0750844b2edfb7edda525e9530c559b08c7 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 19 Nov 2025 14:38:05 +0000 Subject: [PATCH] test(setup): add fmt.Sprintf argument alignment validation test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added TestPVESetupScriptArgumentAlignment to prevent future fmt.Sprintf argument mismatch bugs in the PVE quick setup script template. The test uses sentinel values (SENTINEL_URL, SENTINEL_HOST, deadbeef...) to verify that critical placeholders receive the correct argument types: ✓ Repair block INSTALLER_URL uses pulseURL (not authToken) ✓ Repair --pulse-server flags use pulseURL (not authToken) ✓ Authorization headers use runtime $AUTH_TOKEN variable (not hardcoded) ✓ Token ID uses tokenName (pulse-*) (not pulseURL or authToken) This test would have caught the bugs fixed in commits 2bb73d3c7 and 2053bc5e2, where: - authToken appeared in --pulse-server URLs (argument shift) - Authorization headers were hardcoded instead of using runtime variable Recommended by Codex as a safeguard against this class of regression. --- .../api/config_handlers_setup_script_test.go | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/internal/api/config_handlers_setup_script_test.go b/internal/api/config_handlers_setup_script_test.go index 0db866b..2b657b3 100644 --- a/internal/api/config_handlers_setup_script_test.go +++ b/internal/api/config_handlers_setup_script_test.go @@ -45,3 +45,89 @@ func TestHandleSetupScriptRejectsUnsafePulseURL(t *testing.T) { 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&auth_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 + tests := []struct { + name string + contains string + desc string + }{ + { + name: "repair_installer_url", + contains: `INSTALLER_URL="http://SENTINEL_URL:7656/api/install/install-sensor-proxy.sh"`, + desc: "Repair block INSTALLER_URL should get pulseURL, not authToken", + }, + { + name: "repair_ctid_pulse_server", + contains: `--pulse-server http://SENTINEL_URL:7656`, + desc: "Repair --ctid --pulse-server should get pulseURL, not authToken", + }, + { + name: "runtime_auth_token_ssh_config", + contains: `-H "Authorization: Bearer $AUTH_TOKEN"`, + desc: "SSH config Authorization header should use runtime $AUTH_TOKEN variable", + }, + { + name: "token_id_uses_tokenname", + contains: `Token ID: pulse-monitor@pam!pulse-`, + desc: "Token ID should use tokenName (pulse-*), not pulseURL or authToken", + }, + } + + 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 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] + "..." +}