package api import ( "archive/tar" "bytes" "compress/gzip" "crypto/sha256" "encoding/hex" "errors" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupUnifiedAgentRouter(t *testing.T) (*Router, string) { tempDir := t.TempDir() // Create required directories err := os.MkdirAll(filepath.Join(tempDir, "scripts"), 0755) require.NoError(t, err) err = os.MkdirAll(filepath.Join(tempDir, "bin"), 0755) require.NoError(t, err) router := &Router{ projectRoot: tempDir, checksumCache: make(map[string]checksumCacheEntry), } return router, tempDir } func validTestUnifiedAgentBinary(suffix string) []byte { return []byte("ELF test binary " + canonicalUnifiedAgentReportPath + " " + suffix) } func staleTestUnifiedAgentBinary(suffix string) []byte { return []byte("ELF stale binary " + legacyUnifiedAgentReportPath + " " + suffix) } func encodedTestSSHSignature(payload string) string { return encodeSSHSignatureForHeader([]byte(payload)) } func TestDownloadInstallScript_Local(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) // Create dummy script scriptContent := "#!/bin/bash\necho 'installing'" scriptPath := filepath.Join(tempDir, "scripts", "install.sh") err := os.WriteFile(scriptPath, []byte(scriptContent), 0644) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/install.sh", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedInstallScript(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, scriptContent, w.Body.String()) assert.Equal(t, "text/x-shellscript", w.Header().Get("Content-Type")) } func TestDownloadInstallScriptPS_Local(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) // Create dummy script scriptContent := "Write-Host 'installing'" scriptPath := filepath.Join(tempDir, "scripts", "install.ps1") err := os.WriteFile(scriptPath, []byte(scriptContent), 0644) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/install.ps1", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedInstallScriptPS(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, scriptContent, w.Body.String()) assert.Equal(t, "text/plain", w.Header().Get("Content-Type")) } func TestDownloadUnifiedAgent_Local_Generic(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) // Create dummy binary in project root / bin binContent := validTestUnifiedAgentBinary("generic") binPath := filepath.Join(tempDir, "bin", "pulse-agent") err := os.WriteFile(binPath, binContent, 0755) require.NoError(t, err) // Since cachedSHA256 might not be initialized or working without real file usage pattern, // checking if our manual Router setup handles it. // cachedSHA256 needs 'checksumCache' map initialized which we did in setupUnifiedAgentRouter. req := httptest.NewRequest(http.MethodGet, "/api/install/agent", nil) w := httptest.NewRecorder() // Handle calls r.cachedSHA256 which reads the file router.handleDownloadUnifiedAgent(w, req) // We expect success if cachedSHA256 works if w.Code == http.StatusInternalServerError { // If cachedSHA256 fails (maybe because it's not exported or implemented elsewhere // and depends on something I missed), we will fail here. // cachedSHA256 is called in unified_agent.go but defined presumably in router.go or router_utils.go (unexported). // I initialized checksumCache so it should work. t.Logf("Handler returned 500: %s", w.Body.String()) } require.Equal(t, http.StatusOK, w.Code, w.Body.String()) assert.Equal(t, string(binContent), w.Body.String()) // Verify Checksum Header hash := sha256.Sum256(binContent) expectedChecksum := hex.EncodeToString(hash[:]) assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256")) } func TestDownloadUnifiedAgent_Local_SpecificArch(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) // Create dummy binary for linux-amd64 binContent := validTestUnifiedAgentBinary("linux-amd64") binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64") err := os.WriteFile(binPath, binContent, 0755) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, string(binContent), w.Body.String()) } func TestDownloadUnifiedAgent_LocalReleaseBinaryIncludesSignatureHeader(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0" binContent := validTestUnifiedAgentBinary("linux-amd64") binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64") err := os.WriteFile(binPath, binContent, 0755) require.NoError(t, err) err = os.WriteFile(binPath+".sig", []byte("signed-local-agent"), 0644) require.NoError(t, err) err = os.WriteFile(binPath+".sshsig", []byte("signed-local-agent-ssh"), 0644) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "signed-local-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-local-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_SkipsStaleLocalBinaryAndProxies(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64") require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755)) binaryContent := "fresh github binary" expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64" router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{ {Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent}, {Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"}, {Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"}, }) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, binaryContent, w.Body.String()) assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From")) assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_DevModeRejectsStaleLocalBinary(t *testing.T) { router, tempDir := setupUnifiedAgentRouter(t) router.serverVersion = "dev" stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64") require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755)) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), "stale or incompatible") assert.Contains(t, w.Body.String(), legacyUnifiedAgentReportPath) assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64") } func TestDownloadUnifiedAgent_ProxyFromGitHub(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" // Ensure NO local files exist (temp dir is empty of binaries) // Set up a mock HTTP client to simulate GitHub response binaryContent := "fake binary content for proxy test" expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64" router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{ {Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent}, {Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"}, {Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"}, }) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) // Should proxy the binary with checksum header instead of redirecting require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, binaryContent, w.Body.String()) assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From")) // Verify checksum header is present and correct hash := sha256.Sum256([]byte(binaryContent)) expectedChecksum := hex.EncodeToString(hash[:]) assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256")) assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_ProxyFromGitHub_UsesConfiguredRepo(t *testing.T) { t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork") router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" binaryContent := "fake binary content for proxy test" expectedURL := "https://github.com/example/pulse-fork/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64" router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{ {Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent}, {Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"}, {Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"}, }) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, binaryContent, w.Body.String()) } func TestDownloadUnifiedAgent_ProxyFromGitHub_Windows(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" binaryContent := "MZ fake windows binary" expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-windows-amd64.exe" router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{ {Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent}, {Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"}, {Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"}, }) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=windows-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, binaryContent, w.Body.String()) assert.NotEmpty(t, w.Header().Get("X-Checksum-Sha256")) assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_Darwin(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v9.9.9" binaryContent := []byte("darwin arm64 binary payload") archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent) binaryURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-darwin-arm64" signatureURL := binaryURL + ".sig" sshSignatureURL := binaryURL + ".sshsig" archiveURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz" router.installScriptClient = &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.String() { case binaryURL: return &http.Response{ StatusCode: http.StatusNotFound, Status: "404 Not Found", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("not found")), }, nil case archiveURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(archivePayload)), }, nil case signatureURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("signed-agent")), }, nil case sshSignatureURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("signed-agent-ssh")), }, nil default: t.Fatalf("unexpected URL: %s", req.URL.String()) return nil, nil } }), } req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From")) assert.Equal(t, string(binaryContent), w.Body.String()) hash := sha256.Sum256(binaryContent) expectedChecksum := hex.EncodeToString(hash[:]) assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256")) assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_UsesConfiguredRepo(t *testing.T) { t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork") router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v9.9.9" binaryContent := []byte("darwin arm64 binary payload") archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent) binaryURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-darwin-arm64" signatureURL := binaryURL + ".sig" sshSignatureURL := binaryURL + ".sshsig" archiveURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz" router.installScriptClient = &http.Client{ Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { switch req.URL.String() { case binaryURL: return &http.Response{ StatusCode: http.StatusNotFound, Status: "404 Not Found", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("not found")), }, nil case archiveURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(archivePayload)), }, nil case signatureURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("signed-agent")), }, nil case sshSignatureURL: return &http.Response{ StatusCode: http.StatusOK, Status: "200 OK", Header: make(http.Header), Body: io.NopCloser(strings.NewReader("signed-agent-ssh")), }, nil default: t.Fatalf("unexpected URL: %s", req.URL.String()) return nil, nil } }), } req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From")) assert.Equal(t, string(binaryContent), w.Body.String()) assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName)) assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName)) } func TestDownloadUnifiedAgent_ProxyFromGitHub_NotFound(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{ {Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", Status: http.StatusNotFound, Body: ""}, {Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-v6.0.0-rc.1-linux-amd64.tar.gz", Status: http.StatusNotFound, Body: ""}, }) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusNotFound, w.Code) } func TestDownloadUnifiedAgent_ProxyFromGitHub_Error(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-rc.1" // GitHub is unreachable router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", 0, "", errors.New("connection refused")) req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusServiceUnavailable, w.Code) } func TestDownloadUnifiedAgent_DevPrereleaseRejectsGitHubFallback(t *testing.T) { router, _ := setupUnifiedAgentRouter(t) router.serverVersion = "v6.0.0-dev" req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil) w := httptest.NewRecorder() router.handleDownloadUnifiedAgent(w, req) require.Equal(t, http.StatusNotFound, w.Code) assert.Contains(t, w.Body.String(), "dev mode") assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64") } func TestNormalizeUnifiedAgentArch(t *testing.T) { tests := []struct { input string expected string }{ {"amd64", "linux-amd64"}, {"x86_64", "linux-amd64"}, {"linux-amd64", "linux-amd64"}, {"arm64", "linux-arm64"}, {"aarch64", "linux-arm64"}, {"windows-amd64", "windows-amd64"}, {"darwin-arm64", "darwin-arm64"}, {"unknown", ""}, {"", ""}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { assert.Equal(t, tt.expected, normalizeUnifiedAgentArch(tt.input)) }) } } func buildTestTarGz(t *testing.T, name string, payload []byte) []byte { t.Helper() var buf bytes.Buffer gzw := gzip.NewWriter(&buf) tw := tar.NewWriter(gzw) header := &tar.Header{ Name: name, Mode: 0o755, Size: int64(len(payload)), } require.NoError(t, tw.WriteHeader(header)) _, err := tw.Write(payload) require.NoError(t, err) require.NoError(t, tw.Close()) require.NoError(t, gzw.Close()) return buf.Bytes() }