Pulse/internal/api/router_helpers_more_test.go
2026-04-11 16:47:37 +01:00

448 lines
15 KiB
Go

package api
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestRouterGetMonitor_Defaults(t *testing.T) {
defaultMonitor, _, _ := newTestMonitor(t)
router := &Router{monitor: defaultMonitor}
req := httptest.NewRequest(http.MethodGet, "/", nil)
monitor, err := router.getMonitor(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if monitor != defaultMonitor {
t.Fatalf("expected default monitor to be returned")
}
}
func TestRouterGetMonitor_WithTenant(t *testing.T) {
defaultMonitor, _, _ := newTestMonitor(t)
tenantMonitor, _, _ := newTestMonitor(t)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"tenant-1": tenantMonitor,
})
router := &Router{monitor: defaultMonitor, mtMonitor: mtm}
req := httptest.NewRequest(http.MethodGet, "/", nil)
ctx := context.WithValue(req.Context(), OrgIDContextKey, "tenant-1")
req = req.WithContext(ctx)
monitor, err := router.getMonitor(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if monitor != tenantMonitor {
t.Fatalf("expected tenant monitor to be returned")
}
}
func TestMultiTenantStateProvider_UnifiedReadStateForTenant(t *testing.T) {
defaultMonitor, defaultState, _ := newTestMonitor(t)
defaultState.Hosts = []models.Host{{ID: "host-default", Hostname: "default-host", Status: "online"}}
syncTestResourceStore(t, defaultMonitor, defaultState)
tenantMonitor, tenantState, _ := newTestMonitor(t)
tenantState.Hosts = []models.Host{{ID: "host-tenant", Hostname: "tenant-host", Status: "online"}}
syncTestResourceStore(t, tenantMonitor, tenantState)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"tenant-1": tenantMonitor,
})
provider := NewMultiTenantStateProvider(mtm, defaultMonitor)
defaultReadState := provider.UnifiedReadStateForTenant("default")
if defaultReadState == nil {
t.Fatal("expected default unified read state")
}
if defaultReadState != defaultMonitor.GetUnifiedReadState() {
t.Fatal("expected default monitor-scoped read state")
}
if hosts := defaultReadState.Hosts(); len(hosts) != 1 || hosts[0].Name() != "default-host" {
t.Fatalf("unexpected default hosts: %#v", hosts)
}
tenantReadState := provider.UnifiedReadStateForTenant("tenant-1")
if tenantReadState == nil {
t.Fatal("expected tenant unified read state")
}
if tenantReadState != tenantMonitor.GetUnifiedReadState() {
t.Fatal("expected tenant monitor-scoped read state")
}
if hosts := tenantReadState.Hosts(); len(hosts) != 1 || hosts[0].Name() != "tenant-host" {
t.Fatalf("unexpected tenant hosts: %#v", hosts)
}
}
func TestMultiTenantStateProvider_UnifiedResourceSnapshotForTenant(t *testing.T) {
defaultMonitor, defaultState, _ := newTestMonitor(t)
defaultState.Hosts = []models.Host{{ID: "host-default", Hostname: "default-host", Status: "online"}}
syncTestResourceStore(t, defaultMonitor, defaultState)
tenantMonitor, tenantState, _ := newTestMonitor(t)
tenantState.Hosts = []models.Host{{ID: "host-tenant", Hostname: "tenant-host", Status: "online"}}
syncTestResourceStore(t, tenantMonitor, tenantState)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"tenant-1": tenantMonitor,
})
provider := NewMultiTenantStateProvider(mtm, defaultMonitor)
defaultResources, _ := provider.UnifiedResourceSnapshotForTenant("default")
if len(defaultResources) != 1 || defaultResources[0].Name != "default-host" {
t.Fatalf("unexpected default unified resources: %#v", defaultResources)
}
tenantResources, _ := provider.UnifiedResourceSnapshotForTenant("tenant-1")
if len(tenantResources) != 1 || tenantResources[0].Name != "tenant-host" {
t.Fatalf("unexpected tenant unified resources: %#v", tenantResources)
}
}
func TestMultiTenantStateProvider_FallbackOnError(t *testing.T) {
defaultMonitor, defaultState, _ := newTestMonitor(t)
defaultState.VMs = []models.VM{{ID: "vm-default"}}
mtp := config.NewMultiTenantPersistence(t.TempDir())
mtm := monitoring.NewMultiTenantMonitor(&config.Config{}, mtp, nil)
defer mtm.Stop()
provider := NewMultiTenantStateProvider(mtm, defaultMonitor)
resources, freshness := provider.UnifiedResourceSnapshotForTenant("../bad")
if len(resources) != 0 {
t.Fatalf("expected empty unified resources for tenant error, got %#v", resources)
}
if !freshness.IsZero() {
t.Fatalf("expected zero freshness for tenant error, got %v", freshness)
}
if readState := provider.UnifiedReadStateForTenant("../bad"); readState != nil {
t.Fatalf("expected nil read state for tenant error, got %#v", readState)
}
}
func TestSetMultiTenantMonitor_WiresHandlers(t *testing.T) {
defaultMonitor, _, _ := newTestMonitor(t)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"default": defaultMonitor,
})
router := &Router{
alertHandlers: &AlertHandlers{},
notificationHandlers: &NotificationHandlers{},
dockerAgentHandlers: &DockerAgentHandlers{},
unifiedAgentHandlers: &UnifiedAgentHandlers{},
kubernetesAgentHandlers: &KubernetesAgentHandlers{},
systemSettingsHandler: &SystemSettingsHandler{},
resourceHandlers: &ResourceHandlers{},
}
router.SetMultiTenantMonitor(mtm)
if router.mtMonitor != mtm {
t.Fatalf("expected router mtMonitor to be updated")
}
if router.monitor != defaultMonitor {
t.Fatalf("expected router monitor to be set to default monitor")
}
if router.alertHandlers.mtMonitor != mtm {
t.Fatalf("expected alertHandlers mtMonitor to be set")
}
if router.notificationHandlers.mtMonitor != mtm {
t.Fatalf("expected notificationHandlers mtMonitor to be set")
}
if router.dockerAgentHandlers.mtMonitor != mtm {
t.Fatalf("expected dockerAgentHandlers mtMonitor to be set")
}
if router.unifiedAgentHandlers.mtMonitor != mtm {
t.Fatalf("expected unifiedAgentHandlers mtMonitor to be set")
}
if router.kubernetesAgentHandlers.mtMonitor != mtm {
t.Fatalf("expected kubernetesAgentHandlers mtMonitor to be set")
}
if router.systemSettingsHandler.mtMonitor != mtm {
t.Fatalf("expected systemSettingsHandler mtMonitor to be set")
}
if router.resourceHandlers.tenantStateProvider == nil {
t.Fatalf("expected tenant state provider to be set")
}
}
func TestRouterConfigureMonitorDependencies_UsesTenantSpecificResourceAdapters(t *testing.T) {
defaultAdapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil))
router := &Router{
monitorResourceAdapter: defaultAdapter,
monitorResourceAdapters: make(map[string]*unifiedresources.MonitorAdapter),
}
defaultMonitor := &monitoring.Monitor{}
router.configureMonitorDependencies(defaultMonitor)
defaultReadState := defaultMonitor.GetUnifiedReadState()
if defaultReadState == nil {
t.Fatal("expected default monitor read state to be configured")
}
if defaultReadState != defaultAdapter {
t.Fatalf("expected default monitor to use default adapter, got %#v", defaultReadState)
}
tenantMonitor := &monitoring.Monitor{}
tenantMonitor.SetOrgID("tenant-1")
router.configureMonitorDependencies(tenantMonitor)
tenantReadState := tenantMonitor.GetUnifiedReadState()
if tenantReadState == nil {
t.Fatal("expected tenant monitor read state to be configured")
}
if tenantReadState == defaultAdapter {
t.Fatal("expected tenant monitor adapter to differ from default adapter")
}
router.configureMonitorDependencies(tenantMonitor)
if second := tenantMonitor.GetUnifiedReadState(); second != tenantReadState {
t.Fatal("expected tenant monitor to reuse stable adapter for same org")
}
}
func TestRouterMonitorAdapterForMonitorUsesPersistentStore(t *testing.T) {
router := &Router{
resourceHandlers: NewResourceHandlers(&config.Config{DataPath: t.TempDir()}),
monitorResourceAdapters: make(map[string]*unifiedresources.MonitorAdapter),
}
monitor := &monitoring.Monitor{}
monitor.SetOrgID("tenant-1")
adapter := router.monitorAdapterForMonitor(monitor)
if adapter == nil {
t.Fatal("expected monitor adapter")
}
adapter.PopulateSupplementalRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{
{
SourceID: "docker-host-1",
Resource: unifiedresources.Resource{
Type: unifiedresources.ResourceTypeDockerService,
Name: "svc-1",
Status: unifiedresources.StatusOnline,
},
},
})
store, err := router.resourceHandlers.getStore("tenant-1")
if err != nil {
t.Fatalf("getStore: %v", err)
}
resources := adapter.GetAll()
if len(resources) != 1 {
t.Fatalf("expected 1 resource from adapter, got %d", len(resources))
}
changes, err := store.GetRecentChanges(resources[0].ID, time.Time{}, 10)
if err != nil {
t.Fatalf("GetRecentChanges: %v", err)
}
if len(changes) != 1 {
t.Fatalf("expected 1 stored change, got %d", len(changes))
}
}
func TestRouterDefaultUnifiedResourceProvider_PrefersMonitorScopedAdapter(t *testing.T) {
defaultAdapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil))
defaultMonitor := &monitoring.Monitor{}
defaultMonitor.SetResourceStore(defaultAdapter)
router := &Router{
monitor: defaultMonitor,
monitorResourceAdapter: unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil)),
monitorResourceAdapters: map[string]*unifiedresources.MonitorAdapter{},
}
provider := router.defaultUnifiedResourceProvider()
if provider == nil {
t.Fatal("expected default unified provider")
}
if provider != defaultAdapter {
t.Fatalf("expected monitor-scoped adapter, got %#v", provider)
}
}
func TestRouterPersistenceForOrg_UsesTenantPersistence(t *testing.T) {
tempDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(tempDir)
tenantPersistence, err := mtp.GetPersistence("tenant-1")
if err != nil {
t.Fatalf("GetPersistence tenant-1: %v", err)
}
router := &Router{
persistence: config.NewConfigPersistence(tempDir),
multiTenant: mtp,
}
ctx := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1")
got := router.persistenceForOrg(ctx)
if got != tenantPersistence {
t.Fatalf("expected tenant persistence, got %#v", got)
}
}
func TestRouterPersistenceForOrg_NonDefaultDoesNotFallbackToDefault(t *testing.T) {
router := &Router{persistence: config.NewConfigPersistence(t.TempDir())}
ctx := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1")
if got := router.persistenceForOrg(ctx); got != nil {
t.Fatalf("expected nil persistence for non-default org without mt persistence, got %#v", got)
}
}
func TestRouterStopPatrolForOrg_ClearsLifecycleMarker(t *testing.T) {
router := &Router{
startedPatrolOrgs: map[string]bool{
"default": true,
"tenant-1": true,
},
}
router.StopPatrolForOrg("tenant-1")
if router.startedPatrolOrgs["tenant-1"] {
t.Fatal("expected tenant patrol marker to be cleared")
}
if !router.startedPatrolOrgs["default"] {
t.Fatal("expected default marker to remain set")
}
}
func TestRouterStopPatrol_ClearsAllLifecycleMarkers(t *testing.T) {
router := &Router{
aiSettingsHandler: &AISettingsHandler{},
startedPatrolOrgs: map[string]bool{
"default": true,
"tenant-1": true,
},
}
router.StopPatrol()
if len(router.startedPatrolOrgs) != 0 {
t.Fatalf("expected all patrol markers cleared, got %#v", router.startedPatrolOrgs)
}
}
func TestStartPatrolForContext_DoesNotOverwriteOtherTenantPatrolComponents(t *testing.T) {
setMockModeForTest(t, true)
tempDir := t.TempDir()
mtp := config.NewMultiTenantPersistence(tempDir)
defaultMonitor, _, _ := newTestMonitor(t)
tenantOneMonitor, _, _ := newTestMonitor(t)
tenantTwoMonitor, _, _ := newTestMonitor(t)
tenantOneMonitor.SetResourceStore(unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil)))
tenantTwoMonitor.SetResourceStore(unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil)))
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"default": defaultMonitor,
"tenant-1": tenantOneMonitor,
"tenant-2": tenantTwoMonitor,
})
router := &Router{
monitor: defaultMonitor,
mtMonitor: mtm,
multiTenant: mtp,
aiSettingsHandler: NewAISettingsHandler(mtp, mtm, nil),
startedPatrolOrgs: make(map[string]bool),
monitorResourceAdapters: make(map[string]*unifiedresources.MonitorAdapter),
}
router.aiSettingsHandler.SetStateProvider(&stubStateProvider{})
defer router.ShutdownAIIntelligence()
ctxTenantOne := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1")
if ok := router.startPatrolForContext(ctxTenantOne, "tenant-1"); !ok {
t.Fatal("expected tenant-1 patrol start to succeed")
}
svcTenantOne := router.aiSettingsHandler.GetAIService(ctxTenantOne)
if svcTenantOne == nil {
t.Fatal("expected tenant-1 AI service")
}
patrolTenantOne := svcTenantOne.GetPatrolService()
if patrolTenantOne == nil {
t.Fatal("expected tenant-1 patrol service")
}
baselineOne := patrolTenantOne.GetBaselineStore()
changeDetectorOne := patrolTenantOne.GetChangeDetector()
remediationLogOne := patrolTenantOne.GetRemediationLog()
if baselineOne == nil || changeDetectorOne == nil || remediationLogOne == nil {
t.Fatal("expected tenant-1 patrol components to be initialized")
}
ctxTenantTwo := context.WithValue(context.Background(), OrgIDContextKey, "tenant-2")
if ok := router.startPatrolForContext(ctxTenantTwo, "tenant-2"); !ok {
t.Fatal("expected tenant-2 patrol start to succeed")
}
if got := patrolTenantOne.GetBaselineStore(); got != baselineOne {
t.Fatal("expected tenant-1 baseline store to remain unchanged after tenant-2 startup")
}
if got := patrolTenantOne.GetChangeDetector(); got != changeDetectorOne {
t.Fatal("expected tenant-1 change detector to remain unchanged after tenant-2 startup")
}
if got := patrolTenantOne.GetRemediationLog(); got != remediationLogOne {
t.Fatal("expected tenant-1 remediation log to remain unchanged after tenant-2 startup")
}
}
func TestStartPatrolForContext_RejectsMismatchedAIServiceOrg(t *testing.T) {
setMockModeForTest(t, true)
mtp := config.NewMultiTenantPersistence(t.TempDir())
defaultMonitor, _, _ := newTestMonitor(t)
tenantMonitor, _, _ := newTestMonitor(t)
mtm := &monitoring.MultiTenantMonitor{}
setUnexportedField(t, mtm, "monitors", map[string]*monitoring.Monitor{
"default": defaultMonitor,
"tenant-1": tenantMonitor,
})
handler := NewAISettingsHandler(mtp, mtm, nil)
legacySvc := ai.NewService(config.NewConfigPersistence(t.TempDir()), nil)
legacySvc.SetOrgID("default")
handler.aiServices["tenant-1"] = legacySvc
router := &Router{
monitor: defaultMonitor,
mtMonitor: mtm,
aiSettingsHandler: handler,
startedPatrolOrgs: make(map[string]bool),
monitorResourceAdapters: make(map[string]*unifiedresources.MonitorAdapter),
}
ctx := context.WithValue(context.Background(), OrgIDContextKey, "tenant-1")
if ok := router.startPatrolForContext(ctx, "tenant-1"); ok {
t.Fatal("expected patrol start to fail when AI service org scope mismatches tenant org")
}
}