mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
354 lines
13 KiB
Go
354 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/knowledge"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
|
|
)
|
|
|
|
func TestHandleMetricsStoreStats_MethodNotAllowed(t *testing.T) {
|
|
router := &Router{}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/metrics/store/stats", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleMetricsStoreStats(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func TestHandleMetricsStoreStats_NoMonitor(t *testing.T) {
|
|
router := &Router{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/metrics/store/stats", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleMetricsStoreStats(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func TestHandleMetricsStoreStats_NoStore(t *testing.T) {
|
|
monitor, _, _ := newTestMonitor(t)
|
|
router := &Router{monitor: monitor}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/metrics/store/stats", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleMetricsStoreStats(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["enabled"] != false {
|
|
t.Fatalf("expected enabled=false, got %#v", payload["enabled"])
|
|
}
|
|
}
|
|
|
|
func TestHandleMetricsStoreStats_WithStore(t *testing.T) {
|
|
monitor, _, _ := newTestMonitor(t)
|
|
store, err := metrics.NewStore(metrics.DefaultConfig(t.TempDir()))
|
|
if err != nil {
|
|
t.Fatalf("metrics.NewStore error: %v", err)
|
|
}
|
|
defer store.Close()
|
|
|
|
setUnexportedField(t, monitor, "metricsStore", store)
|
|
router := &Router{monitor: monitor}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/metrics/store/stats", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleMetricsStoreStats(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload["enabled"] != true {
|
|
t.Fatalf("expected enabled=true, got %#v", payload["enabled"])
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_MethodNotAllowed(t *testing.T) {
|
|
router := &Router{}
|
|
req := httptest.NewRequest(http.MethodGet, "/api/diagnostics/docker/prepare-token", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_InvalidJSON(t *testing.T) {
|
|
router := &Router{monitor: &monitoring.Monitor{}, config: &config.Config{}}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", strings.NewReader("{"))
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_MissingAgentID(t *testing.T) {
|
|
router := &Router{monitor: &monitoring.Monitor{}, config: &config.Config{}}
|
|
body := bytes.NewBufferString(`{"agentId":""}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", body)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_AgentNotFound(t *testing.T) {
|
|
monitor, _, _ := newTestMonitor(t)
|
|
router := &Router{monitor: monitor, config: &config.Config{}}
|
|
body := bytes.NewBufferString(`{"agentId":"missing"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", body)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_Success(t *testing.T) {
|
|
monitor, state, _ := newTestMonitor(t)
|
|
state.DockerHosts = []models.DockerHost{{ID: "host-1", DisplayName: "Docker Host"}}
|
|
|
|
router := &Router{monitor: monitor, config: &config.Config{PublicURL: "https://pulse.example.com"}}
|
|
body := bytes.NewBufferString(`{"agentId":"host-1","tokenName":""}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", body)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if ok, _ := payload["success"].(bool); !ok {
|
|
t.Fatalf("expected success true, got %#v", payload["success"])
|
|
}
|
|
if payload["token"] == "" {
|
|
t.Fatalf("expected token in response")
|
|
}
|
|
agent, _ := payload["agent"].(map[string]interface{})
|
|
if agent["id"] != "host-1" {
|
|
t.Fatalf("unexpected agent id: %#v", agent["id"])
|
|
}
|
|
if !strings.Contains(payload["installCommand"].(string), "https://pulse.example.com") {
|
|
t.Fatalf("expected install command to include base URL")
|
|
}
|
|
if !strings.Contains(payload["installCommand"].(string), "--enable-host") ||
|
|
strings.Contains(payload["installCommand"].(string), "--enable-host=false") {
|
|
t.Fatalf("expected install command to enable host metrics by default: %q", payload["installCommand"])
|
|
}
|
|
if !strings.Contains(payload["installCommand"].(string), "--enable-docker") {
|
|
t.Fatalf("expected install command to enable docker metrics: %q", payload["installCommand"])
|
|
}
|
|
if !strings.Contains(payload["installCommand"].(string), `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`) {
|
|
t.Fatalf("expected install command to use lifecycle privilege wrapper: %q", payload["installCommand"])
|
|
}
|
|
if strings.Contains(payload["installCommand"].(string), "| sudo bash -s -- --url") {
|
|
t.Fatalf("expected install command to preserve the governed root-or-sudo wrapper instead of a raw sudo pipe: %q", payload["installCommand"])
|
|
}
|
|
if !strings.Contains(payload["systemdServiceSnippet"].(string), "--enable-host") ||
|
|
strings.Contains(payload["systemdServiceSnippet"].(string), "--enable-host=false") {
|
|
t.Fatalf("expected systemd snippet to enable host metrics by default: %q", payload["systemdServiceSnippet"])
|
|
}
|
|
if len(router.config.APITokens) == 0 {
|
|
t.Fatalf("expected API token to be recorded")
|
|
}
|
|
token := router.config.APITokens[0]
|
|
for _, scope := range []string{config.ScopeDockerReport, config.ScopeAgentReport, config.ScopeAgentConfigRead} {
|
|
if !token.HasScope(scope) {
|
|
t.Fatalf("expected default migration token to include %q, got %#v", scope, token.Scopes)
|
|
}
|
|
}
|
|
if token.HasScope(config.ScopeAgentManage) {
|
|
t.Fatalf("default migration token retained cross-host management scope: %#v", token.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_RollsBackWhenPersistenceFails(t *testing.T) {
|
|
monitor, state, _ := newTestMonitor(t)
|
|
state.DockerHosts = []models.DockerHost{{ID: "host-1", DisplayName: "Docker Host"}}
|
|
|
|
rawToken := "existing-container-runtime-token.12345678"
|
|
existing, err := config.NewAPITokenRecord(rawToken, "existing token", []string{config.ScopeSettingsWrite})
|
|
if err != nil {
|
|
t.Fatalf("new existing token: %v", err)
|
|
}
|
|
cfg := &config.Config{
|
|
PublicURL: "https://pulse.example.com",
|
|
APITokens: []config.APITokenRecord{*existing},
|
|
}
|
|
cfg.SortAPITokens()
|
|
previousPrimaryToken := cfg.APIToken
|
|
|
|
dataDir := filepath.Join(t.TempDir(), "state")
|
|
persistence := config.NewConfigPersistence(dataDir)
|
|
if err := os.RemoveAll(dataDir); err != nil {
|
|
t.Fatalf("remove persistence directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(dataDir, []byte("not a directory"), 0o600); err != nil {
|
|
t.Fatalf("create persistence blocker: %v", err)
|
|
}
|
|
router := &Router{monitor: monitor, config: cfg, persistence: persistence}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", strings.NewReader(`{"agentId":"host-1"}`))
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want %d (body=%q)", rec.Code, http.StatusInternalServerError, rec.Body.String())
|
|
}
|
|
if len(cfg.APITokens) != 1 || cfg.APITokens[0].ID != existing.ID {
|
|
t.Fatalf("live token inventory changed after failed persistence: %+v", cfg.APITokens)
|
|
}
|
|
if cfg.APIToken != previousPrimaryToken {
|
|
t.Fatalf("legacy primary token = %q, want rollback to %q", cfg.APIToken, previousPrimaryToken)
|
|
}
|
|
if strings.Contains(rec.Body.String(), `"token"`) {
|
|
t.Fatalf("failed token preparation exposed an unpersisted token: %q", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_PreservesExplicitWorkloadOnlyMode(t *testing.T) {
|
|
monitor, state, _ := newTestMonitor(t)
|
|
state.DockerHosts = []models.DockerHost{{ID: "host-1", DisplayName: "Docker Host"}}
|
|
|
|
router := &Router{monitor: monitor, config: &config.Config{PublicURL: "https://pulse.example.com"}}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", strings.NewReader(`{"agentId":"host-1","enableHost":false}`))
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if enabled, ok := payload["enableHost"].(bool); !ok || enabled {
|
|
t.Fatalf("expected enableHost=false, got %#v", payload["enableHost"])
|
|
}
|
|
if !strings.Contains(payload["installCommand"].(string), "--enable-host=false") {
|
|
t.Fatalf("expected workload-only install command, got %q", payload["installCommand"])
|
|
}
|
|
if len(router.config.APITokens) != 1 {
|
|
t.Fatalf("expected one migration token, got %d", len(router.config.APITokens))
|
|
}
|
|
token := router.config.APITokens[0]
|
|
if !token.HasScope(config.ScopeDockerReport) || token.HasScope(config.ScopeAgentReport) {
|
|
t.Fatalf("expected Docker-only token scopes, got %#v", token.Scopes)
|
|
}
|
|
}
|
|
|
|
func TestHandleDiagnosticsDockerPrepareToken_NormalizesTrailingSlashPublicURL(t *testing.T) {
|
|
monitor, state, _ := newTestMonitor(t)
|
|
state.DockerHosts = []models.DockerHost{{ID: "host-1", DisplayName: "Docker Host"}}
|
|
|
|
router := &Router{monitor: monitor, config: &config.Config{PublicURL: "https://pulse.example.com/base///"}}
|
|
body := bytes.NewBufferString(`{"agentId":"host-1","tokenName":""}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", body)
|
|
rec := httptest.NewRecorder()
|
|
|
|
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var payload map[string]interface{}
|
|
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
|
|
installCommand, _ := payload["installCommand"].(string)
|
|
if !strings.Contains(installCommand, posixShellQuote("https://pulse.example.com/base/install.sh")) {
|
|
t.Fatalf("expected normalized install script URL, got %q", installCommand)
|
|
}
|
|
if strings.Contains(installCommand, "//install.sh") {
|
|
t.Fatalf("expected normalized install script URL without double slash, got %q", installCommand)
|
|
}
|
|
|
|
systemdSnippet, _ := payload["systemdServiceSnippet"].(string)
|
|
if !strings.Contains(systemdSnippet, `Environment="PULSE_URL=https://pulse.example.com/base"`) {
|
|
t.Fatalf("expected normalized Pulse URL in systemd snippet, got %q", systemdSnippet)
|
|
}
|
|
if strings.Contains(systemdSnippet, `Environment="PULSE_URL=https://pulse.example.com/base/"`) {
|
|
t.Fatalf("expected trailing slash to be trimmed from systemd snippet, got %q", systemdSnippet)
|
|
}
|
|
|
|
if pulseURL, _ := payload["pulseURL"].(string); pulseURL != "https://pulse.example.com/base" {
|
|
t.Fatalf("expected normalized pulseURL, got %q", pulseURL)
|
|
}
|
|
}
|
|
|
|
func TestKnowledgeStoreProviderWrapper(t *testing.T) {
|
|
wrapper := &knowledgeStoreProviderWrapper{}
|
|
if err := wrapper.SaveNote("res-1", "note", "service"); err == nil {
|
|
t.Fatalf("expected error when store is nil")
|
|
}
|
|
if got := wrapper.GetKnowledge("res-1", ""); got != nil {
|
|
t.Fatalf("expected nil knowledge when store is nil")
|
|
}
|
|
|
|
store, err := knowledge.NewStore(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("knowledge.NewStore error: %v", err)
|
|
}
|
|
wrapper.store = store
|
|
|
|
if err := wrapper.SaveNote("res-1", "hello", "service"); err != nil {
|
|
t.Fatalf("SaveNote error: %v", err)
|
|
}
|
|
|
|
entries := wrapper.GetKnowledge("res-1", "service")
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected 1 entry, got %d", len(entries))
|
|
}
|
|
if entries[0].Category != "service" || entries[0].Note != "hello" {
|
|
t.Fatalf("unexpected entry: %#v", entries[0])
|
|
}
|
|
|
|
all := wrapper.GetKnowledge("res-1", "")
|
|
if len(all) != 1 {
|
|
t.Fatalf("expected 1 entry from full query, got %d", len(all))
|
|
}
|
|
}
|