mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 08:57:12 +00:00
448 lines
15 KiB
Go
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")
|
|
}
|
|
}
|