package api import ( "bytes" "context" "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "reflect" "strings" "testing" "time" "github.com/golang-jwt/jwt/v5" "github.com/gorilla/websocket" "github.com/rcourtman/pulse-go-rewrite/internal/ai" "github.com/rcourtman/pulse-go-rewrite/internal/ai/approval" "github.com/rcourtman/pulse-go-rewrite/internal/ai/chat" "github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation" "github.com/rcourtman/pulse-go-rewrite/internal/ai/memory" "github.com/rcourtman/pulse-go-rewrite/internal/ai/providers" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" "github.com/rcourtman/pulse-go-rewrite/internal/config" "github.com/rcourtman/pulse-go-rewrite/internal/license/entitlements" "github.com/rcourtman/pulse-go-rewrite/internal/mock" "github.com/rcourtman/pulse-go-rewrite/internal/models" "github.com/rcourtman/pulse-go-rewrite/internal/monitoring" "github.com/rcourtman/pulse-go-rewrite/internal/recovery" "github.com/rcourtman/pulse-go-rewrite/internal/relay" "github.com/rcourtman/pulse-go-rewrite/internal/truenas" "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources" "github.com/rcourtman/pulse-go-rewrite/internal/updates" "github.com/rcourtman/pulse-go-rewrite/internal/vmware" authpkg "github.com/rcourtman/pulse-go-rewrite/pkg/auth" "github.com/rcourtman/pulse-go-rewrite/pkg/cloudauth" pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing" "github.com/rcourtman/pulse-go-rewrite/pkg/metrics" "github.com/rcourtman/pulse-go-rewrite/pkg/reporting" ) type contractCapturingStreamingProvider struct { lastRequest providers.ChatRequest } type contractSupplementalUsageProvider struct { settled bool readyAt time.Time records []unifiedresources.IngestRecord } func (p *contractSupplementalUsageProvider) SupplementalRecords(*monitoring.Monitor, string) []unifiedresources.IngestRecord { out := make([]unifiedresources.IngestRecord, len(p.records)) copy(out, p.records) return out } func (p *contractSupplementalUsageProvider) SnapshotOwnedSources() []unifiedresources.DataSource { return []unifiedresources.DataSource{unifiedresources.SourceTrueNAS} } func (p *contractSupplementalUsageProvider) SupplementalInventoryReadyAt(*monitoring.Monitor, string) (time.Time, bool) { return p.readyAt, p.settled } func (p *contractSupplementalUsageProvider) settle(count int) { now := time.Now().UTC() p.readyAt = now p.settled = true p.records = make([]unifiedresources.IngestRecord, 0, count) for i := 0; i < count; i++ { host := fmt.Sprintf("contract-truenas-%02d.lab.local", i+1) p.records = append(p.records, unifiedresources.IngestRecord{ SourceID: fmt.Sprintf("system:contract-truenas-%02d", i+1), Resource: unifiedresources.Resource{ ID: fmt.Sprintf("contract-truenas-%02d", i+1), Type: unifiedresources.ResourceTypeAgent, Name: host, Status: unifiedresources.StatusOnline, LastSeen: now, UpdatedAt: now, Identity: unifiedresources.ResourceIdentity{ Hostnames: []string{host}, }, TrueNAS: &unifiedresources.TrueNASData{ Hostname: host, }, }, }) } } func (p *contractCapturingStreamingProvider) Chat(ctx context.Context, req providers.ChatRequest) (*providers.ChatResponse, error) { p.lastRequest = req return &providers.ChatResponse{Content: "ok", Model: req.Model}, nil } func (p *contractCapturingStreamingProvider) ChatStream(ctx context.Context, req providers.ChatRequest, callback providers.StreamCallback) error { p.lastRequest = req callback(providers.StreamEvent{Type: "content", Data: providers.ContentEvent{Text: "hello"}}) callback(providers.StreamEvent{Type: "done", Data: providers.DoneEvent{StopReason: "end_turn"}}) return nil } func (p *contractCapturingStreamingProvider) TestConnection(ctx context.Context) error { return nil } func (p *contractCapturingStreamingProvider) Name() string { return "contract-capture" } func (p *contractCapturingStreamingProvider) ListModels(ctx context.Context) ([]providers.ModelInfo, error) { return nil, nil } func (p *contractCapturingStreamingProvider) SupportsThinking(model string) bool { return false } func TestContract_WebSocketTrustedProxyHostedOrigin(t *testing.T) { t.Setenv("PULSE_TRUSTED_PROXY_CIDRS", "127.0.0.1/32") resetTrustedProxyCIDRsForTests() rawToken := "contract-ws-origin-forwarded-123.12345678" record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil) server, cleanup := newWebSocketRouter(t, []string{}, record) defer cleanup() wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws?org_id=default" headers := http.Header{} headers.Set("X-API-Token", rawToken) headers.Set("Origin", "https://tenant.example.com") headers.Set("X-Forwarded-Proto", "https") headers.Set("X-Forwarded-Host", "tenant.example.com") conn, resp, err := websocket.DefaultDialer.Dial(wsURL, headers) if err != nil { t.Fatalf("expected websocket connection behind trusted proxy, got %v", err) } if resp == nil || resp.StatusCode != http.StatusSwitchingProtocols { conn.Close() t.Fatalf("expected 101 switching protocols, got %v", resp) } conn.Close() } func TestContract_WireAIChatDependencies_WiresTrueNASAppActionProvider(t *testing.T) { router := &Router{ trueNASPoller: monitoring.NewTrueNASPoller(nil, 0, nil), } service := &capturingAIService{} router.wireAIChatDependenciesForService(context.Background(), service) if service.appContainerActionProvider == nil { t.Fatal("expected TrueNAS app action provider to be wired into AI chat dependencies") } if service.appContainerReadProvider == nil { t.Fatal("expected TrueNAS app read provider to be wired into AI chat dependencies") } if service.appContainerConfigProvider == nil { t.Fatal("expected TrueNAS app config provider to be wired into AI chat dependencies") } } func TestContract_ChatServiceAdapterPatrolForwardsExecutionID(t *testing.T) { cfg := chat.Config{ DataDir: t.TempDir(), AIConfig: &config.AIConfig{ Enabled: true, ChatModel: "stub:model", PatrolModel: "stub:model", }, } service := chat.NewService(cfg) provider := &contractCapturingStreamingProvider{} setUnexportedField(t, service, "providerFactory", func(string) (providers.StreamingProvider, error) { return provider, nil }) if err := service.Start(context.Background()); err != nil { t.Fatalf("Start: %v", err) } t.Cleanup(func() { _ = service.Stop(context.Background()) }) adapter := &chatServiceAdapter{svc: service} resp, err := adapter.ExecutePatrolStream(context.Background(), ai.PatrolExecuteRequest{ Prompt: "patrol", ExecutionID: "patrol-run-contract", }, func(ai.ChatStreamEvent) {}) if err != nil { t.Fatalf("ExecutePatrolStream: %v", err) } if resp == nil || resp.Content == "" { t.Fatalf("expected patrol response content, got %#v", resp) } if provider.lastRequest.ExecutionID != "patrol-run-contract" { t.Fatalf("execution_id=%q want patrol-run-contract", provider.lastRequest.ExecutionID) } } func TestContract_AIQuickstartPayloadFieldsRemainCanonical(t *testing.T) { settingsBody, err := json.Marshal(AISettingsResponse{ QuickstartCreditsRemaining: 7, QuickstartCreditsTotal: 25, UsingQuickstart: true, }) if err != nil { t.Fatalf("marshal AI settings response: %v", err) } if !bytes.Contains(settingsBody, []byte(`"quickstart_credits_remaining":7`)) { t.Fatalf("expected AI settings payload to expose quickstart_credits_remaining, got %s", settingsBody) } if !bytes.Contains(settingsBody, []byte(`"quickstart_credits_total":25`)) { t.Fatalf("expected AI settings payload to expose quickstart_credits_total, got %s", settingsBody) } if !bytes.Contains(settingsBody, []byte(`"using_quickstart":true`)) { t.Fatalf("expected AI settings payload to expose using_quickstart, got %s", settingsBody) } statusBody, err := json.Marshal(PatrolStatusResponse{ QuickstartCreditsRemaining: 7, QuickstartCreditsTotal: 25, UsingQuickstart: true, }) if err != nil { t.Fatalf("marshal patrol status response: %v", err) } if !bytes.Contains(statusBody, []byte(`"quickstart_credits_remaining":7`)) { t.Fatalf("expected patrol status payload to expose quickstart_credits_remaining, got %s", statusBody) } if !bytes.Contains(statusBody, []byte(`"quickstart_credits_total":25`)) { t.Fatalf("expected patrol status payload to expose quickstart_credits_total, got %s", statusBody) } if !bytes.Contains(statusBody, []byte(`"using_quickstart":true`)) { t.Fatalf("expected patrol status payload to expose using_quickstart, got %s", statusBody) } } func TestContract_AISettingsUpdateQuickstartBootstrapJSONSnapshot(t *testing.T) { tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) persistQuickstartActivationState(t, persistence) useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) { if got := strings.TrimSpace(r.Header.Get("Authorization")); got != "Bearer pit_live_test" { t.Fatalf("authorization=%q want Bearer pit_live_test", got) } instanceFingerprint, _ := reqBody["instance_fingerprint"].(string) if instanceFingerprint != "fp-live-test" { t.Fatalf("instance_fingerprint=%q want fp-live-test", instanceFingerprint) } if reqBody["use_case"] != "patrol" { t.Fatalf("use_case=%v want patrol", reqBody["use_case"]) } }) handler := newTestAISettingsHandler(cfg, persistence, nil) handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager( persistence, "default", func() *config.AIConfig { cfg, _ := persistence.LoadAIConfig() return cfg }, )) req := httptest.NewRequest(http.MethodPut, "/api/settings/ai/update", strings.NewReader(`{ "enabled": true }`)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() handler.HandleUpdateAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "enabled":true, "model":"quickstart:pulse-hosted", "chat_model":"quickstart:pulse-hosted", "patrol_model":"quickstart:pulse-hosted", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":false, "ollama_base_url":"http://localhost:11434", "ollama_password_set":false, "configured_providers":[], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":25, "quickstart_credits_used":0, "quickstart_credits_remaining":25, "quickstart_credits_available":true, "using_quickstart":true }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_AISettingsUpdateProviderResolutionJSONSnapshot(t *testing.T) { ollama := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/tags": _ = json.NewEncoder(w).Encode(map[string]any{ "models": []map[string]any{ {"name": "llama3:latest"}, {"name": "mistral:latest"}, }, }) default: http.NotFound(w, r) } })) defer ollama.Close() tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) handler := newTestAISettingsHandler(cfg, persistence, nil) req := httptest.NewRequest(http.MethodPut, "/api/settings/ai/update", strings.NewReader(fmt.Sprintf(`{ "enabled": true, "ollama_base_url": %q }`, ollama.URL))) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() handler.HandleUpdateAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } want := fmt.Sprintf(`{ "enabled":true, "model":"ollama:llama3:latest", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":true, "ollama_base_url":%q, "ollama_password_set":false, "configured_providers":["ollama"], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":0, "quickstart_credits_used":0, "quickstart_credits_remaining":0, "quickstart_credits_available":false, "using_quickstart":false }`, ollama.URL) assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_PatrolStatusActivationRequiredSurface(t *testing.T) { handler, _, _, _ := setupAIHandlerWithPatrol(t) persistence := handler.defaultPersistence aiCfg := config.NewDefaultAIConfig() aiCfg.Enabled = true if err := persistence.SaveAIConfig(*aiCfg); err != nil { t.Fatalf("SaveAIConfig: %v", err) } handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager( persistence, "default", func() *config.AIConfig { cfg, _ := persistence.LoadAIConfig() return cfg }, )) if err := handler.defaultAIService.LoadConfig(); err != nil { t.Fatalf("LoadConfig: %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/status", nil) rec := httptest.NewRecorder() handler.HandleGetPatrolStatus(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp PatrolStatusResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode patrol status: %v", err) } if resp.RuntimeState != ai.PatrolRuntimeStateBlocked { t.Fatalf("runtime_state=%q want %q", resp.RuntimeState, ai.PatrolRuntimeStateBlocked) } if !resp.Enabled { t.Fatal("expected patrol status to remain enabled while activation is required") } if resp.Healthy { t.Fatal("expected activation-required patrol status to report healthy=false") } if resp.BlockedReason != ai.QuickstartActivationRequiredReason() { t.Fatalf("blocked_reason=%q want %q", resp.BlockedReason, ai.QuickstartActivationRequiredReason()) } if resp.QuickstartCreditsRemaining != 0 || resp.QuickstartCreditsTotal != 0 { t.Fatalf("quickstart credits=%d/%d want 0/0", resp.QuickstartCreditsRemaining, resp.QuickstartCreditsTotal) } if resp.UsingQuickstart { t.Fatal("expected using_quickstart=false while activation is required") } } func TestContract_AISettingsActivationRequiredSurface(t *testing.T) { tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) handler := newTestAISettingsHandler(cfg, persistence, nil) handler.defaultAIService.SetQuickstartCredits(ai.NewPersistentQuickstartCreditManager( persistence, "default", func() *config.AIConfig { return &config.AIConfig{Enabled: true} }, )) req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp AISettingsResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode ai settings: %v", err) } if resp.QuickstartBlockedReason != ai.QuickstartActivationRequiredReason() { t.Fatalf( "quickstart_blocked_reason=%q want %q", resp.QuickstartBlockedReason, ai.QuickstartActivationRequiredReason(), ) } if resp.QuickstartCreditsRemaining != 0 || resp.QuickstartCreditsTotal != 0 { t.Fatalf("quickstart credits=%d/%d want 0/0", resp.QuickstartCreditsRemaining, resp.QuickstartCreditsTotal) } if resp.QuickstartCreditsAvailable { t.Fatal("expected quickstart_credits_available=false while activation is required") } if !bytes.Contains(rec.Body.Bytes(), []byte(`"quickstart_blocked_reason":"`+ai.QuickstartActivationRequiredReason()+`"`)) { t.Fatalf("expected AI settings payload to expose quickstart_blocked_reason, got %s", rec.Body.Bytes()) } } func TestContract_AISettingsBYOKOverrideRetainsQuickstartInventoryJSONSnapshot(t *testing.T) { tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) aiCfg := config.NewDefaultAIConfig() aiCfg.Enabled = true aiCfg.Model = "openai:gpt-4o" aiCfg.OpenAIAPIKey = "sk-openai-test" if err := persistence.SaveAIConfig(*aiCfg); err != nil { t.Fatalf("SaveAIConfig: %v", err) } handler := newTestAISettingsHandler(cfg, persistence, nil) handler.defaultAIService.SetQuickstartCredits(&stubQuickstartCreditManager{ remaining: 12, total: pkglicensing.QuickstartCreditsTotal, }) req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "enabled":true, "model":"openai:gpt-4o", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":true, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":false, "ollama_base_url":"http://localhost:11434", "ollama_password_set":false, "configured_providers":["openai"], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":25, "quickstart_credits_used":13, "quickstart_credits_remaining":12, "quickstart_credits_available":true, "using_quickstart":false }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_ChartMetricPointsPreserveMillisecondPrecision(t *testing.T) { pointTime := time.Date(2026, time.March, 31, 12, 0, 0, 987_000_000, time.UTC) converted := monitorPointsToAPI([]monitoring.MetricPoint{{ Timestamp: pointTime, Value: 42, }}) if len(converted) != 1 { t.Fatalf("expected one converted point, got %d", len(converted)) } if converted[0].Timestamp != pointTime.UnixMilli() { t.Fatalf("expected millisecond timestamp %d, got %d", pointTime.UnixMilli(), converted[0].Timestamp) } } func TestContract_StorageChartsUseCanonicalMetricsTargetIDs(t *testing.T) { monitor, state, metricsHistory := newTestMonitor(t) now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "usage", 0.25, now) metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "used", 3.57*1024*1024*1024*1024, now) metricsHistory.AddStorageMetric("vc-1:datastore:datastore-202", "avail", 11.03*1024*1024*1024*1024, now) metricsHistory.AddStorageMetric("pool:archive", "usage", 0.36, now) metricsHistory.AddStorageMetric("pool:archive", "used", 5.49*1024*1024*1024*1024, now) metricsHistory.AddStorageMetric("pool:archive", "avail", 4.51*1024*1024*1024*1024, now) adapter := unifiedresources.NewMonitorAdapter(nil) adapter.PopulateSnapshotAndSupplemental(state.GetSnapshot(), map[unifiedresources.DataSource][]unifiedresources.IngestRecord{ unifiedresources.SourceVMware: { { SourceID: "vc-1:datastore:datastore-202", Resource: unifiedresources.Resource{ ID: "storage-vmware-1", Type: unifiedresources.ResourceTypeStorage, Name: "archive-tier", Status: unifiedresources.StatusOnline, LastSeen: now, Storage: &unifiedresources.StorageMeta{ Type: "datastore", Platform: "vmware", Nodes: []string{"esxi-01.lab.local"}, }, VMware: &unifiedresources.VMwareData{ ConnectionID: "vc-1", EntityType: "datastore", ManagedObjectID: "datastore-202", RuntimeHostName: "esxi-01.lab.local", }, }, }, }, unifiedresources.SourceTrueNAS: { { SourceID: "pool:archive", Resource: unifiedresources.Resource{ ID: "storage-truenas-1", Type: unifiedresources.ResourceTypeStorage, Name: "archive", Status: unifiedresources.StatusOnline, LastSeen: now, Storage: &unifiedresources.StorageMeta{ Type: "zfs-pool", Platform: "truenas", }, TrueNAS: &unifiedresources.TrueNASData{ Hostname: "truenas-main", }, }, }, }, }) setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter)) router := &Router{monitor: monitor} req := httptest.NewRequest(http.MethodGet, "/api/storage-charts?range=60", nil) rec := httptest.NewRecorder() router.handleStorageCharts(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var decoded StorageChartsResponse if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil { t.Fatalf("unmarshal storage charts response: %v", err) } if _, ok := decoded.Pools["vc-1:datastore:datastore-202"]; !ok { t.Fatalf("expected VMware datastore chart keyed by canonical metrics target, got %v", decoded.Pools) } if _, ok := decoded.Pools["pool:archive"]; !ok { t.Fatalf("expected TrueNAS pool chart keyed by canonical metrics target, got %v", decoded.Pools) } if _, ok := decoded.Pools["storage-vmware-1"]; ok { t.Fatalf("expected raw VMware resource id to stay out of chart payload, got %v", decoded.Pools) } if _, ok := decoded.Pools["storage-truenas-1"]; ok { t.Fatalf("expected raw TrueNAS resource id to stay out of chart payload, got %v", decoded.Pools) } } func TestContract_TrueNASConnectionsDisabledMessageIsExplicit(t *testing.T) { setTrueNASFeatureForTest(t, false) handler, _, _ := newTrueNASHandlersForTest(t, nil) req := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("expected 404 when TrueNAS integration is disabled, got %d: %s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "explicitly disabled") { t.Fatalf("expected explicit disable message, got %s", rec.Body.String()) } } func TestContract_TrueNASSavedConnectionTestsUpdateRuntimeSummary(t *testing.T) { setTrueNASFeatureForTest(t, true) connection := config.TrueNASInstance{ ID: "conn-1", Name: "tower", Host: "truenas.local", Port: 443, APIKey: "super-secret", UseHTTPS: true, InsecureSkipVerify: false, Enabled: true, } handler, persistence, _ := newTrueNASHandlersForTest(t, nil) if err := persistence.SaveTrueNASConfig([]config.TrueNASInstance{connection}); err != nil { t.Fatalf("seed truenas config: %v", err) } poller := monitoring.NewTrueNASPoller(nil, time.Minute, nil) handler.getPoller = func(context.Context) *monitoring.TrueNASPoller { return poller } handler.newClient = func(cfg truenas.ClientConfig) (trueNASClient, error) { return &fakeTrueNASClient{}, nil } req := httptest.NewRequest(http.MethodPost, "/api/truenas/connections/conn-1/test", nil) rec := httptest.NewRecorder() handler.HandleTestSavedConnection(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } summary := poller.ConnectionSummaries("default", []config.TrueNASInstance{connection})[connection.ID] if summary.Poll == nil || summary.Poll.LastSuccessAt == nil { t.Fatalf("expected saved manual test to refresh poll summary, got %+v", summary.Poll) } } func TestContract_VMwareConnectionsDisabledMessageIsExplicit(t *testing.T) { setVMwareFeatureForTest(t, false) handler, _ := newVMwareHandlersForTest(t) req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("expected 404 when VMware integration is disabled, got %d: %s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), "explicitly disabled") { t.Fatalf("expected explicit disable message, got %s", rec.Body.String()) } } func TestContract_VMwareSavedConnectionTestsUpdateRuntimeSummary(t *testing.T) { setVMwareFeatureForTest(t, true) connection := config.VMwareVCenterInstance{ ID: "conn-1", Name: "lab-vcenter", Host: "vcsa.lab.local", Port: 443, Username: "administrator@vsphere.local", Password: "super-secret", Enabled: true, } handler, persistence := newVMwareHandlersForTest(t) poller := monitoring.NewVMwarePoller(nil, time.Minute) handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller } if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil { t.Fatalf("seed vmware config: %v", err) } handler.newClient = func(cfg vmware.ClientConfig) (vmwareClient, error) { return &fakeVMwareClient{ testConnection: func(context.Context) (*vmware.InventorySummary, error) { return &vmware.InventorySummary{Hosts: 3, VMs: 20, Datastores: 4, VIRelease: "8.0.3"}, nil }, }, nil } req := httptest.NewRequest(http.MethodPost, "/api/vmware/connections/conn-1/test", nil) rec := httptest.NewRecorder() handler.HandleTestSavedConnection(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } summary := poller.ConnectionSummaries("default", []config.VMwareVCenterInstance{connection})[connection.ID] if summary.Poll == nil || summary.Poll.LastSuccessAt == nil { t.Fatalf("expected saved manual test to refresh runtime summary, got %+v", summary.Poll) } if summary.Observed == nil || summary.Observed.VMs != 20 { t.Fatalf("expected saved manual test to refresh observed summary, got %+v", summary.Observed) } } func TestContract_VMwareConnectionListCarriesObservedSummary(t *testing.T) { setVMwareFeatureForTest(t, true) connection := config.VMwareVCenterInstance{ ID: "conn-1", Name: "lab-vcenter", Host: "vcsa.lab.local", Port: 443, Username: "administrator@vsphere.local", Password: "super-secret", Enabled: true, } handler, persistence := newVMwareHandlersForTest(t) poller := monitoring.NewVMwarePoller(nil, time.Minute) handler.getPoller = func(context.Context) *monitoring.VMwarePoller { return poller } if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil { t.Fatalf("seed vmware config: %v", err) } collectedAt := time.Date(2026, 3, 30, 18, 0, 0, 0, time.UTC) poller.RecordConnectionTestSuccess("default", connection.ID, &vmware.InventorySummary{ Hosts: 4, VMs: 24, Datastores: 6, VIRelease: "8.0.3", }, collectedAt) req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var responses []vmwareConnectionResponse if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil { t.Fatalf("unmarshal vmware list response: %v", err) } if len(responses) != 1 { t.Fatalf("expected 1 vmware connection response, got %d", len(responses)) } if responses[0].Password != "********" { t.Fatalf("expected redacted password in vmware list response, got %q", responses[0].Password) } if responses[0].Poll == nil || responses[0].Poll.LastSuccessAt == nil { t.Fatalf("expected poll summary in vmware list response, got %+v", responses[0]) } if responses[0].Observed == nil { t.Fatalf("expected observed summary in vmware list response, got %+v", responses[0]) } if got := responses[0].Observed.Hosts; got != 4 { t.Fatalf("observed hosts = %d, want 4", got) } if got := responses[0].Observed.VMs; got != 24 { t.Fatalf("observed vms = %d, want 24", got) } if got := responses[0].Observed.Datastores; got != 6 { t.Fatalf("observed datastores = %d, want 6", got) } if got := responses[0].Observed.VIRelease; got != "8.0.3" { t.Fatalf("observed viRelease = %q, want 8.0.3", got) } if responses[0].Observed.CollectedAt == nil || !responses[0].Observed.CollectedAt.Equal(collectedAt) { t.Fatalf("observed collectedAt = %+v, want %s", responses[0].Observed.CollectedAt, collectedAt.Format(time.RFC3339)) } } func TestContract_InfrastructureChartsNormalizeLongRangeMixedCadence(t *testing.T) { store := newTestMetricsStore(t) monitor, state, _ := newTestMonitor(t) setTestUnexportedField(t, monitor, "metricsStore", store) state.Nodes = []models.Node{{ ID: "node-contract-1", Name: "node-contract-1", Instance: "pve1", Status: "online", CPU: 0.75, Memory: models.Memory{Usage: 42.0}, Disk: models.Disk{Usage: 55.0}, }} syncTestResourceStore(t, monitor, state) now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC) windowStart := now.Add(-7 * 24 * time.Hour) seed := make([]metrics.WriteMetric, 0, 1200) appendMetric := func(ts time.Time, value float64) { for _, metricType := range []string{"cpu", "memory", "disk"} { seed = append(seed, metrics.WriteMetric{ ResourceType: "node", ResourceID: "node-contract-1", MetricType: metricType, Value: value, Timestamp: ts, Tier: metrics.TierMinute, }) } } for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) { appendMetric(ts, 20) } for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) { appendMetric(ts, 40) } for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) { appendMetric(ts, 60) } appendMetric(now, 75) store.WriteBatchSync(seed) router := &Router{monitor: monitor} req := httptest.NewRequest(http.MethodGet, "/api/charts/infrastructure?range=7d", nil) rec := httptest.NewRecorder() router.handleInfrastructureCharts(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var decoded InfrastructureChartsResponse if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil { t.Fatalf("unmarshal infrastructure charts response: %v", err) } cpuSeries := decoded.NodeData["node-contract-1"]["cpu"] if len(cpuSeries) == 0 { t.Fatal("expected normalized cpu series") } if len(cpuSeries) > infrastructureSummaryMaxSeriesPoints { t.Fatalf("expected cpu series <= %d points, got %d", infrastructureSummaryMaxSeriesPoints, len(cpuSeries)) } if cpuSeries[len(cpuSeries)-1].Timestamp != now.UnixMilli() { t.Fatalf("expected latest cpu timestamp %d, got %d", now.UnixMilli(), cpuSeries[len(cpuSeries)-1].Timestamp) } if cpuSeries[len(cpuSeries)-1].Value != 75 { t.Fatalf("expected latest cpu value 75, got %.2f", cpuSeries[len(cpuSeries)-1].Value) } recentWindowStart := now.Add(-24 * time.Hour).UnixMilli() recentCount := 0 for _, point := range cpuSeries { if point.Timestamp >= recentWindowStart { recentCount++ } } if recentCount > 20 { t.Fatalf("expected day-proportional recent summary buckets, got %d recent cpu points", recentCount) } } func TestContract_WorkloadChartsCapLongRangeMixedCadenceByTime(t *testing.T) { store := newTestMetricsStore(t) monitor, state, _ := newTestMonitor(t) setTestUnexportedField(t, monitor, "metricsStore", store) state.Nodes = []models.Node{{ ID: "node-contract-1", Name: "node-contract-1", Instance: "pve1", Status: "online", }} state.VMs = []models.VM{{ ID: "vm-contract-1", VMID: 101, Name: "vm-contract-1", Node: "node-contract-1", Instance: "pve1", Status: "running", CPU: 0.75, Memory: models.Memory{Usage: 42.0}, Disk: models.Disk{Usage: 55.0}, NetworkIn: 128, NetworkOut: 256, DiskRead: 512, DiskWrite: 256, }} syncTestResourceStore(t, monitor, state) readState := monitor.GetUnifiedReadStateOrSnapshot() vms := readState.VMs() if len(vms) != 1 { t.Fatalf("expected 1 vm view, got %d", len(vms)) } sourceID := strings.TrimSpace(vms[0].SourceID()) if sourceID == "" { t.Fatal("expected vm source ID") } now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC) windowStart := now.Add(-7 * 24 * time.Hour) seed := make([]metrics.WriteMetric, 0, 2000) appendMetric := func(ts time.Time, percentValue, rateValue float64) { for _, metricType := range []string{"cpu", "memory", "disk"} { seed = append(seed, metrics.WriteMetric{ ResourceType: "vm", ResourceID: sourceID, MetricType: metricType, Value: percentValue, Timestamp: ts, Tier: metrics.TierMinute, }) } for _, metricType := range []string{"diskread", "diskwrite", "netin", "netout"} { seed = append(seed, metrics.WriteMetric{ ResourceType: "vm", ResourceID: sourceID, MetricType: metricType, Value: rateValue, Timestamp: ts, Tier: metrics.TierMinute, }) } } for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) { appendMetric(ts, 20, 50) } for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) { appendMetric(ts, 40, 75) } for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) { appendMetric(ts, 60, 100) } appendMetric(now, 75, 125) store.WriteBatchSync(seed) router := &Router{monitor: monitor} req := httptest.NewRequest(http.MethodGet, "/api/charts/workloads?range=7d&maxPoints=30", nil) rec := httptest.NewRecorder() router.handleWorkloadCharts(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var decoded WorkloadChartsResponse if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil { t.Fatalf("unmarshal workload charts response: %v", err) } if len(decoded.ChartData) != 1 { t.Fatalf("expected 1 workload chart entry, got %d", len(decoded.ChartData)) } var cpuSeries []MetricPoint for _, chartData := range decoded.ChartData { cpuSeries = chartData["cpu"] break } if len(cpuSeries) == 0 { t.Fatal("expected capped cpu series") } if len(cpuSeries) > 30 { t.Fatalf("expected cpu series <= 30 points, got %d", len(cpuSeries)) } if cpuSeries[len(cpuSeries)-1].Timestamp != now.UnixMilli() { t.Fatalf("expected latest cpu timestamp %d, got %d", now.UnixMilli(), cpuSeries[len(cpuSeries)-1].Timestamp) } recentWindowStart := now.Add(-24 * time.Hour).UnixMilli() recentCount := 0 for _, point := range cpuSeries { if point.Timestamp >= recentWindowStart { recentCount++ } } if recentCount > 8 { t.Fatalf("expected day-proportional capped workload points, got %d recent cpu points", recentCount) } } func TestContract_WorkloadChartsUseCanonicalWorkloadIDsForProviderBackedVMs(t *testing.T) { monitor, state, history := newTestMonitor(t) now := time.Date(2026, time.April, 1, 12, 0, 0, 0, time.UTC) metricID := "vc-1:vm:vm-201" history.AddGuestMetric(metricID, "cpu", 51, now.Add(-10*time.Minute)) history.AddGuestMetric(metricID, "memory", 64, now.Add(-5*time.Minute)) history.AddGuestMetric(metricID, "disk", 43, now.Add(-3*time.Minute)) history.AddGuestMetric(metricID, "netin", 1200, now.Add(-2*time.Minute)) history.AddGuestMetric(metricID, "netout", 800, now.Add(-2*time.Minute)) adapter := unifiedresources.NewMonitorAdapter(nil) adapter.PopulateSnapshotAndSupplemental(state.GetSnapshot(), map[unifiedresources.DataSource][]unifiedresources.IngestRecord{ unifiedresources.SourceVMware: { { SourceID: metricID, Resource: unifiedresources.Resource{ ID: "vm-vmware-contract", Type: unifiedresources.ResourceTypeVM, Name: "warehouse-api-01", Status: unifiedresources.StatusOnline, LastSeen: now, MetricsTarget: &unifiedresources.MetricsTarget{ ResourceType: "vm", ResourceID: metricID, }, VMware: &unifiedresources.VMwareData{ ConnectionID: "vc-1", EntityType: "vm", ManagedObjectID: "vm-201", }, }, }, }, }) setTestUnexportedField(t, monitor, "resourceStore", monitoring.ResourceStoreInterface(adapter)) readState := monitor.GetUnifiedReadStateOrSnapshot() if readState == nil || len(readState.VMs()) != 1 || readState.VMs()[0] == nil { t.Fatalf("expected one provider-backed VM in read state, got %+v", readState) } resourceID, _, ok := vmChartRequest(readState.VMs()[0]) if !ok { t.Fatal("expected canonical vm chart request") } router := &Router{monitor: monitor} workloadReq := httptest.NewRequest(http.MethodGet, "/api/charts/workloads?range=1h", nil) workloadRec := httptest.NewRecorder() router.handleWorkloadCharts(workloadRec, workloadReq) if workloadRec.Code != http.StatusOK { t.Fatalf("expected workload charts 200, got %d: %s", workloadRec.Code, workloadRec.Body.String()) } var workloadDecoded WorkloadChartsResponse if err := json.Unmarshal(workloadRec.Body.Bytes(), &workloadDecoded); err != nil { t.Fatalf("unmarshal workload charts response: %v", err) } if _, ok := workloadDecoded.ChartData[resourceID]; !ok { t.Fatalf("expected workload charts keyed by canonical workload id %q, got %v", resourceID, workloadDecoded.ChartData) } if _, ok := workloadDecoded.ChartData[metricID]; ok { t.Fatalf("expected provider metrics target %q to stay out of workload chart response keys", metricID) } if workloadDecoded.GuestTypes[resourceID] != "vm" { t.Fatalf("expected vm guest type for %q, got %q", resourceID, workloadDecoded.GuestTypes[resourceID]) } summaryReq := httptest.NewRequest(http.MethodGet, "/api/charts/workloads-summary?range=1h", nil) summaryRec := httptest.NewRecorder() router.handleWorkloadsSummaryCharts(summaryRec, summaryReq) if summaryRec.Code != http.StatusOK { t.Fatalf("expected workloads summary 200, got %d: %s", summaryRec.Code, summaryRec.Body.String()) } var summaryDecoded WorkloadsSummaryChartsResponse if err := json.Unmarshal(summaryRec.Body.Bytes(), &summaryDecoded); err != nil { t.Fatalf("unmarshal workloads summary response: %v", err) } if summaryDecoded.GuestCounts.Total != 1 || summaryDecoded.GuestCounts.Running != 1 { t.Fatalf("expected stable provider-backed guest counts, got %+v", summaryDecoded.GuestCounts) } if len(summaryDecoded.TopContributors.CPU) == 0 { t.Fatal("expected provider-backed cpu top contributor") } if summaryDecoded.TopContributors.CPU[0].ID != resourceID { t.Fatalf("expected workloads summary contributor id %q, got %+v", resourceID, summaryDecoded.TopContributors.CPU[0]) } if summaryDecoded.TopContributors.CPU[0].ID == metricID { t.Fatalf("expected workloads summary contributor id to avoid raw metrics target %q", metricID) } } func TestContract_WorkloadsSummaryChartsNormalizeLongRangeMixedCadence(t *testing.T) { store := newTestMetricsStore(t) monitor, state, _ := newTestMonitor(t) setTestUnexportedField(t, monitor, "metricsStore", store) state.Nodes = []models.Node{{ ID: "node-contract-1", Name: "node-contract-1", Instance: "pve1", Status: "online", }} state.VMs = []models.VM{{ ID: "vm-contract-1", VMID: 101, Name: "vm-contract-1", Node: "node-contract-1", Instance: "pve1", Status: "running", CPU: 0.75, Memory: models.Memory{Usage: 42.0}, Disk: models.Disk{Usage: 55.0}, NetworkIn: 128, NetworkOut: 256, }} syncTestResourceStore(t, monitor, state) readState := monitor.GetUnifiedReadStateOrSnapshot() vms := readState.VMs() if len(vms) != 1 { t.Fatalf("expected 1 vm view, got %d", len(vms)) } sourceID := strings.TrimSpace(vms[0].SourceID()) if sourceID == "" { t.Fatal("expected vm source ID") } now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC) windowStart := now.Add(-7 * 24 * time.Hour) seed := make([]metrics.WriteMetric, 0, 1500) appendMetric := func(ts time.Time, percentValue, rateValue float64) { for _, metricType := range []string{"cpu", "memory", "disk"} { seed = append(seed, metrics.WriteMetric{ ResourceType: "vm", ResourceID: sourceID, MetricType: metricType, Value: percentValue, Timestamp: ts, Tier: metrics.TierMinute, }) } for _, metricType := range []string{"netin", "netout"} { seed = append(seed, metrics.WriteMetric{ ResourceType: "vm", ResourceID: sourceID, MetricType: metricType, Value: rateValue, Timestamp: ts, Tier: metrics.TierMinute, }) } } for ts := windowStart; ts.Before(now.Add(-24 * time.Hour)); ts = ts.Add(65 * time.Minute) { appendMetric(ts, 20, 50) } for ts := now.Add(-24 * time.Hour); ts.Before(now.Add(-2 * time.Hour)); ts = ts.Add(2 * time.Minute) { appendMetric(ts, 40, 75) } for ts := now.Add(-2 * time.Hour); ts.Before(now); ts = ts.Add(time.Minute) { appendMetric(ts, 60, 100) } appendMetric(now, 75, 125) store.WriteBatchSync(seed) router := &Router{monitor: monitor} req := httptest.NewRequest(http.MethodGet, "/api/charts/workloads-summary?range=7d", nil) rec := httptest.NewRecorder() router.handleWorkloadsSummaryCharts(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var decoded WorkloadsSummaryChartsResponse if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil { t.Fatalf("unmarshal workloads summary charts response: %v", err) } if len(decoded.CPU.P50) == 0 { t.Fatal("expected normalized workload summary p50 series") } if len(decoded.CPU.P50) > workloadsSummaryMaxSeriesPoints { t.Fatalf("expected workload summary p50 <= %d points, got %d", workloadsSummaryMaxSeriesPoints, len(decoded.CPU.P50)) } if len(decoded.CPU.P95) > workloadsSummaryMaxSeriesPoints { t.Fatalf("expected workload summary p95 <= %d points, got %d", workloadsSummaryMaxSeriesPoints, len(decoded.CPU.P95)) } if decoded.CPU.P50[len(decoded.CPU.P50)-1].Timestamp != now.UnixMilli() { t.Fatalf("expected latest workload summary timestamp %d, got %d", now.UnixMilli(), decoded.CPU.P50[len(decoded.CPU.P50)-1].Timestamp) } recentWindowStart := now.Add(-24 * time.Hour).UnixMilli() recentCount := 0 for _, point := range decoded.CPU.P50 { if point.Timestamp >= recentWindowStart { recentCount++ } } if recentCount > 20 { t.Fatalf("expected day-proportional summary buckets, got %d recent p50 points", recentCount) } } func TestContract_GenerateStyledMockSeries_UsesTimestampBasedCurve(t *testing.T) { now := time.Date(2026, time.March, 31, 12, 0, 0, 0, time.UTC).UnixMilli() coarse := generateStyledMockSeries( now, time.Hour, 7, 51.9, "dockerContainer", "orion-2-f54579833f9c", "memory", ) fine := generateStyledMockSeries( now, time.Hour, 13, 51.9, "dockerContainer", "orion-2-f54579833f9c", "memory", ) if len(coarse) != 7 || len(fine) != 13 { t.Fatalf("unexpected synthetic series lengths coarse=%d fine=%d", len(coarse), len(fine)) } for i, point := range coarse { fineIndex := i * 2 if fine[fineIndex].Timestamp != point.Timestamp { t.Fatalf( "expected shared timestamp at coarse[%d]=%d to match fine[%d]=%d", i, point.Timestamp, fineIndex, fine[fineIndex].Timestamp, ) } if fine[fineIndex].Value != point.Value { t.Fatalf( "expected shared timestamp value at coarse[%d]=%f to match fine[%d]=%f", i, point.Value, fineIndex, fine[fineIndex].Value, ) } } } func TestContract_PlatformMockToggleRebindsRuntimeConnectionsAndResources(t *testing.T) { t.Setenv("PULSE_MOCK_MODE", "false") prevMock := mock.IsMockEnabled() mock.SetEnabled(false) t.Cleanup(func() { mock.SetEnabled(prevMock) }) cfg := &config.Config{DataPath: t.TempDir()} monitor, err := monitoring.New(cfg) if err != nil { t.Fatalf("new monitor: %v", err) } monitor.SetMockMode(false) router := NewRouter(cfg, monitor, nil, nil, nil, "1.0.0") t.Cleanup(func() { router.shutdownBackgroundWorkers() }) toggleReq := httptest.NewRequest(http.MethodPost, "/api/system/mock-mode", strings.NewReader(`{"enabled":true}`)) toggleRec := httptest.NewRecorder() router.configHandlers.HandleUpdateMockMode(toggleRec, toggleReq) if toggleRec.Code != http.StatusOK { t.Fatalf("toggle status = %d, body=%s", toggleRec.Code, toggleRec.Body.String()) } truenasListRec := httptest.NewRecorder() truenasListReq := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil) router.trueNASHandlers.HandleList(truenasListRec, truenasListReq) if truenasListRec.Code != http.StatusOK { t.Fatalf("truenas connections status = %d, body=%s", truenasListRec.Code, truenasListRec.Body.String()) } var truenasConnections []trueNASConnectionResponse if err := json.Unmarshal(truenasListRec.Body.Bytes(), &truenasConnections); err != nil { t.Fatalf("decode truenas connections: %v", err) } if len(truenasConnections) != 1 || truenasConnections[0].ID != "truenas-mock-1" { t.Fatalf("expected mock truenas connection, got %#v", truenasConnections) } vmwareListRec := httptest.NewRecorder() vmwareListReq := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil) router.vmwareHandlers.HandleList(vmwareListRec, vmwareListReq) if vmwareListRec.Code != http.StatusOK { t.Fatalf("vmware connections status = %d, body=%s", vmwareListRec.Code, vmwareListRec.Body.String()) } var vmwareConnections []vmwareConnectionResponse if err := json.Unmarshal(vmwareListRec.Body.Bytes(), &vmwareConnections); err != nil { t.Fatalf("decode vmware connections: %v", err) } if len(vmwareConnections) != 1 || vmwareConnections[0].ID != "vc-mock-1" { t.Fatalf("expected mock vmware connection, got %#v", vmwareConnections) } assertResourceSource := func(path string, wantSource unifiedresources.DataSource) { t.Helper() rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, path, nil) router.resourceHandlers.HandleListResources(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s status = %d, body=%s", path, rec.Code, rec.Body.String()) } var resp ResourcesResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode %s response: %v", path, err) } for _, resource := range resp.Data { for _, source := range resource.Sources { if source == wantSource { return } } } t.Fatalf("expected %s response to include source %q, got %#v", path, wantSource, resp.Data) } assertResourceSource("/api/resources?source=truenas", unifiedresources.SourceTrueNAS) assertResourceSource("/api/resources?source=vmware-vsphere", unifiedresources.SourceVMware) assertResourceCount := func(path string, want int) { t.Helper() rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, path, nil) router.resourceHandlers.HandleListResources(rec, req) if rec.Code != http.StatusOK { t.Fatalf("%s status = %d, body=%s", path, rec.Code, rec.Body.String()) } var resp ResourcesResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("decode %s response: %v", path, err) } if len(resp.Data) != want { t.Fatalf("%s returned %d resources, want %d", path, len(resp.Data), want) } } assertResourceCount("/api/resources?source=truenas&type=app-container", len(truenas.DefaultFixtures().Apps)) assertResourceCount("/api/resources?source=vmware-vsphere&type=storage", len(vmware.DefaultFixtures().Datastores)) } func TestContract_PlatformMockConnectionListsUseSharedFixtureMetadata(t *testing.T) { setTrueNASFeatureForTest(t, true) setVMwareFeatureForTest(t, true) prevMock := mock.IsMockEnabled() mock.SetEnabled(true) t.Cleanup(func() { mock.SetEnabled(prevMock) }) t.Run("truenas", func(t *testing.T) { fixture := mock.DefaultTrueNASConnectionFixture() if fixture.CollectedAt.IsZero() { t.Fatal("expected canonical truenas mock fixture timestamp") } handler, _, _ := newTrueNASHandlersForTest(t, nil) req := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var responses []trueNASConnectionResponse if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil { t.Fatalf("decode truenas mock list response: %v", err) } if len(responses) != 1 { t.Fatalf("expected 1 mock truenas connection, got %d", len(responses)) } response := responses[0] if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port { t.Fatalf("unexpected truenas mock connection metadata: got %+v want fixture %+v", response.TrueNASInstance, fixture) } if response.APIKey != "********" { t.Fatalf("expected redacted truenas api key, got %q", response.APIKey) } if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds { t.Fatalf("expected truenas poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll) } if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) { t.Fatalf("expected truenas last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll) } if response.Observed == nil { t.Fatal("expected truenas observed summary") } if response.Observed.Host != fixture.Host || response.Observed.ResourceID != fixture.ResourceID || response.Observed.Systems != fixture.Systems || response.Observed.StoragePools != fixture.StoragePools || response.Observed.Datasets != fixture.Datasets || response.Observed.Apps != fixture.Apps || response.Observed.Disks != fixture.Disks || response.Observed.RecoveryArtifacts != fixture.RecoveryArtifacts { t.Fatalf("unexpected truenas observed summary: got %+v want fixture %+v", response.Observed, fixture) } if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) { t.Fatalf("expected truenas observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed) } }) t.Run("vmware", func(t *testing.T) { fixture := mock.DefaultVMwareConnectionFixture() if fixture.CollectedAt.IsZero() { t.Fatal("expected canonical vmware mock fixture timestamp") } handler, _ := newVMwareHandlersForTest(t) req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var responses []vmwareConnectionResponse if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil { t.Fatalf("decode vmware mock list response: %v", err) } if len(responses) != 1 { t.Fatalf("expected 1 mock vmware connection, got %d", len(responses)) } response := responses[0] if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port || response.Username != fixture.Username { t.Fatalf("unexpected vmware mock connection metadata: got %+v want fixture %+v", response.VMwareVCenterInstance, fixture) } if response.Password != "********" { t.Fatalf("expected redacted vmware password, got %q", response.Password) } if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds { t.Fatalf("expected vmware poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll) } if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) { t.Fatalf("expected vmware last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll) } if response.Observed == nil { t.Fatal("expected vmware observed summary") } if response.Observed.Hosts != fixture.Hosts || response.Observed.VMs != fixture.VMs || response.Observed.Datastores != fixture.Datastores || response.Observed.VIRelease != fixture.VIRelease { t.Fatalf("unexpected vmware observed summary: got %+v want fixture %+v", response.Observed, fixture) } if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) { t.Fatalf("expected vmware observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed) } }) } func TestContract_VMwareConnectionListCarriesDegradedObservedSummary(t *testing.T) { setVMwareFeatureForTest(t, true) connection := config.VMwareVCenterInstance{ ID: "conn-1", Name: "lab-vcenter", Host: "vcsa.lab.local", Port: 443, Username: "administrator@vsphere.local", Password: "super-secret", Enabled: true, } handler, persistence := newVMwareHandlersForTest(t) if err := persistence.SaveVMwareConfig([]config.VMwareVCenterInstance{connection}); err != nil { t.Fatalf("seed vmware config: %v", err) } collectedAt := time.Date(2026, 3, 31, 18, 15, 0, 0, time.UTC) handler.statusMu.Lock() handler.statuses = map[string]vmwareConnectionRuntimeStatus{ connection.ID: { Poll: &monitoring.VMwareConnectionPollStatus{ IntervalSeconds: 60, LastSuccessAt: &collectedAt, }, Observed: &monitoring.VMwareConnectionObservedSummary{ CollectedAt: &collectedAt, Hosts: 4, VMs: 24, Datastores: 6, VIRelease: "8.0.3", Degraded: true, IssueCount: 3, Issues: []monitoring.VMwareConnectionObservedIssue{ { Stage: "signals", Category: "permission", Message: "VMware permissions are insufficient for HostSystem overall status", Occurrences: 2, }, { Stage: "topology", Category: "unavailable", Message: "VMware vm guest identity is temporarily unavailable", Occurrences: 1, }, }, }, }, } handler.statusMu.Unlock() req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil) rec := httptest.NewRecorder() handler.HandleList(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var responses []vmwareConnectionResponse if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil { t.Fatalf("unmarshal vmware degraded list response: %v", err) } if len(responses) != 1 || responses[0].Observed == nil { t.Fatalf("expected 1 vmware connection with degraded observed summary, got %+v", responses) } if !responses[0].Observed.Degraded { t.Fatalf("expected degraded observed summary, got %+v", responses[0].Observed) } if responses[0].Observed.IssueCount != 3 { t.Fatalf("observed issueCount = %d, want 3", responses[0].Observed.IssueCount) } if len(responses[0].Observed.Issues) != 2 { t.Fatalf("observed issues len = %d, want 2", len(responses[0].Observed.Issues)) } if responses[0].Observed.Issues[0].Stage != "signals" || responses[0].Observed.Issues[0].Occurrences != 2 { t.Fatalf("unexpected first observed issue: %+v", responses[0].Observed.Issues[0]) } if responses[0].Observed.Issues[1].Stage != "topology" || responses[0].Observed.Issues[1].Occurrences != 1 { t.Fatalf("unexpected second observed issue: %+v", responses[0].Observed.Issues[1]) } } func TestContract_SSOTestRejectsMetadataURLWithUserinfo(t *testing.T) { called := make(chan struct{}, 1) metadataServer := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case called <- struct{}{}: default: } w.Header().Set("Content-Type", "application/xml") w.Write([]byte(testSAMLMetadata)) })) defer metadataServer.Close() metadataURL, err := url.Parse(metadataServer.URL) if err != nil { t.Fatalf("parse metadata server url: %v", err) } metadataURL.User = url.UserPassword("user", "pass") reqBody := SSOTestRequest{ Type: "saml", SAML: &SAMLTestConfig{ IDPMetadataURL: metadataURL.String(), }, } body, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal request: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") setTestIP(req) rec := httptest.NewRecorder() router := &Router{} router.handleTestSSOProvider(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } var resp SSOTestResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if resp.Success { t.Fatalf("expected failed response, got success: %+v", resp) } select { case <-called: t.Fatal("expected SAML metadata URL with userinfo to be rejected before outbound fetch") default: } } func TestContract_SSOTestRejectsManualSLOURLWithUserinfo(t *testing.T) { reqBody := SSOTestRequest{ Type: "saml", SAML: &SAMLTestConfig{ IDPSSOURL: "https://idp.example.com/sso", IDPSLOURL: "https://user:pass@idp.example.com/slo", }, } body, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal request: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") setTestIP(req) rec := httptest.NewRecorder() router := &Router{} router.handleTestSSOProvider(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } var resp SSOTestResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if resp.Success { t.Fatalf("expected failed response, got success: %+v", resp) } if resp.Message != "Invalid SLO URL format" { t.Fatalf("expected invalid SLO URL message, got %+v", resp) } } func TestContract_SSOTestOIDCDiscoveryKeepsIssuerBasePath(t *testing.T) { var issuerURL string discoveryServer := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/auth/realms/pulse/.well-known/openid-configuration" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{ "issuer": %q, "authorization_endpoint": %q, "token_endpoint": %q, "userinfo_endpoint": %q, "jwks_uri": %q, "scopes_supported": ["openid", "profile", "email"] }`, issuerURL, issuerURL+"/protocol/openid-connect/auth", issuerURL+"/protocol/openid-connect/token", issuerURL+"/protocol/openid-connect/userinfo", issuerURL+"/protocol/openid-connect/certs") })) defer discoveryServer.Close() issuerURL = discoveryServer.URL + "/auth/realms/pulse" reqBody := SSOTestRequest{ Type: "oidc", OIDC: &OIDCTestConfig{ IssuerURL: issuerURL, }, } body, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal request: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") setTestIP(req) rec := httptest.NewRecorder() router := &Router{} router.handleTestSSOProvider(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp SSOTestResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal response: %v", err) } if !resp.Success { t.Fatalf("expected success response, got %+v", resp) } if resp.Details == nil { t.Fatal("expected OIDC details in response") } if resp.Details.EntityID != issuerURL { t.Fatalf("issuer=%q, want %q", resp.Details.EntityID, issuerURL) } } func TestContract_SSOTestRejectsCrossOriginSAMLMetadataRedirect(t *testing.T) { targetCalled := make(chan struct{}, 1) target := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case targetCalled <- struct{}{}: default: } w.Header().Set("Content-Type", "application/xml") w.Write([]byte(testSAMLMetadata)) })) defer target.Close() origin := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound) })) defer origin.Close() reqBody := SSOTestRequest{ Type: "saml", SAML: &SAMLTestConfig{ IDPMetadataURL: origin.URL + "/metadata", }, } body, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal request: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") setTestIP(req) rec := httptest.NewRecorder() router := &Router{} router.handleTestSSOProvider(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } select { case <-targetCalled: t.Fatal("expected cross-origin SAML redirect to be rejected before fetching the target origin") default: } } func TestContract_SSOTestRejectsCrossOriginOIDCDiscoveryRedirect(t *testing.T) { targetCalled := make(chan struct{}, 1) var targetURL string target := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/.well-known/openid-configuration" { http.NotFound(w, r) return } select { case targetCalled <- struct{}{}: default: } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{ "issuer": %q, "authorization_endpoint": %q, "token_endpoint": %q, "userinfo_endpoint": %q, "jwks_uri": %q, "scopes_supported": ["openid", "profile", "email"] }`, targetURL, targetURL+"/auth", targetURL+"/token", targetURL+"/userinfo", targetURL+"/jwks") })) defer target.Close() targetURL = target.URL origin := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/.well-known/openid-configuration" { http.NotFound(w, r) return } http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound) })) defer origin.Close() reqBody := SSOTestRequest{ Type: "oidc", OIDC: &OIDCTestConfig{ IssuerURL: origin.URL, }, } body, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal request: %v", err) } req := httptest.NewRequest(http.MethodPost, "/api/security/sso/providers/test", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") setTestIP(req) rec := httptest.NewRecorder() router := &Router{} router.handleTestSSOProvider(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } select { case <-targetCalled: t.Fatal("expected cross-origin OIDC redirect to be rejected before fetching the target origin") default: } } func TestContract_SSOLocalRedirectTargetsStayCanonical(t *testing.T) { if got := buildLocalRedirectTarget("https://evil.example.com/pwn", map[string]string{ "oidc": "error", }); got != "/?oidc=error" { t.Fatalf("absolute redirect target = %q, want %q", got, "/?oidc=error") } if got := buildLocalRedirectTarget("/login?foo=bar#section", map[string]string{ "saml": "success", }); got != "/login?foo=bar&saml=success#section" { t.Fatalf("local redirect target = %q, want %q", got, "/login?foo=bar&saml=success#section") } } func TestContract_FindingJSONSnapshot(t *testing.T) { now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) lastSeen := now.Add(5 * time.Minute) resolvedAt := now.Add(10 * time.Minute) ackAt := now.Add(11 * time.Minute) snoozedUntil := now.Add(12 * time.Minute) lastInvestigated := now.Add(15 * time.Minute) lastRegression := now.Add(30 * time.Minute) payload := ai.Finding{ ID: "finding-1", Key: "cpu-high", Severity: ai.FindingSeverityCritical, Category: ai.FindingCategoryPerformance, ResourceID: "vm-100", ResourceName: "web-server", ResourceType: "vm", Node: "pve-1", Title: "High CPU usage", Description: "CPU sustained above 95%", Recommendation: "Investigate processes and load", Evidence: "cpu=96%", Source: "ai-analysis", DetectedAt: now, LastSeenAt: lastSeen, ResolvedAt: &resolvedAt, AutoResolved: true, ResolveReason: "No longer detected", AcknowledgedAt: &ackAt, SnoozedUntil: &snoozedUntil, AlertIdentifier: "alert-1", DismissedReason: "expected_behavior", UserNote: "Runs nightly batch", TimesRaised: 4, Suppressed: true, InvestigationSessionID: "inv-session-1", InvestigationStatus: "completed", InvestigationOutcome: "fix_queued", LastInvestigatedAt: &lastInvestigated, InvestigationAttempts: 2, LoopState: "remediation_planned", Lifecycle: []ai.FindingLifecycleEvent{ { At: now, Type: "state_change", Message: "Moved to investigating", From: "detected", To: "investigating", Metadata: map[string]string{ "from": "detected", "to": "investigating", }, }, }, RegressionCount: 1, LastRegressionAt: &lastRegression, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal finding: %v", err) } const want = `{ "id":"finding-1", "key":"cpu-high", "severity":"critical", "category":"performance", "resource_id":"vm-100", "resource_name":"web-server", "resource_type":"vm", "node":"pve-1", "title":"High CPU usage", "description":"CPU sustained above 95%", "recommendation":"Investigate processes and load", "evidence":"cpu=96%", "source":"ai-analysis", "detected_at":"2026-02-08T13:14:15Z", "last_seen_at":"2026-02-08T13:19:15Z", "resolved_at":"2026-02-08T13:24:15Z", "auto_resolved":true, "resolve_reason":"No longer detected", "acknowledged_at":"2026-02-08T13:25:15Z", "snoozed_until":"2026-02-08T13:26:15Z", "alert_identifier":"alert-1", "dismissed_reason":"expected_behavior", "user_note":"Runs nightly batch", "times_raised":4, "suppressed":true, "investigation_session_id":"inv-session-1", "investigation_status":"completed", "investigation_outcome":"fix_queued", "last_investigated_at":"2026-02-08T13:29:15Z", "investigation_attempts":2, "loop_state":"remediation_planned", "lifecycle":[{"at":"2026-02-08T13:14:15Z","type":"state_change","message":"Moved to investigating","from":"detected","to":"investigating","metadata":{"from":"detected","to":"investigating"}}], "regression_count":1, "last_regression_at":"2026-02-08T13:44:15Z" }` assertJSONSnapshot(t, got, want) } func TestContract_ApprovalJSONSnapshot(t *testing.T) { now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) expires := now.Add(5 * time.Minute) decided := now.Add(2 * time.Minute) payload := approval.ApprovalRequest{ ID: "approval-1", ExecutionID: "exec-1", ToolID: "tool-1", Command: "rm -rf /tmp/cache", TargetType: "agent", TargetID: "host-1", TargetName: "alpha", Context: "Cleanup temporary cache", RiskLevel: approval.RiskHigh, Status: approval.StatusApproved, RequestedAt: now, ExpiresAt: expires, DecidedAt: &decided, DecidedBy: "admin", DenyReason: "not needed", CommandHash: "sha256:abc", Consumed: true, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal approval: %v", err) } const want = `{ "id":"approval-1", "executionId":"exec-1", "toolId":"tool-1", "command":"rm -rf /tmp/cache", "targetType":"agent", "targetId":"host-1", "targetName":"alpha", "context":"Cleanup temporary cache", "riskLevel":"high", "status":"approved", "requestedAt":"2026-02-08T13:14:15Z", "expiresAt":"2026-02-08T13:19:15Z", "decidedAt":"2026-02-08T13:16:15Z", "decidedBy":"admin", "denyReason":"not needed", "commandHash":"sha256:abc", "consumed":true }` assertJSONSnapshot(t, got, want) } func TestContract_ApprovalListResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "approvals": []approval.ApprovalRequest{}, "stats": map[string]int{ "approved": 0, "denied": 0, "executions": 0, "expired": 0, "pending": 0, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal approval list response: %v", err) } const want = `{ "approvals":[], "stats":{ "approved":0, "denied":0, "executions":0, "expired":0, "pending":0 } }` assertJSONSnapshot(t, got, want) } func TestContract_HostedSignupResponseJSONSnapshot(t *testing.T) { payload := hostedSignupResponse{ OrgID: "org-123", UserID: "owner@example.com", Message: "Check your email for a magic link to finish signing in.", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal hosted signup response: %v", err) } const want = `{ "org_id":"org-123", "user_id":"owner@example.com", "message":"Check your email for a magic link to finish signing in." }` assertJSONSnapshot(t, got, want) } func TestContract_TrialStartHostedSignupRedirectContract(t *testing.T) { baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) h := NewLicenseHandlers(mtp, false, &config.Config{ PublicURL: "https://pulse.example.com", ProTrialSignupURL: "https://billing.example.com/start-pro-trial?source=contract", }) req := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext( context.WithValue(context.Background(), OrgIDContextKey, "default"), ) rec := httptest.NewRecorder() h.HandleStartTrial(rec, req) if rec.Code != http.StatusConflict { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusConflict, rec.Body.String()) } var payload APIError if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Code != "trial_signup_required" { t.Fatalf("code=%q, want %q", payload.Code, "trial_signup_required") } if strings.TrimSpace(payload.Details["action_url"]) == "" { t.Fatal("expected action_url in contract payload") } } func TestContract_TrialStartRateLimitContract(t *testing.T) { baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) h := NewLicenseHandlers(mtp, false, &config.Config{ PublicURL: "https://pulse.example.com", ProTrialSignupURL: "https://billing.example.com/start-pro-trial?source=contract", }) h.trialLimiter = NewRateLimiter(1, time.Minute) ctx := context.WithValue(context.Background(), OrgIDContextKey, "default") firstReq := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext(ctx) firstRec := httptest.NewRecorder() h.HandleStartTrial(firstRec, firstReq) if firstRec.Code != http.StatusConflict { t.Fatalf("first status=%d, want %d: %s", firstRec.Code, http.StatusConflict, firstRec.Body.String()) } secondReq := httptest.NewRequest(http.MethodPost, "/api/license/trial/start", nil).WithContext(ctx) secondRec := httptest.NewRecorder() h.HandleStartTrial(secondRec, secondReq) if secondRec.Code != http.StatusTooManyRequests { t.Fatalf("second status=%d, want %d: %s", secondRec.Code, http.StatusTooManyRequests, secondRec.Body.String()) } retryAfter := secondRec.Header().Get("Retry-After") if retryAfter == "" { t.Fatal("expected Retry-After header") } var payload APIError if err := json.NewDecoder(secondRec.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload.Code != "trial_rate_limited" { t.Fatalf("code=%q, want %q", payload.Code, "trial_rate_limited") } if payload.Details["retry_after_seconds"] != retryAfter { t.Fatalf("retry_after_seconds=%q, want %q", payload.Details["retry_after_seconds"], retryAfter) } } func TestContract_BillingStateQuickstartJSONSnapshot(t *testing.T) { grantedAt := time.Date(2026, 3, 25, 14, 30, 0, 0, time.UTC).Unix() payload := billingState{ Capabilities: []string{"ai_autofix", "ai_patrol"}, Limits: map[string]int64{"max_monitored_systems": 25}, MetersEnabled: []string{}, PlanVersion: "cloud_starter", SubscriptionState: subscriptionStateActiveValue, QuickstartCreditsGranted: true, QuickstartCreditsUsed: 3, QuickstartCreditsGrantedAt: &grantedAt, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal billing state: %v", err) } const want = `{ "capabilities":["ai_autofix","ai_patrol"], "limits":{"max_monitored_systems":25}, "meters_enabled":[], "plan_version":"cloud_starter", "subscription_state":"active", "quickstart_credits_granted":true, "quickstart_credits_used":3, "quickstart_credits_granted_at":1774449000 }` assertJSONSnapshot(t, got, want) } func TestContract_HostedAISettingsAutoBootstrapJSONSnapshot(t *testing.T) { t.Setenv("PULSE_HOSTED_MODE", "true") useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) { authHeader := strings.TrimSpace(r.Header.Get("Authorization")) if !strings.HasPrefix(authHeader, "Bearer ") { t.Fatalf("expected Bearer auth, got %q", authHeader) } if parts := strings.Split(strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")), "."); len(parts) != 3 { t.Fatalf("expected entitlement JWT bearer token, got %q", authHeader) } if _, hasFingerprint := reqBody["instance_fingerprint"]; hasFingerprint { t.Fatalf("expected hosted quickstart bootstrap to omit instance_fingerprint, got %v", reqBody) } if got := reqBody["use_case"]; got != "patrol" { t.Fatalf("use_case=%v want patrol", got) } }) baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) persistence, err := mtp.GetPersistence("default") if err != nil { t.Fatalf("GetPersistence(default): %v", err) } seedHostedAIBillingState(t, mtp, "default") handler := NewAISettingsHandler(mtp, nil, nil) handler.defaultConfig = &config.Config{DataPath: baseDir} req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } if !persistence.HasAIConfig() { t.Fatal("expected hosted AI settings contract to persist canonical ai.enc bootstrap") } const want = `{ "enabled":true, "model":"quickstart:pulse-hosted", "chat_model":"quickstart:pulse-hosted", "patrol_model":"quickstart:pulse-hosted", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":false, "ollama_base_url":"http://localhost:11434", "ollama_password_set":false, "configured_providers":[], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":25, "quickstart_credits_used":0, "quickstart_credits_remaining":25, "quickstart_credits_available":true, "using_quickstart":true }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_AISettingsLegacyQuickstartAliasJSONSnapshot(t *testing.T) { tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) aiCfg := config.NewDefaultAIConfig() aiCfg.Enabled = true aiCfg.Model = "quickstart:minimax-2.5m" aiCfg.ChatModel = "quickstart:minimax-2.5m" aiCfg.PatrolModel = "quickstart:minimax-2.5m" aiCfg.DiscoveryModel = "quickstart:minimax-2.5m" aiCfg.AutoFixModel = "quickstart:minimax-2.5m" if err := persistence.SaveAIConfig(*aiCfg); err != nil { t.Fatalf("SaveAIConfig: %v", err) } handler := newTestAISettingsHandler(cfg, persistence, nil) req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "enabled":true, "model":"quickstart:pulse-hosted", "chat_model":"quickstart:pulse-hosted", "patrol_model":"quickstart:pulse-hosted", "auto_fix_model":"quickstart:pulse-hosted", "configured":false, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":false, "ollama_base_url":"http://localhost:11434", "ollama_password_set":false, "configured_providers":[], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":0, "quickstart_credits_used":0, "quickstart_credits_remaining":0, "quickstart_credits_available":false, "using_quickstart":false }` assertJSONSnapshot(t, rec.Body.Bytes(), want) if bytes.Contains(rec.Body.Bytes(), []byte("quickstart:minimax-2.5m")) { t.Fatalf("expected AI settings payload to suppress legacy hosted quickstart aliases, got %s", rec.Body.Bytes()) } } func TestContract_AISettingsOllamaAuthJSONSnapshot(t *testing.T) { tmp := t.TempDir() cfg := &config.Config{DataPath: tmp} persistence := config.NewConfigPersistence(tmp) aiCfg := config.NewDefaultAIConfig() aiCfg.Enabled = true aiCfg.Model = "openai:gpt-4o" aiCfg.PatrolModel = "ollama:llama3" aiCfg.OllamaBaseURL = "http://ollama.example:11434" aiCfg.OllamaUsername = "unai" aiCfg.OllamaPassword = "secret" if err := persistence.SaveAIConfig(*aiCfg); err != nil { t.Fatalf("SaveAIConfig: %v", err) } handler := newTestAISettingsHandler(cfg, persistence, nil) req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "enabled":true, "model":"openai:gpt-4o", "patrol_model":"ollama:llama3", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":true, "ollama_base_url":"http://ollama.example:11434", "ollama_username":"unai", "ollama_password_set":true, "configured_providers":["ollama"], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":0, "quickstart_credits_used":0, "quickstart_credits_remaining":0, "quickstart_credits_available":false, "using_quickstart":false }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_VMInventoryExportCSVHeaders(t *testing.T) { handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/inventory/vms/export?format=csv", nil) rec := httptest.NewRecorder() handler.HandleExportVMInventory(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); got != "text/csv; charset=utf-8" { t.Fatalf("expected csv content type, got %q", got) } if got := rec.Header().Get("Content-Disposition"); !strings.HasPrefix(got, "attachment; filename=\"vm-inventory-") { t.Fatalf("expected VM inventory attachment filename, got %q", got) } const want = "Resource ID,Instance,Node,Pool,VMID,VM Name,Status,CPU Cores,Memory Allocated Bytes,Disk Allocated Bytes,Disk Used Bytes,Disk Status Reason\n" if got := rec.Body.String(); got != want { t.Fatalf("unexpected VM inventory CSV header row:\nwant %q\ngot %q", want, got) } } func TestContract_ReportingCatalogJSONSnapshot(t *testing.T) { handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil) rec := httptest.NewRecorder() handler.HandleGetReportingCatalog(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); got != "application/json" { t.Fatalf("expected json content type, got %q", got) } const want = `{ "id":"advanced_reporting", "title":"Detailed Reporting", "description":"Generate performance reports and current-state exports across infrastructure and workloads.", "lockedState":{ "title":"Advanced Reporting (Pro)", "description":"Generate PDF and CSV performance reports plus current-state VM inventory exports across infrastructure and workload resources." }, "guidance":{ "title":"Advanced Insights", "description":"Performance reports come from the historical metrics store, while VM inventory export captures the current runtime state for spreadsheet-friendly fleet reviews. Use reports for trends and the inventory export for current allocation and usage snapshots." }, "performanceReport":{ "id":"performance_reports", "title":"Performance Reports", "description":"Generate PDF summaries or CSV metric exports from historical monitoring data for one or more selected resources.", "singleResourceEndpoint":"/api/admin/reports/generate", "multiResourceEndpoint":"/api/admin/reports/generate-multi", "singleFilenamePrefix":"report", "singleFilenameSubject":"resource_id", "multiFilenamePrefix":"fleet-report", "filenameDateStyle":"utc_yyyymmdd", "formats":[ { "value":"pdf", "label":"PDF Report" }, { "value":"csv", "label":"CSV Data" } ], "defaultFormat":"pdf", "ranges":[ { "key":"24h", "label":"Last 24 Hours", "description":"Current-day operational summary for short-term regressions.", "windowHours":24 }, { "key":"7d", "label":"Last 7 Days", "description":"Weekly trend window for recent performance changes.", "windowHours":168 }, { "key":"30d", "label":"Last 30 Days", "description":"Monthly review window for sustained capacity or reliability shifts.", "windowHours":720 } ], "defaultRange":"24h", "multiResourceMax":50, "supportsMetricFilter":true, "supportsCustomTitle":true }, "vmInventoryExport":{ "id":"vm_inventory", "title":"VM Inventory Export", "description":"Export the current fleet-wide VM inventory as CSV using the canonical runtime model. Includes VM identity, placement, CPU, memory allocation, disk allocation, and disk usage columns.", "format":"csv", "exportEndpoint":"/api/admin/reports/inventory/vms/export", "filenamePrefix":"vm-inventory", "filenameDateStyle":"utc_yyyymmdd", "columns":[ { "key":"resource_id", "label":"Resource ID", "description":"Canonical Pulse resource ID for the VM." }, { "key":"instance", "label":"Instance", "description":"Configured Proxmox instance or cluster name." }, { "key":"node", "label":"Node", "description":"Proxmox node currently hosting the VM." }, { "key":"pool", "label":"Pool", "description":"Canonical Proxmox pool membership when the platform reports one." }, { "key":"vmid", "label":"VMID", "description":"Numeric Proxmox VM identifier." }, { "key":"vm_name", "label":"VM Name", "description":"Current VM display name from the runtime model." }, { "key":"status", "label":"Status", "description":"Canonical runtime status for the VM." }, { "key":"cpu_cores", "label":"CPU Cores", "description":"Allocated virtual CPU core count." }, { "key":"memory_allocated_bytes", "label":"Memory Allocated Bytes", "description":"Configured memory allocation in bytes." }, { "key":"disk_allocated_bytes", "label":"Disk Allocated Bytes", "description":"Total allocated disk capacity in bytes across the VM." }, { "key":"disk_used_bytes", "label":"Disk Used Bytes", "description":"Current used disk bytes from the canonical runtime disk view." }, { "key":"disk_status_reason", "label":"Disk Status Reason", "description":"Reason disk usage is partial or unavailable when the runtime cannot provide a full guest view." } ] } }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_PerformanceReportTransportUsesCatalogDefinition(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest( http.MethodGet, "/api/reporting?resourceType=node&resourceId=node-1&metricType=+cpu+&title=+Node+report+", nil, ) rec := httptest.NewRecorder() handler.HandleGenerateReport(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } definition := reporting.DescribePerformanceReport() if got := rec.Header().Get("Content-Disposition"); !strings.HasPrefix(got, fmt.Sprintf("attachment; filename=\"%s-node-1-", definition.SingleFilenamePrefix)) { t.Fatalf("expected canonical performance-report attachment filename, got %q", got) } if engine.lastReq.Format != definition.DefaultFormat { t.Fatalf("expected default format %q, got %q", definition.DefaultFormat, engine.lastReq.Format) } if engine.lastReq.MetricType != "cpu" || engine.lastReq.Title != "Node report" { t.Fatalf("expected trimmed canonical optional fields, got %+v", engine.lastReq) } } func TestContract_ReportingCatalogRouteAccessibleWithoutReportingFeature(t *testing.T) { rawToken := "reporting-catalog-contract-token-123.12345678" record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil) cfg := newTestConfigWithTokens(t, record) router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil) req.Header.Set("X-API-Token", rawToken) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200 for ungated reporting catalog route, got %d body=%s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") { t.Fatalf("expected json content type, got %q", got) } } func TestContract_PerformanceReportTransportUsesCatalogDefaultRange(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) end := time.Date(2026, 3, 25, 15, 0, 0, 0, time.UTC) req := httptest.NewRequest( http.MethodGet, "/api/reporting?resourceType=node&resourceId=node-1&end="+url.QueryEscape(end.Format(time.RFC3339)), nil, ) rec := httptest.NewRecorder() handler.HandleGenerateReport(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) } definition := reporting.DescribePerformanceReport() if got := engine.lastReq.Start; !got.Equal(end.Add(-definition.DefaultRangeDuration())) { t.Fatalf("expected canonical default start time, got %s", got) } if !engine.lastReq.End.Equal(end) { t.Fatalf("expected canonical end time, got %s", engine.lastReq.End) } } func TestContract_PerformanceReportTransportRejectsInvalidTimeRange(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest( http.MethodGet, "/api/reporting?resourceType=node&resourceId=node-1&start=2026-03-25T12:00:00Z&end=2026-03-25T11:00:00Z", nil, ) rec := httptest.NewRecorder() handler.HandleGenerateReport(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String()) } var payload struct { Code string `json:"code"` Error string `json:"error"` } if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode invalid-time-range response: %v", err) } if payload.Code != "invalid_time_range" { t.Fatalf("expected invalid_time_range code, got %q", payload.Code) } if payload.Error != "end must be after start" { t.Fatalf("expected canonical invalid_time_range message, got %q", payload.Error) } } func TestContract_PerformanceReportTransportRejectsOversizedMultiBody(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) padding := strings.Repeat("x", reportingMultiReportBodyMax) req := httptest.NewRequest( http.MethodPost, "/api/reporting/generate-multi", strings.NewReader(fmt.Sprintf(`{"resources":[{"resourceType":"vm","resourceId":"vm-1"}],"format":"pdf","title":"%s"}`, padding)), ) rec := httptest.NewRecorder() handler.HandleGenerateMultiReport(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String()) } var payload struct { Code string `json:"code"` Error string `json:"error"` } if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode oversized-body response: %v", err) } if payload.Code != "body_too_large" { t.Fatalf("expected body_too_large code, got %q", payload.Code) } if payload.Error != "Request body must be 1MB or less" { t.Fatalf("expected canonical oversized-body message, got %q", payload.Error) } } func TestContract_PerformanceReportTransportRejectsInvalidOptionalFieldWithStableCode(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest( http.MethodGet, "/api/reporting?resourceType=node&resourceId=node-1&metricType="+url.QueryEscape("cpu usage"), nil, ) rec := httptest.NewRecorder() handler.HandleGenerateReport(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d body=%s", rec.Code, rec.Body.String()) } var payload struct { Code string `json:"code"` Error string `json:"error"` } if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode invalid optional-field response: %v", err) } if payload.Code != "invalid_metric_type" { t.Fatalf("expected invalid_metric_type code, got %q", payload.Code) } if payload.Error != "metricType must match [a-zA-Z0-9._:-]+ and be <= 64 chars" { t.Fatalf("expected canonical invalid_metric_type message, got %q", payload.Error) } } func TestContract_ReportingInvalidFormatErrorsUseCatalogDefinitions(t *testing.T) { engine := &stubReportingEngine{data: []byte("report"), contentType: "application/pdf"} original := reporting.GetEngine() reporting.SetEngine(engine) t.Cleanup(func() { reporting.SetEngine(original) }) handler := NewReportingHandlers(nil, nil) reportReq := httptest.NewRequest( http.MethodGet, "/api/reporting?format=xlsx&resourceType=node&resourceId=node-1", nil, ) reportRec := httptest.NewRecorder() handler.HandleGenerateReport(reportRec, reportReq) if reportRec.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid performance report format, got %d body=%s", reportRec.Code, reportRec.Body.String()) } var reportPayload struct { Error string `json:"error"` } if err := json.NewDecoder(reportRec.Body).Decode(&reportPayload); err != nil { t.Fatalf("decode performance invalid-format response: %v", err) } if reportPayload.Error != reporting.DescribePerformanceReport().InvalidFormatError() { t.Fatalf("expected canonical performance invalid-format error, got %q", reportPayload.Error) } inventoryReq := httptest.NewRequest( http.MethodGet, "/api/admin/reports/inventory/vms/export?format=pdf", nil, ) inventoryRec := httptest.NewRecorder() handler.HandleExportVMInventory(inventoryRec, inventoryReq) if inventoryRec.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid inventory format, got %d body=%s", inventoryRec.Code, inventoryRec.Body.String()) } var inventoryPayload struct { Error string `json:"error"` } if err := json.NewDecoder(inventoryRec.Body).Decode(&inventoryPayload); err != nil { t.Fatalf("decode inventory invalid-format response: %v", err) } if inventoryPayload.Error != reporting.DescribeVMInventoryExport().InvalidFormatError() { t.Fatalf("expected canonical inventory invalid-format error, got %q", inventoryPayload.Error) } } func TestContract_HostedTenantAISettingsFallbackJSONSnapshot(t *testing.T) { t.Setenv("PULSE_HOSTED_MODE", "true") useTestQuickstartBootstrapServer(t, func(r *http.Request, reqBody map[string]any) { authHeader := strings.TrimSpace(r.Header.Get("Authorization")) if !strings.HasPrefix(authHeader, "Bearer ") { t.Fatalf("expected Bearer auth, got %q", authHeader) } if parts := strings.Split(strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")), "."); len(parts) != 3 { t.Fatalf("expected entitlement JWT bearer token, got %q", authHeader) } if _, hasFingerprint := reqBody["instance_fingerprint"]; hasFingerprint { t.Fatalf("expected hosted quickstart bootstrap to omit instance_fingerprint, got %v", reqBody) } if got := reqBody["use_case"]; got != "patrol" { t.Fatalf("use_case=%v want patrol", got) } }) baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) persistence, err := mtp.GetPersistence("t-tenant") if err != nil { t.Fatalf("GetPersistence(t-tenant): %v", err) } seedHostedAIBillingState(t, mtp, "default") handler := NewAISettingsHandler(mtp, nil, nil) handler.defaultConfig = &config.Config{DataPath: baseDir} req := httptest.NewRequest(http.MethodGet, "/api/settings/ai", nil) req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "t-tenant")) rec := httptest.NewRecorder() handler.HandleGetAISettings(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } if !persistence.HasAIConfig() { t.Fatal("expected hosted tenant AI settings contract to persist tenant ai.enc bootstrap") } const want = `{ "enabled":true, "model":"quickstart:pulse-hosted", "chat_model":"quickstart:pulse-hosted", "patrol_model":"quickstart:pulse-hosted", "configured":true, "custom_context":"", "auth_method":"api_key", "oauth_connected":false, "patrol_interval_minutes":360, "patrol_enabled":true, "patrol_auto_fix":false, "alert_triggered_analysis":true, "patrol_event_triggers_enabled":true, "patrol_alert_triggers_enabled":true, "patrol_anomaly_triggers_enabled":true, "use_proactive_thresholds":false, "available_models":[], "anthropic_configured":false, "openai_configured":false, "openrouter_configured":false, "deepseek_configured":false, "gemini_configured":false, "ollama_configured":false, "ollama_base_url":"http://localhost:11434", "ollama_password_set":false, "configured_providers":[], "control_level":"read_only", "protected_guests":[], "discovery_enabled":false, "quickstart_credits_total":25, "quickstart_credits_used":0, "quickstart_credits_remaining":25, "quickstart_credits_available":true, "using_quickstart":true }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_StripeWebhookHandlersUseCanonicalRuntimeDataDir(t *testing.T) { envDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", envDir) persistence := config.NewMultiTenantPersistence(envDir) billingStore := config.NewFileBillingStore(envDir) rbacProvider := NewTenantRBACProvider(envDir) withExplicitDir := NewStripeWebhookHandlers(billingStore, persistence, rbacProvider, nil, nil, true, envDir) if got := filepath.Dir(withExplicitDir.deduper.dir); got != filepath.Join(envDir, "stripe") { t.Fatalf("explicit dedupe dir root = %q, want %q", got, filepath.Join(envDir, "stripe")) } if got := filepath.Dir(withExplicitDir.index.dir); got != filepath.Join(envDir, "stripe") { t.Fatalf("explicit customer index dir root = %q, want %q", got, filepath.Join(envDir, "stripe")) } withEnvFallback := NewStripeWebhookHandlers(billingStore, persistence, rbacProvider, nil, nil, true, "") if got := filepath.Dir(withEnvFallback.deduper.dir); got != filepath.Join(envDir, "stripe") { t.Fatalf("env fallback dedupe dir root = %q, want %q", got, filepath.Join(envDir, "stripe")) } if got := filepath.Dir(withEnvFallback.index.dir); got != filepath.Join(envDir, "stripe") { t.Fatalf("env fallback customer index dir root = %q, want %q", got, filepath.Join(envDir, "stripe")) } } func TestContract_NotificationWebhookTestResponseJSONSnapshot(t *testing.T) { payload := map[string]interface{}{ "success": true, "status": 200, "response": "OK", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal webhook test response: %v", err) } const want = `{ "response":"OK", "status":200, "success":true }` assertJSONSnapshot(t, got, want) } func TestContract_NotificationPushoverWebhookResponseJSONSnapshot(t *testing.T) { payload := map[string]interface{}{ "id": "hook-1", "name": "Pushover", "url": "https://api.pushover.net/1/messages.json", "service": "pushover", "customFields": map[string]string{ "token": "app-token", "user": "user-key", }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal pushover webhook response: %v", err) } const want = `{ "customFields":{"token":"app-token","user":"user-key"}, "id":"hook-1", "name":"Pushover", "service":"pushover", "url":"https://api.pushover.net/1/messages.json" }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceIntelligenceIncludesRecentChanges(t *testing.T) { svc := newEnabledAIService(t) canonicalStore := unifiedresources.NewMemoryStore() observedAt := time.Now().Add(-15 * time.Minute) if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{ ID: "change-contract", ObservedAt: observedAt, ResourceID: "vm-100", Kind: unifiedresources.ChangeRestart, SourceType: unifiedresources.SourcePlatformEvent, Reason: "guest restarted", }); err != nil { t.Fatalf("record canonical change: %v", err) } correlationDetector := correlation.NewDetector(correlation.Config{ MinOccurrences: 1, CorrelationWindow: 2 * time.Hour, RetentionWindow: 24 * time.Hour, }) correlationBase := observedAt.Add(-10 * time.Minute) correlationDetector.RecordEvent(correlation.Event{ ResourceID: "storage-1", ResourceName: "storage-1", ResourceType: "storage", EventType: correlation.EventDiskFull, Timestamp: correlationBase, }) correlationDetector.RecordEvent(correlation.Event{ ResourceID: "vm-100", ResourceName: "vm-100", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: correlationBase.Add(1 * time.Minute), }) correlationDetector.RecordEvent(correlation.Event{ ResourceID: "storage-1", ResourceName: "storage-1", ResourceType: "storage", EventType: correlation.EventDiskFull, Timestamp: correlationBase.Add(2 * time.Minute), }) correlationDetector.RecordEvent(correlation.Event{ ResourceID: "vm-100", ResourceName: "vm-100", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: correlationBase.Add(3 * time.Minute), }) svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{ resources: []unifiedresources.Resource{ {ID: "public-1", Type: unifiedresources.ResourceTypeVM, Tags: []string{"public"}}, {ID: "internal-1", Type: unifiedresources.ResourceTypeAgent, Agent: &unifiedresources.AgentData{Hostname: "agent-1"}}, }, }) setUnexportedField(t, svc.GetPatrolService(), "correlationDetector", correlationDetector) setUnexportedField(t, svc, "resourceExportStore", canonicalStore) setUnexportedField(t, svc.GetPatrolService(), "aiService", svc) handlers := &AISettingsHandler{defaultAIService: svc} req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence?resource_id=vm-100", nil) rec := httptest.NewRecorder() handlers.HandleGetIntelligence(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) } var payload map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } recentChanges, ok := payload["recent_changes"].([]interface{}) if !ok { t.Fatalf("expected recent_changes array in response, got %T", payload["recent_changes"]) } if len(recentChanges) != 1 { t.Fatalf("expected 1 recent change, got %d", len(recentChanges)) } if _, ok := payload["policy_posture"]; ok { t.Fatal("did not expect policy_posture in resource intelligence response") } dependencies, ok := payload["dependencies"].([]interface{}) if !ok { t.Fatalf("expected dependencies array in response, got %T", payload["dependencies"]) } if len(dependencies) == 0 { t.Fatal("expected at least one dependency in response") } correlations, ok := payload["correlations"].([]interface{}) if !ok { t.Fatalf("expected correlations array in response, got %T", payload["correlations"]) } if len(correlations) == 0 { t.Fatal("expected at least one correlation in response") } firstCorrelation, ok := correlations[0].(map[string]interface{}) if !ok { t.Fatalf("expected correlation object, got %T", correlations[0]) } if firstCorrelation["event_pattern"] == "" { t.Fatal("expected correlation event_pattern in response") } if firstCorrelation["avg_delay"] == nil { t.Fatal("expected correlation avg_delay in response") } if firstCorrelation["confidence"] == nil { t.Fatal("expected correlation confidence in response") } } func TestContract_IntelligenceSummaryIncludesRecentChanges(t *testing.T) { svc := newEnabledAIService(t) canonicalStore := unifiedresources.NewMemoryStore() if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{ ID: "change-summary", ObservedAt: time.Now().Add(-15 * time.Minute), ResourceID: "vm-100", Kind: unifiedresources.ChangeRestart, SourceType: unifiedresources.SourcePlatformEvent, Reason: "guest restarted", }); err != nil { t.Fatalf("record canonical change: %v", err) } svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{ resources: []unifiedresources.Resource{ { ID: "public-1", Name: "public-vm", Type: unifiedresources.ResourceTypeVM, Tags: []string{"public"}, }, { ID: "restricted-1", Name: "mail-gw", Type: unifiedresources.ResourceTypePMG, PMG: &unifiedresources.PMGData{Hostname: "mail.internal"}, }, }, }) setUnexportedField(t, svc, "resourceExportStore", canonicalStore) setUnexportedField(t, svc.GetPatrolService(), "aiService", svc) handlers := &AISettingsHandler{defaultAIService: svc} req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence", nil) rec := httptest.NewRecorder() handlers.HandleGetIntelligence(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) } var payload map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } recentChanges, ok := payload["recent_changes"].([]interface{}) if !ok { t.Fatalf("expected recent_changes array in response, got %T", payload["recent_changes"]) } if len(recentChanges) != 1 { t.Fatalf("expected 1 recent change, got %d", len(recentChanges)) } policyPosture, ok := payload["policy_posture"].(map[string]interface{}) if !ok { t.Fatalf("expected policy_posture object in response, got %T", payload["policy_posture"]) } if got := int(policyPosture["total_resources"].(float64)); got != 2 { t.Fatalf("expected total_resources=2, got %d", got) } } func TestContract_RecentChangesEndpointUsesCanonicalTimeline(t *testing.T) { svc := newEnabledAIService(t) canonicalStore := unifiedresources.NewMemoryStore() if err := canonicalStore.RecordChange(unifiedresources.ResourceChange{ ID: "change-canonical", ObservedAt: time.Now().Add(-25 * time.Minute), ResourceID: "vm-canonical", Kind: unifiedresources.ChangeRestart, From: "running", To: "restarting", SourceType: unifiedresources.SourcePlatformEvent, SourceAdapter: unifiedresources.AdapterProxmox, Reason: "guest restarted after maintenance", }); err != nil { t.Fatalf("record canonical change: %v", err) } svc.SetUnifiedResourceProvider(&stubUnifiedResourceProvider{ resources: []unifiedresources.Resource{ { ID: "vm-canonical", Name: "canonical-vm", Type: unifiedresources.ResourceTypeVM, }, }, }) setUnexportedField(t, svc, "resourceExportStore", canonicalStore) setUnexportedField(t, svc.GetPatrolService(), "aiService", svc) handlers := &AISettingsHandler{defaultAIService: svc} req := httptest.NewRequest(http.MethodGet, "/api/ai/intelligence/changes?hours=1", nil) rec := httptest.NewRecorder() handlers.HandleGetRecentChanges(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String()) } var payload map[string]interface{} if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode response: %v", err) } changes, ok := payload["changes"].([]interface{}) if !ok { t.Fatalf("expected changes array in response, got %T", payload["changes"]) } if len(changes) != 1 { t.Fatalf("expected 1 recent change, got %d", len(changes)) } change, ok := changes[0].(map[string]interface{}) if !ok { t.Fatalf("expected object change, got %#v", changes[0]) } if change["resource_name"] != "canonical-vm" { t.Fatalf("expected canonical resource name, got %#v", change["resource_name"]) } if change["resource_type"] != string(unifiedresources.ResourceTypeVM) { t.Fatalf("expected resource type vm, got %#v", change["resource_type"]) } if change["change_type"] != string(unifiedresources.ChangeRestart) { t.Fatalf("expected canonical change type, got %#v", change["change_type"]) } if desc, ok := change["description"].(string); !ok || !strings.Contains(desc, "Restart") { t.Fatalf("expected canonical change description, got %#v", change["description"]) } } func TestContract_AIIntelligenceCorrelationsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 30, 0, 0, time.UTC) payload := map[string]any{ "correlations": []map[string]any{ { "source_id": "node-1", "source_name": "node-1", "source_type": "node", "target_id": "vm-1", "target_name": "vm-1", "target_type": "vm", "event_pattern": "high_cpu -> restart", "occurrences": 1, "avg_delay": "1m0s", "confidence": 0.1, "last_seen": now, "description": "When node-1 experiences high_cpu, vm-1 often follows within 1m0s", }, }, "count": 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal correlations response: %v", err) } const want = `{ "correlations":[ { "avg_delay":"1m0s", "confidence":0.1, "description":"When node-1 experiences high_cpu, vm-1 often follows within 1m0s", "event_pattern":"high_cpu -\u003e restart", "last_seen":"2026-03-18T17:30:00Z", "occurrences":1, "source_id":"node-1", "source_name":"node-1", "source_type":"node", "target_id":"vm-1", "target_name":"vm-1", "target_type":"vm" } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) { payload := MonitoredSystemLedgerResponse{ Systems: []MonitoredSystemLedgerEntry{ { Name: "Tower", Type: "host", Status: "warning", StatusExplanation: MonitoredSystemLedgerStatusExplanation{ Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", Reasons: []MonitoredSystemLedgerStatusReason{ { Kind: "source-stale", Name: "Tower", Type: "host", Source: "agent", Status: "stale", ReportedAt: "2026-03-18T17:25:00Z", Summary: "Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z).", }, }, }, LatestIncludedSignal: MonitoredSystemLedgerLatestSignal{ Name: "Tower", Type: "host", Source: "agent", At: "2026-03-18T17:30:00Z", }, Source: "agent", Explanation: MonitoredSystemLedgerExplanation{ Summary: "Counts as one monitored system because Pulse sees one top-level host view from agent.", Reasons: []MonitoredSystemLedgerExplanationReason{ { Kind: "standalone", Signal: "single-top-level-view", Summary: "No overlapping top-level source matched this system.", }, }, Surfaces: []MonitoredSystemLedgerExplanationSurface{ { Name: "Tower", Type: "host", Source: "agent", }, }, }, }, }, Total: 1, Limit: 5, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal monitored system ledger response: %v", err) } const want = `{ "systems":[ { "name":"Tower", "type":"host", "status":"warning", "status_explanation":{ "summary":"At least one included source is stale, so Pulse marks this monitored system as warning.", "reasons":[ { "kind":"source-stale", "name":"Tower", "type":"host", "source":"agent", "status":"stale", "reported_at":"2026-03-18T17:25:00Z", "summary":"Agent data for Tower is stale (last reported 2026-03-18T17:25:00Z)." } ] }, "latest_included_signal":{ "name":"Tower", "type":"host", "source":"agent", "at":"2026-03-18T17:30:00Z" }, "source":"agent", "explanation":{ "summary":"Counts as one monitored system because Pulse sees one top-level host view from agent.", "reasons":[ { "kind":"standalone", "signal":"single-top-level-view", "summary":"No overlapping top-level source matched this system." } ], "surfaces":[ { "name":"Tower", "type":"host", "source":"agent" } ] } } ], "total":1, "limit":5 }` assertJSONSnapshot(t, got, want) } func TestContract_MonitoredSystemLedgerDoesNotEmitCompatibilityAliases(t *testing.T) { entry := monitoredSystemLedgerEntry(unifiedresources.MonitoredSystemRecord{ Name: "Tower", Type: "host", Status: unifiedresources.StatusWarning, StatusExplanation: unifiedresources.MonitoredSystemStatusExplanation{ Summary: "At least one included source is stale, so Pulse marks this monitored system as warning.", Reasons: []unifiedresources.MonitoredSystemStatusReason{}, }, LastSeen: time.Date(2026, 3, 18, 17, 35, 0, 0, time.UTC), LatestIncludedSignal: unifiedresources.MonitoredSystemLatestSignal{ Name: "tower.local", Type: "docker-host", Source: "docker", At: time.Date(2026, 3, 18, 17, 30, 0, 0, time.UTC), }, Source: "multiple", Explanation: unifiedresources.MonitoredSystemGroupingExplanation{ Summary: "Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.", Reasons: []unifiedresources.MonitoredSystemGroupingReason{}, Surfaces: []unifiedresources.MonitoredSystemGroupingSurface{}, }, }) payload := MonitoredSystemLedgerResponse{ Systems: []MonitoredSystemLedgerEntry{entry}, Total: 1, Limit: 5, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal monitored system ledger response: %v", err) } const want = `{ "systems":[ { "name":"Tower", "type":"host", "status":"warning", "status_explanation":{ "summary":"At least one included source is stale, so Pulse marks this monitored system as warning.", "reasons":[] }, "latest_included_signal":{ "name":"tower.local", "type":"docker-host", "source":"docker", "at":"2026-03-18T17:30:00Z" }, "source":"multiple", "explanation":{ "summary":"Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.", "reasons":[], "surfaces":[] } } ], "total":1, "limit":5 }` assertJSONSnapshot(t, got, want) } func TestContract_ResolveAuthEnvPathUsesCanonicalRuntimeDataDir(t *testing.T) { envDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", envDir) explicitDir := t.TempDir() if got := resolveAuthEnvPath(explicitDir); got != filepath.Join(explicitDir, ".env") { t.Fatalf("resolveAuthEnvPath(explicit) = %q, want %q", got, filepath.Join(explicitDir, ".env")) } if got := resolveAuthEnvPath(""); got != filepath.Join(envDir, ".env") { t.Fatalf("resolveAuthEnvPath(env fallback) = %q, want %q", got, filepath.Join(envDir, ".env")) } } func TestContract_ResolveAuthEnvWritePathsDeduplicatesCanonicalFallback(t *testing.T) { envDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", envDir) paths := resolveAuthEnvWritePaths("", "") if len(paths) != 1 { t.Fatalf("resolveAuthEnvWritePaths() len = %d, want 1", len(paths)) } if want := filepath.Join(envDir, ".env"); paths[0] != want { t.Fatalf("resolveAuthEnvWritePaths()[0] = %q, want %q", paths[0], want) } } func TestContract_WriteAuthEnvFileFallsBackToDataPath(t *testing.T) { configPathFile := filepath.Join(t.TempDir(), "blocked") if err := os.WriteFile(configPathFile, []byte("blocked"), 0600); err != nil { t.Fatalf("write blocked config path file: %v", err) } dataDir := t.TempDir() writtenPath, err := writeAuthEnvFile(configPathFile, dataDir, []byte("PULSE_AUTH_USER='pulse'\n")) if err != nil { t.Fatalf("writeAuthEnvFile() error = %v", err) } wantPath := filepath.Join(dataDir, ".env") if writtenPath != wantPath { t.Fatalf("writeAuthEnvFile() path = %q, want %q", writtenPath, wantPath) } if _, err := os.Stat(wantPath); err != nil { t.Fatalf("stat fallback auth env: %v", err) } } func TestContract_RecoveryTokenPersistenceJSONSnapshot(t *testing.T) { payload := []*RecoveryToken{ { TokenHash: recoveryTokenHash("raw-recovery-token"), CreatedAt: time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC), ExpiresAt: time.Date(2026, 2, 8, 14, 14, 15, 0, time.UTC), Used: true, UsedAt: time.Date(2026, 2, 8, 13, 24, 15, 0, time.UTC), IP: "192.168.1.10", }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal recovery token persistence: %v", err) } const want = `[ { "token_hash":"59b5880d54ca8c991c09269834d59ea09ab4f467fd4d580a932cd70c5b993fa4", "created_at":"2026-02-08T13:14:15Z", "expires_at":"2026-02-08T14:14:15Z", "used":true, "used_at":"2026-02-08T13:24:15Z", "ip":"192.168.1.10" } ]` assertJSONSnapshot(t, got, want) } func TestContract_PersistentAuthStoresRequireExplicitInitialization(t *testing.T) { resetPersistentAuthStoresForTests() t.Cleanup(resetPersistentAuthStoresForTests) assertPanics := func(name string, fn func()) { t.Helper() defer func() { if recover() == nil { t.Fatalf("%s should require explicit initialization", name) } }() fn() } assertPanics("session store", func() { _ = GetSessionStore() }) assertPanics("csrf store", func() { _ = GetCSRFStore() }) assertPanics("recovery token store", func() { _ = GetRecoveryTokenStore() }) } func TestContract_HostedSessionAuthPrecedesAnonymousFallback(t *testing.T) { resetPersistentAuthStoresForTests() t.Cleanup(resetPersistentAuthStoresForTests) InitSessionStore(t.TempDir()) store := GetSessionStore() sessionToken := generateSessionToken() store.CreateSession(sessionToken, 24*time.Hour, "contract-test", "127.0.0.1", "hosted-owner@example.com") record, err := config.NewAPITokenRecord("hosted-contract-token.12345678", "hosted-contract", []string{config.ScopeSettingsWrite}) if err != nil { t.Fatalf("NewAPITokenRecord: %v", err) } cfg := &config.Config{ APITokens: []config.APITokenRecord{*record}, } req := httptest.NewRequest(http.MethodGet, "/api/security/tokens/relay-mobile", nil) req.AddCookie(&http.Cookie{ Name: "pulse_session", Value: sessionToken, }) rec := httptest.NewRecorder() if !CheckAuth(cfg, rec, req) { t.Fatal("CheckAuth() = false, want true for valid hosted browser session") } if got := rec.Header().Get("X-Authenticated-User"); got != "hosted-owner@example.com" { t.Fatalf("X-Authenticated-User = %q, want hosted-owner@example.com", got) } if got := rec.Header().Get("X-Auth-Method"); got != "session" { t.Fatalf("X-Auth-Method = %q, want session", got) } } func TestContract_UniversalRateLimitStateIsScopedPerRouterConfig(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) first := UniversalRateLimitMiddlewareWithConfig(newEndpointRateLimitConfig(), handler) second := UniversalRateLimitMiddlewareWithConfig(newEndpointRateLimitConfig(), handler) makeRequest := func(target http.Handler) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, "/api/auth/login", nil) req.RemoteAddr = "198.51.100.25:12345" rec := httptest.NewRecorder() target.ServeHTTP(rec, req) return rec } for i := 0; i < 10; i++ { if rec := makeRequest(first); rec.Code != http.StatusOK { t.Fatalf("first router request %d status = %d, want %d", i+1, rec.Code, http.StatusOK) } } if rec := makeRequest(first); rec.Code != http.StatusTooManyRequests { t.Fatalf("first router overflow status = %d, want %d", rec.Code, http.StatusTooManyRequests) } if rec := makeRequest(second); rec.Code != http.StatusOK { t.Fatalf("second router first request status = %d, want %d", rec.Code, http.StatusOK) } } func TestContract_RouterShutdownClosesOwnedPersistentAuthStoresAfterGlobalRebind(t *testing.T) { resetPersistentAuthStoresForTests() t.Cleanup(resetPersistentAuthStoresForTests) routerOne := NewRouter(&config.Config{DataPath: t.TempDir()}, nil, nil, nil, nil, "1.0.0") routerTwo := NewRouter(&config.Config{DataPath: t.TempDir()}, nil, nil, nil, nil, "1.0.0") t.Cleanup(routerTwo.shutdownBackgroundWorkers) if routerOne.sessionStore == nil || routerOne.csrfStore == nil { t.Fatal("routerOne should capture initialized persistent auth stores") } if routerOne.recoveryTokenStore == nil { t.Fatal("routerOne should capture initialized recovery token store") } if routerTwo.sessionStore == nil || routerTwo.csrfStore == nil { t.Fatal("routerTwo should capture initialized persistent auth stores") } if routerTwo.recoveryTokenStore == nil { t.Fatal("routerTwo should capture initialized recovery token store") } if routerOne.sessionStore == routerTwo.sessionStore { t.Fatal("router instances should not share the same session store after rebind") } if routerOne.csrfStore == routerTwo.csrfStore { t.Fatal("router instances should not share the same csrf store after rebind") } if routerOne.recoveryTokenStore == routerTwo.recoveryTokenStore { t.Fatal("router instances should not share the same recovery token store after rebind") } routerOne.shutdownBackgroundWorkers() select { case <-routerOne.sessionStore.workerDone: default: t.Fatal("routerOne session store worker should be closed after router shutdown") } select { case <-routerOne.csrfStore.workerDone: default: t.Fatal("routerOne csrf store worker should be closed after router shutdown") } select { case <-routerTwo.sessionStore.workerDone: t.Fatal("routerTwo session store should remain active when routerOne shuts down") default: } select { case <-routerTwo.csrfStore.workerDone: t.Fatal("routerTwo csrf store should remain active when routerOne shuts down") default: } select { case <-routerOne.recoveryTokenStore.stopCleanup: default: t.Fatal("routerOne recovery token store should be closed after router shutdown") } select { case <-routerTwo.recoveryTokenStore.stopCleanup: t.Fatal("routerTwo recovery token store should remain active when routerOne shuts down") default: } } func TestContract_HostedOrgManagerSessionCanMintRelayMobileToken(t *testing.T) { defer SetMultiTenantEnabled(false) SetMultiTenantEnabled(true) t.Setenv("PULSE_DEV", "true") dataDir := t.TempDir() hashed, err := authpkg.HashPassword("Password!1") if err != nil { t.Fatalf("hash password: %v", err) } cfg := &config.Config{ DataPath: dataDir, ConfigPath: dataDir, AuthUser: "platform-admin", AuthPass: hashed, } mtp := config.NewMultiTenantPersistence(dataDir) org := &models.Organization{ ID: "org-a", DisplayName: "Org A", OwnerUserID: "operator-owner", Members: []models.OrganizationMember{ {UserID: "legacy-owner", Role: models.OrgRoleOwner, AddedAt: time.Now()}, {UserID: "operator-owner", Role: models.OrgRoleOwner, AddedAt: time.Now()}, }, } if err := mtp.SaveOrganization(org); err != nil { t.Fatalf("save organization: %v", err) } router := newMultiTenantRouter(t, cfg) sessionToken := "relay-owner-session-" + strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339Nano), ":", "-") GetSessionStore().CreateSession(sessionToken, time.Hour, "agent", "127.0.0.1", "legacy-owner") req := httptest.NewRequest(http.MethodPost, "/api/security/tokens/relay-mobile", strings.NewReader(`{}`)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-CSRF-Token", generateCSRFToken(sessionToken)) req.Header.Set("X-Pulse-Org-ID", "org-a") req.AddCookie(&http.Cookie{Name: cookieNameSession, Value: sessionToken}) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("relay mobile token route status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload struct { Token string `json:"token"` Record apiTokenDTO `json:"record"` } if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode relay mobile response: %v", err) } if payload.Token == "" { t.Fatal("expected relay mobile token in response") } if payload.Record.OwnerUserID != "legacy-owner" { t.Fatalf("ownerUserId = %q, want legacy-owner", payload.Record.OwnerUserID) } if len(cfg.APITokens) != 1 { t.Fatalf("expected one stored token, got %d", len(cfg.APITokens)) } if cfg.APITokens[0].OrgID != "org-a" { t.Fatalf("stored token orgId = %q, want org-a", cfg.APITokens[0].OrgID) } } func TestContract_RelayMobileScopeCanReadOnboardingDeepLink(t *testing.T) { rawToken := "relay-mobile-onboarding-token-123.12345678" record := newTokenRecord(t, rawToken, []string{config.ScopeRelayMobileAccess}, nil) cfg := newTestConfigWithTokens(t, record) router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodGet, "/api/onboarding/deep-link", nil) req.Header.Set("X-API-Token", rawToken) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code == http.StatusForbidden && strings.Contains(rec.Body.String(), "missing_scope") { t.Fatalf("relay mobile scope should satisfy onboarding deep-link gating, got %d: %s", rec.Code, rec.Body.String()) } } func TestContract_RelayMobileScopeCannotReadApprovalDetail(t *testing.T) { rawToken := "relay-mobile-approval-detail-token-123.12345678" record := newTokenRecord(t, rawToken, []string{config.ScopeRelayMobileAccess}, nil) cfg := newTestConfigWithTokens(t, record) router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodGet, "/api/ai/approvals/approval-1", nil) req.Header.Set("X-API-Token", rawToken) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("relay mobile scope should not satisfy approval detail gating, got %d: %s", rec.Code, rec.Body.String()) } if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) { t.Fatalf("relay mobile approval detail rejection should mention %q, got %s", config.ScopeAIExecute, rec.Body.String()) } } func TestContract_UnifiedAgentReportResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "success": true, "agentId": "agent-123", "lastSeen": "2026-02-08T13:14:15Z", "platform": "linux", "osName": "Debian GNU/Linux", "osVersion": "12", "config": map[string]any{ "commandsEnabled": true, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal unified agent report response: %v", err) } const want = `{ "agentId":"agent-123", "config":{"commandsEnabled":true}, "lastSeen":"2026-02-08T13:14:15Z", "osName":"Debian GNU/Linux", "osVersion":"12", "platform":"linux", "success":true }` assertJSONSnapshot(t, got, want) } func TestContract_UnifiedAgentLookupFailsClosedOnAmbiguousHostname(t *testing.T) { handler := newUnifiedAgentHandlerForTests(t, models.Host{ID: "host-1", Hostname: "webserver.corp.example.com", DisplayName: "Web One"}, models.Host{ID: "host-2", Hostname: "webserver.other.example.com", DisplayName: "Web Two"}, ) req := httptest.NewRequest(http.MethodGet, "/api/agents/agent/lookup?hostname=webserver", nil) rec := httptest.NewRecorder() handler.HandleLookup(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("expected not found status for ambiguous hostname lookup, got %d: %s", rec.Code, rec.Body.String()) } var resp struct { Error string `json:"error"` Code string `json:"code"` } if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode ambiguous lookup error response: %v", err) } if resp.Code != "agent_not_found" { t.Fatalf("expected agent_not_found code, got %q", resp.Code) } } func TestContract_HostsShareResolvedIdentityTreatsLoopbackAliasAsSameNode(t *testing.T) { if !hostsShareResolvedIdentity("https://localhost:7655", "https://127.0.0.1:7655") { t.Fatal("expected localhost and loopback IP to resolve as the same host identity") } if hostsShareResolvedIdentity("https://192.0.2.10:7655", "https://192.0.2.11:7655") { t.Fatal("expected different IP endpoints to remain distinct host identities") } } func TestContract_DiagnosticsDockerPrepareTokenInstallCommandUsesLifecycleTransport(t *testing.T) { baseURL := "https://pulse.example.com/base" got := buildContainerRuntimeAgentInstallCommand(baseURL, "token-123") if !strings.Contains(got, posixShellQuote(baseURL+"/install.sh")) { t.Fatalf("install command missing normalized install script URL: %s", got) } if !strings.Contains(got, "--enable-host=false") { t.Fatalf("install command missing canonical host-disable flag: %s", got) } if strings.Contains(got, "--disable-host") { t.Fatalf("install command preserved stale disable-host flag: %s", got) } if !strings.Contains(got, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`) { t.Fatalf("install command missing governed root-or-sudo wrapper: %s", got) } if strings.Contains(got, "curl -fsSL "+posixShellQuote(baseURL+"/install.sh")+" | sudo bash -s --") { t.Fatalf("install command preserved raw sudo pipe instead of governed wrapper: %s", got) } } func TestContract_DiagnosticsDockerPrepareTokenOptionalAuthInstallCommandOmitsToken(t *testing.T) { got := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "") if strings.Contains(got, "--token") { t.Fatalf("optional-auth install command preserved token flag: %s", got) } if !strings.Contains(got, "--insecure") { t.Fatalf("optional-auth install command missing insecure flag for plain HTTP Pulse URL: %s", got) } } func TestContract_SetupScriptURLCommandUsesFailFastQuotedTransport(t *testing.T) { url := "https://pulse.example.com/api/setup-script?type=pve&host=pve1.local" got := buildSetupScriptCommand(url, "token-123") if !strings.Contains(got, "curl -fsSL "+posixShellQuote(url)+" | ") { t.Fatalf("setup-script command missing canonical fail-fast transport: %s", got) } if !strings.Contains(got, `if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`) { t.Fatalf("setup-script command missing direct-root execution path: %s", got) } if !strings.Contains(got, `elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN=`+posixShellQuote("token-123")+` bash`) { t.Fatalf("setup-script command missing sudo execution path: %s", got) } if strings.Contains(got, "curl -sSL ") { t.Fatalf("setup-script command preserved stale non-fail-fast curl transport: %s", got) } } func TestContract_SetupScriptEmbedsFailFastGuidance(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) rec := httptest.NewRecorder() handlers.HandleSetupScript(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } script := rec.Body.String() if !strings.Contains(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("setup script missing canonical bootstrap command owner: %s", script) } if strings.Contains(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'"'"' | { if [ "$(id -u)" -eq 0 ]; then PULSE_SETUP_TOKEN=`) { t.Fatalf("setup script bootstrap command should defer setup token to runtime hydration, got: %s", script) } if !strings.Contains(script, `echo " $PULSE_BOOTSTRAP_COMMAND_WITH_ENV"`) { t.Fatalf("setup script missing bootstrap-command retry guidance: %s", script) } if !strings.Contains(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("setup script missing canonical encoded retry URL: %s", script) } if !strings.Contains(script, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-`) { t.Fatalf("setup script missing canonical PVE setup-token initialization before rerun guidance: %s", script) } if !strings.Contains(script, `echo "Root privileges required. Run as root (su -) and retry."`) { t.Fatalf("setup script missing canonical root requirement guidance: %s", script) } if !strings.Contains(script, `echo "This setup flow must run on the Proxmox host so Pulse can create"`) { t.Fatalf("setup script missing canonical off-host rerun guidance: %s", script) } if strings.Contains(script, `echo " curl -sSL \"$SETUP_SCRIPT_URL\" | bash"`) || strings.Contains(script, `echo " curl -sSL \"$PULSE_URL/api/setup-script?type=pve&host=YOUR_PVE_URL&pulse_url=$PULSE_URL\" | bash"`) { t.Fatalf("setup script preserved stale non-fail-fast guidance: %s", script) } if strings.Contains(script, `echo "Manual setup steps:"`) || strings.Contains(script, `echo " 2. In Pulse: Settings → Nodes → Add Node (enter token from above)"`) { t.Fatalf("setup script preserved stale off-host manual token flow: %s", script) } if !strings.Contains(script, `done <<< "$OLD_TOKENS_PVE"`) { t.Fatalf("setup script missing explicit pve old-token cleanup loop: %s", script) } if !strings.Contains(script, `done <<< "$OLD_TOKENS_PAM"`) { t.Fatalf("setup script missing explicit pam old-token cleanup loop: %s", script) } if strings.Contains(script, `done <<< "$OLD_TOKENS"`) { t.Fatalf("setup script preserved stale undefined old-token cleanup variable: %s", script) } if !strings.Contains(script, `pveum user token remove pulse-monitor@pve "$TOKEN"`) { t.Fatalf("setup script missing pve token cleanup command: %s", script) } if !strings.Contains(script, `pveum user token remove pulse-monitor@pam "$TOKEN"`) { t.Fatalf("setup script missing pam token cleanup command: %s", script) } if !strings.Contains(script, `TOKEN_MATCH_PREFIX="pulse-sentinel-url"`) { t.Fatalf("setup script missing canonical token-match prefix for cleanup discovery: %s", script) } if !strings.Contains(script, `grep -E "^${TOKEN_MATCH_PREFIX}(-[0-9]+)?$"`) { t.Fatalf("setup script missing canonical cleanup token discovery matcher: %s", script) } if !strings.Contains(script, `awk 'NR>3 {print $2}' | grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) { t.Fatalf("setup script missing exact PVE token rotation detection: %s", script) } if strings.Contains(script, `PULSE_IP_PATTERN=`) { t.Fatalf("setup script preserved stale ip-pattern cleanup discovery: %s", script) } if strings.Contains(script, `grep -q "$TOKEN_NAME"`) { t.Fatalf("setup script preserved stale broad PVE token rotation detection: %s", script) } if strings.Contains(script, `echo "Please run this script as root"`) { t.Fatalf("setup script preserved stale root-only guidance: %s", script) } if !strings.Contains(script, `grep -Eq '"status"[[:space:]]*:[[:space:]]*"success"'`) { t.Fatalf("setup script missing secure success detection: %s", script) } if strings.Contains(script, `grep -q "success"`) { t.Fatalf("setup script preserved broad success substring detection: %s", script) } if !strings.Contains(script, `curl -fsS -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("setup script missing fail-fast auto-register transport: %s", script) } if !strings.Contains(script, `"source":"script"`) { t.Fatalf("setup script missing canonical /api/auto-register source marker: %s", script) } if !strings.Contains(script, `REGISTER_RC=$?`) { t.Fatalf("setup script missing explicit auto-register curl exit-code handling: %s", script) } if !strings.Contains(script, `echo "⚠️ Auto-registration skipped: token value unavailable"`) { t.Fatalf("setup script missing fail-closed token-value-unavailable guidance: %s", script) } if strings.Contains(script, `curl -s -X POST "$PULSE_URL/api/auto-register"`) { t.Fatalf("setup script preserved stale non-fail-fast auto-register transport: %s", script) } if !strings.Contains(script, `echo "The provided Pulse setup token was invalid or expired"`) { t.Fatalf("setup script missing invalid setup-token guidance: %s", script) } if !strings.Contains(script, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) { t.Fatalf("setup script missing fresh setup-token rerun guidance: %s", script) } if !strings.Contains(script, `SETUP_TOKEN_INVALID=true`) { t.Fatalf("setup script missing PVE auth-failure state tracking: %s", script) } if !strings.Contains(script, `echo "Pulse setup token authentication failed."`) { t.Fatalf("setup script missing PVE auth-failure completion guidance: %s", script) } if !strings.Contains(script, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) { t.Fatalf("setup script missing PVE auth-failure footer guard: %s", script) } if !strings.Contains(script, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("setup script missing canonical auto-register failure continuation guidance: %s", script) } if strings.Contains(script, `echo "To enable auto-registration, add your API token to the setup URL"`) { t.Fatalf("setup script preserved stale API-token auth guidance: %s", script) } if strings.Contains(script, `echo "The provided API token was invalid"`) { t.Fatalf("setup script preserved stale invalid API-token guidance: %s", script) } if strings.Contains(script, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) { t.Fatalf("setup script preserved stale split setup-token auth guidance: %s", script) } if strings.Contains(script, `echo "📝 For manual setup:"`) { t.Fatalf("setup script preserved stale numbered manual-setup fallback: %s", script) } if strings.Contains(script, `echo " 2. Add this node manually in Pulse Settings"`) { t.Fatalf("setup script preserved stale auto-register failure continuation guidance: %s", script) } if !strings.Contains(script, `echo "Pulse monitoring token setup completed."`) { t.Fatalf("setup script missing truthful manual completion messaging: %s", script) } if !strings.Contains(script, `echo "Pulse monitoring token setup failed."`) { t.Fatalf("setup script missing token-create failure completion messaging: %s", script) } if !strings.Contains(script, `echo "Fix the token creation error above and rerun this script on the node."`) { t.Fatalf("setup script missing immediate token-create failure rerun guidance: %s", script) } if !strings.Contains(script, `echo "Resolve the token creation error shown above and rerun this script on the node."`) { t.Fatalf("setup script missing token-create failure rerun guidance: %s", script) } if !strings.Contains(script, `echo " Resolve the token output issue above and rerun this script on the node."`) { t.Fatalf("setup script missing token-extract failure rerun guidance: %s", script) } if !strings.Contains(script, `echo "Successfully registered with Pulse monitoring."`) { t.Fatalf("setup script missing canonical success messaging: %s", script) } if !strings.Contains(script, `echo " Token Value: [See token output above]"`) { t.Fatalf("setup script missing canonical token placeholder guidance: %s", script) } if !strings.Contains(script, `echo "Finish registration in Pulse using the manual setup details below."`) { t.Fatalf("setup script missing truthful manual registration guidance: %s", script) } if !strings.Contains(script, `echo "Add this server to Pulse with:"`) { t.Fatalf("setup script missing canonical manual-add heading: %s", script) } if !strings.Contains(script, `echo "Use these details in Pulse Settings → Nodes to finish registration."`) { t.Fatalf("setup script missing canonical manual-add continuation guidance: %s", script) } if !strings.Contains(script, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) { t.Fatalf("setup script missing canonical auto-register failure summary: %s", script) } if !strings.Contains(script, `echo " Host URL: $SERVER_HOST"`) { t.Fatalf("setup script missing canonical manual host continuity: %s", script) } if strings.Contains(script, `echo "Manual setup instructions:"`) { t.Fatalf("setup script preserved stale manual setup heading: %s", script) } if strings.Contains(script, `echo "Node registered successfully"`) || strings.Contains(script, `echo "Node successfully registered with Pulse monitoring."`) || strings.Contains(script, `echo "✅ Successfully registered with Pulse!"`) || strings.Contains(script, `echo "Server successfully registered with Pulse monitoring."`) { t.Fatalf("setup script preserved stale success copy variants: %s", script) } if strings.Contains(script, `echo " Token Value: [See above]"`) || strings.Contains(script, `echo " Token Value: [Check the output above for the token or instructions]"`) { t.Fatalf("setup script preserved stale token placeholder guidance: %s", script) } if strings.Contains(script, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) { t.Fatalf("setup script preserved stale auto-register failure summary: %s", script) } if strings.Contains(script, `PULSE_REG_TOKEN=your-token ./setup.sh`) { t.Fatalf("setup script preserved stale rerun token guidance: %s", script) } if strings.Contains(script, `echo "Manual registration may be required."`) { t.Fatalf("setup script preserved stale manual-registration token failure guidance: %s", script) } if strings.Contains(script, `echo " Host URL: YOUR_PROXMOX_HOST:8006"`) { t.Fatalf("setup script preserved stale placeholder manual host guidance: %s", script) } if !strings.Contains(script, `echo "Pulse monitoring token setup could not be completed."`) { t.Fatalf("setup script missing token-extract failure completion messaging: %s", script) } if !strings.Contains(script, `echo "Resolve the token output issue shown above and rerun this script on the node."`) { t.Fatalf("setup script missing token-extract completion rerun guidance: %s", script) } if !strings.Contains(script, `if [ "$TOKEN_READY" = true ]; then attempt_auto_registration else AUTO_REG_SUCCESS=false fi`) { t.Fatalf("setup script does not skip PVE auto-registration when no usable token is ready: %s", script) } if !strings.Contains(script, `if [ "$TOKEN_READY" = true ]; then echo "Add this server to Pulse with:"`) { t.Fatalf("setup script does not gate PVE manual token details on usable token extraction: %s", script) } if strings.Contains(script, `elif [ "$TOKEN_READY" != true ]; then echo "Pulse monitoring token setup completed."`) { t.Fatalf("setup script lets PVE token-extract failure fall through to completed token setup: %s", script) } pbsReq := httptest.NewRequest(http.MethodGet, "/api/setup-script?type=pbs&host=https://sentinel-pbs:8007&pulse_url=http://sentinel-url:7656", nil) pbsRec := httptest.NewRecorder() handlers.HandleSetupScript(pbsRec, pbsReq) if pbsRec.Code != http.StatusOK { t.Fatalf("pbs status = %d, want %d", pbsRec.Code, http.StatusOK) } pbsScript := pbsRec.Body.String() if !strings.Contains(pbsScript, `echo " Host URL: $HOST_URL"`) { t.Fatalf("setup script missing canonical PBS manual host continuity: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Pulse monitoring token setup failed."`) { t.Fatalf("setup script missing PBS token-create failure completion messaging: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Pulse monitoring token setup could not be completed."`) { t.Fatalf("setup script missing PBS token-extract failure completion messaging: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: no setup token provided"`) { t.Fatalf("setup script missing PBS setup-token-skip guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `PULSE_SETUP_TOKEN="${PULSE_SETUP_TOKEN:-`) { t.Fatalf("setup script missing canonical PBS setup-token initialization before rerun guidance: %s", pbsScript) } if strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: no setup token provided" AUTO_REG_SUCCESS=false REGISTER_RESPONSE="" REGISTER_RC=1`) { t.Fatalf("setup script still forces fake PBS request-failure state after missing setup-token skip: %s", pbsScript) } if strings.Contains(pbsScript, `echo "⚠️ Auto-registration skipped: token value unavailable" AUTO_REG_SUCCESS=false REGISTER_RESPONSE="" REGISTER_RC=1`) { t.Fatalf("setup script still forces fake PBS request-failure state after token-value skip: %s", pbsScript) } if !strings.Contains(pbsScript, `if [ "$REGISTER_ATTEMPTED" != true ]; then`) { t.Fatalf("setup script does not distinguish skipped PBS auto-registration paths from attempted requests: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "The provided Pulse setup token was invalid or expired"`) { t.Fatalf("setup script missing invalid PBS setup-token guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Get a fresh setup token from Pulse Settings → Nodes and rerun this script."`) { t.Fatalf("setup script missing fresh PBS setup-token rerun guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `SETUP_TOKEN_INVALID=true`) { t.Fatalf("setup script missing PBS auth-failure state tracking: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Pulse setup token authentication failed."`) { t.Fatalf("setup script missing PBS auth-failure completion guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `if [ "$AUTO_REG_SUCCESS" != true ] && [ "$SETUP_TOKEN_INVALID" != true ]; then`) { t.Fatalf("setup script missing PBS auth-failure footer guard: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Fix the token creation error above and rerun this script on the node."`) { t.Fatalf("setup script missing PBS immediate token-create failure rerun guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Resolve the token creation error shown above and rerun this script on the node."`) { t.Fatalf("setup script missing PBS token-create failure rerun guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `echo " Resolve the token output issue above and rerun this script on the node."`) { t.Fatalf("setup script missing PBS token-extract failure rerun guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "Resolve the token output issue shown above and rerun this script on the node."`) { t.Fatalf("setup script missing PBS token-extract completion rerun guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) { t.Fatalf("setup script missing canonical PBS host binding: %s", pbsScript) } if !strings.Contains(pbsScript, `TOKEN_MATCH_PREFIX="pulse-sentinel-url"`) { t.Fatalf("pbs setup script missing canonical token-match prefix for cleanup discovery: %s", pbsScript) } if !strings.Contains(pbsScript, `grep -oE "${TOKEN_MATCH_PREFIX}(-[0-9]+)?" | sort -u || true`) { t.Fatalf("pbs setup script missing canonical cleanup token discovery matcher: %s", pbsScript) } if !strings.Contains(pbsScript, `awk '{print $1}' | grep -Fx "$TOKEN_NAME" >/dev/null 2>&1`) { t.Fatalf("pbs setup script missing exact token rotation detection: %s", pbsScript) } tokenCreateIndex := strings.Index(pbsScript, `TOKEN_CREATE_RC=$?`) bannerIndex := strings.Index(pbsScript, `echo "IMPORTANT: Copy the token value below - it's only shown once!"`) successBranchIndex := strings.Index(pbsScript, "else\n TOKEN_CREATED=true") if tokenCreateIndex == -1 || bannerIndex == -1 || successBranchIndex == -1 { t.Fatalf("pbs setup script missing token-create truth markers: %s", pbsScript) } if bannerIndex < tokenCreateIndex { t.Fatalf("pbs setup script prints token-copy banner before token creation result is known: %s", pbsScript) } if bannerIndex < successBranchIndex { t.Fatalf("pbs setup script prints token-copy banner outside the successful token-create branch: %s", pbsScript) } if strings.Contains(pbsScript, `PULSE_IP_PATTERN=`) { t.Fatalf("pbs setup script preserved stale ip-pattern cleanup discovery: %s", pbsScript) } if strings.Contains(pbsScript, `grep -q "$TOKEN_NAME"`) { t.Fatalf("pbs setup script preserved stale broad token rotation detection: %s", pbsScript) } if strings.Index(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) > strings.Index(pbsScript, `if [ -z "$PULSE_SETUP_TOKEN" ]; then`) { t.Fatalf("setup script binds PBS host too late for manual fallback continuity: %s", pbsScript) } if strings.Index(pbsScript, `HOST_URL="https://sentinel-pbs:8007"`) > strings.Index(pbsScript, `if [ "$TOKEN_CREATE_RC" -ne 0 ]; then`) { t.Fatalf("setup script binds PBS host too late for token-create failure continuity: %s", pbsScript) } if !strings.Contains(pbsScript, `if [ "$TOKEN_READY" = true ]; then echo "Add this server to Pulse with:"`) { t.Fatalf("setup script does not gate PBS manual token details on usable token extraction: %s", pbsScript) } attemptBannerIndex := strings.Index(pbsScript, `echo "🔄 Attempting auto-registration with Pulse..."`) authTokenGateIndex := strings.Index(pbsScript, `if [ -n "$PULSE_SETUP_TOKEN" ]; then`) tokenSkipIndex := strings.Index(pbsScript, `echo "⚠️ Auto-registration skipped: token value unavailable"`) if attemptBannerIndex == -1 || authTokenGateIndex == -1 || tokenSkipIndex == -1 { t.Fatalf("setup script missing PBS auto-registration truth markers: %s", pbsScript) } if attemptBannerIndex < authTokenGateIndex { t.Fatalf("setup script prints PBS auto-registration attempt banner before the real request path: %s", pbsScript) } if attemptBannerIndex < tokenSkipIndex { t.Fatalf("setup script prints PBS auto-registration attempt banner before token-unavailable skip handling: %s", pbsScript) } if strings.Contains(pbsScript, `elif [ "$TOKEN_READY" != true ]; then echo "Pulse monitoring token setup completed."`) { t.Fatalf("setup script lets PBS token-extract failure fall through to completed token setup: %s", pbsScript) } if strings.Contains(pbsScript, `echo " Host URL: https://$SERVER_IP:8007"`) { t.Fatalf("setup script preserved stale PBS runtime-IP host guidance: %s", pbsScript) } if strings.Contains(pbsScript, `echo "Manual registration may be required."`) { t.Fatalf("setup script preserved stale PBS manual-registration token failure guidance: %s", pbsScript) } if strings.Contains(pbsScript, `echo "To enable auto-registration, rerun with a valid Pulse setup token"`) { t.Fatalf("setup script preserved stale split PBS setup-token auth guidance: %s", pbsScript) } if !strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Finish registration manually in Pulse Settings → Nodes."`) { t.Fatalf("setup script missing canonical PBS auto-register failure summary: %s", pbsScript) } if strings.Count(pbsScript, `echo "📝 Use the token details below in Pulse Settings → Nodes to finish registration."`) < 2 { t.Fatalf("setup script missing canonical PBS request-failure/manual-response continuity: %s", pbsScript) } if strings.Contains(pbsScript, `echo "⚠️ Auto-registration failed. Manual configuration may be needed."`) { t.Fatalf("setup script preserved stale PBS auto-register failure summary: %s", pbsScript) } } func TestContract_SetupScriptRequiresCanonicalHost(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) rec := httptest.NewRecorder() handlers.HandleSetupScript(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } if got := strings.TrimSpace(rec.Body.String()); got != "Missing required parameter: host" { t.Fatalf("body = %q, want canonical missing host guidance", got) } } func TestContract_SetupScriptUsesCanonicalTypeAndHostValidation(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handlers := newTestConfigHandlers(t, cfg) invalidTypeReq := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pmg&host=https://node.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) invalidTypeRec := httptest.NewRecorder() handlers.HandleSetupScript(invalidTypeRec, invalidTypeReq) if invalidTypeRec.Code != http.StatusBadRequest { t.Fatalf("invalid type status = %d, want %d", invalidTypeRec.Code, http.StatusBadRequest) } if got := strings.TrimSpace(invalidTypeRec.Body.String()); got != "type must be 'pve' or 'pbs'" { t.Fatalf("invalid type body = %q, want canonical type guidance", got) } normalizedHostReq := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https://pve-node.example.internal&pulse_url=https://pulse.example.com:7655", nil, ) normalizedHostRec := httptest.NewRecorder() handlers.HandleSetupScript(normalizedHostRec, normalizedHostReq) if normalizedHostRec.Code != http.StatusOK { t.Fatalf("normalized host status = %d, want %d: %s", normalizedHostRec.Code, http.StatusOK, normalizedHostRec.Body.String()) } body := normalizedHostRec.Body.String() if !strings.Contains(body, `SERVER_HOST="https://pve-node.example.internal:8006"`) { t.Fatalf("normalized host body missing canonical host, got: %s", truncate(body, 500)) } if !strings.Contains(body, `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("normalized host body missing canonical rerun URL, got: %s", truncate(body, 700)) } } func TestContract_SetupScriptRequiresCanonicalPulseURL(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.local:8006", nil) rec := httptest.NewRecorder() handlers.HandleSetupScript(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest) } if got := strings.TrimSpace(rec.Body.String()); got != "Missing required parameter: pulse_url" { t.Fatalf("body = %q, want canonical missing pulse_url guidance", got) } } func TestContract_SetupScriptUsesCanonicalShellDownloadHeaders(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://sentinel-pbs:8007&pulse_url=http://sentinel-url:7656", nil) rec := httptest.NewRecorder() handlers.HandleSetupScript(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } if got := rec.Header().Get("Content-Type"); got != "text/x-shellscript; charset=utf-8" { t.Fatalf("setup-script content type = %q, want %q", got, "text/x-shellscript; charset=utf-8") } if got := rec.Header().Get("Content-Disposition"); got != "attachment; filename=\"pulse-setup-pbs.sh\"" { t.Fatalf("setup-script content disposition = %q, want %q", got, "attachment; filename=\"pulse-setup-pbs.sh\"") } } func TestContract_SetupScriptDerivesRenderedServerNameFromCanonicalHost(t *testing.T) { handlers := newTestConfigHandlers(t, &config.Config{ DataPath: t.TempDir(), ConfigPath: t.TempDir(), }) req := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pve&host=https://derived-pve.example.internal:8006&pulse_url=https://pulse.example.com:7655", nil, ) rec := httptest.NewRecorder() handlers.HandleSetupScript(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } script := rec.Body.String() if !strings.Contains(script, "# Pulse Monitoring Setup Script for derived-pve.example.internal") { t.Fatalf("setup script missing derived canonical host label: %s", script) } if strings.Contains(script, "# Pulse Monitoring Setup Script for your-server") { t.Fatalf("setup script preserved placeholder server label for canonical host: %s", script) } } func TestContract_AssignProfileRejectsMissingProfile(t *testing.T) { tempDir := t.TempDir() mtp := config.NewMultiTenantPersistence(tempDir) if _, err := mtp.GetPersistence("default"); err != nil { t.Fatalf("init default persistence: %v", err) } handler := NewConfigProfileHandler(mtp) body := bytes.NewBufferString(`{"agent_id":"agent-1","profile_id":"missing-profile"}`) req := httptest.NewRequest(http.MethodPost, "/assignments", body) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusNotFound, rec.Body.String()) } if got := strings.TrimSpace(rec.Body.String()); got != "Profile not found" { t.Fatalf("body = %q, want %q", got, "Profile not found") } } func TestContract_ResolveLoopbackAwarePublicBaseURLPreservesConfiguredHTTPS(t *testing.T) { cfg := &config.Config{ PublicURL: "https://public.example.com/base/", FrontendPort: 7655, } req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "127.0.0.1:7655" if got := resolveLoopbackAwarePublicBaseURL(req, cfg); got != "https://public.example.com/base" { t.Fatalf("baseURL = %q, want %q", got, "https://public.example.com/base") } } func TestContract_CanonicalPulseMonitorTokenNamePrefersPulseURL(t *testing.T) { got := buildPulseMonitorTokenName("https://public.example.com/base", "127.0.0.1:7655") if got != "pulse-public-example-com" { t.Fatalf("tokenName = %q, want %q", got, "pulse-public-example-com") } } func TestContract_FilterRecoveryPointsForRollupsIncludesNormalizedFilters(t *testing.T) { verified := true unverified := false points := []recovery.RecoveryPoint{ { ID: "point-1", Provider: recovery.ProviderKubernetes, Kind: recovery.KindBackup, Mode: recovery.ModeRemote, Outcome: recovery.OutcomeSuccess, SubjectResourceID: "pod-1", Verified: &verified, Display: &recovery.RecoveryPointDisplay{ SubjectLabel: "pod-1", SubjectType: "pod", ItemType: "pod", ClusterLabel: "prod-cluster", NodeHostLabel: "worker-1", NamespaceLabel: "default", RepositoryLabel: "repo-a", IsWorkload: true, }, }, { ID: "point-2", Provider: recovery.ProviderKubernetes, Kind: recovery.KindBackup, Mode: recovery.ModeRemote, Outcome: recovery.OutcomeFailed, SubjectResourceID: "pod-2", Verified: &unverified, Display: &recovery.RecoveryPointDisplay{ SubjectLabel: "pod-2", SubjectType: "pod", ItemType: "pod", ClusterLabel: "other-cluster", NodeHostLabel: "worker-2", NamespaceLabel: "kube-system", IsWorkload: true, }, }, } filtered := filterRecoveryPointsForRollups(points, recovery.ListPointsOptions{ Query: "repo-a", ItemType: "pod", ClusterLabel: "prod-cluster", NodeHostLabel: "worker-1", NamespaceLabel: "default", Verification: "verified", WorkloadOnly: true, }) if len(filtered) != 1 { t.Fatalf("len(filtered) = %d, want 1", len(filtered)) } if got := filtered[0].SubjectResourceID; got != "pod-1" { t.Fatalf("subjectResourceID = %q, want %q", got, "pod-1") } } func TestContract_ParseRecoveryPlatformQueryPrefersCanonicalPlatformAlias(t *testing.T) { t.Parallel() if got := parseRecoveryPlatformQuery(url.Values{ "platform": []string{" truenas "}, "provider": []string{"proxmox-pve"}, }); got != recovery.Provider("truenas") { t.Fatalf("parseRecoveryPlatformQuery(platform first) = %q, want %q", got, "truenas") } if got := parseRecoveryPlatformQuery(url.Values{ "provider": []string{" proxmox-pbs "}, }); got != recovery.Provider("proxmox-pbs") { t.Fatalf("parseRecoveryPlatformQuery(provider fallback) = %q, want %q", got, "proxmox-pbs") } } func TestContract_RecoveryPointPayloadUsesCanonicalPlatformField(t *testing.T) { payload := buildRecoveryPointPayload(recovery.RecoveryPoint{ ID: "point-1", Provider: recovery.Provider("truenas"), Kind: recovery.Kind("snapshot"), Mode: recovery.Mode("snapshot"), Outcome: recovery.Outcome("success"), }) got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal recovery point payload: %v", err) } const want = `{ "id":"point-1", "platform":"truenas", "provider":"truenas", "kind":"snapshot", "mode":"snapshot", "outcome":"success" }` assertJSONSnapshot(t, got, want) } func TestContract_RecoveryPointsMockPathReturnsCanonicalProviderBackedFixtures(t *testing.T) { previousEnabled := mock.IsMockEnabled() previousConfig := mock.GetConfig() t.Cleanup(func() { mock.SetEnabled(false) mock.SetMockConfig(previousConfig) if previousEnabled { mock.SetEnabled(true) mock.SetMockConfig(previousConfig) } }) t.Setenv("PULSE_MOCK_NODES", "1") t.Setenv("PULSE_MOCK_VMS_PER_NODE", "0") t.Setenv("PULSE_MOCK_LXCS_PER_NODE", "0") t.Setenv("PULSE_MOCK_DOCKER_HOSTS", "0") t.Setenv("PULSE_MOCK_DOCKER_CONTAINERS", "0") t.Setenv("PULSE_MOCK_GENERIC_HOSTS", "0") t.Setenv("PULSE_MOCK_K8S_CLUSTERS", "0") t.Setenv("PULSE_MOCK_K8S_NODES", "0") t.Setenv("PULSE_MOCK_K8S_PODS", "0") t.Setenv("PULSE_MOCK_K8S_DEPLOYMENTS", "0") mock.SetEnabled(false) mock.SetEnabled(true) req := httptest.NewRequest(http.MethodGet, "/api/recovery/points?platform=truenas&limit=10", nil) rec := httptest.NewRecorder() NewRecoveryHandlers(nil).HandleListPoints(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) } var resp recoveryPointsResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("unmarshal recovery points response: %v", err) } if len(resp.Data) == 0 { t.Fatal("expected provider-backed mock recovery points in response") } if resp.Data[0].Platform != recovery.Provider("truenas") { t.Fatalf("platform = %q, want %q", resp.Data[0].Platform, "truenas") } if resp.Data[0].Display == nil { t.Fatal("expected normalized recovery display payload on mock points response") } } func TestContract_RecoveryRollupPayloadUsesCanonicalPlatformsField(t *testing.T) { payload := buildRecoveryRollupPayload(recovery.ProtectionRollup{ RollupID: "rollup-1", LastOutcome: recovery.Outcome("success"), Providers: []recovery.Provider{ recovery.Provider("proxmox-pbs"), recovery.Provider("truenas"), }, }) got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal recovery rollup payload: %v", err) } const want = `{ "rollupId":"rollup-1", "lastOutcome":"success", "platforms":["proxmox-pbs","truenas"], "providers":["proxmox-pbs","truenas"] }` assertJSONSnapshot(t, got, want) } func TestContract_BillingStateJSONSnapshot(t *testing.T) { payload := entitlements.BillingState{ Capabilities: []string{"relay", "mobile_app"}, Limits: map[string]int64{"max_monitored_systems": 10}, MetersEnabled: []string{"api_requests"}, PlanVersion: "cloud_starter", SubscriptionState: entitlements.SubStateActive, StripeCustomerID: "cus_123", StripeSubscriptionID: "sub_123", StripePriceID: "price_123", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal billing state: %v", err) } const want = `{ "capabilities":["relay","mobile_app"], "limits":{"max_monitored_systems":10}, "meters_enabled":["api_requests"], "plan_version":"cloud_starter", "subscription_state":"active", "stripe_customer_id":"cus_123", "stripe_subscription_id":"sub_123", "stripe_price_id":"price_123" }` assertJSONSnapshot(t, got, want) } func TestContract_HostedTenantEntitlementsFallbackToDefaultBillingState(t *testing.T) { baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) if _, err := mtp.GetPersistence("default"); err != nil { t.Fatalf("init default persistence: %v", err) } if _, err := mtp.GetPersistence("t-tenant"); err != nil { t.Fatalf("init tenant persistence: %v", err) } store := config.NewFileBillingStore(baseDir) if err := store.SaveBillingState("default", &entitlements.BillingState{ Capabilities: []string{pkglicensing.FeatureRelay, pkglicensing.FeatureRBAC}, Limits: map[string]int64{"max_monitored_systems": 50}, PlanVersion: "msp_starter", SubscriptionState: entitlements.SubStateActive, }); err != nil { t.Fatalf("save default billing state: %v", err) } handlers := NewLicenseHandlers(mtp, true) ctx := context.WithValue(context.Background(), OrgIDContextKey, "t-tenant") req := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx) rec := httptest.NewRecorder() handlers.HandleEntitlements(rec, req) if rec.Code != http.StatusOK { t.Fatalf("entitlements status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload EntitlementPayload if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode entitlements payload: %v", err) } if payload.SubscriptionState != string(pkglicensing.SubStateActive) { t.Fatalf("subscription_state=%q, want %q", payload.SubscriptionState, pkglicensing.SubStateActive) } if !sliceContainsString(payload.Capabilities, pkglicensing.FeatureRelay) { t.Fatalf("expected hosted tenant payload to include %q from default hosted billing state", pkglicensing.FeatureRelay) } foundMonitoredSystemLimit := false for _, limit := range payload.Limits { if limit.Key == pkglicensing.MaxMonitoredSystemsLicenseGateKey { foundMonitoredSystemLimit = true if limit.Limit != 50 { t.Fatalf("max_monitored_systems limit=%d, want 50", limit.Limit) } } } if !foundMonitoredSystemLimit { t.Fatalf("expected max_monitored_systems limit in payload, got %+v", payload.Limits) } } func TestContract_HostedTenantEntitlementRefreshFallsBackToDefaultBillingState(t *testing.T) { baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) if _, err := mtp.GetPersistence("default"); err != nil { t.Fatalf("init default persistence: %v", err) } if _, err := mtp.GetPersistence("t-tenant"); err != nil { t.Fatalf("init tenant persistence: %v", err) } pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("GenerateKey: %v", err) } t.Setenv(pkglicensing.TrialActivationPublicKeyEnvVar, base64.StdEncoding.EncodeToString(pub)) refreshServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/entitlements/refresh" { http.NotFound(w, r) return } var req hostedTrialLeaseRefreshRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode refresh request: %v", err) } if req.OrgID != "default" { t.Fatalf("req.OrgID=%q, want %q", req.OrgID, "default") } if req.InstanceHost != "pulse.example.com" { t.Fatalf("req.InstanceHost=%q, want %q", req.InstanceHost, "pulse.example.com") } if req.EntitlementRefreshToken != "etr_hosted_default" { t.Fatalf("req.EntitlementRefreshToken=%q, want %q", req.EntitlementRefreshToken, "etr_hosted_default") } entitlementJWT, err := pkglicensing.SignEntitlementLeaseToken(priv, pkglicensing.EntitlementLeaseClaims{ OrgID: "default", InstanceHost: "pulse.example.com", PlanVersion: "msp_starter", SubscriptionState: pkglicensing.SubStateActive, Capabilities: []string{ pkglicensing.FeatureRelay, pkglicensing.FeatureAIAutoFix, }, }) if err != nil { t.Fatalf("SignEntitlementLeaseToken: %v", err) } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(hostedTrialLeaseRefreshResponse{ EntitlementJWT: entitlementJWT, }) })) defer refreshServer.Close() store := config.NewFileBillingStore(baseDir) expiredLease, err := pkglicensing.SignEntitlementLeaseToken(priv, pkglicensing.EntitlementLeaseClaims{ OrgID: "default", InstanceHost: "pulse.example.com", PlanVersion: "msp_starter", SubscriptionState: pkglicensing.SubStateActive, Capabilities: []string{pkglicensing.FeatureRelay}, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), }, }) if err != nil { t.Fatalf("SignEntitlementLeaseToken(expired): %v", err) } if err := store.SaveBillingState("default", &entitlements.BillingState{ EntitlementJWT: expiredLease, EntitlementRefreshToken: "etr_hosted_default", }); err != nil { t.Fatalf("save default billing state: %v", err) } handlers := NewLicenseHandlers(mtp, true, &config.Config{ PublicURL: "https://pulse.example.com", ProTrialSignupURL: refreshServer.URL + "/start-pro-trial", }) refreshed, permanent, err := handlers.refreshHostedEntitlementLeaseOnce("t-tenant", nil) if err != nil { t.Fatalf("refreshHostedEntitlementLeaseOnce: %v", err) } if !refreshed || permanent { t.Fatalf("refreshed=%v permanent=%v, want refreshed=true permanent=false", refreshed, permanent) } state, err := store.GetBillingState("default") if err != nil { t.Fatalf("GetBillingState(default): %v", err) } if state == nil { t.Fatal("expected default billing state after hosted tenant refresh") } if state.SubscriptionState != entitlements.SubStateActive { t.Fatalf("subscription_state=%q, want %q", state.SubscriptionState, entitlements.SubStateActive) } if state.PlanVersion != "msp_starter" { t.Fatalf("plan_version=%q, want %q", state.PlanVersion, "msp_starter") } if !sliceContainsString(state.Capabilities, pkglicensing.FeatureAIAutoFix) { t.Fatalf("expected default hosted billing state to include %q after tenant refresh, got %v", pkglicensing.FeatureAIAutoFix, state.Capabilities) } } func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.T) { payload := buildEntitlementPayloadWithUsage(&licenseStatus{ Valid: true, Tier: pkglicensing.TierPro, Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), MaxMonitoredSystems: 15, }, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{ MonitoredSystems: 7, LegacyConnections: legacyConnectionCountsModel{ ProxmoxNodes: 2, DockerHosts: 1, KubernetesClusters: 1, }, }, nil) got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal entitlement payload: %v", err) } const want = `{ "capabilities":["update_alerts","sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","advanced_sso","rbac","audit_logging","advanced_reporting"], "limits":[{"key":"max_monitored_systems","limit":15,"current":7,"current_available":true,"state":"ok"}], "subscription_state":"active", "upgrade_reasons":[], "tier":"pro", "hosted_mode":false, "valid":true, "is_lifetime":false, "days_remaining":0, "trial_eligible":false, "max_history_days":90, "legacy_connections":{"proxmox_nodes":2,"docker_hosts":1,"kubernetes_clusters":1}, "has_migration_gap":false }` assertJSONSnapshot(t, got, want) } func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(t *testing.T) { payload := buildEntitlementPayloadWithUsage(&licenseStatus{ Valid: true, Tier: pkglicensing.TierPro, Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...), MaxMonitoredSystems: 15, MonitoredSystemContinuity: &pkglicensing.MonitoredSystemContinuityStatus{ PlanLimit: 15, EffectiveLimit: 15, CapturePending: true, }, }, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{ MonitoredSystemsUnavailableReason: "supplemental_inventory_unsettled", }, nil) got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal entitlement payload: %v", err) } const want = `{ "capabilities":["update_alerts","sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","advanced_sso","rbac","audit_logging","advanced_reporting"], "limits":[{"key":"max_monitored_systems","limit":15,"current":0,"current_available":false,"current_unavailable_reason":"supplemental_inventory_unsettled","state":"ok"}], "subscription_state":"active", "upgrade_reasons":[], "tier":"pro", "hosted_mode":false, "valid":true, "is_lifetime":false, "days_remaining":0, "trial_eligible":false, "max_history_days":90, "legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0}, "has_migration_gap":false, "monitored_system_continuity":{"plan_limit":15,"effective_limit":15,"capture_pending":true} }` assertJSONSnapshot(t, got, want) } func TestContract_EntitlementUsageSnapshotWaitsForSettledSupplementalInventory(t *testing.T) { provider := &contractSupplementalUsageProvider{} monitor := &monitoring.Monitor{} monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(nil)) monitor.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, provider) handlers := &LicenseHandlers{monitor: monitor} usage := handlers.entitlementUsageSnapshot(context.Background()) if usage.MonitoredSystemsAvailable { t.Fatalf("expected unsettled supplemental inventory to keep usage unavailable, got %+v", usage) } provider.settle(1) usage = handlers.entitlementUsageSnapshot(context.Background()) if usage.MonitoredSystemsAvailable { t.Fatalf("expected stale store freshness to keep usage unavailable, got %+v", usage) } monitor.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, provider) usage = handlers.entitlementUsageSnapshot(context.Background()) if !usage.MonitoredSystemsAvailable { t.Fatalf("expected usage to become available after canonical store rebuild, got %+v", usage) } if usage.MonitoredSystems != 1 { t.Fatalf("MonitoredSystems=%d, want 1", usage.MonitoredSystems) } } func TestContract_LegacyMigrationGrandfatherFloorJSONSnapshot(t *testing.T) { t.Setenv("PULSE_LICENSE_DEV_MODE", "false") grantJWT, grantPublicKey, err := pkglicensing.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{ LicenseID: "lic_contract_floor", Tier: "pro", PlanKey: "v5_pro_monthly_grandfathered", State: "active", Features: []string{"relay"}, MaxMonitoredSystems: 10, IssuedAt: time.Now().Unix(), ExpiresAt: time.Now().Add(72 * time.Hour).Unix(), Email: "contract-floor@example.com", }) if err != nil { t.Fatalf("generate grant jwt: %v", err) } pkglicensing.SetPublicKey(grantPublicKey) t.Cleanup(func() { pkglicensing.SetPublicKey(nil) }) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/licenses/exchange" { t.Fatalf("path = %q, want /v1/licenses/exchange", r.URL.Path) } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{ License: pkglicensing.ActivateResponseLicense{ LicenseID: "lic_contract_floor", State: "active", Tier: "pro", Features: []string{"relay"}, MaxMonitoredSystems: 10, }, Installation: pkglicensing.ActivateResponseInstallation{ InstallationID: "inst_contract_floor", InstallationToken: "pit_live_contract_floor", Status: "active", }, Grant: pkglicensing.GrantEnvelope{ JWT: grantJWT, JTI: "grant_contract_floor", ExpiresAt: time.Now().Add(72 * time.Hour).UTC().Format(time.RFC3339), }, }) })) defer server.Close() t.Setenv("PULSE_LICENSE_SERVER_URL", server.URL) baseDir := t.TempDir() mtp := config.NewMultiTenantPersistence(baseDir) cp, err := mtp.GetPersistence("default") if err != nil { t.Fatalf("init default persistence: %v", err) } persistence, err := pkglicensing.NewPersistence(cp.GetConfigDir()) if err != nil { t.Fatalf("new persistence: %v", err) } legacyJWT, err := pkglicensing.GenerateLicenseForTesting("contract-floor@example.com", pkglicensing.TierPro, 24*time.Hour) if err != nil { t.Fatalf("generate test license: %v", err) } if err := persistence.Save(legacyJWT); err != nil { t.Fatalf("save legacy jwt: %v", err) } handlers := NewLicenseHandlers(mtp, false) handlers.SetMonitors(buildGrandfatherFloorMonitor(23), nil) t.Cleanup(handlers.StopAllBackgroundLoops) ctx := context.WithValue(context.Background(), OrgIDContextKey, "default") statusReq := httptest.NewRequest(http.MethodGet, "/api/license/status", nil).WithContext(ctx) statusRec := httptest.NewRecorder() handlers.HandleLicenseStatus(statusRec, statusReq) if statusRec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", statusRec.Code, http.StatusOK, statusRec.Body.String()) } entReq := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx) entRec := httptest.NewRecorder() handlers.HandleEntitlements(entRec, entReq) if entRec.Code != http.StatusOK { t.Fatalf("entitlements status=%d, want %d: %s", entRec.Code, http.StatusOK, entRec.Body.String()) } var status pkglicensing.LicenseStatus if err := json.Unmarshal(statusRec.Body.Bytes(), &status); err != nil { t.Fatalf("decode status: %v", err) } var payload EntitlementPayload if err := json.Unmarshal(entRec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode entitlements: %v", err) } statusContinuity := status.MonitoredSystemContinuity if statusContinuity != nil { copied := *statusContinuity if copied.CapturedAt > 0 { copied.CapturedAt = 123 } statusContinuity = &copied } payloadContinuity := payload.MonitoredSystemContinuity if payloadContinuity != nil { copied := *payloadContinuity if copied.CapturedAt > 0 { copied.CapturedAt = 123 } payloadContinuity = &copied } got, err := json.Marshal(struct { Status struct { Tier pkglicensing.Tier `json:"tier"` PlanVersion string `json:"plan_version"` MaxMonitoredSystems int `json:"max_monitored_systems"` Valid bool `json:"valid"` MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"` } `json:"status"` Entitlements struct { Tier string `json:"tier"` PlanVersion string `json:"plan_version"` SubscriptionState string `json:"subscription_state"` Limits []pkglicensing.LimitStatus `json:"limits"` MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"` } `json:"entitlements"` }{ Status: struct { Tier pkglicensing.Tier `json:"tier"` PlanVersion string `json:"plan_version"` MaxMonitoredSystems int `json:"max_monitored_systems"` Valid bool `json:"valid"` MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"` }{ Tier: status.Tier, PlanVersion: status.PlanVersion, MaxMonitoredSystems: status.MaxMonitoredSystems, Valid: status.Valid, MonitoredSystemContinuity: statusContinuity, }, Entitlements: struct { Tier string `json:"tier"` PlanVersion string `json:"plan_version"` SubscriptionState string `json:"subscription_state"` Limits []pkglicensing.LimitStatus `json:"limits"` MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"` }{ Tier: payload.Tier, PlanVersion: payload.PlanVersion, SubscriptionState: payload.SubscriptionState, Limits: payload.Limits, MonitoredSystemContinuity: payloadContinuity, }, }) if err != nil { t.Fatalf("marshal snapshot payload: %v", err) } const want = `{ "status":{ "tier":"pro", "plan_version":"v5_pro_monthly_grandfathered", "max_monitored_systems":23, "valid":true, "monitored_system_continuity":{"plan_limit":10,"grandfathered_floor":23,"effective_limit":23,"capture_pending":false,"captured_at":123} }, "entitlements":{ "tier":"pro", "plan_version":"v5_pro_monthly_grandfathered", "subscription_state":"active", "limits":[{"key":"max_monitored_systems","limit":23,"current":23,"current_available":true,"state":"enforced"}], "monitored_system_continuity":{"plan_limit":10,"grandfathered_floor":23,"effective_limit":23,"capture_pending":false,"captured_at":123} } }` assertJSONSnapshot(t, got, want) } func TestContract_HostedBillingStateFallbackJSONSnapshot(t *testing.T) { baseDir := t.TempDir() store := config.NewFileBillingStore(baseDir) if err := store.SaveBillingState("default", &entitlements.BillingState{ Capabilities: []string{pkglicensing.FeatureRelay, pkglicensing.FeatureRBAC}, Limits: map[string]int64{"max_monitored_systems": 50}, MetersEnabled: []string{}, PlanVersion: "msp_starter", SubscriptionState: entitlements.SubStateActive, StripeCustomerID: "cus_hosted", StripeSubscriptionID: "sub_hosted", StripePriceID: "price_hosted", }); err != nil { t.Fatalf("save default billing state: %v", err) } handlers := NewBillingStateHandlers(store, true) req := httptest.NewRequest(http.MethodGet, "/api/admin/orgs/t-tenant/billing-state", nil) req.SetPathValue("id", "t-tenant") rec := httptest.NewRecorder() handlers.HandleGetBillingState(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "capabilities":["relay","rbac"], "limits":{"max_monitored_systems":50}, "meters_enabled":[], "plan_version":"msp_starter", "subscription_state":"active", "stripe_customer_id":"cus_hosted", "stripe_subscription_id":"sub_hosted", "stripe_price_id":"price_hosted" }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_DemoModeCommercialSurfacePolicy(t *testing.T) { t.Run("hidden routes return not found", func(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatalf("hidden commercial route should not reach downstream handler: %s %s", r.Method, r.URL.Path) }) handler := DemoModeMiddleware(&config.Config{DemoMode: true}, next) testCases := []struct { method string path string }{ {method: http.MethodGet, path: "/api/license/status"}, {method: http.MethodGet, path: "/api/license/features"}, {method: http.MethodGet, path: "/api/license/commercial-posture"}, {method: http.MethodGet, path: "/api/license/entitlements"}, {method: http.MethodPost, path: "/api/license/activate"}, {method: http.MethodPost, path: "/api/license/clear"}, {method: http.MethodPost, path: "/api/license/trial/start"}, {method: http.MethodGet, path: "/api/license/monitored-system-ledger"}, {method: http.MethodGet, path: "/api/admin/orgs/t-tenant/billing-state"}, {method: http.MethodPut, path: "/api/admin/orgs/t-tenant/billing-state"}, {method: http.MethodGet, path: "/api/upgrade-metrics/stats"}, {method: http.MethodPost, path: "/api/upgrade-metrics/events"}, {method: http.MethodGet, path: licensePurchaseStartPath}, {method: http.MethodGet, path: licensePurchaseHandoffPath}, {method: http.MethodGet, path: "/auth/trial-activate"}, } for _, tc := range testCases { req := httptest.NewRequest(tc.method, tc.path, nil) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("%s %s status=%d, want %d: %s", tc.method, tc.path, rec.Code, http.StatusNotFound, rec.Body.String()) } } }) t.Run("runtime capabilities stay available but sanitize public demo limit details", func(t *testing.T) { t.Setenv("PULSE_LICENSE_DEV_MODE", "true") handlers := createTestHandler(t) handlers.SetConfig(&config.Config{DemoMode: true}) licenseKey, err := pkglicensing.GenerateLicenseForTesting("contract-demo@example.com", pkglicensing.TierPro, 24*time.Hour) if err != nil { t.Fatalf("GenerateLicenseForTesting: %v", err) } if _, err := handlers.Service(context.Background()).Activate(licenseKey); err != nil { t.Fatalf("Activate() error = %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/license/runtime-capabilities", nil) rec := httptest.NewRecorder() handlers.HandleRuntimeCapabilities(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } var payload RuntimeCapabilitiesPayload if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode payload: %v", err) } if len(payload.Capabilities) == 0 { t.Fatalf("expected sanitized runtime capabilities to preserve capabilities, got %+v", payload) } for _, limit := range payload.Limits { if limit.Limit != 0 || limit.Current != 0 || limit.State != "ok" { t.Fatalf("sanitized limit=%+v, want limit=0 current=0 state=ok", limit) } } }) } func TestContract_SelfHostedPurchaseHandoffJSONSnapshot(t *testing.T) { handler := createTestHandler(t) handler.SetConfig(&config.Config{PublicURL: "https://pulse.example.com"}) var capturedReq struct { Feature string `json:"feature"` SuccessURL string `json:"success_url"` CancelURL string `json:"cancel_url"` PurchaseReturnJTI string `json:"purchase_return_jti"` } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/v1/checkout/portal-handoff" { t.Fatalf("unexpected path %q", r.URL.Path) } if err := json.NewDecoder(r.Body).Decode(&capturedReq); err != nil { t.Fatalf("decode checkout portal handoff request: %v", err) } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "portal_handoff_id": "cph_placeholder", "feature": capturedReq.Feature, }) })) defer server.Close() t.Setenv("PULSE_LICENSE_SERVER_URL", server.URL) req := httptest.NewRequest( http.MethodGet, "https://pulse.example.com"+licensePurchaseStartPath+ "?feature=max_monitored_systems&utm_content=legacy-bookmark", nil, ) rec := httptest.NewRecorder() handler.HandleCheckoutStart(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusSeeOther, rec.Body.String()) } location := rec.Header().Get("Location") redirectURL, err := url.Parse(location) if err != nil { t.Fatalf("parse redirect location: %v", err) } if redirectURL.Scheme != "https" || redirectURL.Host != "cloud.pulserelay.pro" || redirectURL.Path != "/portal" { t.Fatalf("redirect location=%q, want Pulse Account portal", location) } if got := redirectURL.Query().Get("feature"); got != "" { t.Fatalf("feature=%q, want omitted portal query", got) } if got := redirectURL.Query().Get("return_url"); got != "" { t.Fatalf("return_url=%q, want omitted portal query", got) } if got := redirectURL.Query().Get("purchase_return_token"); got != "" { t.Fatalf("purchase_return_token=%q, want omitted portal query", got) } if got := redirectURL.Query().Get("purchase_handoff_url"); got != "" { t.Fatalf("purchase_handoff_url=%q, want omitted portal query", got) } if got := redirectURL.Query().Get("portal_handoff_id"); got != "cph_placeholder" { t.Fatalf("portal_handoff_id=%q, want cph_placeholder", got) } activationURL, err := url.Parse(capturedReq.SuccessURL) if err != nil { t.Fatalf("parse success_url: %v", err) } returnToken := strings.TrimSpace(activationURL.Query().Get(licensePurchaseReturnTokenField)) if returnToken == "" { t.Fatal("expected signed purchase_return_token in success_url") } signingKey, err := handler.purchaseReturnSigningKey() if err != nil { t.Fatalf("purchaseReturnSigningKey: %v", err) } claims, err := verifyPurchaseReturnTokenFromLicensing(returnToken, signingKey, "pulse.example.com", time.Now().UTC()) if err != nil { t.Fatalf("verifyPurchaseReturnTokenFromLicensing: %v", err) } if claims.Feature != "max_monitored_systems" { t.Fatalf("claims.Feature=%q, want max_monitored_systems", claims.Feature) } if claims.OrgID != "default" { t.Fatalf("claims.OrgID=%q, want default", claims.OrgID) } if capturedReq.PurchaseReturnJTI != claims.ID { t.Fatalf("purchase_return_jti=%q, want %q", capturedReq.PurchaseReturnJTI, claims.ID) } activationQuery := activationURL.Query() activationQuery.Set(licensePurchaseReturnTokenField, "placeholder") activationURL.RawQuery = activationQuery.Encode() got, err := json.Marshal(struct { PortalUtmContent string `json:"portal_utm_content"` PortalHandoffID string `json:"portal_handoff_id"` IntentFeature string `json:"intent_feature"` PurchaseReturnJTI string `json:"purchase_return_jti"` SuccessURLTemplate string `json:"success_url_template"` CancelURL string `json:"cancel_url"` }{ PortalUtmContent: redirectURL.Query().Get("utm_content"), PortalHandoffID: redirectURL.Query().Get("portal_handoff_id"), IntentFeature: capturedReq.Feature, PurchaseReturnJTI: "placeholder", SuccessURLTemplate: activationURL.String(), CancelURL: capturedReq.CancelURL, }) if err != nil { t.Fatalf("marshal normalized handoff snapshot: %v", err) } const want = `{ "portal_utm_content":"legacy-bookmark", "portal_handoff_id":"cph_placeholder", "intent_feature":"max_monitored_systems", "purchase_return_jti":"placeholder", "success_url_template":"https://pulse.example.com/auth/license-purchase-activate?purchase_return_token=placeholder\u0026session_id=%7BCHECKOUT_SESSION_ID%7D", "cancel_url":"https://pulse.example.com/settings/system/billing/plan?intent=max_monitored_systems\u0026purchase=cancelled" }` assertJSONSnapshot(t, got, want) } func TestContract_SelfHostedPurchaseLegacyHandoffJSONSnapshot(t *testing.T) { handler := createTestHandler(t) handler.SetConfig(&config.Config{PublicURL: "https://pulse.example.com"}) returnToken := issuePurchaseReturnToken(t, handler, "default", "max_monitored_systems") req := httptest.NewRequest( http.MethodGet, "https://pulse.example.com"+licensePurchaseHandoffPath+ "?purchase_return_token="+url.QueryEscape(returnToken), nil, ) rec := httptest.NewRecorder() handler.HandleCheckoutHandoff(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } if got := rec.Header().Get("Access-Control-Allow-Origin"); got != "*" { t.Fatalf("allow-origin=%q, want *", got) } if got := rec.Header().Get("Cache-Control"); got != "no-store" { t.Fatalf("cache-control=%q, want no-store", got) } var payload struct { Feature string `json:"feature"` ActivationURLTemplate string `json:"activation_url_template"` } if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil { t.Fatalf("decode payload: %v", err) } wantTemplate, err := licensePurchaseActivationTemplateURL( "https://pulse.example.com"+licensePurchaseActivationPath, returnToken, ) if err != nil { t.Fatalf("licensePurchaseActivationTemplateURL: %v", err) } if payload.Feature != "max_monitored_systems" { t.Fatalf("feature=%q, want max_monitored_systems", payload.Feature) } if payload.ActivationURLTemplate != wantTemplate { t.Fatalf("activation_url_template=%q, want %q", payload.ActivationURLTemplate, wantTemplate) } } func TestContract_HandoffExchangeJSONSnapshot(t *testing.T) { key := []byte("test-handoff-key") configDir := t.TempDir() secretsDir := filepath.Join(configDir, "secrets") if err := os.MkdirAll(secretsDir, 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } if err := os.WriteFile(filepath.Join(secretsDir, "handoff.key"), key, 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } handler := HandleHandoffExchange(configDir) tenantID := "tenant-contract" t.Setenv("PULSE_TENANT_ID", "") token := signHandoffToken(t, key, cloudHandoffClaims{ AccountID: "acct-contract", Email: "Operator.Owner+Mixed@PulseRelay.Pro", Role: "owner", RegisteredClaims: jwt.RegisteredClaims{ ID: "jti-contract", Subject: "user-contract", Issuer: cloudHandoffIssuer, Audience: jwt.ClaimStrings{tenantID}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, }) req := httptest.NewRequest(http.MethodPost, "/api/cloud/handoff/exchange?token="+token+"&format=json", nil) req.Host = tenantID + ".cloud.pulserelay.pro" req.Header.Set("Accept", "application/json") req.Header.Set("X-Forwarded-Host", req.Host) req.Header.Set("X-Forwarded-For", "127.0.0.1") req.RemoteAddr = "127.0.0.1:1234" rec := httptest.NewRecorder() handler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } const want = `{ "account_id":"acct-contract", "email":"operator.owner+mixed@pulserelay.pro", "exp":"placeholder", "jti":"jti-contract", "ok":true, "role":"owner", "tenant_id":"tenant-contract", "user_id":"user-contract" }` var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode handoff payload: %v", err) } if _, ok := payload["exp"].(string); !ok { t.Fatalf("exp missing or not a string: %+v", payload) } payload["exp"] = "placeholder" got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal normalized handoff payload: %v", err) } assertJSONSnapshot(t, got, want) } func TestContract_TenantAIServiceAvoidsSnapshotProviderBridge(t *testing.T) { tmp := t.TempDir() mtp := config.NewMultiTenantPersistence(tmp) defaultMonitor, _, _ := newTestMonitor(t) tenantAdapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil)) tenantMonitor := &monitoring.Monitor{} tenantMonitor.SetResourceStore(tenantAdapter) mtm := &monitoring.MultiTenantMonitor{} setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{ "default": defaultMonitor, "tenant-1": tenantMonitor, }) handler := NewAISettingsHandler(mtp, mtm, nil) handler.SetStateProvider(defaultMonitor) ctx := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1") svc := handler.GetAIService(ctx) if svc == nil { t.Fatal("expected tenant AI service") } if svc.GetStateProvider() != nil { t.Fatal("expected tenant AI service to avoid snapshot provider bridge") } if svc.GetPatrolService() == nil { t.Fatal("expected tenant patrol service to initialize from canonical providers") } } func TestContract_HostedCloudHandoffEnsuresTenantOrganizationMembership(t *testing.T) { key := []byte("test-handoff-key") configDir := t.TempDir() resetSessionStoreForTests() t.Cleanup(resetSessionStoreForTests) resetCSRFStoreForTests() t.Cleanup(resetCSRFStoreForTests) InitSessionStore(configDir) InitCSRFStore(configDir) secretsDir := filepath.Join(configDir, "secrets") if err := os.MkdirAll(secretsDir, 0o755); err != nil { t.Fatalf("mkdir secrets dir: %v", err) } if err := os.WriteFile(filepath.Join(secretsDir, "handoff.key"), key, 0o600); err != nil { t.Fatalf("write handoff key: %v", err) } tenantID := "tenant-contract-membership" mtp := config.NewMultiTenantPersistence(configDir) if err := mtp.SaveOrganization(&models.Organization{ ID: tenantID, DisplayName: "Contract Membership", Status: models.OrgStatusActive, CreatedAt: time.Now().UTC(), OwnerUserID: "legacy-owner@example.com", Members: []models.OrganizationMember{ {UserID: "legacy-owner@example.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()}, }, }); err != nil { t.Fatalf("save organization: %v", err) } token := signHandoffToken(t, key, cloudHandoffClaims{ AccountID: "acct-contract-membership", Email: "courtmanr@gmail.com", Role: "owner", RegisteredClaims: jwt.RegisteredClaims{ ID: "jti-contract-membership", Subject: "user-contract-membership", Issuer: cloudHandoffIssuer, Audience: jwt.ClaimStrings{tenantID}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, }) form := url.Values{} form.Set("token", token) t.Setenv("PULSE_HOSTED_MODE", "true") t.Setenv("PULSE_TENANT_ID", "") t.Setenv("PULSE_PUBLIC_URL", "") req := httptest.NewRequest(http.MethodPost, "/api/cloud/handoff/exchange?format=json", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.Host = tenantID + ".cloud.pulserelay.pro" req.RemoteAddr = "198.51.100.20:1234" rec := httptest.NewRecorder() HandleHandoffExchange(configDir).ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } org, err := mtp.LoadOrganization(tenantID) if err != nil { t.Fatalf("load organization: %v", err) } if org.OwnerUserID != "legacy-owner@example.com" { t.Fatalf("ownerUserID=%q, want %q", org.OwnerUserID, "legacy-owner@example.com") } if got := org.GetMemberRole("courtmanr@gmail.com"); got != models.OrgRoleOwner { t.Fatalf("member role=%q, want %q", got, models.OrgRoleOwner) } } func TestContract_HostedDirectCloudHandoffPreservesMembershipClaims(t *testing.T) { key := []byte("test-direct-handoff-key") configDir := t.TempDir() resetPersistentAuthStoresForTests() t.Cleanup(resetPersistentAuthStoresForTests) if err := os.WriteFile(filepath.Join(configDir, cloudauth.HandoffKeyFile), key, 0o600); err != nil { t.Fatalf("write direct handoff key: %v", err) } tenantID := "tenant-direct-contract" mtp := config.NewMultiTenantPersistence(configDir) if err := mtp.SaveOrganization(&models.Organization{ ID: tenantID, DisplayName: "Direct Contract Membership", Status: models.OrgStatusActive, CreatedAt: time.Now().UTC(), OwnerUserID: "legacy-owner@example.com", Members: []models.OrganizationMember{ {UserID: "legacy-owner@example.com", Role: models.OrgRoleOwner, AddedAt: time.Now().UTC()}, }, }); err != nil { t.Fatalf("save organization: %v", err) } token, err := cloudauth.SignWithClaims(key, cloudauth.Claims{ Email: "courtmanr@gmail.com", TenantID: tenantID, AccountID: "acct-direct-contract", UserID: "user-direct-contract", Role: "owner", }, time.Hour) if err != nil { t.Fatalf("sign direct handoff claims: %v", err) } req := httptest.NewRequest(http.MethodGet, "/auth/cloud-handoff?token="+url.QueryEscape(token), nil) rec := httptest.NewRecorder() HandleCloudHandoff(configDir).ServeHTTP(rec, req) if rec.Code != http.StatusTemporaryRedirect { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusTemporaryRedirect, rec.Body.String()) } if got := rec.Header().Get("Location"); got != "/" { t.Fatalf("redirect=%q, want %q", got, "/") } org, err := mtp.LoadOrganization(tenantID) if err != nil { t.Fatalf("load organization: %v", err) } if org.OwnerUserID != "legacy-owner@example.com" { t.Fatalf("ownerUserID=%q, want %q", org.OwnerUserID, "legacy-owner@example.com") } if got := org.GetMemberRole("courtmanr@gmail.com"); got != models.OrgRoleOwner { t.Fatalf("member role=%q, want %q", got, models.OrgRoleOwner) } } func TestContract_APITokenDeleteRejectsScopeEscalation(t *testing.T) { router := &Router{ config: &config.Config{ APITokens: []config.APITokenRecord{ { ID: "broad-token", Name: "broad", Hash: "hash-broad", CreatedAt: time.Now().Add(-time.Hour), Scopes: []string{config.ScopeWildcard}, OrgID: "default", }, }, }, } caller, err := config.NewAPITokenRecord( "limited-caller-token-123.12345678", "limited", []string{config.ScopeSettingsWrite}, ) if err != nil { t.Fatalf("NewAPITokenRecord: %v", err) } req := httptest.NewRequest(http.MethodDelete, "/api/security/tokens/broad-token", nil) req = req.WithContext(authpkg.WithAPIToken(req.Context(), caller)) rec := httptest.NewRecorder() router.handleDeleteAPIToken(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden) } if !strings.Contains(rec.Body.String(), `Cannot delete token with scope "*"`) { t.Fatalf("expected delete scope-escalation contract message, got %q", rec.Body.String()) } if len(router.config.APITokens) != 1 || router.config.APITokens[0].ID != "broad-token" { t.Fatalf("expected broader token to remain configured, got %+v", router.config.APITokens) } } func TestContract_OnboardingQRResponseJSONSnapshot(t *testing.T) { payload := onboardingQRResponse{ Schema: onboardingSchemaVersion, InstanceURL: "https://pulse.example.test", InstanceID: "relay_abc123", Relay: onboardingRelayDetails{ Enabled: true, URL: "wss://relay.example.test/ws/app", IdentityFingerprint: "AA:BB:CC", IdentityPublicKey: "base64-key", }, AuthToken: "token-123", DeepLink: "pulse://connect?schema=pulse-mobile-onboarding-v1&instance_url=https%3A%2F%2Fpulse.example.test&instance_id=relay_abc123&relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp&auth_token=token-123&identity_fingerprint=AA%3ABB%3ACC&identity_public_key=base64-key", }.normalizeCollections() got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal onboarding qr response: %v", err) } const want = `{ "schema":"pulse-mobile-onboarding-v1", "instance_url":"https://pulse.example.test", "instance_id":"relay_abc123", "relay":{"enabled":true,"url":"wss://relay.example.test/ws/app","identity_fingerprint":"AA:BB:CC","identity_public_key":"base64-key"}, "auth_token":"token-123", "deep_link":"pulse://connect?schema=pulse-mobile-onboarding-v1\u0026instance_url=https%3A%2F%2Fpulse.example.test\u0026instance_id=relay_abc123\u0026relay_url=wss%3A%2F%2Frelay.example.test%2Fws%2Fapp\u0026auth_token=token-123\u0026identity_fingerprint=AA%3ABB%3ACC\u0026identity_public_key=base64-key", "diagnostics":[] }` assertJSONSnapshot(t, got, want) } func TestContract_HostedRelayConfigResponseJSONSnapshot(t *testing.T) { router, _, instanceHost := newHostedRelayRuntimeTestRouter(t) req := httptest.NewRequest(http.MethodGet, "/api/settings/relay", nil) rec := httptest.NewRecorder() router.handleGetRelayConfig(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String()) } cfg, err := router.loadRelayConfigForRuntime(context.Background()) if err != nil { t.Fatalf("loadRelayConfigForRuntime() error = %v", err) } body := rec.Body.String() if strings.Contains(body, instanceHost) { t.Fatalf("relay config response leaked hosted instance secret %q: %s", instanceHost, body) } if strings.Contains(body, cfg.IdentityPrivateKey) { t.Fatalf("relay config response leaked identity private key: %s", body) } want := fmt.Sprintf(`{ "enabled":true, "server_url":"%s", "identity_public_key":"%s", "identity_fingerprint":"%s" }`, relay.DefaultServerURL, cfg.IdentityPublicKey, cfg.IdentityFingerprint) assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_UpdatePlanManualFallbackJSONSnapshot(t *testing.T) { payload := updates.UpdatePlan{ CanAutoUpdate: false, RequiresRoot: false, RollbackSupport: true, EstimatedTime: "5-10 minutes", Instructions: []string{ "Check out or build Pulse 6.0.0-rc.1 in your development workspace.", "Stop the current development instance.", "Restart Pulse with the rebuilt binary or release artifact against the existing data directory.", }, Prerequisites: []string{ "A local development workspace for Pulse", "Build tooling for the target version", "A backup of the active data directory before replacing the binary", }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal update plan: %v", err) } const want = `{ "canAutoUpdate":false, "instructions":["Check out or build Pulse 6.0.0-rc.1 in your development workspace.","Stop the current development instance.","Restart Pulse with the rebuilt binary or release artifact against the existing data directory."], "prerequisites":["A local development workspace for Pulse","Build tooling for the target version","A backup of the active data directory before replacing the binary"], "estimatedTime":"5-10 minutes", "requiresRoot":false, "rollbackSupport":true }` assertJSONSnapshot(t, got, want) } func TestContract_EmptyUpdatePlanJSONSnapshot(t *testing.T) { payload := updates.EmptyUpdatePlan() got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal empty update plan: %v", err) } const want = `{ "canAutoUpdate":false, "instructions":[], "prerequisites":[], "requiresRoot":false, "rollbackSupport":false }` assertJSONSnapshot(t, got, want) } func TestContract_APITokenDTOJSONSnapshot(t *testing.T) { now := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) lastUsed := now.Add(30 * time.Minute) expires := now.Add(24 * time.Hour) payload := apiTokenDTO{ ID: "token-1", Name: "Deploy token", Prefix: "pulse_", Suffix: "1234", CreatedAt: now, LastUsedAt: &lastUsed, ExpiresAt: &expires, Scopes: []string{"monitoring:read", "settings:write"}, OwnerUserID: "owner@example.com", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal API token dto: %v", err) } const want = `{ "id":"token-1", "name":"Deploy token", "prefix":"pulse_", "suffix":"1234", "createdAt":"2026-02-08T13:14:15Z", "lastUsedAt":"2026-02-08T13:44:15Z", "expiresAt":"2026-02-09T13:14:15Z", "scopes":["monitoring:read","settings:write"], "ownerUserId":"owner@example.com" }` assertJSONSnapshot(t, got, want) } func TestContract_APITokenScopeAliasNormalization(t *testing.T) { raw := []string{"host-agent:report", "host-agent:config:read", "host-agent:manage", "host-agent:enroll"} got, err := normalizeRequestedScopes(&raw) if err != nil { t.Fatalf("normalize requested scopes: %v", err) } want := []string{ config.ScopeAgentConfigRead, config.ScopeAgentEnroll, config.ScopeAgentManage, config.ScopeAgentReport, } if !reflect.DeepEqual(got, want) { t.Fatalf("normalized scopes = %#v, want %#v", got, want) } for _, legacy := range raw { if strings.HasPrefix(legacy, "agent:") { t.Fatalf("expected legacy alias input, got canonical scope %q", legacy) } } } func TestContract_HostedSubscriptionRequiredErrorJSONSnapshot(t *testing.T) { rec := httptest.NewRecorder() writeHostedSubscriptionRequiredError(rec) if rec.Code != http.StatusPaymentRequired { t.Fatalf("status = %d, want %d", rec.Code, http.StatusPaymentRequired) } const want = `{ "error":"subscription_required", "message":"Your Cloud subscription is not active. Please check your billing status." }` assertJSONSnapshot(t, rec.Body.Bytes(), want) } func TestContract_InstallScriptReleaseAssetURL(t *testing.T) { router := &Router{serverVersion: "v6.0.0-rc.1"} got, err := router.installScriptReleaseAssetURL("install.sh") if err != nil { t.Fatalf("install script release asset URL: %v", err) } const want = "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/install.sh" if got != want { t.Fatalf("install script release asset URL = %q, want %q", got, want) } } func TestContract_InstallScriptReleaseAssetURLUsesConfiguredRepo(t *testing.T) { t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork") router := &Router{serverVersion: "v6.0.0-rc.1"} got, err := router.installScriptReleaseAssetURL("install.sh") if err != nil { t.Fatalf("install script release asset URL: %v", err) } const want = "https://github.com/example/pulse-fork/releases/download/v6.0.0-rc.1/install.sh" if got != want { t.Fatalf("install script release asset URL = %q, want %q", got, want) } } func TestContract_InstallScriptReleaseAssetURLRejectsUnreleasedBuild(t *testing.T) { router := &Router{serverVersion: "dev"} if _, err := router.installScriptReleaseAssetURL("install.sh"); err == nil { t.Fatalf("expected development build to reject release asset lookup") } } func TestContract_InstallScriptReleaseAssetURLRejectsDevPrereleaseBuild(t *testing.T) { router := &Router{serverVersion: "v6.0.0-dev"} if _, err := router.installScriptReleaseAssetURL("install.sh"); err == nil { t.Fatalf("expected dev prerelease build to reject release asset lookup") } } func TestContract_ProxmoxInstallCommandIncludesInsecureForPlainHTTP(t *testing.T) { got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{ BaseURL: "http://pulse.example.com:7655/", Token: "token-123", InstallType: "pve", IncludeInstallType: true, }) if !strings.Contains(got, "--url "+posixShellQuote("http://pulse.example.com:7655")) { t.Fatalf("install command missing canonical base URL: %s", got) } if !strings.Contains(got, "--insecure") { t.Fatalf("install command missing insecure flag for plain HTTP Pulse URL: %s", got) } } func TestContract_ProxmoxInstallCommandUsesPrivilegeEscalationWrapper(t *testing.T) { got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{ BaseURL: "https://pulse.example.com/", Token: "token-123", InstallType: "pve", IncludeInstallType: true, }) if !strings.Contains(got, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`) { t.Fatalf("install command missing root-or-sudo wrapper: %s", got) } if !strings.Contains(got, `sudo bash -s --`) { t.Fatalf("install command missing sudo fallback: %s", got) } if strings.Contains(got, "| bash -s -- --url") { t.Fatalf("install command preserved raw bash pipe instead of governed wrapper: %s", got) } } func TestContract_OptionalAuthProxmoxInstallCommandOmitsToken(t *testing.T) { got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{ BaseURL: "https://pulse.example.com/", Token: "", InstallType: "pve", IncludeInstallType: true, }) if strings.Contains(got, "--token") { t.Fatalf("optional-auth install command preserved token flag: %s", got) } if !strings.Contains(got, "--url "+posixShellQuote("https://pulse.example.com")) { t.Fatalf("optional-auth install command missing canonical base URL: %s", got) } } func TestContract_ProxmoxInstallCommandNormalizesTrailingSlashBaseURL(t *testing.T) { got := buildProxmoxAgentInstallCommand(agentInstallCommandOptions{ BaseURL: "https://pulse.example.com/base///", Token: "token-123", InstallType: "pbs", IncludeInstallType: true, }) if !strings.Contains(got, posixShellQuote("https://pulse.example.com/base/install.sh")) { t.Fatalf("install command missing normalized install script URL: %s", got) } if !strings.Contains(got, "--url "+posixShellQuote("https://pulse.example.com/base")) { t.Fatalf("install command missing normalized base URL: %s", got) } if strings.Contains(got, "//install.sh") { t.Fatalf("install command preserved double-slash install path: %s", got) } } func TestContract_SystemSettingsResponseJSONSnapshot(t *testing.T) { payload := EmptySystemSettingsResponse() payload.SystemSettings = config.SystemSettings{ PVEPollingInterval: 30, PBSPollingInterval: 60, PMGPollingInterval: 60, BackupPollingInterval: 3600, UpdateChannel: "rc", AutoUpdateEnabled: false, AutoUpdateCheckInterval: 24, AutoUpdateTime: "03:00", DiscoveryEnabled: true, DiscoverySubnet: "10.0.0.0/24", DiscoveryConfig: config.DefaultDiscoveryConfig(), Theme: "dark", TemperatureMonitoringEnabled: true, DisableDockerUpdateActions: true, } payload.EnvOverrides = map[string]bool{ "PULSE_TELEMETRY": true, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal system settings response: %v", err) } const want = `{ "pvePollingInterval":30, "pbsPollingInterval":60, "pmgPollingInterval":60, "backupPollingInterval":3600, "updateChannel":"rc", "autoUpdateEnabled":false, "autoUpdateCheckInterval":24, "autoUpdateTime":"03:00", "discoveryEnabled":true, "discoverySubnet":"10.0.0.0/24", "discoveryConfig":{ "environment_override":"auto", "subnet_blocklist":["169.254.0.0/16"], "max_hosts_per_scan":1024, "max_concurrent":50, "enable_reverse_dns":true, "scan_gateways":true, "dial_timeout_ms":1000, "http_timeout_ms":2000 }, "theme":"dark", "fullWidthMode":false, "allowEmbedding":false, "temperatureMonitoringEnabled":true, "hideLocalLogin":false, "disableDockerUpdateActions":true, "envOverrides":{"PULSE_TELEMETRY":true} }` assertJSONSnapshot(t, got, want) } func TestContract_CachedDiscoveryResponseJSONSnapshot(t *testing.T) { response := map[string]interface{}{ "servers": []map[string]interface{}{ { "ip": "10.0.0.1", "port": 8006, "type": "pve", }, }, "errors": []string{ "Docker bridge network [10.0.0.2:8007]: request timed out", }, "structured_errors": []map[string]interface{}{ { "ip": "10.0.0.2", "port": 8007, "phase": "docker_bridge_network", "error_type": "timeout", "message": "request timed out", "timestamp": "2023-11-14T22:13:20Z", }, }, "environment": nil, "cached": true, "updated": int64(1700000010), "age": float64(0), } got, err := json.Marshal(response) if err != nil { t.Fatalf("marshal cached discovery response: %v", err) } const want = `{ "age":0, "cached":true, "environment":null, "errors":["Docker bridge network [10.0.0.2:8007]: request timed out"], "servers":[{"ip":"10.0.0.1","port":8006,"type":"pve"}], "structured_errors":[{"error_type":"timeout","ip":"10.0.0.2","message":"request timed out","phase":"docker_bridge_network","port":8007,"timestamp":"2023-11-14T22:13:20Z"}], "updated":1700000010 }` var wantValue interface{} if err := json.Unmarshal([]byte(want), &wantValue); err != nil { t.Fatalf("unmarshal wanted discovery response: %v", err) } var gotValue interface{} if err := json.Unmarshal(got, &gotValue); err != nil { t.Fatalf("unmarshal got discovery response: %v", err) } if !reflect.DeepEqual(gotValue, wantValue) { t.Fatalf("cached discovery response mismatch\nwant: %s\ngot: %s", want, string(got)) } } func TestContract_AutoRegisterRequestJSONSnapshot(t *testing.T) { payload := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", ServerName: "pve-node-1", AuthToken: "setup-token-123", Source: "agent", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal auto-register request: %v", err) } const want = `{ "type":"pve", "host":"https://pve.local:8006", "tokenId":"pulse-monitor@pve!pulse-homelab", "tokenValue":"secret-token", "serverName":"pve-node-1", "authToken":"setup-token-123", "source":"agent" }` assertJSONSnapshot(t, got, want) } func TestContract_AutoRegisterScriptRequestJSONSnapshot(t *testing.T) { payload := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", ServerName: "pve-node-1", AuthToken: "setup-token-123", Source: "script", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal script auto-register request: %v", err) } const want = `{ "type":"pve", "host":"https://pve.local:8006", "tokenId":"pulse-monitor@pve!pulse-homelab", "tokenValue":"secret-token", "serverName":"pve-node-1", "authToken":"setup-token-123", "source":"script" }` assertJSONSnapshot(t, got, want) } func TestContract_AutoRegisterCheckRequestJSONSnapshot(t *testing.T) { payload := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", CandidateHosts: []string{"https://pve.local:8006", "https://10.0.0.5:8006"}, ServerName: "pve-node-1", AuthToken: "setup-token-123", Source: "agent", CheckRegistration: true, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal auto-register check request: %v", err) } const want = `{ "type":"pve", "host":"https://pve.local:8006", "candidateHosts":["https://pve.local:8006","https://10.0.0.5:8006"], "serverName":"pve-node-1", "authToken":"setup-token-123", "source":"agent", "checkRegistration":true }` assertJSONSnapshot(t, got, want) } func TestContract_AutoRegisterScriptRequestRequiresExplicitSourceMarker(t *testing.T) { payload := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", ServerName: "pve-node-1", AuthToken: "setup-token-123", Source: "script", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal script auto-register request: %v", err) } var decoded map[string]any if err := json.Unmarshal(got, &decoded); err != nil { t.Fatalf("decode script auto-register request: %v", err) } if decoded["source"] != "script" { t.Fatalf("source = %#v, want explicit script marker", decoded["source"]) } } func TestContract_CanonicalAutoRegisterRequiresExplicitServerName(t *testing.T) { payload := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", AuthToken: "setup-token-123", Source: "script", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal serverName-free auto-register request: %v", err) } var decoded map[string]any if err := json.Unmarshal(got, &decoded); err != nil { t.Fatalf("decode serverName-free auto-register request: %v", err) } if _, ok := decoded["serverName"]; ok { t.Fatalf("serverName = %#v, want omitted when caller does not send it", decoded["serverName"]) } } func TestContract_CanonicalAutoRegisterSetupTokenAuthFailureText(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handler := newTestConfigHandlers(t, cfg) requestBody := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", ServerName: "pve-node-1", Source: "script", } missingAuthJSON, err := json.Marshal(requestBody) if err != nil { t.Fatalf("marshal missing-auth auto-register request: %v", err) } missingReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(missingAuthJSON)) missingRec := httptest.NewRecorder() handler.HandleAutoRegister(missingRec, missingReq) if missingRec.Code != http.StatusUnauthorized { t.Fatalf("missing-auth status = %d, want 401", missingRec.Code) } if got := missingRec.Body.String(); got != "Pulse setup token required\n" { t.Fatalf("missing-auth body = %q, want canonical missing-setup-token guidance", got) } requestBody.AuthToken = "invalid-setup-token" invalidAuthJSON, err := json.Marshal(requestBody) if err != nil { t.Fatalf("marshal invalid-auth auto-register request: %v", err) } invalidReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(invalidAuthJSON)) invalidRec := httptest.NewRecorder() handler.HandleAutoRegister(invalidRec, invalidReq) if invalidRec.Code != http.StatusUnauthorized { t.Fatalf("invalid-auth status = %d, want 401", invalidRec.Code) } if got := invalidRec.Body.String(); got != "Invalid or expired setup token\n" { t.Fatalf("invalid-auth body = %q, want canonical setup-token auth failure text", got) } const validSetupToken = "setup-token-123" tokenHash := authpkg.HashAPIToken(validSetupToken) handler.codeMutex.Lock() handler.setupTokens[tokenHash] = &SetupTokenRecord{ ExpiresAt: time.Now().Add(5 * time.Minute), NodeType: "pve", } handler.codeMutex.Unlock() requestBody.AuthToken = validSetupToken requestBody.TokenValue = "" mismatchedJSON, err := json.Marshal(requestBody) if err != nil { t.Fatalf("marshal mismatched-completion auto-register request: %v", err) } mismatchedReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(mismatchedJSON)) mismatchedRec := httptest.NewRecorder() handler.HandleAutoRegister(mismatchedRec, mismatchedReq) if mismatchedRec.Code != http.StatusBadRequest { t.Fatalf("mismatched-completion status = %d, want 400", mismatchedRec.Code) } if got := mismatchedRec.Body.String(); got != "tokenId and tokenValue must be provided together\n" { t.Fatalf("mismatched-completion body = %q, want canonical token-pair guidance", got) } } func TestContract_BootstrapTokenPersistenceJSONSnapshot(t *testing.T) { tempDir := t.TempDir() token, created, path, err := loadOrCreateBootstrapToken(tempDir) if err != nil { t.Fatalf("loadOrCreateBootstrapToken() error = %v", err) } if !created { t.Fatal("expected bootstrap token to be created") } data, err := os.ReadFile(path) if err != nil { t.Fatalf("read persisted bootstrap token: %v", err) } snapshot := string(data) if !strings.Contains(snapshot, `"version":2`) { t.Fatalf("bootstrap token snapshot missing version: %s", snapshot) } if !strings.Contains(snapshot, `"token_ciphertext":"`) { t.Fatalf("bootstrap token snapshot missing ciphertext field: %s", snapshot) } if !strings.Contains(snapshot, `"token_hash":"`) { t.Fatalf("bootstrap token snapshot missing token hash field: %s", snapshot) } if strings.Contains(snapshot, token) { t.Fatalf("bootstrap token snapshot leaked raw token: %s", snapshot) } } func TestContract_QuickSecuritySetupBootstrapRetrievalGuidance(t *testing.T) { tempDir := t.TempDir() cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } router := &Router{ config: cfg, persistence: config.NewConfigPersistence(cfg.DataPath), } router.initializeBootstrapToken() handler := handleQuickSecuritySetupFixed(router) body := `{"username":"bootstrap","password":"StrongPass!1","apiToken":"` + strings.Repeat("aa", 32) + `"}` req := httptest.NewRequest(http.MethodPost, "/api/security/quick-setup", strings.NewReader(body)) req.RemoteAddr = "198.51.100.40:54321" rec := httptest.NewRecorder() authLimiter.Reset("198.51.100.40") handler(rec, req) if rec.Code != http.StatusUnauthorized { t.Fatalf("quick setup status = %d, want 401 (%s)", rec.Code, rec.Body.String()) } if got := rec.Body.String(); !strings.Contains(got, "pulse bootstrap-token") { t.Fatalf("quick setup guidance = %q, want pulse bootstrap-token retrieval guidance", got) } if got := rec.Body.String(); strings.Contains(got, ".bootstrap_token") { t.Fatalf("quick setup guidance = %q, want no raw .bootstrap_token scraping guidance", got) } } func TestContract_ResetFirstRunSecurityResponseJSONSnapshot(t *testing.T) { t.Setenv("PULSE_DEV", "true") t.Setenv("NODE_ENV", "") record := newTokenRecord(t, "contract-reset-first-run-token-123.12345678", []string{config.ScopeSettingsWrite}, nil) cfg := newTestConfigWithTokens(t, record) cfg.AuthUser = "admin" cfg.AuthPass = "hashed-password" envPath, err := writeAuthEnvFile(cfg.ConfigPath, cfg.DataPath, []byte("PULSE_AUTH_USER='admin'\n")) if err != nil { t.Fatalf("writeAuthEnvFile: %v", err) } router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodPost, "/api/security/dev/reset-first-run", nil) req.Header.Set("X-API-Token", "contract-reset-first-run-token-123.12345678") rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("reset-first-run status = %d, want 200 (%s)", rec.Code, rec.Body.String()) } var payload firstRunResetResponse if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode reset-first-run response: %v", err) } if strings.TrimSpace(payload.BootstrapToken) == "" { t.Fatal("reset-first-run response missing bootstrapToken") } if got, want := payload.BootstrapTokenPath, filepath.Join(cfg.DataPath, bootstrapTokenFilename); got != want { t.Fatalf("reset-first-run bootstrapTokenPath = %q, want %q", got, want) } if _, err := os.Stat(envPath); !os.IsNotExist(err) { t.Fatalf("reset-first-run should remove auth env file, stat err = %v", err) } got, err := json.Marshal(firstRunResetResponse{ BootstrapToken: "bootstrap-token-placeholder", BootstrapTokenPath: "bootstrap-token-path-placeholder", }) if err != nil { t.Fatalf("marshal reset-first-run response snapshot: %v", err) } const want = `{ "bootstrapToken":"bootstrap-token-placeholder", "bootstrapTokenPath":"bootstrap-token-path-placeholder" }` assertJSONSnapshot(t, got, want) } func TestContract_ResetFirstRunSecurityClearsEnvBackedStatus(t *testing.T) { t.Setenv("PULSE_DEV", "true") t.Setenv("NODE_ENV", "") t.Setenv("PULSE_AUTH_USER", "admin") t.Setenv("PULSE_AUTH_PASS", "hashed-password") record := newTokenRecord(t, "contract-reset-first-run-token-456.12345678", []string{config.ScopeSettingsWrite}, nil) cfg := newTestConfigWithTokens(t, record) cfg.AuthUser = "admin" cfg.AuthPass = "hashed-password" router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") resetReq := httptest.NewRequest(http.MethodPost, "/api/security/dev/reset-first-run", nil) resetReq.Header.Set("X-API-Token", "contract-reset-first-run-token-456.12345678") resetRec := httptest.NewRecorder() router.Handler().ServeHTTP(resetRec, resetReq) if resetRec.Code != http.StatusOK { t.Fatalf("reset-first-run status = %d, want 200 (%s)", resetRec.Code, resetRec.Body.String()) } statusReq := httptest.NewRequest(http.MethodGet, "/api/security/status", nil) statusRec := httptest.NewRecorder() router.Handler().ServeHTTP(statusRec, statusReq) if statusRec.Code != http.StatusOK { t.Fatalf("security status = %d, want 200 (%s)", statusRec.Code, statusRec.Body.String()) } var payload map[string]any if err := json.Unmarshal(statusRec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode security status payload: %v", err) } if got, _ := payload["hasAuthentication"].(bool); got { t.Fatalf("hasAuthentication = %v, want false", payload["hasAuthentication"]) } if got, _ := payload["bootstrapTokenPath"].(string); strings.TrimSpace(got) == "" { t.Fatalf("bootstrapTokenPath = %v, want non-empty", payload["bootstrapTokenPath"]) } } func TestContract_SetupScriptURLResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "type": "pve", "host": "https://pve.local:8006", "url": "https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve", "downloadURL": "https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&setup_token=setup-token-123&type=pve", "scriptFileName": "pulse-setup-pve.sh", "command": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }", "commandWithEnv": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo >/dev/null 2>&1; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }", "commandWithoutEnv": "curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006&pulse_url=https%3A%2F%2Fpulse.example&type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then bash; elif command -v sudo >/dev/null 2>&1; then sudo bash; else echo \"Root privileges required. Run as root (su -) and retry.\" >&2; exit 1; fi; }", "expires": int64(1900000000), "setupToken": "setup-token-123", "tokenHint": "set…123", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal setup-script-url response: %v", err) } const want = `{ "command":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }", "commandWithEnv":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then PULSE_SETUP_TOKEN='setup-token-123' bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo env PULSE_SETUP_TOKEN='setup-token-123' bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }", "commandWithoutEnv":"curl -fsSL 'https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve' | { if [ \"$(id -u)\" -eq 0 ]; then bash; elif command -v sudo \u003e/dev/null 2\u003e\u00261; then sudo bash; else echo \"Root privileges required. Run as root (su -) and retry.\" \u003e\u00262; exit 1; fi; }", "downloadURL":"https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026setup_token=setup-token-123\u0026type=pve", "expires":1900000000, "host":"https://pve.local:8006", "scriptFileName":"pulse-setup-pve.sh", "setupToken":"setup-token-123", "tokenHint":"set…123", "type":"pve", "url":"https://pulse.example/api/setup-script?host=https%3A%2F%2Fpve.local%3A8006\u0026pulse_url=https%3A%2F%2Fpulse.example\u0026type=pve" }` assertJSONSnapshot(t, got, want) } func TestContract_SecurityStatusIncludesSessionCapabilitiesDemoMode(t *testing.T) { cfg := newTestConfigWithTokens(t) cfg.DemoMode = true router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodGet, "/api/security/status", nil) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("security status = %d, want 200 (%s)", rec.Code, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode security status payload: %v", err) } sessionCapabilities, ok := payload["sessionCapabilities"].(map[string]any) if !ok { t.Fatalf("sessionCapabilities = %#v, want object", payload["sessionCapabilities"]) } if got, _ := sessionCapabilities["demoMode"].(bool); !got { t.Fatalf("sessionCapabilities.demoMode = %v, want true", sessionCapabilities["demoMode"]) } presentationPolicy, ok := payload["presentationPolicy"].(map[string]any) if !ok { t.Fatalf("presentationPolicy = %#v, want object", payload["presentationPolicy"]) } for key, want := range map[string]bool{ "demoMode": true, "readOnly": true, "hideCommercial": true, "hideUpgrade": true, } { if got, _ := presentationPolicy[key].(bool); got != want { t.Fatalf("presentationPolicy.%s = %v, want %v", key, presentationPolicy[key], want) } } } func TestContract_SecurityStatusPresentationPolicyDefaultsClosedOutsideDemo(t *testing.T) { cfg := newTestConfigWithTokens(t) cfg.DemoMode = false router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") req := httptest.NewRequest(http.MethodGet, "/api/security/status", nil) rec := httptest.NewRecorder() router.Handler().ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("security status = %d, want 200 (%s)", rec.Code, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode security status payload: %v", err) } presentationPolicy, ok := payload["presentationPolicy"].(map[string]any) if !ok { t.Fatalf("presentationPolicy = %#v, want object", payload["presentationPolicy"]) } for key, want := range map[string]bool{ "demoMode": false, "readOnly": false, "hideCommercial": false, "hideUpgrade": false, } { if got, _ := presentationPolicy[key].(bool); got != want { t.Fatalf("presentationPolicy.%s = %v, want %v", key, presentationPolicy[key], want) } } } func TestContract_SetupScriptURLRejectsNonCanonicalRequestJSON(t *testing.T) { handler := newTestConfigHandlers(t, &config.Config{DataPath: t.TempDir()}) req := httptest.NewRequest( http.MethodPost, "/api/setup-script-url", bytes.NewBufferString(`{"type":"pve","host":"pve.local","setupToken":"unexpected"}`), ) rec := httptest.NewRecorder() handler.HandleSetupScriptURL(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } if got := rec.Body.String(); got != "Invalid request\n" { t.Fatalf("body = %q, want canonical invalid-request guidance", got) } } func TestContract_SetupBootstrapRejectsPBSBackupPerms(t *testing.T) { handler := newTestConfigHandlers(t, &config.Config{DataPath: t.TempDir()}) setupScriptReq := httptest.NewRequest( http.MethodGet, "/api/setup-script?type=pbs&host=https://pbs.local:8007&pulse_url=https://pulse.example&backup_perms=true", nil, ) setupScriptRec := httptest.NewRecorder() handler.HandleSetupScript(setupScriptRec, setupScriptReq) if setupScriptRec.Code != http.StatusBadRequest { t.Fatalf("setup-script status = %d, want 400", setupScriptRec.Code) } if got := setupScriptRec.Body.String(); got != "backup_perms is only supported for type 'pve'\n" { t.Fatalf("setup-script body = %q, want canonical backup-perms guidance", got) } setupURLReq := httptest.NewRequest( http.MethodPost, "/api/setup-script-url", bytes.NewBufferString(`{"type":"pbs","host":"pbs.local","backupPerms":true}`), ) setupURLRec := httptest.NewRecorder() handler.HandleSetupScriptURL(setupURLRec, setupURLReq) if setupURLRec.Code != http.StatusBadRequest { t.Fatalf("setup-script-url status = %d, want 400", setupURLRec.Code) } if got := setupURLRec.Body.String(); got != "backupPerms is only supported for type 'pve'\n" { t.Fatalf("setup-script-url body = %q, want canonical backup-perms guidance", got) } } func TestContract_CanonicalAutoRegisterSourceContract(t *testing.T) { if !isCanonicalAutoRegisterSource("agent") { t.Fatalf("agent source should be accepted") } if !isCanonicalAutoRegisterSource("script") { t.Fatalf("script source should be accepted") } if isCanonicalAutoRegisterSource("manual") { t.Fatalf("manual source should be rejected") } } func TestContract_CanonicalAutoRegisterTypeContract(t *testing.T) { if !isCanonicalAutoRegisterType("pve") { t.Fatalf("pve type should be accepted") } if !isCanonicalAutoRegisterType("pbs") { t.Fatalf("pbs type should be accepted") } if isCanonicalAutoRegisterType("pmg") { t.Fatalf("pmg type should be rejected") } } func TestContract_CanonicalAutoRegisterTokenIDContract(t *testing.T) { if !isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!pulse-homelab") { t.Fatalf("canonical pve token id should be accepted") } if !isCanonicalAutoRegisterTokenID("pbs", "pulse-monitor@pbs!pulse-backup") { t.Fatalf("canonical pbs token id should be accepted") } if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!token") { t.Fatalf("non-pulse-managed pve token suffix should be rejected") } if isCanonicalAutoRegisterTokenID("pve", "pulse@pve!token") { t.Fatalf("non-canonical token id should be rejected") } if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pbs!pulse-backup") { t.Fatalf("cross-type token id should be rejected") } if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!") { t.Fatalf("empty canonical token suffix should be rejected") } if isCanonicalAutoRegisterTokenID("pve", "pulse-monitor@pve!pulse-") { t.Fatalf("empty pulse-managed token slug should be rejected") } } func TestContract_CanonicalAutoRegisterMatchMessageContract(t *testing.T) { if got := canonicalAutoRegisterMatchMessage("resolved host identity"); got != "Canonical auto-register matched existing node by resolved host identity" { t.Fatalf("resolved-host message = %q", got) } if got := canonicalAutoRegisterMatchMessage("DHCP continuity token identity"); got != "Canonical auto-register matched existing node by DHCP continuity token identity" { t.Fatalf("dhcp message = %q", got) } if got := canonicalAutoRegisterMatchMessage("host; updated token in-place"); got != "Canonical auto-register matched existing node by host; updated token in-place" { t.Fatalf("host-update message = %q", got) } if strings.Contains(canonicalAutoRegisterMatchMessage("resolved host identity"), "Secure auto-register") { t.Fatalf("canonical match message must not preserve secure auto-register wording") } } func TestContract_CanonicalAutoRegisterCompletionPayloadMessageContract(t *testing.T) { if got := canonicalAutoRegisterCompletionPayloadMessage(); got != "Incomplete canonical auto-register token completion payload" { t.Fatalf("completion-payload message = %q", got) } if strings.Contains(canonicalAutoRegisterCompletionPayloadMessage(), "secure token completion") { t.Fatalf("canonical completion-payload message must not preserve secure wording") } } func TestContract_CanonicalAutoRegisterMissingFieldsMessageContract(t *testing.T) { if got := canonicalAutoRegisterMissingFieldsMessage("", "", false, ""); got != "Missing required canonical auto-register fields: type, host, tokenId/tokenValue, serverName" { t.Fatalf("all-missing message = %q", got) } if got := canonicalAutoRegisterMissingFieldsMessage("pve", "https://pve.local:8006", true, ""); got != "Missing required canonical auto-register fields: serverName" { t.Fatalf("serverName-only message = %q", got) } } func TestContract_CanonicalAutoRegisterDirectValidationContract(t *testing.T) { tempDir := t.TempDir() t.Setenv("PULSE_DATA_DIR", tempDir) cfg := &config.Config{ DataPath: tempDir, ConfigPath: tempDir, } handler := newTestConfigHandlers(t, cfg) reqBody := AutoRegisterRequest{ Type: "pve", Host: "https://pve.local:8006", TokenID: "pulse-monitor@pve!pulse-homelab", TokenValue: "secret-token", } missingServerJSON, err := json.Marshal(reqBody) if err != nil { t.Fatalf("marshal missing-serverName canonical request: %v", err) } missingServerReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(missingServerJSON)) missingServerRec := httptest.NewRecorder() handler.handleCanonicalAutoRegister(missingServerRec, missingServerReq, &reqBody, "127.0.0.1") if missingServerRec.Code != http.StatusBadRequest { t.Fatalf("missing-serverName status = %d, want 400", missingServerRec.Code) } if got := missingServerRec.Body.String(); got != "Missing required canonical auto-register fields: serverName\n" { t.Fatalf("missing-serverName body = %q, want canonical missing-field guidance", got) } reqBody.ServerName = "pve-node-1" reqBody.TokenValue = "" mismatchedReq := httptest.NewRequest(http.MethodPost, "/api/auto-register", nil) mismatchedRec := httptest.NewRecorder() handler.handleCanonicalAutoRegister(mismatchedRec, mismatchedReq, &reqBody, "127.0.0.1") if mismatchedRec.Code != http.StatusBadRequest { t.Fatalf("mismatched-completion status = %d, want 400", mismatchedRec.Code) } if got := mismatchedRec.Body.String(); got != "tokenId and tokenValue must be provided together\n" { t.Fatalf("mismatched-completion body = %q, want canonical token-pair guidance", got) } } func TestContract_AutoRegisterResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "status": "success", "message": "Node pve-node-1 registered successfully at https://pve.local:8006", "action": "use_token", "type": "pve", "source": "script", "host": "https://pve.local:8006", "nodeId": "pve-node-1", "nodeName": "pve-node-1", "tokenId": "pulse-monitor@pve!pulse-homelab", "tokenValue": "secret-token", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal auto-register response: %v", err) } const want = `{ "action":"use_token", "host":"https://pve.local:8006", "message":"Node pve-node-1 registered successfully at https://pve.local:8006", "nodeId":"pve-node-1", "nodeName":"pve-node-1", "source":"script", "status":"success", "tokenId":"pulse-monitor@pve!pulse-homelab", "tokenValue":"secret-token", "type":"pve" }` assertJSONSnapshot(t, got, want) } func TestContract_AutoRegisterWebSocketEventJSONSnapshot(t *testing.T) { payload := map[string]any{ "type": "pve", "host": "https://pve.local:8006", "name": "pve-node-1", "nodeId": "pve-node-1", "nodeName": "pve-node-1", "tokenId": "pulse-monitor@pve!pulse-homelab", "hasToken": true, "verifySSL": true, "status": "connected", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal auto-register websocket event: %v", err) } const want = `{ "hasToken":true, "host":"https://pve.local:8006", "name":"pve-node-1", "nodeId":"pve-node-1", "nodeName":"pve-node-1", "status":"connected", "tokenId":"pulse-monitor@pve!pulse-homelab", "type":"pve", "verifySSL":true }` assertJSONSnapshot(t, got, want) } func TestContract_CanonicalAutoRegisterEventJSONSnapshot(t *testing.T) { payload := map[string]any{ "type": "pbs", "host": "https://pbs.local:8007", "name": "backup-node (2)", "nodeId": "backup-node (2)", "nodeName": "backup-node (2)", "tokenId": "pulse-monitor@pbs!pulse-backup", "hasToken": true, "verifySSL": true, "status": "connected", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal canonical /api/auto-register websocket event: %v", err) } const want = `{ "hasToken":true, "host":"https://pbs.local:8007", "name":"backup-node (2)", "nodeId":"backup-node (2)", "nodeName":"backup-node (2)", "status":"connected", "tokenId":"pulse-monitor@pbs!pulse-backup", "type":"pbs", "verifySSL":true }` assertJSONSnapshot(t, got, want) } func TestContract_CanonicalAutoRegisterReusedTokenResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "status": "success", "message": "Node pve-node-1 registered successfully at https://pve.local:8006", "action": "use_token", "type": "pve", "source": "script", "host": "https://pve.local:8006", "nodeId": "pve-node-1", "nodeName": "pve-node-1", "tokenId": "pulse-monitor@pve!pulse-existing-node", "tokenValue": "existing-token", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal canonical /api/auto-register response: %v", err) } const want = `{ "action":"use_token", "host":"https://pve.local:8006", "message":"Node pve-node-1 registered successfully at https://pve.local:8006", "nodeId":"pve-node-1", "nodeName":"pve-node-1", "source":"script", "status":"success", "tokenId":"pulse-monitor@pve!pulse-existing-node", "tokenValue":"existing-token", "type":"pve" }` assertJSONSnapshot(t, got, want) } func TestContract_CanonicalAutoRegisterCallerProvidedTokenResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "status": "success", "message": "Node pve-node-1 registered successfully at https://pve.local:8006", "action": "use_token", "type": "pve", "source": "agent", "host": "https://pve.local:8006", "nodeId": "pve-node-1", "nodeName": "pve-node-1", "tokenId": "pulse-monitor@pve!pulse-server", "tokenValue": "created-locally", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal canonical /api/auto-register caller-provided token response: %v", err) } const want = `{ "action":"use_token", "host":"https://pve.local:8006", "message":"Node pve-node-1 registered successfully at https://pve.local:8006", "nodeId":"pve-node-1", "nodeName":"pve-node-1", "source":"agent", "status":"success", "tokenId":"pulse-monitor@pve!pulse-server", "tokenValue":"created-locally", "type":"pve" }` assertJSONSnapshot(t, got, want) } func TestContract_CanonicalAutoRegisterRotatedTokenResponseJSONSnapshot(t *testing.T) { payload := map[string]any{ "status": "success", "message": "Node pve-node-1 registered successfully at https://pve.local:8006", "action": "use_token", "type": "pve", "source": "agent", "host": "https://pve.local:8006", "nodeId": "pve-node-1", "nodeName": "pve-node-1", "tokenId": "pulse-monitor@pve!pulse-existing-node", "tokenValue": "rotated-token", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal canonical /api/auto-register rotated token response: %v", err) } const want = `{ "action":"use_token", "host":"https://pve.local:8006", "message":"Node pve-node-1 registered successfully at https://pve.local:8006", "nodeId":"pve-node-1", "nodeName":"pve-node-1", "source":"agent", "status":"success", "tokenId":"pulse-monitor@pve!pulse-existing-node", "tokenValue":"rotated-token", "type":"pve" }` assertJSONSnapshot(t, got, want) } func TestContract_CanonicalAutoRegisterResponseUsesCanonicalStoredNodeIdentity(t *testing.T) { payload := map[string]any{ "status": "success", "message": "Node pve-node-1 (2) registered successfully at https://pve.local:8006", "action": "use_token", "type": "pve", "source": "agent", "host": "https://pve.local:8006", "nodeId": "pve-node-1 (2)", "nodeName": "pve-node-1 (2)", "tokenId": "pulse-monitor@pve!pulse-existing-node", "tokenValue": "existing-token", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal canonical /api/auto-register disambiguated response: %v", err) } const want = `{ "action":"use_token", "host":"https://pve.local:8006", "message":"Node pve-node-1 (2) registered successfully at https://pve.local:8006", "nodeId":"pve-node-1 (2)", "nodeName":"pve-node-1 (2)", "source":"agent", "status":"success", "tokenId":"pulse-monitor@pve!pulse-existing-node", "tokenValue":"existing-token", "type":"pve" }` assertJSONSnapshot(t, got, want) } func TestContract_MetricsHistoryLiveFallbackJSONSnapshot(t *testing.T) { state := models.NewState() state.UpdateVMsForInstance("pve1", []models.VM{{ ID: "pve1:node1:101", VMID: 101, Name: "vm-101", Node: "node1", Instance: "pve1", Status: "running", Type: "qemu", CPU: 0.42, Memory: models.Memory{ Usage: 55.0, }, Disk: models.Disk{ Usage: 33.0, }, }}) monitor := &monitoring.Monitor{} setUnexportedField(t, monitor, "state", state) setUnexportedField(t, monitor, "metricsHistory", monitoring.NewMetricsHistory(10, time.Hour)) tempDir := t.TempDir() mtp := config.NewMultiTenantPersistence(tempDir) if _, err := mtp.GetPersistence("default"); err != nil { t.Fatalf("failed to init persistence: %v", err) } router := &Router{ monitor: monitor, licenseHandlers: NewLicenseHandlers(mtp, false), } req := httptest.NewRequest( http.MethodGet, "/api/metrics-store/history?resourceType=vm&resourceId=pve1:node1:101&metric=cpu&start=2026-03-11T00:00:00Z&end=2026-03-12T00:00:00Z", nil, ) rec := httptest.NewRecorder() router.handleMetricsHistory(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("unmarshal metrics history response: %v", err) } points, ok := payload["points"].([]any) if !ok || len(points) != 1 { t.Fatalf("unexpected points payload: %#v", payload["points"]) } point, ok := points[0].(map[string]any) if !ok { t.Fatalf("unexpected point payload: %#v", points[0]) } point["timestamp"] = float64(1700000000000) payload["range"] = "24h" payload["start"] = float64(1741651200000) payload["end"] = float64(1741737600000) got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal normalized metrics history response: %v", err) } const want = `{ "end":1741737600000, "metric":"cpu", "points":[ { "max":42, "min":42, "timestamp":1700000000000, "value":42 } ], "range":"24h", "resourceId":"pve1:node1:101", "resourceType":"vm", "source":"live", "start":1741651200000 }` assertJSONSnapshot(t, got, want) } func TestContract_MetricsHistoryPhysicalDiskIOLiveWindowUsesCanonicalDiskTarget(t *testing.T) { mh := monitoring.NewMetricsHistory(1000, time.Hour) now := time.Now().UTC().Truncate(time.Second) resourceID := "SERIAL884006359727" for i, value := range []float64{1.5, 2.25, 3.0} { mh.AddDiskMetric(resourceID, "diskread", value*1024*1024, now.Add(time.Duration(i-2)*10*time.Minute)) } router := &Router{ monitor: &monitoring.Monitor{}, } setUnexportedField(t, router.monitor, "metricsHistory", mh) req := httptest.NewRequest( http.MethodGet, "/api/metrics-store/history?resourceType=disk&resourceId="+resourceID+"&metric=diskread&range=30m", nil, ) rec := httptest.NewRecorder() router.handleMetricsHistory(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var resp metricsHistoryResponse if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } if resp.ResourceType != "disk" { t.Fatalf("expected resourceType disk, got %q", resp.ResourceType) } if resp.ResourceId != resourceID { t.Fatalf("expected canonical disk resource id %q, got %q", resourceID, resp.ResourceId) } if resp.Metric != "diskread" { t.Fatalf("expected metric diskread, got %q", resp.Metric) } if resp.Range != "30m" { t.Fatalf("expected range 30m, got %q", resp.Range) } if resp.Source != "memory" { t.Fatalf("expected source memory, got %q", resp.Source) } if len(resp.Points) != 3 { t.Fatalf("expected 3 diskread points, got %d", len(resp.Points)) } } func TestContract_PatrolStatusResponseJSONSnapshot(t *testing.T) { lastPatrolAt := time.Date(2026, 3, 12, 9, 30, 0, 0, time.UTC) lastActivityAt := lastPatrolAt.Add(5 * time.Minute) nextPatrolAt := lastPatrolAt.Add(6 * time.Hour) blockedAt := lastPatrolAt.Add(15 * time.Minute) payload := PatrolStatusResponse{ RuntimeState: ai.PatrolRuntimeStateBlocked, Running: false, Enabled: true, LastPatrolAt: &lastPatrolAt, LastActivityAt: &lastActivityAt, TriggerStatus: &ai.TriggerStatus{ Running: true, PendingTriggers: 3, CurrentInterval: 300000, RecentEvents: 6, IsBusyMode: true, AlertTriggersEnabled: true, AnomalyTriggersEnabled: false, }, NextPatrolAt: &nextPatrolAt, LastDurationMs: 12345, ResourcesChecked: 18, FindingsCount: 3, ErrorCount: 1, Healthy: false, IntervalMs: 21600000, FixedCount: 2, BlockedReason: "Awaiting AI provider configuration", BlockedAt: &blockedAt, QuickstartCreditsRemaining: 7, QuickstartCreditsTotal: pkglicensing.QuickstartCreditsTotal, UsingQuickstart: true, LicenseRequired: true, LicenseStatus: "none", UpgradeURL: "https://pulserelay.pro/upgrade?feature=ai_autofix", } payload.Summary.Critical = 1 payload.Summary.Warning = 2 payload.Summary.Watch = 0 payload.Summary.Info = 4 got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal patrol status response: %v", err) } const want = `{ "runtime_state":"blocked", "running":false, "enabled":true, "last_patrol_at":"2026-03-12T09:30:00Z", "last_activity_at":"2026-03-12T09:35:00Z", "trigger_status":{"running":true,"pending_triggers":3,"current_interval_ms":300000,"recent_events":6,"is_busy_mode":true,"alert_triggers_enabled":true,"anomaly_triggers_enabled":false}, "next_patrol_at":"2026-03-12T15:30:00Z", "last_duration_ms":12345, "resources_checked":18, "findings_count":3, "error_count":1, "healthy":false, "interval_ms":21600000, "fixed_count":2, "blocked_reason":"Awaiting AI provider configuration", "blocked_at":"2026-03-12T09:45:00Z", "quickstart_credits_remaining":7, "quickstart_credits_total":25, "using_quickstart":true, "license_required":true, "license_status":"none", "upgrade_url":"https://pulserelay.pro/upgrade?feature=ai_autofix", "summary":{"critical":1,"warning":2,"watch":0,"info":4} }` assertJSONSnapshot(t, got, want) } func TestContract_PatrolRunRecordJSONSnapshot(t *testing.T) { startedAt := time.Date(2026, 3, 12, 10, 0, 0, 0, time.UTC) completedAt := startedAt.Add(90 * time.Second) payload := ai.PatrolRunRecord{ ID: "run-1", StartedAt: startedAt, CompletedAt: completedAt, DurationMs: 90000, Type: "scoped", TriggerReason: "alert_fired", ScopeResourceIDs: []string{"seed-resource"}, EffectiveScopeResourceIDs: []string{}, ScopeResourceTypes: []string{"vm"}, ResourcesChecked: 4, NodesChecked: 0, GuestsChecked: 2, DockerChecked: 0, StorageChecked: 0, HostsChecked: 0, TrueNASChecked: 1, PBSChecked: 0, PMGChecked: 1, KubernetesChecked: 1, NewFindings: 0, ExistingFindings: 2, RejectedFindings: 1, ResolvedFindings: 1, AutoFixCount: 0, FindingsSummary: "All clear", FindingIDs: []string{}, ErrorCount: 0, Status: "healthy", TriageFlags: 3, TriageSkippedLLM: true, ToolCallCount: 0, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal patrol run record: %v", err) } const want = `{ "id":"run-1", "started_at":"2026-03-12T10:00:00Z", "completed_at":"2026-03-12T10:01:30Z", "duration_ms":90000, "type":"scoped", "trigger_reason":"alert_fired", "scope_resource_ids":["seed-resource"], "effective_scope_resource_ids":[], "scope_resource_types":["vm"], "resources_checked":4, "nodes_checked":0, "guests_checked":2, "docker_checked":0, "storage_checked":0, "hosts_checked":0, "truenas_checked":1, "pbs_checked":0, "pmg_checked":1, "kubernetes_checked":1, "new_findings":0, "existing_findings":2, "rejected_findings":1, "resolved_findings":1, "findings_summary":"All clear", "finding_ids":[], "error_count":0, "status":"healthy", "triage_flags":3, "triage_skipped_llm":true, "tool_call_count":0 }` assertJSONSnapshot(t, got, want) } func TestContract_ChatStreamEventJSONSnapshots(t *testing.T) { cases := []struct { name string event chat.StreamEvent want string }{ { name: "content", event: mustStreamEvent(t, "content", chat.ContentData{ Text: "hello", }), want: `{"type":"content","data":{"text":"hello"}}`, }, { name: "explore_status", event: mustStreamEvent(t, "explore_status", chat.ExploreStatusData{ Phase: "started", Message: "Explore pre-pass running (read-only context).", Model: "openai:explore-fast", }), want: `{"type":"explore_status","data":{"phase":"started","message":"Explore pre-pass running (read-only context).","model":"openai:explore-fast"}}`, }, { name: "tool_start", event: mustStreamEvent(t, "tool_start", chat.ToolStartData{ ID: "tool-1", Name: "pulse_read", Input: `{"path":"/tmp/x.log"}`, RawInput: `{"path":"/tmp/x.log"}`, }), want: `{"type":"tool_start","data":{"id":"tool-1","name":"pulse_read","input":"{\"path\":\"/tmp/x.log\"}","raw_input":"{\"path\":\"/tmp/x.log\"}"}}`, }, { name: "tool_end", event: mustStreamEvent(t, "tool_end", chat.ToolEndData{ ID: "tool-1", Name: "pulse_read", Input: `{"path":"/tmp/x.log"}`, RawInput: `{"path":"/tmp/x.log"}`, Output: "ok", Success: true, }), want: `{"type":"tool_end","data":{"id":"tool-1","name":"pulse_read","input":"{\"path\":\"/tmp/x.log\"}","raw_input":"{\"path\":\"/tmp/x.log\"}","output":"ok","success":true}}`, }, { name: "approval_needed", event: mustStreamEvent(t, "approval_needed", chat.ApprovalNeededData{ ApprovalID: "approval-1", ToolID: "tool-2", ToolName: "pulse_exec", Command: "systemctl restart nginx", RunOnHost: true, TargetHost: "node-1", Risk: "high", Description: "Restart web service", }), want: `{"type":"approval_needed","data":{"approval_id":"approval-1","tool_id":"tool-2","tool_name":"pulse_exec","command":"systemctl restart nginx","run_on_host":true,"target_host":"node-1","risk":"high","description":"Restart web service"}}`, }, { name: "question", event: mustStreamEvent(t, "question", chat.QuestionData{ SessionID: "session-1", QuestionID: "question-1", Questions: []chat.Question{ { ID: "target", Type: "select", Question: "Which node should I inspect?", Header: "Target", Options: []chat.QuestionOption{ {Label: "Node A", Value: "node-a", Description: "Primary compute node"}, {Label: "Node B", Value: "node-b", Description: "Replica node"}, }, }, }, }), want: `{"type":"question","data":{"session_id":"session-1","question_id":"question-1","questions":[{"id":"target","type":"select","question":"Which node should I inspect?","header":"Target","options":[{"label":"Node A","value":"node-a","description":"Primary compute node"},{"label":"Node B","value":"node-b","description":"Replica node"}]}]}}`, }, { name: "done", event: mustStreamEvent(t, "done", chat.DoneData{ SessionID: "session-1", InputTokens: 120, OutputTokens: 80, }), want: `{"type":"done","data":{"session_id":"session-1","input_tokens":120,"output_tokens":80}}`, }, { name: "error", event: mustStreamEvent(t, "error", chat.ErrorData{ Message: "request failed", }), want: `{"type":"error","data":{"message":"request failed"}}`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := json.Marshal(tc.event) if err != nil { t.Fatalf("marshal stream event: %v", err) } assertJSONSnapshot(t, got, tc.want) }) } } func TestContract_PushNotificationJSONSnapshots(t *testing.T) { cases := []struct { name string payload relay.PushNotificationPayload want string }{ { name: "patrol_finding", payload: relay.NewPatrolFindingNotification("finding-1", "warning", "capacity", "Disk pressure detected"), want: `{"type":"patrol_finding","priority":"normal","title":"Disk pressure detected","body":"New warning capacity finding detected","action_type":"view_finding","action_id":"finding-1","category":"capacity","severity":"warning"}`, }, { name: "patrol_critical", payload: relay.NewPatrolFindingNotification("finding-2", "critical", "performance", "CPU saturation detected"), want: `{"type":"patrol_critical","priority":"high","title":"CPU saturation detected","body":"New critical performance finding detected","action_type":"view_finding","action_id":"finding-2","category":"performance","severity":"critical"}`, }, { name: "approval_request", payload: relay.NewApprovalRequestNotification("approval-1", "Fix queued", "high"), want: `{"type":"approval_request","priority":"high","title":"Fix queued","body":"A high-risk fix requires your approval","action_type":"approve_fix","action_id":"approval-1"}`, }, { name: "fix_completed_success", payload: relay.NewFixCompletedNotification("finding-3", "CPU saturation detected", true), want: `{"type":"fix_completed","priority":"normal","title":"CPU saturation detected","body":"Fix applied successfully","action_type":"view_fix_result","action_id":"finding-3"}`, }, { name: "fix_completed_failed", payload: relay.NewFixCompletedNotification("finding-4", "Disk pressure detected", false), want: `{"type":"fix_completed","priority":"normal","title":"Disk pressure detected","body":"Fix attempt failed — review needed","action_type":"view_fix_result","action_id":"finding-4"}`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := json.Marshal(tc.payload) if err != nil { t.Fatalf("marshal push payload: %v", err) } assertJSONSnapshot(t, got, tc.want) }) } } func TestContract_AlertJSONSnapshot(t *testing.T) { start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) lastSeen := start.Add(3 * time.Minute) payload := alerts.Alert{ ID: "cluster/qemu/100-cpu", Type: "cpu", Level: alerts.AlertLevelWarning, ResourceID: "cluster/qemu/100", ResourceName: "test-vm", Node: "pve-1", Instance: "cpu0", Message: "VM cpu at 95%", Value: 95.0, Threshold: 90.0, StartTime: start, LastSeen: lastSeen, Acknowledged: false, Metadata: map[string]interface{}{ "resourceType": "VM", "clearThreshold": 70.0, "unit": "%", "monitorOnly": true, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal alert: %v", err) } const want = `{ "id":"cluster/qemu/100-cpu", "type":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "node":"pve-1", "instance":"cpu0", "message":"VM cpu at 95%", "value":95, "threshold":90, "startTime":"2026-02-08T13:14:15Z", "lastSeen":"2026-02-08T13:17:15Z", "acknowledged":false, "metadata":{"clearThreshold":70,"monitorOnly":true,"resourceType":"VM","unit":"%"} }` assertJSONSnapshot(t, got, want) } func TestContract_AlertAllFieldsJSONSnapshot(t *testing.T) { start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) lastSeen := start.Add(3 * time.Minute) ackTime := start.Add(5 * time.Minute) lastNotified := start.Add(2 * time.Minute) escalationTimes := []time.Time{start.Add(1 * time.Minute), start.Add(3 * time.Minute)} payload := alerts.Alert{ ID: "cluster/qemu/100-cpu", Type: "cpu", Level: alerts.AlertLevelWarning, ResourceID: "cluster/qemu/100", ResourceName: "test-vm", Node: "pve-1", NodeDisplayName: "Proxmox Node 1", Instance: "cpu0", Message: "VM cpu at 95%", Value: 95.0, Threshold: 90.0, StartTime: start, LastSeen: lastSeen, Acknowledged: true, AckTime: &ackTime, AckUser: "admin", Metadata: map[string]interface{}{ "resourceType": "VM", "clearThreshold": 70.0, "unit": "%", "monitorOnly": true, }, LastNotified: &lastNotified, LastEscalation: 2, EscalationTimes: escalationTimes, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal alert with all fields: %v", err) } const want = `{ "id":"cluster/qemu/100-cpu", "type":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "node":"pve-1", "nodeDisplayName":"Proxmox Node 1", "instance":"cpu0", "message":"VM cpu at 95%", "value":95, "threshold":90, "startTime":"2026-02-08T13:14:15Z", "lastSeen":"2026-02-08T13:17:15Z", "acknowledged":true, "ackTime":"2026-02-08T13:19:15Z", "ackUser":"admin", "metadata":{"clearThreshold":70,"monitorOnly":true,"resourceType":"VM","unit":"%"}, "lastNotified":"2026-02-08T13:16:15Z", "lastEscalation":2, "escalationTimes":["2026-02-08T13:15:15Z","2026-02-08T13:17:15Z"] }` assertJSONSnapshot(t, got, want) } func TestContract_ModelAlertJSONSnapshot(t *testing.T) { start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) ackTime := start.Add(5 * time.Minute) resolvedTime := start.Add(10 * time.Minute) t.Run("alert", func(t *testing.T) { payload := models.Alert{ ID: "cluster/qemu/100-cpu", Type: "cpu", Level: "warning", ResourceID: "cluster/qemu/100", ResourceName: "test-vm", Node: "pve-1", NodeDisplayName: "Proxmox Node 1", Instance: "cpu0", Message: "VM cpu at 95%", Value: 95.0, Threshold: 90.0, StartTime: start, Acknowledged: true, AckTime: &ackTime, AckUser: "admin", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal model alert: %v", err) } forbidden := []string{`"lastSeen"`, `"metadata"`, `"lastNotified"`, `"lastEscalation"`, `"escalationTimes"`} for _, field := range forbidden { if strings.Contains(string(got), field) { t.Fatalf("model alert json unexpectedly contains %s: %s", field, string(got)) } } const want = `{ "id":"cluster/qemu/100-cpu", "type":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "node":"pve-1", "nodeDisplayName":"Proxmox Node 1", "instance":"cpu0", "message":"VM cpu at 95%", "value":95, "threshold":90, "startTime":"2026-02-08T13:14:15Z", "acknowledged":true, "ackTime":"2026-02-08T13:19:15Z", "ackUser":"admin" }` assertJSONSnapshot(t, got, want) }) t.Run("resolved_alert", func(t *testing.T) { payload := models.ResolvedAlert{ Alert: models.Alert{ ID: "cluster/qemu/100-cpu", Type: "cpu", Level: "warning", ResourceID: "cluster/qemu/100", ResourceName: "test-vm", Node: "pve-1", NodeDisplayName: "Proxmox Node 1", Instance: "cpu0", Message: "VM cpu at 95%", Value: 95.0, Threshold: 90.0, StartTime: start, Acknowledged: true, AckTime: &ackTime, AckUser: "admin", }, ResolvedTime: resolvedTime, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal model resolved alert: %v", err) } forbidden := []string{`"lastSeen"`, `"metadata"`, `"lastNotified"`, `"lastEscalation"`, `"escalationTimes"`} for _, field := range forbidden { if strings.Contains(string(got), field) { t.Fatalf("model resolved alert json unexpectedly contains %s: %s", field, string(got)) } } const want = `{ "id":"cluster/qemu/100-cpu", "type":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "node":"pve-1", "nodeDisplayName":"Proxmox Node 1", "instance":"cpu0", "message":"VM cpu at 95%", "value":95, "threshold":90, "startTime":"2026-02-08T13:14:15Z", "acknowledged":true, "ackTime":"2026-02-08T13:19:15Z", "ackUser":"admin", "resolvedTime":"2026-02-08T13:24:15Z" }` assertJSONSnapshot(t, got, want) }) } func TestContract_IncidentJSONSnapshot(t *testing.T) { start := time.Date(2026, 2, 8, 13, 14, 15, 0, time.UTC) ackTime := start.Add(5 * time.Minute) closedAt := start.Add(10 * time.Minute) t.Run("open", func(t *testing.T) { payload := memory.Incident{ ID: "incident-1", AlertIdentifier: "cluster/qemu/100-cpu", AlertType: "cpu", Level: "warning", ResourceID: "cluster/qemu/100", ResourceName: "test-vm", ResourceType: "guest", Node: "pve-1", Instance: "cpu0", Message: "VM cpu at 95%", Status: memory.IncidentStatusOpen, OpenedAt: start, Acknowledged: true, AckUser: "admin", AckTime: &ackTime, Events: []memory.IncidentEvent{ { ID: "evt-1", Type: memory.IncidentEventAlertFired, Timestamp: start.Add(1 * time.Minute), Summary: "CPU alert fired", Details: map[string]interface{}{ "type": "cpu", "level": "warning", "value": 95, "threshold": 90, }, }, { ID: "evt-2", Type: memory.IncidentEventAlertAcknowledged, Timestamp: start.Add(5 * time.Minute), Summary: "Alert acknowledged", Details: map[string]interface{}{ "user": "admin", }, }, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal open incident: %v", err) } const want = `{ "id":"incident-1", "alertIdentifier":"cluster/qemu/100-cpu", "alertType":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "resourceType":"guest", "node":"pve-1", "instance":"cpu0", "message":"VM cpu at 95%", "status":"open", "openedAt":"2026-02-08T13:14:15Z", "acknowledged":true, "ackUser":"admin", "ackTime":"2026-02-08T13:19:15Z", "events":[ {"id":"evt-1","type":"alert_fired","timestamp":"2026-02-08T13:15:15Z","summary":"CPU alert fired","details":{"level":"warning","threshold":90,"type":"cpu","value":95}}, {"id":"evt-2","type":"alert_acknowledged","timestamp":"2026-02-08T13:19:15Z","summary":"Alert acknowledged","details":{"user":"admin"}} ] }` assertJSONSnapshot(t, got, want) }) t.Run("resolved", func(t *testing.T) { payload := memory.Incident{ ID: "incident-1", AlertIdentifier: "cluster/qemu/100-cpu", AlertType: "cpu", Level: "warning", ResourceID: "cluster/qemu/100", ResourceName: "test-vm", ResourceType: "guest", Node: "pve-1", Instance: "cpu0", Message: "VM cpu at 95%", Status: memory.IncidentStatusResolved, OpenedAt: start, ClosedAt: &closedAt, Acknowledged: true, AckUser: "admin", AckTime: &ackTime, Events: []memory.IncidentEvent{ { ID: "evt-1", Type: memory.IncidentEventAlertFired, Timestamp: start.Add(1 * time.Minute), Summary: "CPU alert fired", Details: map[string]interface{}{ "type": "cpu", "level": "warning", "value": 95, "threshold": 90, }, }, { ID: "evt-2", Type: memory.IncidentEventAlertAcknowledged, Timestamp: start.Add(5 * time.Minute), Summary: "Alert acknowledged", Details: map[string]interface{}{ "user": "admin", }, }, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resolved incident: %v", err) } const want = `{ "id":"incident-1", "alertIdentifier":"cluster/qemu/100-cpu", "alertType":"cpu", "level":"warning", "resourceId":"cluster/qemu/100", "resourceName":"test-vm", "resourceType":"guest", "node":"pve-1", "instance":"cpu0", "message":"VM cpu at 95%", "status":"resolved", "openedAt":"2026-02-08T13:14:15Z", "closedAt":"2026-02-08T13:24:15Z", "acknowledged":true, "ackUser":"admin", "ackTime":"2026-02-08T13:19:15Z", "events":[ {"id":"evt-1","type":"alert_fired","timestamp":"2026-02-08T13:15:15Z","summary":"CPU alert fired","details":{"level":"warning","threshold":90,"type":"cpu","value":95}}, {"id":"evt-2","type":"alert_acknowledged","timestamp":"2026-02-08T13:19:15Z","summary":"Alert acknowledged","details":{"user":"admin"}} ] }` assertJSONSnapshot(t, got, want) }) } func TestContract_IncidentEventTypeEnumSnapshot(t *testing.T) { type envelope struct { Type memory.IncidentEventType `json:"type"` } cases := []struct { name string typ memory.IncidentEventType want string }{ {name: "alert_fired", typ: memory.IncidentEventAlertFired, want: `{"type":"alert_fired"}`}, {name: "alert_acknowledged", typ: memory.IncidentEventAlertAcknowledged, want: `{"type":"alert_acknowledged"}`}, {name: "alert_unacknowledged", typ: memory.IncidentEventAlertUnacknowledged, want: `{"type":"alert_unacknowledged"}`}, {name: "alert_resolved", typ: memory.IncidentEventAlertResolved, want: `{"type":"alert_resolved"}`}, {name: "ai_analysis", typ: memory.IncidentEventAnalysis, want: `{"type":"ai_analysis"}`}, {name: "command", typ: memory.IncidentEventCommand, want: `{"type":"command"}`}, {name: "runbook", typ: memory.IncidentEventRunbook, want: `{"type":"runbook"}`}, {name: "note", typ: memory.IncidentEventNote, want: `{"type":"note"}`}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := json.Marshal(envelope{Type: tc.typ}) if err != nil { t.Fatalf("marshal incident event type %q: %v", tc.name, err) } assertJSONSnapshot(t, got, tc.want) }) } } func TestContract_AlertFieldNamingConsistency(t *testing.T) { cases := []struct { name string typ reflect.Type }{ {name: "alerts.Alert", typ: reflect.TypeOf(alerts.Alert{})}, {name: "memory.Incident", typ: reflect.TypeOf(memory.Incident{})}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { for i := 0; i < tc.typ.NumField(); i++ { field := tc.typ.Field(i) if !field.IsExported() { continue } jsonTag := field.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { continue } tagName := strings.Split(jsonTag, ",")[0] if strings.Contains(tagName, "_") { t.Fatalf("field %s on %s uses snake_case json tag %q", field.Name, tc.name, tagName) } } }) } } func TestContract_AlertResourceTypeConsistency(t *testing.T) { cases := []struct { resourceType string want []string }{ {resourceType: "VM", want: []string{"vm", "guest"}}, {resourceType: "Container", want: []string{}}, {resourceType: "Node", want: []string{"node"}}, {resourceType: "Agent", want: []string{"agent", "node"}}, {resourceType: "Agent Disk", want: []string{}}, {resourceType: "PBS", want: []string{"pbs", "node"}}, {resourceType: "Docker Container", want: []string{}}, {resourceType: "DockerHost", want: []string{}}, {resourceType: "Docker Service", want: []string{}}, {resourceType: "Storage", want: []string{"storage"}}, {resourceType: "PMG", want: []string{"pmg", "node"}}, {resourceType: "K8s", want: []string{}}, } for _, tc := range cases { t.Run(tc.resourceType, func(t *testing.T) { got := alerts.CanonicalResourceTypeKeys(tc.resourceType) if len(tc.want) > 0 && len(got) == 0 { t.Fatalf("resource type %q returned no canonical keys", tc.resourceType) } if len(tc.want) == 0 && len(got) == 0 { return } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("canonical keys mismatch for %q: got %v want %v", tc.resourceType, got, tc.want) } }) } } func TestContract_TenantResourcesDoNotFallbackToRawSnapshotSeeding(t *testing.T) { now := time.Date(2026, 3, 17, 9, 0, 0, 0, time.UTC) h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()}) h.SetStateProvider(resourceStateProvider{snapshot: models.StateSnapshot{ Hosts: []models.Host{{ID: "host-default", Hostname: "default", Status: "online", LastSeen: now}}, }}) h.SetTenantStateProvider(tenantResourceStateProvider{snapshots: map[string]models.StateSnapshot{ "acme": { Hosts: []models.Host{{ID: "host-tenant-snapshot", Hostname: "tenant-snapshot", Status: "online", LastSeen: now}}, LastUpdate: time.Time{}, }, }}) req := httptest.NewRequest(http.MethodGet, "/api/resources?type=agent", nil) req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "acme")) rec := httptest.NewRecorder() h.HandleListResources(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } const want = `{"data":[],"meta":{"page":1,"limit":50,"total":0,"totalPages":0},"aggregations":{"total":0,"byType":{},"byStatus":{},"bySource":{}}}` if got := strings.TrimSpace(rec.Body.String()); got != want { t.Fatalf("tenant resource fallback contract = %s, want %s", got, want) } } func TestContract_ResourceListPolicyMetadata(t *testing.T) { now := time.Date(2026, 3, 17, 10, 0, 0, 0, time.UTC) h := NewResourceHandlers(&config.Config{DataPath: t.TempDir()}) h.SetStateProvider(resourceUnifiedSeedProvider{ snapshot: models.StateSnapshot{LastUpdate: now}, resources: []unifiedresources.Resource{ { ID: "vm-sensitive", Type: unifiedresources.ResourceTypeVM, Name: "payments-vm", Status: unifiedresources.StatusOnline, LastSeen: now, Sources: []unifiedresources.DataSource{unifiedresources.SourceProxmox}, Tags: []string{"customer-data"}, Identity: unifiedresources.ResourceIdentity{ Hostnames: []string{"payments.internal"}, IPAddresses: []string{"10.0.0.44"}, }, }, }, }) req := httptest.NewRequest(http.MethodGet, "/api/resources?type=vm", nil) rec := httptest.NewRecorder() h.HandleListResources(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } var resp ResourcesResponse if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("decode response: %v", err) } if len(resp.Data) != 1 { t.Fatalf("expected 1 resource, got %d", len(resp.Data)) } resource := resp.Data[0] if resource.Canonical == nil { t.Fatal("expected canonical identity metadata in resource contract") } if strings.TrimSpace(resource.Canonical.DisplayName) == "" { t.Fatal("expected canonical display name in resource contract") } if resource.Policy == nil { t.Fatal("expected policy metadata in resource contract") } if got := resource.Policy.Sensitivity; got != unifiedresources.ResourceSensitivityRestricted { t.Fatalf("policy sensitivity = %q, want %q", got, unifiedresources.ResourceSensitivityRestricted) } if got := resource.Policy.Routing.Scope; got != unifiedresources.ResourceRoutingScopeLocalOnly { t.Fatalf("routing scope = %q, want %q", got, unifiedresources.ResourceRoutingScopeLocalOnly) } wantRedactions := []unifiedresources.ResourceRedactionHint{ unifiedresources.ResourceRedactionHostname, unifiedresources.ResourceRedactionIPAddress, unifiedresources.ResourceRedactionPlatformID, unifiedresources.ResourceRedactionAlias, } if !reflect.DeepEqual(resource.Policy.Routing.Redact, wantRedactions) { t.Fatalf("policy redact = %#v, want %#v", resource.Policy.Routing.Redact, wantRedactions) } if got := resource.AISafeSummary; !strings.Contains(got, "virtual machine resource;") || !strings.Contains(got, "local-only context") { t.Fatalf("aiSafeSummary = %q", got) } } func TestContract_ResourceListCarriesTimelineAndCapabilityContracts(t *testing.T) { now := time.Date(2026, 3, 18, 12, 0, 0, 0, time.UTC) occurredAt := now.Add(-2 * time.Minute) payload := ResourcesResponse{ Data: []unifiedresources.Resource{ { ID: "vm-42", Type: unifiedresources.ResourceTypeVM, Name: "web-42", Status: unifiedresources.StatusOnline, LastSeen: now, Capabilities: []unifiedresources.ResourceCapability{ { Name: "restart", Type: unifiedresources.CapabilityTypeCommon, Description: "Restart the VM", MinimumApprovalLevel: unifiedresources.ApprovalAdmin, Params: []unifiedresources.CapabilityParam{ { Name: "force", Type: "boolean", Required: false, Description: "Restart without graceful shutdown", }, }, }, }, Relationships: []unifiedresources.ResourceRelationship{ { SourceID: "vm-42", TargetID: "node-1", Type: unifiedresources.RelRunsOn, Confidence: 1, Active: true, Discoverer: "proxmox_adapter", ObservedAt: now, LastSeenAt: now, Metadata: map[string]any{ "source": "live", "cluster": "pve-prod", }, }, }, RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-42", ObservedAt: now, OccurredAt: &occurredAt, ResourceID: "vm-42", Kind: unifiedresources.ChangeStateTransition, From: "offline", To: "online", SourceType: unifiedresources.SourcePlatformEvent, SourceAdapter: unifiedresources.AdapterProxmox, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"node-1"}, Reason: "vm started", Metadata: map[string]any{ "source": "snapshot", "ticket": "INC-1234", }, }, }, FacetCounts: unifiedresources.ResourceFacetCounts{ RecentChanges: 1, }, }, }, Meta: ResourcesMeta{ Page: 1, Limit: 50, Total: 1, TotalPages: 1, }, Aggregations: unifiedresources.ResourceStats{ Total: 1, ByType: map[unifiedresources.ResourceType]int{unifiedresources.ResourceTypeVM: 1}, ByStatus: map[unifiedresources.ResourceStatus]int{unifiedresources.StatusOnline: 1}, BySource: map[unifiedresources.DataSource]int{unifiedresources.SourceProxmox: 1}, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource response: %v", err) } const want = `{ "data":[ { "id":"vm-42", "type":"vm", "name":"web-42", "status":"online", "lastSeen":"2026-03-18T12:00:00Z", "updatedAt":"0001-01-01T00:00:00Z", "sources":null, "identity":{}, "capabilities":[ { "name":"restart", "type":"common", "description":"Restart the VM", "minimumApprovalLevel":"admin", "params":[ { "name":"force", "type":"boolean", "required":false, "isSensitive":false, "description":"Restart without graceful shutdown" } ] } ], "relationships":[ { "sourceId":"vm-42", "targetId":"node-1", "type":"runs_on", "confidence":1, "active":true, "discoverer":"proxmox_adapter", "observedAt":"2026-03-18T12:00:00Z", "lastSeenAt":"2026-03-18T12:00:00Z", "metadata":{"cluster":"pve-prod","source":"live"} } ], "recentChanges":[ { "id":"chg-42", "observedAt":"2026-03-18T12:00:00Z", "occurredAt":"2026-03-18T11:58:00Z", "resourceId":"vm-42", "kind":"state_transition", "from":"offline", "to":"online", "sourceType":"platform_event", "sourceAdapter":"proxmox_adapter", "confidence":"high", "relatedResources":["node-1"], "reason":"vm started", "metadata":{"source":"snapshot","ticket":"INC-1234"} } ], "facetCounts":{ "recentChanges":1 } } ], "meta":{"page":1,"limit":50,"total":1,"totalPages":1}, "aggregations":{"total":1,"byType":{"vm":1},"byStatus":{"online":1},"bySource":{"proxmox":1}} }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceListUsesTenantStateProviderAtStartup(t *testing.T) { cfg := &config.Config{ DataPath: t.TempDir(), ConfigPath: t.TempDir(), } router := NewRouter(cfg, nil, &monitoring.MultiTenantMonitor{}, nil, nil, "1.0.0") rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/resources?page=1&limit=100", nil) req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "tenant-a")) router.resourceHandlers.HandleListResources(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) } } func TestContract_ResourceCapabilitiesJSONSnapshot(t *testing.T) { payload := struct { ResourceID string `json:"resourceId"` Capabilities []unifiedresources.ResourceCapability `json:"capabilities"` Count int `json:"count"` }{ ResourceID: "vm:42", Capabilities: []unifiedresources.ResourceCapability{ { Name: "restart", Type: unifiedresources.CapabilityTypeCommon, Description: "Restart the VM", MinimumApprovalLevel: unifiedresources.ApprovalAdmin, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource capabilities response: %v", err) } const want = `{ "resourceId":"vm:42", "capabilities":[ { "name":"restart", "type":"common", "description":"Restart the VM", "minimumApprovalLevel":"admin" } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceRelationshipsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` Relationships []unifiedresources.ResourceRelationship `json:"relationships"` Count int `json:"count"` }{ ResourceID: "vm:42", Relationships: []unifiedresources.ResourceRelationship{ { SourceID: "vm:42", TargetID: "node-1", Type: unifiedresources.RelRunsOn, Confidence: 1, Active: true, Discoverer: "proxmox_adapter", ObservedAt: now, LastSeenAt: now, Metadata: map[string]any{ "source": "live", "cluster": "pve-prod", }, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource relationships response: %v", err) } const want = `{ "resourceId":"vm:42", "relationships":[ { "sourceId":"vm:42", "targetId":"node-1", "type":"runs_on", "confidence":1, "active":true, "discoverer":"proxmox_adapter", "observedAt":"2026-03-18T17:00:00Z", "lastSeenAt":"2026-03-18T17:00:00Z", "metadata":{"cluster":"pve-prod","source":"live"} } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceTimelineJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Count int `json:"count"` }{ ResourceID: "vm:42", RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-42", ResourceID: "vm:42", ObservedAt: now, OccurredAt: &now, Kind: unifiedresources.ChangeStateTransition, From: "offline", To: "online", SourceType: unifiedresources.SourcePlatformEvent, SourceAdapter: unifiedresources.AdapterProxmox, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"node-1"}, Reason: "vm started", Metadata: map[string]any{"source": "snapshot", "ticket": "INC-1234"}, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource timeline response: %v", err) } const want = `{ "resourceId":"vm:42", "recentChanges":[ { "id":"chg-42", "observedAt":"2026-03-18T17:00:00Z", "occurredAt":"2026-03-18T17:00:00Z", "resourceId":"vm:42", "kind":"state_transition", "from":"offline", "to":"online", "sourceType":"platform_event", "sourceAdapter":"proxmox_adapter", "confidence":"high", "relatedResources":["node-1"], "reason":"vm started", "metadata":{"source":"snapshot","ticket":"INC-1234"} } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceTimelineRelationshipJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 5, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Count int `json:"count"` }{ ResourceID: "vm:42", RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-relationship-42", ObservedAt: now, OccurredAt: &now, ResourceID: "vm:42", Kind: unifiedresources.ChangeRelationship, From: "node-1", To: "node-2", SourceType: unifiedresources.SourcePulseDiff, SourceAdapter: unifiedresources.AdapterProxmox, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"db:alpha", "service:beta"}, Reason: "relationship updated", Metadata: map[string]any{ "edgeType": "depends_on", "active": true, }, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource timeline relationship response: %v", err) } const want = `{ "resourceId":"vm:42", "recentChanges":[ { "id":"chg-relationship-42", "observedAt":"2026-03-18T17:05:00Z", "occurredAt":"2026-03-18T17:05:00Z", "resourceId":"vm:42", "kind":"relationship_change", "from":"node-1", "to":"node-2", "sourceType":"pulse_diff", "sourceAdapter":"proxmox_adapter", "confidence":"high", "relatedResources":["db:alpha","service:beta"], "reason":"relationship updated", "metadata":{"active":true,"edgeType":"depends_on"} } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceTimelineRestartJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 10, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Count int `json:"count"` }{ ResourceID: "container:7", RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-restart-7", ObservedAt: now, OccurredAt: &now, ResourceID: "container:7", Kind: unifiedresources.ChangeRestart, From: "online|docker.restartCount=1|docker.uptimeSeconds=3600", To: "online|docker.restartCount=2|docker.uptimeSeconds=120", SourceType: unifiedresources.SourcePlatformEvent, SourceAdapter: unifiedresources.AdapterDocker, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"node:1", "service:api"}, Reason: "resource restart detected", Metadata: map[string]any{ "changedFields": []string{"docker.restartCount", "docker.uptimeSeconds"}, }, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource timeline restart response: %v", err) } const want = `{ "resourceId":"container:7", "recentChanges":[ { "id":"chg-restart-7", "observedAt":"2026-03-18T17:10:00Z", "occurredAt":"2026-03-18T17:10:00Z", "resourceId":"container:7", "kind":"restart", "from":"online|docker.restartCount=1|docker.uptimeSeconds=3600", "to":"online|docker.restartCount=2|docker.uptimeSeconds=120", "sourceType":"platform_event", "sourceAdapter":"docker_adapter", "confidence":"high", "relatedResources":["node:1","service:api"], "reason":"resource restart detected", "metadata":{"changedFields":["docker.restartCount","docker.uptimeSeconds"]} } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceTimelineAnomalyJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 12, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Count int `json:"count"` }{ ResourceID: "storage:1", RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-anomaly-1", ObservedAt: now, OccurredAt: &now, ResourceID: "storage:1", Kind: unifiedresources.ChangeAnomaly, From: "none", To: "capacity_runway_low[warning]:PBS datastore archive is READ_ONLY", SourceType: unifiedresources.SourcePulseDiff, SourceAdapter: unifiedresources.AdapterProxmox, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"node-2", "service:db"}, Reason: "resource incident changed", Metadata: map[string]any{ "changedFields": []string{"incidents"}, }, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource timeline anomaly response: %v", err) } const want = `{ "resourceId":"storage:1", "recentChanges":[ { "id":"chg-anomaly-1", "observedAt":"2026-03-18T17:12:00Z", "occurredAt":"2026-03-18T17:12:00Z", "resourceId":"storage:1", "kind":"metric_anomaly", "from":"none", "to":"capacity_runway_low[warning]:PBS datastore archive is READ_ONLY", "sourceType":"pulse_diff", "sourceAdapter":"proxmox_adapter", "confidence":"high", "relatedResources":["node-2","service:db"], "reason":"resource incident changed", "metadata":{"changedFields":["incidents"]} } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceFacetsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 17, 0, 0, 0, time.UTC) payload := struct { ResourceID string `json:"resourceId"` RecentChanges []unifiedresources.ResourceChange `json:"recentChanges"` Counts struct { RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"` RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"` } `json:"counts"` }{ ResourceID: "vm:42", RecentChanges: []unifiedresources.ResourceChange{ { ID: "chg-42", ResourceID: "vm:42", ObservedAt: now, OccurredAt: &now, Kind: unifiedresources.ChangeStateTransition, From: "offline", To: "online", SourceType: unifiedresources.SourcePlatformEvent, SourceAdapter: unifiedresources.AdapterProxmox, Confidence: unifiedresources.ConfidenceHigh, RelatedResources: []string{"node-1"}, Metadata: map[string]any{ "source": "snapshot", "ticket": "INC-1234", }, }, }, Counts: struct { RecentChanges int `json:"recentChanges"` RecentChangeKinds map[unifiedresources.ChangeKind]int `json:"recentChangeKinds"` RecentChangeSourceTypes map[unifiedresources.ChangeSourceType]int `json:"recentChangeSourceTypes"` RecentChangeSourceAdapters map[unifiedresources.ChangeSourceAdapter]int `json:"recentChangeSourceAdapters"` }{ RecentChanges: 3, RecentChangeKinds: map[unifiedresources.ChangeKind]int{unifiedresources.ChangeRestart: 1, unifiedresources.ChangeAnomaly: 2}, RecentChangeSourceTypes: map[unifiedresources.ChangeSourceType]int{ unifiedresources.SourcePlatformEvent: 1, unifiedresources.SourcePulseDiff: 2, }, RecentChangeSourceAdapters: map[unifiedresources.ChangeSourceAdapter]int{ unifiedresources.AdapterProxmox: 1, unifiedresources.AdapterDocker: 2, }, }, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal resource facets response: %v", err) } const want = `{ "resourceId":"vm:42", "recentChanges":[ { "id":"chg-42", "observedAt":"2026-03-18T17:00:00Z", "occurredAt":"2026-03-18T17:00:00Z", "resourceId":"vm:42", "kind":"state_transition", "from":"offline", "to":"online", "sourceType":"platform_event", "sourceAdapter":"proxmox_adapter", "confidence":"high", "relatedResources":["node-1"], "metadata":{"source":"snapshot","ticket":"INC-1234"} } ], "counts":{ "recentChanges":3, "recentChangeKinds":{ "metric_anomaly":2, "restart":1 }, "recentChangeSourceTypes":{ "platform_event":1, "pulse_diff":2 }, "recentChangeSourceAdapters":{ "docker_adapter":2, "proxmox_adapter":1 } } }` assertJSONSnapshot(t, got, want) } func TestContract_ResourceTimelineRejectsInvalidSourceAdapter(t *testing.T) { _, err := unifiedresources.ParseResourceChangeFilters(nil, nil, []string{"unsupported_adapter"}) if err == nil { t.Fatal("expected invalid sourceAdapter to be rejected") } } func TestContract_UnifiedActionAuditsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC) payload := struct { Audits []unifiedresources.ActionAuditRecord `json:"audits"` Count int `json:"count"` ResourceID string `json:"resourceId,omitempty"` }{ Audits: []unifiedresources.ActionAuditRecord{ { ID: "action-1", CreatedAt: now, UpdatedAt: now, State: unifiedresources.ActionStateCompleted, Request: unifiedresources.ActionRequest{ RequestID: "req-1", ResourceID: "vm:42", CapabilityName: "restart", Reason: "maintenance", RequestedBy: "agent:ops", }, Plan: unifiedresources.ActionPlan{ ActionID: "action-1", RequestID: "req-1", Allowed: true, RequiresApproval: false, ApprovalPolicy: unifiedresources.ApprovalNone, RollbackAvailable: false, PlannedAt: now, ExpiresAt: now.Add(5 * time.Minute), ResourceVersion: "rv-1", PolicyVersion: "pv-1", PlanHash: "hash-1", }, Approvals: []unifiedresources.ActionApprovalRecord{ { Actor: "admin@example.com", Method: unifiedresources.MethodUI, Timestamp: now.Add(time.Minute), Outcome: unifiedresources.OutcomeApproved, Reason: "approved", }, }, Result: &unifiedresources.ExecutionResult{ Success: true, Output: "done", }, }, }, Count: 1, ResourceID: "vm:42", } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal action audits response: %v", err) } const want = `{ "audits":[ { "id":"action-1", "createdAt":"2026-03-18T16:00:00Z", "updatedAt":"2026-03-18T16:00:00Z", "state":"completed", "request":{ "requestId":"req-1", "resourceId":"vm:42", "capabilityName":"restart", "reason":"maintenance", "requestedBy":"agent:ops" }, "plan":{ "actionId":"action-1", "requestId":"req-1", "allowed":true, "requiresApproval":false, "approvalPolicy":"none", "rollbackAvailable":false, "plannedAt":"2026-03-18T16:00:00Z", "expiresAt":"2026-03-18T16:05:00Z", "resourceVersion":"rv-1", "policyVersion":"pv-1", "planHash":"hash-1" }, "approvals":[ { "actor":"admin@example.com", "method":"ui", "timestamp":"2026-03-18T16:01:00Z", "outcome":"approved", "reason":"approved" } ], "result":{ "success":true, "output":"done" } } ], "count":1, "resourceId":"vm:42" }` assertJSONSnapshot(t, got, want) } func TestContract_UnifiedActionLifecycleEventsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC) payload := struct { ActionID string `json:"actionId"` Events []unifiedresources.ActionLifecycleEvent `json:"events"` Count int `json:"count"` }{ ActionID: "action-1", Events: []unifiedresources.ActionLifecycleEvent{ { ActionID: "action-1", Timestamp: now, State: unifiedresources.ActionStatePlanned, Actor: "system", Message: "planned", }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal action lifecycle response: %v", err) } const want = `{ "actionId":"action-1", "events":[ { "actionId":"action-1", "timestamp":"2026-03-18T16:00:00Z", "state":"planned", "actor":"system", "message":"planned" } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_UnifiedExportAuditsJSONSnapshot(t *testing.T) { now := time.Date(2026, 3, 18, 16, 0, 0, 0, time.UTC) payload := struct { Audits []unifiedresources.ExportAuditRecord `json:"audits"` Count int `json:"count"` }{ Audits: []unifiedresources.ExportAuditRecord{ { ID: "export-1", Timestamp: now, Actor: "agent:ops", EnvelopeHash: "hash-1", Decision: unifiedresources.ExportRedacted, Destination: "local-llama", Redactions: []string{"metadata.hostname"}, }, }, Count: 1, } got, err := json.Marshal(payload) if err != nil { t.Fatalf("marshal export audits response: %v", err) } const want = `{ "audits":[ { "id":"export-1", "timestamp":"2026-03-18T16:00:00Z", "actor":"agent:ops", "envelopeHash":"hash-1", "decision":"redacted", "destination":"local-llama", "redactions":["metadata.hostname"] } ], "count":1 }` assertJSONSnapshot(t, got, want) } func TestContract_UnifiedAuditLimitCapsOversizedRequests(t *testing.T) { if got := parseAuditLimit("5000", 100); got != 1000 { t.Fatalf("parseAuditLimit oversized request = %d, want 1000", got) } if got := parseAuditLimit("250", 100); got != 250 { t.Fatalf("parseAuditLimit normal request = %d, want 250", got) } } func TestContract_EmbeddedFrontendWarningUsesCanonicalDevEntrypoints(t *testing.T) { path := filepath.Join("DO_NOT_EDIT_FRONTEND_HERE.md") body, err := os.ReadFile(path) if err != nil { t.Fatalf("read embedded frontend warning: %v", err) } text := string(body) if !strings.Contains(text, "http://127.0.0.1:5173") { t.Fatalf("embedded frontend warning must point to the frontend dev shell on 5173") } if !strings.Contains(text, "http://127.0.0.1:7655") { t.Fatalf("embedded frontend warning must identify the backend on 7655") } if strings.Contains(text, "The dev server (port 7655) will hot-reload") { t.Fatalf("embedded frontend warning must not describe 7655 as the hot-reload dev server") } } func TestContract_ShippedSecurityDocReferencesStayLocal(t *testing.T) { if shippedSecurityDocPath != "/docs/SECURITY.md" { t.Fatalf("expected shipped security doc path, got %q", shippedSecurityDocPath) } if shippedSecurityContainerNoticeDocAnchor != "/docs/SECURITY.md#critical-security-notice-for-container-deployments" { t.Fatalf("expected shipped security container notice path, got %q", shippedSecurityContainerNoticeDocAnchor) } } func mustStreamEvent(t *testing.T, eventType string, data interface{}) chat.StreamEvent { t.Helper() raw, err := json.Marshal(data) if err != nil { t.Fatalf("marshal stream data: %v", err) } return chat.StreamEvent{ Type: eventType, Data: raw, } } func assertJSONSnapshot(t *testing.T, got []byte, want string) { t.Helper() var gotCompact bytes.Buffer var wantCompact bytes.Buffer if err := json.Compact(&gotCompact, got); err != nil { t.Fatalf("compact got json: %v", err) } if err := json.Compact(&wantCompact, []byte(want)); err != nil { t.Fatalf("compact want json: %v", err) } if gotCompact.String() != wantCompact.String() { t.Fatalf("json snapshot mismatch\nwant: %s\ngot: %s", wantCompact.String(), gotCompact.String()) } }