mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 02:55:51 +00:00
0622169be8
Revalidate and harden the Pulse multi-tenant GA surface across org ownership transfer, MSP invite lifecycle, tenant state fallback, registry workspace limits, and the embedded Pulse Account portal bundle.
477 lines
16 KiB
Go
477 lines
16 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 TestRouterGetMonitor_NonDefaultWithoutTenantMonitorFailsClosed(t *testing.T) {
|
|
defaultMonitor, _, _ := newTestMonitor(t)
|
|
router := &Router{monitor: defaultMonitor}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, "tenant-1"))
|
|
|
|
monitor, err := router.getMonitor(req)
|
|
if err == nil {
|
|
t.Fatal("expected non-default org without tenant monitor to fail closed")
|
|
}
|
|
if monitor != nil {
|
|
t.Fatalf("monitor = %#v, want nil", monitor)
|
|
}
|
|
}
|
|
|
|
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{},
|
|
configHandlers: &ConfigHandlers{},
|
|
dockerAgentHandlers: &DockerAgentHandlers{},
|
|
unifiedAgentHandlers: &UnifiedAgentHandlers{},
|
|
kubernetesAgentHandlers: &KubernetesAgentHandlers{},
|
|
systemSettingsHandler: &SystemSettingsHandler{},
|
|
aiSettingsHandler: &AISettingsHandler{},
|
|
aiHandler: &AIHandler{},
|
|
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.configHandlers.mtMonitor != mtm {
|
|
t.Fatalf("expected configHandlers 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.aiSettingsHandler.mtMonitor != mtm {
|
|
t.Fatalf("expected aiSettingsHandler mtMonitor to be set")
|
|
}
|
|
if router.aiHandler.mtMonitor != mtm {
|
|
t.Fatalf("expected aiHandler 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")
|
|
}
|
|
}
|