mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
Unify mock fixtures across supported platforms
This commit is contained in:
@@ -370,6 +370,15 @@ TrueNAS and VMware inventory plus mock metrics-history seeding must derive from
|
||||
one shared platform fixture owner in `internal/mock/` so settings payloads,
|
||||
supplemental ingest, unified read-state, and seeded charts cannot drift from
|
||||
each other when the v6 runtime runs in mock mode.
|
||||
That same fixture authority now also includes legacy snapshot-backed platforms.
|
||||
`internal/monitoring/monitor.go` and
|
||||
`internal/monitoring/mock_metrics_history.go` must treat the canonical
|
||||
`internal/mock/fixture_graph.go` runtime graph as the one mock owner for
|
||||
legacy Proxmox/Docker/Kubernetes/agent/PBS/PMG snapshot state plus
|
||||
provider-backed TrueNAS and VMware fixtures. Monitoring must not rebuild mock
|
||||
provider context from standalone defaults or mix a legacy `GenerateMockData`
|
||||
snapshot with separate provider fixtures when seeding read-state or metrics
|
||||
history.
|
||||
That same boundary now also owns native disk-history fallback when Pulse's own
|
||||
history is shallow. `internal/truenas/client.go`,
|
||||
`internal/truenas/provider.go`, `internal/monitoring/truenas_poller.go`, and
|
||||
|
||||
@@ -148,12 +148,13 @@ querying, and the operator-facing storage health presentation layer.
|
||||
data remains inventory-only context and must not be treated as proof of
|
||||
restore capability, recovery artifacts, or widened platform recovery
|
||||
support.
|
||||
11. Keep runtime mock platform context derived from one shared fixture owner.
|
||||
When shared `internal/api/` and monitoring wiring surface mock TrueNAS or
|
||||
VMware storage/recovery-adjacent inventory, that data must come from the
|
||||
canonical `internal/mock/` platform fixture layer so settings payloads,
|
||||
unified inventory, and recovery/storage context stay aligned instead of
|
||||
drifting through recovery-local fixture assembly.
|
||||
11. Keep runtime mock platform context derived from one shared fixture graph.
|
||||
When shared `internal/api/` and monitoring wiring surface mock
|
||||
storage/recovery-adjacent inventory or recovery artifacts, that data must
|
||||
come from the canonical `internal/mock/fixture_graph.go` owner so legacy
|
||||
snapshot-backed platforms, provider-backed fixtures, unified inventory,
|
||||
and recovery/storage context stay aligned instead of drifting through
|
||||
recovery-local fixture assembly.
|
||||
|
||||
## Current State
|
||||
|
||||
@@ -200,6 +201,13 @@ The recovery backend is a real product boundary, not just a helper package:
|
||||
`internal/recovery/` owns per-tenant SQLite persistence, rollup derivation,
|
||||
query filtering, and recovery-point indexing for the `/api/recovery/*`
|
||||
surfaces.
|
||||
That same recovery boundary now also assumes mock recovery context is projected
|
||||
from one canonical mock graph. `internal/mock/recovery_points.go` may synthesize
|
||||
inventory-only recovery artifacts for supported mock platforms, but those
|
||||
subjects must derive from the shared `internal/mock/fixture_graph.go` owner
|
||||
instead of a separate hardcoded recovery cache, so recovery filters, rollups,
|
||||
and shared route handoffs see the same platform set as settings and
|
||||
infrastructure.
|
||||
That same shared `internal/api/` dependency also assumes auth-persistence
|
||||
teardown is synchronous when recovery-adjacent runtimes reinitialize. Session,
|
||||
CSRF, and recovery-token workers may not leave stale background goroutines or
|
||||
|
||||
@@ -262,6 +262,14 @@ mode. When mock-backed TrueNAS or VMware supplemental records are present, they
|
||||
must enter the shared resource graph through the same canonical source/type
|
||||
rules as live providers instead of introducing a mock-only source family or
|
||||
resource kind.
|
||||
That same mock seed contract now also includes legacy snapshot-backed sources.
|
||||
`internal/mock/fixture_graph.go` must own the runtime mock snapshot and the
|
||||
provider-backed TrueNAS/VMware fixtures together, and
|
||||
`internal/mock/platform_fixtures.go` must project unified resources from that
|
||||
one graph instead of combining a legacy snapshot read with standalone provider
|
||||
defaults. The shared resource graph must therefore see one coherent mock
|
||||
platform set regardless of whether a platform is snapshot-backed or
|
||||
supplemental-provider-backed.
|
||||
TrueNAS-managed applications now follow the same canonical workload rule. One
|
||||
TrueNAS app instance from `app.query` must project as one canonical
|
||||
`app-container` resource under `SourceTrueNAS`, reusing the shared workload and
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
|
||||
)
|
||||
|
||||
// FixtureGraph is the canonical mock runtime owner for snapshot-backed and
|
||||
// provider-backed platform fixtures. All mock projections should derive from
|
||||
// this graph rather than mixing independent snapshot and provider helpers.
|
||||
type FixtureGraph struct {
|
||||
State models.StateSnapshot
|
||||
AlertHistory []models.Alert
|
||||
PlatformFixtures PlatformFixtures
|
||||
}
|
||||
|
||||
func emptyFixtureGraph() FixtureGraph {
|
||||
return FixtureGraph{
|
||||
State: models.EmptyStateSnapshot(),
|
||||
}
|
||||
}
|
||||
|
||||
func buildFixtureGraph(cfg MockConfig, now time.Time) FixtureGraph {
|
||||
state := GenerateMockData(cfg)
|
||||
state.LastUpdate = now
|
||||
|
||||
return FixtureGraph{
|
||||
State: state,
|
||||
AlertHistory: GenerateAlertHistory(state.Nodes, state.VMs, state.Containers),
|
||||
PlatformFixtures: DefaultPlatformFixtures(),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneFixtureGraph(in FixtureGraph) FixtureGraph {
|
||||
return FixtureGraph{
|
||||
State: cloneState(in.State),
|
||||
AlertHistory: append([]models.Alert(nil), in.AlertHistory...),
|
||||
PlatformFixtures: clonePlatformFixtures(in.PlatformFixtures),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *FixtureGraph) UpdateMetrics(cfg MockConfig, now time.Time) {
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
UpdateMetrics(&g.State, cfg)
|
||||
g.State.LastUpdate = now
|
||||
}
|
||||
|
||||
func (g *FixtureGraph) UpdateAlertSnapshots(active []alerts.Alert, resolved []models.ResolvedAlert) {
|
||||
if g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
converted := make([]models.Alert, 0, len(active))
|
||||
for _, alert := range active {
|
||||
converted = append(converted, models.Alert{
|
||||
ID: alert.ID,
|
||||
Type: alert.Type,
|
||||
Level: string(alert.Level),
|
||||
ResourceID: alert.ResourceID,
|
||||
ResourceName: alert.ResourceName,
|
||||
Node: alert.Node,
|
||||
Instance: alert.Instance,
|
||||
Message: alert.Message,
|
||||
Value: alert.Value,
|
||||
Threshold: alert.Threshold,
|
||||
StartTime: alert.StartTime,
|
||||
Acknowledged: alert.Acknowledged,
|
||||
})
|
||||
}
|
||||
|
||||
g.State.ActiveAlerts = converted
|
||||
g.State.RecentlyResolved = append([]models.ResolvedAlert(nil), resolved...)
|
||||
}
|
||||
|
||||
func CurrentFixtureGraph() FixtureGraph {
|
||||
if !IsMockEnabled() {
|
||||
return emptyFixtureGraph()
|
||||
}
|
||||
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
return cloneFixtureGraph(mockGraph)
|
||||
}
|
||||
|
||||
func GetPlatformFixtures() PlatformFixtures {
|
||||
if !IsMockEnabled() {
|
||||
return DefaultPlatformFixtures()
|
||||
}
|
||||
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
return clonePlatformFixtures(mockGraph.PlatformFixtures)
|
||||
}
|
||||
|
||||
func clonePlatformFixtures(in PlatformFixtures) PlatformFixtures {
|
||||
return PlatformFixtures{
|
||||
TrueNAS: cloneTrueNASFixtureSnapshot(in.TrueNAS),
|
||||
VMware: cloneVMwareInventorySnapshot(in.VMware),
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTrueNASFixtureSnapshot(in truenas.FixtureSnapshot) truenas.FixtureSnapshot {
|
||||
out := in
|
||||
out.System = cloneTrueNASSystemInfo(in.System)
|
||||
out.Pools = append([]truenas.Pool(nil), in.Pools...)
|
||||
out.Datasets = append([]truenas.Dataset(nil), in.Datasets...)
|
||||
out.Disks = append([]truenas.Disk(nil), in.Disks...)
|
||||
out.Alerts = append([]truenas.Alert(nil), in.Alerts...)
|
||||
out.Apps = cloneTrueNASApps(in.Apps)
|
||||
out.ZFSSnapshots = cloneTrueNASZFSSnapshots(in.ZFSSnapshots)
|
||||
out.ReplicationTasks = cloneTrueNASReplicationTasks(in.ReplicationTasks)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASSystemInfo(in truenas.SystemInfo) truenas.SystemInfo {
|
||||
out := in
|
||||
if len(in.TemperatureCelsius) > 0 {
|
||||
out.TemperatureCelsius = make(map[string]float64, len(in.TemperatureCelsius))
|
||||
for key, value := range in.TemperatureCelsius {
|
||||
out.TemperatureCelsius[key] = value
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASApps(in []truenas.App) []truenas.App {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.App, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].UsedHostIPs = append([]string(nil), in[i].UsedHostIPs...)
|
||||
out[i].UsedPorts = cloneTrueNASAppPorts(in[i].UsedPorts)
|
||||
out[i].Containers = cloneTrueNASAppContainers(in[i].Containers)
|
||||
out[i].Volumes = append([]truenas.AppVolume(nil), in[i].Volumes...)
|
||||
out[i].Images = append([]string(nil), in[i].Images...)
|
||||
out[i].Networks = cloneTrueNASAppNetworks(in[i].Networks)
|
||||
out[i].Stats = cloneTrueNASAppStats(in[i].Stats)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASAppPorts(in []truenas.AppPort) []truenas.AppPort {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.AppPort, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].HostPorts = append([]truenas.AppHostPort(nil), in[i].HostPorts...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASAppContainers(in []truenas.AppContainer) []truenas.AppContainer {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.AppContainer, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].PortConfig = cloneTrueNASAppPorts(in[i].PortConfig)
|
||||
out[i].VolumeMounts = append([]truenas.AppVolume(nil), in[i].VolumeMounts...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASAppNetworks(in []truenas.AppNetwork) []truenas.AppNetwork {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.AppNetwork, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].Labels = cloneStringMap(in[i].Labels)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASAppStats(in *truenas.AppStats) *truenas.AppStats {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := *in
|
||||
out.Interfaces = append([]truenas.AppInterfaceStats(nil), in.Interfaces...)
|
||||
return &out
|
||||
}
|
||||
|
||||
func cloneTrueNASZFSSnapshots(in []truenas.ZFSSnapshot) []truenas.ZFSSnapshot {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.ZFSSnapshot, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].CreatedAt = cloneTimePtr(in[i].CreatedAt)
|
||||
out[i].UsedBytes = cloneInt64Ptr(in[i].UsedBytes)
|
||||
out[i].Referenced = cloneInt64Ptr(in[i].Referenced)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneTrueNASReplicationTasks(in []truenas.ReplicationTask) []truenas.ReplicationTask {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]truenas.ReplicationTask, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].SourceDatasets = append([]string(nil), in[i].SourceDatasets...)
|
||||
out[i].LastRun = cloneTimePtr(in[i].LastRun)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneVMwareInventorySnapshot(in vmware.InventorySnapshot) vmware.InventorySnapshot {
|
||||
out := in
|
||||
out.Hosts = cloneVMwareInventoryHosts(in.Hosts)
|
||||
out.VMs = cloneVMwareInventoryVMs(in.VMs)
|
||||
out.Datastores = cloneVMwareInventoryDatastores(in.Datastores)
|
||||
out.EnrichmentIssues = append([]vmware.InventoryEnrichmentIssue(nil), in.EnrichmentIssues...)
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneVMwareInventoryHosts(in []vmware.InventoryHost) []vmware.InventoryHost {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]vmware.InventoryHost, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].DatastoreIDs = append([]string(nil), in[i].DatastoreIDs...)
|
||||
out[i].DatastoreNames = append([]string(nil), in[i].DatastoreNames...)
|
||||
out[i].TriggeredAlarms = append([]vmware.InventoryAlarm(nil), in[i].TriggeredAlarms...)
|
||||
out[i].RecentTasks = append([]vmware.InventoryTask(nil), in[i].RecentTasks...)
|
||||
out[i].RecentEvents = append([]vmware.InventoryEvent(nil), in[i].RecentEvents...)
|
||||
out[i].Metrics = cloneVMwareInventoryMetrics(in[i].Metrics)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneVMwareInventoryVMs(in []vmware.InventoryVM) []vmware.InventoryVM {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]vmware.InventoryVM, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].DatastoreIDs = append([]string(nil), in[i].DatastoreIDs...)
|
||||
out[i].DatastoreNames = append([]string(nil), in[i].DatastoreNames...)
|
||||
out[i].GuestIPAddresses = append([]string(nil), in[i].GuestIPAddresses...)
|
||||
out[i].TriggeredAlarms = append([]vmware.InventoryAlarm(nil), in[i].TriggeredAlarms...)
|
||||
out[i].RecentTasks = append([]vmware.InventoryTask(nil), in[i].RecentTasks...)
|
||||
out[i].RecentEvents = append([]vmware.InventoryEvent(nil), in[i].RecentEvents...)
|
||||
out[i].Metrics = cloneVMwareInventoryMetrics(in[i].Metrics)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneVMwareInventoryDatastores(in []vmware.InventoryDatastore) []vmware.InventoryDatastore {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]vmware.InventoryDatastore, len(in))
|
||||
for i := range in {
|
||||
out[i] = in[i]
|
||||
out[i].HostIDs = append([]string(nil), in[i].HostIDs...)
|
||||
out[i].HostNames = append([]string(nil), in[i].HostNames...)
|
||||
out[i].VMIDs = append([]string(nil), in[i].VMIDs...)
|
||||
out[i].VMNames = append([]string(nil), in[i].VMNames...)
|
||||
out[i].Accessible = cloneBoolPtr(in[i].Accessible)
|
||||
out[i].MultipleHostAccess = cloneBoolPtr(in[i].MultipleHostAccess)
|
||||
out[i].TriggeredAlarms = append([]vmware.InventoryAlarm(nil), in[i].TriggeredAlarms...)
|
||||
out[i].RecentTasks = append([]vmware.InventoryTask(nil), in[i].RecentTasks...)
|
||||
out[i].RecentEvents = append([]vmware.InventoryEvent(nil), in[i].RecentEvents...)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneVMwareInventoryMetrics(in *vmware.InventoryMetrics) *vmware.InventoryMetrics {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := *in
|
||||
out.CPUPercent = cloneFloat64Ptr(in.CPUPercent)
|
||||
out.MemoryPercent = cloneFloat64Ptr(in.MemoryPercent)
|
||||
out.MemoryUsedBytes = cloneInt64Ptr(in.MemoryUsedBytes)
|
||||
out.MemoryTotalBytes = cloneInt64Ptr(in.MemoryTotalBytes)
|
||||
out.NetInBytesPerSecond = cloneFloat64Ptr(in.NetInBytesPerSecond)
|
||||
out.NetOutBytesPerSecond = cloneFloat64Ptr(in.NetOutBytesPerSecond)
|
||||
out.DiskReadBytesPerSecond = cloneFloat64Ptr(in.DiskReadBytesPerSecond)
|
||||
out.DiskWriteBytesPerSecond = cloneFloat64Ptr(in.DiskWriteBytesPerSecond)
|
||||
return &out
|
||||
}
|
||||
|
||||
func cloneFloat64Ptr(n *float64) *float64 {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
value := *n
|
||||
return &value
|
||||
}
|
||||
@@ -18,8 +18,7 @@ var (
|
||||
dataMu sync.RWMutex
|
||||
setEnabledMu sync.Mutex
|
||||
updateLoopMu sync.Mutex
|
||||
mockData = models.EmptyStateSnapshot()
|
||||
mockAlerts []models.Alert
|
||||
mockGraph = emptyFixtureGraph()
|
||||
mockConfig = DefaultConfig
|
||||
enabled atomic.Bool
|
||||
updateTicker *time.Ticker
|
||||
@@ -105,12 +104,11 @@ func readMockEnv(name string) (string, bool) {
|
||||
|
||||
func enableMockMode(fromInit bool) {
|
||||
config := LoadMockConfig()
|
||||
now := time.Now()
|
||||
|
||||
dataMu.Lock()
|
||||
mockConfig = config
|
||||
mockData = GenerateMockData(config)
|
||||
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
mockData.LastUpdate = time.Now()
|
||||
mockGraph = buildFixtureGraph(config, now)
|
||||
enabled.Store(true)
|
||||
dataMu.Unlock()
|
||||
startUpdateLoop()
|
||||
@@ -143,8 +141,7 @@ func disableMockMode() {
|
||||
stopUpdateLoop()
|
||||
|
||||
dataMu.Lock()
|
||||
mockData = models.EmptyStateSnapshot()
|
||||
mockAlerts = nil
|
||||
mockGraph = emptyFixtureGraph()
|
||||
dataMu.Unlock()
|
||||
|
||||
log.Info().Msg("mock mode disabled")
|
||||
@@ -209,8 +206,7 @@ func updateMetrics(cfg MockConfig) {
|
||||
dataMu.Lock()
|
||||
defer dataMu.Unlock()
|
||||
|
||||
UpdateMetrics(&mockData, cfg)
|
||||
mockData.LastUpdate = time.Now()
|
||||
mockGraph.UpdateMetrics(cfg, time.Now())
|
||||
}
|
||||
|
||||
// GetConfig returns the current mock configuration.
|
||||
@@ -412,9 +408,7 @@ func SetMockConfig(cfg MockConfig) {
|
||||
dataMu.Lock()
|
||||
mockConfig = normalized
|
||||
if enabled.Load() {
|
||||
mockData = GenerateMockData(normalized)
|
||||
mockAlerts = GenerateAlertHistory(mockData.Nodes, mockData.VMs, mockData.Containers)
|
||||
mockData.LastUpdate = time.Now()
|
||||
mockGraph = buildFixtureGraph(normalized, time.Now())
|
||||
}
|
||||
dataMu.Unlock()
|
||||
|
||||
@@ -443,7 +437,7 @@ func GetMockState() models.StateSnapshot {
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
return cloneState(mockData)
|
||||
return cloneState(mockGraph.State)
|
||||
}
|
||||
|
||||
// UpdateAlertSnapshots replaces the active and recently resolved alert lists used for mock mode.
|
||||
@@ -454,25 +448,7 @@ func UpdateAlertSnapshots(active []alerts.Alert, resolved []models.ResolvedAlert
|
||||
dataMu.Lock()
|
||||
defer dataMu.Unlock()
|
||||
|
||||
converted := make([]models.Alert, 0, len(active))
|
||||
for _, alert := range active {
|
||||
converted = append(converted, models.Alert{
|
||||
ID: alert.ID,
|
||||
Type: alert.Type,
|
||||
Level: string(alert.Level),
|
||||
ResourceID: alert.ResourceID,
|
||||
ResourceName: alert.ResourceName,
|
||||
Node: alert.Node,
|
||||
Instance: alert.Instance,
|
||||
Message: alert.Message,
|
||||
Value: alert.Value,
|
||||
Threshold: alert.Threshold,
|
||||
StartTime: alert.StartTime,
|
||||
Acknowledged: alert.Acknowledged,
|
||||
})
|
||||
}
|
||||
mockData.ActiveAlerts = converted
|
||||
mockData.RecentlyResolved = append([]models.ResolvedAlert(nil), resolved...)
|
||||
mockGraph.UpdateAlertSnapshots(active, resolved)
|
||||
}
|
||||
|
||||
// GetMockAlertHistory returns mock alert history.
|
||||
@@ -484,10 +460,10 @@ func GetMockAlertHistory(limit int) []models.Alert {
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
if limit > 0 && limit < len(mockAlerts) {
|
||||
return append([]models.Alert(nil), mockAlerts[:limit]...)
|
||||
if limit > 0 && limit < len(mockGraph.AlertHistory) {
|
||||
return append([]models.Alert(nil), mockGraph.AlertHistory[:limit]...)
|
||||
}
|
||||
return append([]models.Alert(nil), mockAlerts...)
|
||||
return append([]models.Alert(nil), mockGraph.AlertHistory...)
|
||||
}
|
||||
|
||||
func cloneState(state models.StateSnapshot) models.StateSnapshot {
|
||||
|
||||
@@ -30,8 +30,7 @@ func resetMockIntegrationState(t *testing.T) {
|
||||
|
||||
stopUpdateLoopLocked()
|
||||
dataMu.Lock()
|
||||
mockData = models.EmptyStateSnapshot()
|
||||
mockAlerts = nil
|
||||
mockGraph = emptyFixtureGraph()
|
||||
mockConfig = DefaultConfig
|
||||
dataMu.Unlock()
|
||||
enabled.Store(false)
|
||||
@@ -43,8 +42,7 @@ func resetMockIntegrationState(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
stopUpdateLoopLocked()
|
||||
dataMu.Lock()
|
||||
mockData = models.EmptyStateSnapshot()
|
||||
mockAlerts = nil
|
||||
mockGraph = emptyFixtureGraph()
|
||||
mockConfig = DefaultConfig
|
||||
dataMu.Unlock()
|
||||
enabled.Store(false)
|
||||
@@ -185,10 +183,12 @@ func TestDisableMockModeResetsToNormalizedEmptySnapshot(t *testing.T) {
|
||||
|
||||
enabled.Store(true)
|
||||
dataMu.Lock()
|
||||
mockData = models.StateSnapshot{
|
||||
Nodes: []models.Node{{ID: "node-1", Name: "node-1"}},
|
||||
mockGraph = FixtureGraph{
|
||||
State: models.StateSnapshot{
|
||||
Nodes: []models.Node{{ID: "node-1", Name: "node-1"}},
|
||||
},
|
||||
AlertHistory: []models.Alert{{ID: "alert-1"}},
|
||||
}
|
||||
mockAlerts = []models.Alert{{ID: "alert-1"}}
|
||||
dataMu.Unlock()
|
||||
|
||||
disableMockMode()
|
||||
@@ -200,17 +200,17 @@ func TestDisableMockModeResetsToNormalizedEmptySnapshot(t *testing.T) {
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
if mockAlerts != nil {
|
||||
t.Fatalf("expected alert cache to be cleared, got %+v", mockAlerts)
|
||||
if mockGraph.AlertHistory != nil {
|
||||
t.Fatalf("expected alert cache to be cleared, got %+v", mockGraph.AlertHistory)
|
||||
}
|
||||
if mockData.Nodes == nil {
|
||||
if mockGraph.State.Nodes == nil {
|
||||
t.Fatal("expected nodes slice to be normalized")
|
||||
}
|
||||
if mockData.ConnectionHealth == nil {
|
||||
if mockGraph.State.ConnectionHealth == nil {
|
||||
t.Fatal("expected connection health map to be normalized")
|
||||
}
|
||||
if len(mockData.Nodes) != 0 {
|
||||
t.Fatalf("expected mock data to reset to empty snapshot, got %d nodes", len(mockData.Nodes))
|
||||
if len(mockGraph.State.Nodes) != 0 {
|
||||
t.Fatalf("expected mock data to reset to empty snapshot, got %d nodes", len(mockGraph.State.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,9 @@ func TestSetMockConfigOnlyRegeneratesWhenEnabled(t *testing.T) {
|
||||
resetMockIntegrationState(t)
|
||||
|
||||
dataMu.Lock()
|
||||
mockData = models.StateSnapshot{Nodes: []models.Node{{ID: "node-existing", Name: "existing"}}}
|
||||
mockGraph = FixtureGraph{
|
||||
State: models.StateSnapshot{Nodes: []models.Node{{ID: "node-existing", Name: "existing"}}},
|
||||
}
|
||||
dataMu.Unlock()
|
||||
|
||||
cfg := DefaultConfig
|
||||
@@ -234,7 +236,7 @@ func TestSetMockConfigOnlyRegeneratesWhenEnabled(t *testing.T) {
|
||||
}
|
||||
|
||||
dataMu.RLock()
|
||||
nodesWhenDisabled := len(mockData.Nodes)
|
||||
nodesWhenDisabled := len(mockGraph.State.Nodes)
|
||||
dataMu.RUnlock()
|
||||
if nodesWhenDisabled != 1 {
|
||||
t.Fatalf("expected data unchanged while disabled, got %d nodes", nodesWhenDisabled)
|
||||
@@ -247,13 +249,13 @@ func TestSetMockConfigOnlyRegeneratesWhenEnabled(t *testing.T) {
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
|
||||
if len(mockData.Nodes) != 3 {
|
||||
t.Fatalf("expected regenerated node count, got %d", len(mockData.Nodes))
|
||||
if len(mockGraph.State.Nodes) != 3 {
|
||||
t.Fatalf("expected regenerated node count, got %d", len(mockGraph.State.Nodes))
|
||||
}
|
||||
if mockData.LastUpdate.IsZero() {
|
||||
if mockGraph.State.LastUpdate.IsZero() {
|
||||
t.Fatalf("expected regenerated state to update last-update timestamp")
|
||||
}
|
||||
if len(mockAlerts) == 0 {
|
||||
if len(mockGraph.AlertHistory) == 0 {
|
||||
t.Fatalf("expected regenerated alert history when enabled")
|
||||
}
|
||||
}
|
||||
@@ -271,15 +273,15 @@ func TestUpdateMetricsGuardAndTimestamp(t *testing.T) {
|
||||
cfg.RandomMetrics = false
|
||||
|
||||
dataMu.Lock()
|
||||
mockData = GenerateMockData(cfg)
|
||||
mockData.LastUpdate = time.Time{}
|
||||
mockGraph = buildFixtureGraph(cfg, time.Time{})
|
||||
mockGraph.State.LastUpdate = time.Time{}
|
||||
dataMu.Unlock()
|
||||
|
||||
enabled.Store(false)
|
||||
updateMetrics(cfg)
|
||||
|
||||
dataMu.RLock()
|
||||
lastWhenDisabled := mockData.LastUpdate
|
||||
lastWhenDisabled := mockGraph.State.LastUpdate
|
||||
dataMu.RUnlock()
|
||||
if !lastWhenDisabled.IsZero() {
|
||||
t.Fatalf("expected no update while disabled")
|
||||
@@ -289,7 +291,7 @@ func TestUpdateMetricsGuardAndTimestamp(t *testing.T) {
|
||||
updateMetrics(cfg)
|
||||
|
||||
dataMu.RLock()
|
||||
lastWhenEnabled := mockData.LastUpdate
|
||||
lastWhenEnabled := mockGraph.State.LastUpdate
|
||||
dataMu.RUnlock()
|
||||
if lastWhenEnabled.IsZero() {
|
||||
t.Fatalf("expected update timestamp while enabled")
|
||||
@@ -352,11 +354,11 @@ func TestUpdateAlertSnapshotsAndHistoryAccessors(t *testing.T) {
|
||||
UpdateAlertSnapshots(active, resolved)
|
||||
|
||||
dataMu.RLock()
|
||||
if len(mockData.ActiveAlerts) != 1 {
|
||||
if len(mockGraph.State.ActiveAlerts) != 1 {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("expected one active alert snapshot, got %d", len(mockData.ActiveAlerts))
|
||||
t.Fatalf("expected one active alert snapshot, got %d", len(mockGraph.State.ActiveAlerts))
|
||||
}
|
||||
snapshot := mockData.ActiveAlerts[0]
|
||||
snapshot := mockGraph.State.ActiveAlerts[0]
|
||||
if snapshot.Level != "critical" {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("expected level conversion to string, got %q", snapshot.Level)
|
||||
@@ -365,9 +367,9 @@ func TestUpdateAlertSnapshotsAndHistoryAccessors(t *testing.T) {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("unexpected converted active alert snapshot: %+v", snapshot)
|
||||
}
|
||||
if len(mockData.RecentlyResolved) != 1 || mockData.RecentlyResolved[0].ID != "r-1" {
|
||||
if len(mockGraph.State.RecentlyResolved) != 1 || mockGraph.State.RecentlyResolved[0].ID != "r-1" {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("unexpected resolved snapshot data: %+v", mockData.RecentlyResolved)
|
||||
t.Fatalf("unexpected resolved snapshot data: %+v", mockGraph.State.RecentlyResolved)
|
||||
}
|
||||
dataMu.RUnlock()
|
||||
|
||||
@@ -375,18 +377,18 @@ func TestUpdateAlertSnapshotsAndHistoryAccessors(t *testing.T) {
|
||||
resolved[0].ID = "mutated"
|
||||
|
||||
dataMu.RLock()
|
||||
if mockData.ActiveAlerts[0].Message != "CPU critical" {
|
||||
if mockGraph.State.ActiveAlerts[0].Message != "CPU critical" {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("expected active alert snapshot to be independent from source slice")
|
||||
}
|
||||
if mockData.RecentlyResolved[0].ID != "r-1" {
|
||||
if mockGraph.State.RecentlyResolved[0].ID != "r-1" {
|
||||
dataMu.RUnlock()
|
||||
t.Fatalf("expected resolved alert snapshot to be copied")
|
||||
}
|
||||
dataMu.RUnlock()
|
||||
|
||||
dataMu.Lock()
|
||||
mockAlerts = []models.Alert{{ID: "h-1"}, {ID: "h-2"}, {ID: "h-3"}}
|
||||
mockGraph.AlertHistory = []models.Alert{{ID: "h-1"}, {ID: "h-2"}, {ID: "h-3"}}
|
||||
dataMu.Unlock()
|
||||
|
||||
enabled.Store(false)
|
||||
@@ -406,7 +408,7 @@ func TestUpdateAlertSnapshotsAndHistoryAccessors(t *testing.T) {
|
||||
|
||||
dataMu.RLock()
|
||||
defer dataMu.RUnlock()
|
||||
if mockAlerts[0].ID != "h-1" {
|
||||
if mockGraph.AlertHistory[0].ID != "h-1" {
|
||||
t.Fatalf("expected history accessor to return defensive copies")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,32 +59,11 @@ func DefaultPlatformFixtures() PlatformFixtures {
|
||||
}
|
||||
|
||||
func DefaultTrueNASConnectionFixture() TrueNASConnectionFixture {
|
||||
fixtures := DefaultPlatformFixtures().TrueNAS
|
||||
collectedAt := trueNASCollectedAt(fixtures)
|
||||
host := strings.TrimSpace(fixtures.System.Hostname)
|
||||
|
||||
return TrueNASConnectionFixture{
|
||||
ID: "truenas-mock-1",
|
||||
Name: "Archive NAS",
|
||||
Host: host,
|
||||
Port: 443,
|
||||
APIKey: "mock-truenas-api-key",
|
||||
UseHTTPS: true,
|
||||
Enabled: true,
|
||||
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
|
||||
CollectedAt: collectedAt,
|
||||
ResourceID: host,
|
||||
Systems: 1,
|
||||
StoragePools: len(fixtures.Pools),
|
||||
Datasets: len(fixtures.Datasets),
|
||||
Apps: len(fixtures.Apps),
|
||||
Disks: len(fixtures.Disks),
|
||||
RecoveryArtifacts: len(fixtures.ZFSSnapshots) + len(fixtures.ReplicationTasks),
|
||||
}
|
||||
return defaultTrueNASConnectionFixture(GetPlatformFixtures())
|
||||
}
|
||||
|
||||
func DefaultVMwareConnectionFixture() VMwareConnectionFixture {
|
||||
fixtures := DefaultPlatformFixtures().VMware
|
||||
fixtures := GetPlatformFixtures().VMware
|
||||
|
||||
return VMwareConnectionFixture{
|
||||
ID: strings.TrimSpace(fixtures.ConnectionID),
|
||||
@@ -103,12 +82,38 @@ func DefaultVMwareConnectionFixture() VMwareConnectionFixture {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTrueNASConnectionFixture(fixtures PlatformFixtures) TrueNASConnectionFixture {
|
||||
snapshot := fixtures.TrueNAS
|
||||
collectedAt := trueNASCollectedAt(snapshot)
|
||||
host := strings.TrimSpace(snapshot.System.Hostname)
|
||||
|
||||
return TrueNASConnectionFixture{
|
||||
ID: "truenas-mock-1",
|
||||
Name: "Archive NAS",
|
||||
Host: host,
|
||||
Port: 443,
|
||||
APIKey: "mock-truenas-api-key",
|
||||
UseHTTPS: true,
|
||||
Enabled: true,
|
||||
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
|
||||
CollectedAt: collectedAt,
|
||||
ResourceID: host,
|
||||
Systems: 1,
|
||||
StoragePools: len(snapshot.Pools),
|
||||
Datasets: len(snapshot.Datasets),
|
||||
Apps: len(snapshot.Apps),
|
||||
Disks: len(snapshot.Disks),
|
||||
RecoveryArtifacts: len(snapshot.ZFSSnapshots) + len(snapshot.ReplicationTasks),
|
||||
}
|
||||
}
|
||||
|
||||
func SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
|
||||
fixtures := GetPlatformFixtures()
|
||||
switch normalizePlatformSource(source) {
|
||||
case unifiedresources.SourceTrueNAS:
|
||||
return truenas.FixtureRecords(DefaultPlatformFixtures().TrueNAS)
|
||||
return truenas.FixtureRecords(fixtures.TrueNAS)
|
||||
case unifiedresources.SourceVMware:
|
||||
return vmware.FixtureRecords(DefaultPlatformFixtures().VMware)
|
||||
return vmware.FixtureRecords(fixtures.VMware)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -126,23 +131,25 @@ func UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
||||
return nil, time.Time{}
|
||||
}
|
||||
|
||||
fixtures := DefaultPlatformFixtures()
|
||||
snapshot := GetMockState()
|
||||
return CurrentFixtureGraph().UnifiedResourceSnapshot()
|
||||
}
|
||||
|
||||
func (g FixtureGraph) UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
||||
registry := unifiedresources.NewRegistry(nil)
|
||||
registry.IngestSnapshot(unifiedresources.SnapshotWithoutSources(snapshot, PlatformOwnedSources()))
|
||||
registry.IngestSnapshot(unifiedresources.SnapshotWithoutSources(g.State, PlatformOwnedSources()))
|
||||
|
||||
for _, source := range PlatformOwnedSources() {
|
||||
records := SupplementalRecords(source)
|
||||
records := g.SupplementalRecords(source)
|
||||
if len(records) == 0 {
|
||||
continue
|
||||
}
|
||||
registry.IngestRecords(source, records)
|
||||
}
|
||||
|
||||
freshness := snapshot.LastUpdate
|
||||
freshness := g.State.LastUpdate
|
||||
for _, candidate := range []time.Time{
|
||||
trueNASCollectedAt(fixtures.TrueNAS),
|
||||
fixtures.VMware.CollectedAt,
|
||||
trueNASCollectedAt(g.PlatformFixtures.TrueNAS),
|
||||
g.PlatformFixtures.VMware.CollectedAt,
|
||||
} {
|
||||
if candidate.IsZero() {
|
||||
continue
|
||||
@@ -155,6 +162,17 @@ func UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
|
||||
return registry.List(), freshness
|
||||
}
|
||||
|
||||
func (g FixtureGraph) SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
|
||||
switch normalizePlatformSource(source) {
|
||||
case unifiedresources.SourceTrueNAS:
|
||||
return truenas.FixtureRecords(g.PlatformFixtures.TrueNAS)
|
||||
case unifiedresources.SourceVMware:
|
||||
return vmware.FixtureRecords(g.PlatformFixtures.VMware)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func trueNASCollectedAt(fixtures truenas.FixtureSnapshot) time.Time {
|
||||
if !fixtures.CollectedAt.IsZero() {
|
||||
return fixtures.CollectedAt
|
||||
|
||||
@@ -11,6 +11,20 @@ func TestUnifiedResourceSnapshotIncludesPlatformFixtures(t *testing.T) {
|
||||
SetEnabled(true)
|
||||
t.Cleanup(func() { SetEnabled(previous) })
|
||||
|
||||
graph := CurrentFixtureGraph()
|
||||
legacyName := ""
|
||||
if len(graph.State.VMs) > 0 {
|
||||
legacyName = graph.State.VMs[0].Name
|
||||
} else if len(graph.State.Containers) > 0 {
|
||||
legacyName = graph.State.Containers[0].Name
|
||||
}
|
||||
if legacyName == "" {
|
||||
t.Fatal("expected canonical mock graph to include at least one legacy resource name")
|
||||
}
|
||||
if len(graph.PlatformFixtures.VMware.Hosts) == 0 {
|
||||
t.Fatal("expected canonical mock graph to include VMware host fixtures")
|
||||
}
|
||||
|
||||
resources, freshness := UnifiedResourceSnapshot()
|
||||
if len(resources) == 0 {
|
||||
t.Fatal("expected unified resources in mock mode")
|
||||
@@ -20,9 +34,9 @@ func TestUnifiedResourceSnapshotIncludesPlatformFixtures(t *testing.T) {
|
||||
}
|
||||
|
||||
wantNames := map[string]bool{
|
||||
"truenas-main": false,
|
||||
"esxi-01.lab.local": false,
|
||||
"orders-api-01": false,
|
||||
graph.PlatformFixtures.TrueNAS.System.Hostname: false,
|
||||
graph.PlatformFixtures.VMware.Hosts[0].Name: false,
|
||||
legacyName: false,
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if _, ok := wantNames[resource.Name]; ok {
|
||||
|
||||
@@ -5,36 +5,30 @@ import (
|
||||
"encoding/hex"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
|
||||
)
|
||||
|
||||
var (
|
||||
mockRecoveryPointsOnce sync.Once
|
||||
mockRecoveryPoints []recovery.RecoveryPoint
|
||||
)
|
||||
|
||||
func GetMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
mockRecoveryPointsOnce.Do(func() {
|
||||
mockRecoveryPoints = generateMockRecoveryPoints()
|
||||
})
|
||||
return cloneRecoveryPoints(mockRecoveryPoints)
|
||||
if !IsMockEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CurrentFixtureGraph().RecoveryPoints()
|
||||
}
|
||||
|
||||
func generateMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
func (g FixtureGraph) RecoveryPoints() []recovery.RecoveryPoint {
|
||||
return generateMockRecoveryPoints(g.State, g.PlatformFixtures)
|
||||
}
|
||||
|
||||
func generateMockRecoveryPoints(snapshot models.StateSnapshot, fixtures PlatformFixtures) []recovery.RecoveryPoint {
|
||||
// Anchor timestamps to midnight UTC so results are stable across requests
|
||||
// (pagination, sorting) while still staying within the "last 30 days" window.
|
||||
anchor := time.Now().UTC().Truncate(24 * time.Hour)
|
||||
|
||||
clusters := []struct {
|
||||
id string
|
||||
name string
|
||||
}{
|
||||
{id: "k8s-mock-cluster-1", name: "dev-cluster"},
|
||||
{id: "k8s-mock-cluster-2", name: "prod-cluster"},
|
||||
}
|
||||
clusters := recoveryKubernetesClusters(snapshot)
|
||||
|
||||
points := make([]recovery.RecoveryPoint, 0, 64)
|
||||
|
||||
@@ -42,17 +36,7 @@ func generateMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
int64Ptr := func(v int64) *int64 { return &v }
|
||||
|
||||
// Kubernetes PVC snapshot subjects: 3 PVCs with multiple points each (success/running/failed).
|
||||
k8sPVCSubjects := []struct {
|
||||
clusterID string
|
||||
clusterName string
|
||||
namespace string
|
||||
pvc string
|
||||
class string
|
||||
}{
|
||||
{clusterID: clusters[0].id, clusterName: clusters[0].name, namespace: "default", pvc: "postgres-pvc", class: "csi-ceph-rbd"},
|
||||
{clusterID: clusters[0].id, clusterName: clusters[0].name, namespace: "monitoring", pvc: "prometheus-pvc", class: "csi-local-path"},
|
||||
{clusterID: clusters[1].id, clusterName: clusters[1].name, namespace: "media", pvc: "nextcloud-pvc", class: "csi-ebs-gp3"},
|
||||
}
|
||||
k8sPVCSubjects := recoveryKubernetesPVCSubjects(clusters)
|
||||
for si, s := range k8sPVCSubjects {
|
||||
for i := 0; i < 6; i++ {
|
||||
ageDays := 2 + (si*7+i*4)%28
|
||||
@@ -249,19 +233,7 @@ func generateMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
|
||||
// Proxmox: a few guest subjects with multiple backup points each.
|
||||
// This keeps the Backups page platform-agnostic while still showing familiar PVE/PBS-like artifacts.
|
||||
proxmoxSubjects := []struct {
|
||||
instance string
|
||||
node string
|
||||
vmid int
|
||||
typ string // "vm" or "lxc"
|
||||
name string
|
||||
storage string
|
||||
isPBS bool
|
||||
}{
|
||||
{instance: "pve-1", node: "pve-a", vmid: 101, typ: "vm", name: "web-01", storage: "pbs-1", isPBS: true},
|
||||
{instance: "pve-1", node: "pve-a", vmid: 102, typ: "vm", name: "db-01", storage: "local-zfs", isPBS: false},
|
||||
{instance: "pve-2", node: "pve-b", vmid: 201, typ: "lxc", name: "cache-01", storage: "pbs-1", isPBS: true},
|
||||
}
|
||||
proxmoxSubjects := recoveryProxmoxSubjects(snapshot)
|
||||
for si, s := range proxmoxSubjects {
|
||||
for i := 0; i < 5; i++ {
|
||||
ageDays := 2 + (si*8+i*6)%27
|
||||
@@ -376,11 +348,11 @@ func generateMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
}
|
||||
|
||||
// TrueNAS: 3 dataset subjects with multiple points over time.
|
||||
truenasConnection := DefaultTrueNASConnectionFixture()
|
||||
truenasConnection := defaultTrueNASConnectionFixture(fixtures)
|
||||
truenasConnID := truenasConnection.ID
|
||||
truenasHost := truenasConnection.Host
|
||||
truenasDatasets := make([]string, 0, 3)
|
||||
for _, dataset := range DefaultPlatformFixtures().TrueNAS.Datasets {
|
||||
for _, dataset := range fixtures.TrueNAS.Datasets {
|
||||
name := strings.TrimSpace(dataset.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
@@ -586,6 +558,175 @@ func generateMockRecoveryPoints() []recovery.RecoveryPoint {
|
||||
return points
|
||||
}
|
||||
|
||||
type mockRecoveryCluster struct {
|
||||
id string
|
||||
name string
|
||||
}
|
||||
|
||||
type mockRecoveryPVCSubject struct {
|
||||
clusterID string
|
||||
clusterName string
|
||||
namespace string
|
||||
pvc string
|
||||
class string
|
||||
}
|
||||
|
||||
type mockProxmoxRecoverySubject struct {
|
||||
instance string
|
||||
node string
|
||||
vmid int
|
||||
typ string
|
||||
name string
|
||||
storage string
|
||||
isPBS bool
|
||||
}
|
||||
|
||||
func recoveryKubernetesClusters(snapshot models.StateSnapshot) []mockRecoveryCluster {
|
||||
clusters := make([]mockRecoveryCluster, 0, len(snapshot.KubernetesClusters))
|
||||
seen := make(map[string]struct{}, len(snapshot.KubernetesClusters))
|
||||
|
||||
for _, cluster := range snapshot.KubernetesClusters {
|
||||
id := strings.TrimSpace(cluster.ID)
|
||||
if id == "" {
|
||||
id = strings.TrimSpace(cluster.AgentID)
|
||||
}
|
||||
name := strings.TrimSpace(cluster.DisplayName)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(cluster.CustomDisplayName)
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(cluster.Name)
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(cluster.Context)
|
||||
}
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(cluster.Server)
|
||||
}
|
||||
if id == "" && name == "" {
|
||||
continue
|
||||
}
|
||||
if id == "" {
|
||||
id = rpStableID("k8s", "cluster", name)
|
||||
}
|
||||
if name == "" {
|
||||
name = id
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
clusters = append(clusters, mockRecoveryCluster{id: id, name: name})
|
||||
}
|
||||
|
||||
if len(clusters) > 0 {
|
||||
return clusters
|
||||
}
|
||||
|
||||
return []mockRecoveryCluster{
|
||||
{id: "k8s-mock-cluster-1", name: "dev-cluster"},
|
||||
{id: "k8s-mock-cluster-2", name: "prod-cluster"},
|
||||
}
|
||||
}
|
||||
|
||||
func recoveryKubernetesPVCSubjects(clusters []mockRecoveryCluster) []mockRecoveryPVCSubject {
|
||||
if len(clusters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
templates := []struct {
|
||||
namespace string
|
||||
pvc string
|
||||
class string
|
||||
}{
|
||||
{namespace: "default", pvc: "postgres-pvc", class: "csi-ceph-rbd"},
|
||||
{namespace: "monitoring", pvc: "prometheus-pvc", class: "csi-local-path"},
|
||||
{namespace: "media", pvc: "nextcloud-pvc", class: "csi-ebs-gp3"},
|
||||
}
|
||||
|
||||
subjects := make([]mockRecoveryPVCSubject, 0, len(templates))
|
||||
for i := 0; i < len(templates); i++ {
|
||||
cluster := clusters[i%len(clusters)]
|
||||
template := templates[i]
|
||||
subjects = append(subjects, mockRecoveryPVCSubject{
|
||||
clusterID: cluster.id,
|
||||
clusterName: cluster.name,
|
||||
namespace: template.namespace,
|
||||
pvc: template.pvc,
|
||||
class: template.class,
|
||||
})
|
||||
}
|
||||
|
||||
return subjects
|
||||
}
|
||||
|
||||
func recoveryProxmoxSubjects(snapshot models.StateSnapshot) []mockProxmoxRecoverySubject {
|
||||
subjects := make([]mockProxmoxRecoverySubject, 0, 3)
|
||||
seen := map[string]struct{}{}
|
||||
|
||||
remoteStorage := "pbs-1"
|
||||
if len(snapshot.PBSInstances) > 0 {
|
||||
if candidate := strings.TrimSpace(snapshot.PBSInstances[0].Name); candidate != "" {
|
||||
remoteStorage = candidate
|
||||
}
|
||||
}
|
||||
localStorage := "local-zfs"
|
||||
|
||||
appendSubject := func(instance, node string, vmid int, typ, name string, preferPBS bool) {
|
||||
if len(subjects) >= 3 {
|
||||
return
|
||||
}
|
||||
key := strings.TrimSpace(typ) + ":" + strings.TrimSpace(instance) + ":" + strings.TrimSpace(node) + ":" + rpItoa(vmid)
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
|
||||
storage := localStorage
|
||||
isPBS := false
|
||||
if preferPBS {
|
||||
storage = remoteStorage
|
||||
isPBS = true
|
||||
}
|
||||
|
||||
subjects = append(subjects, mockProxmoxRecoverySubject{
|
||||
instance: strings.TrimSpace(instance),
|
||||
node: strings.TrimSpace(node),
|
||||
vmid: vmid,
|
||||
typ: strings.TrimSpace(typ),
|
||||
name: strings.TrimSpace(name),
|
||||
storage: storage,
|
||||
isPBS: isPBS,
|
||||
})
|
||||
}
|
||||
|
||||
for index, vm := range snapshot.VMs {
|
||||
appendSubject(vm.Instance, vm.Node, vm.VMID, "vm", firstNonEmptyTrimmed(vm.Name, vm.ID), index%2 == 0)
|
||||
}
|
||||
for index, container := range snapshot.Containers {
|
||||
appendSubject(container.Instance, container.Node, container.VMID, "lxc", firstNonEmptyTrimmed(container.Name, container.ID), index%2 == 0)
|
||||
}
|
||||
|
||||
if len(subjects) > 0 {
|
||||
return subjects
|
||||
}
|
||||
|
||||
return []mockProxmoxRecoverySubject{
|
||||
{instance: "pve-1", node: "pve-a", vmid: 101, typ: "vm", name: "web-01", storage: "pbs-1", isPBS: true},
|
||||
{instance: "pve-1", node: "pve-a", vmid: 102, typ: "vm", name: "db-01", storage: "local-zfs", isPBS: false},
|
||||
{instance: "pve-2", node: "pve-b", vmid: 201, typ: "lxc", name: "cache-01", storage: "pbs-1", isPBS: true},
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmptyTrimmed(values ...string) string {
|
||||
for _, value := range values {
|
||||
if trimmed := strings.TrimSpace(value); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func cloneRecoveryPoints(src []recovery.RecoveryPoint) []recovery.RecoveryPoint {
|
||||
if len(src) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
|
||||
)
|
||||
|
||||
func TestCurrentFixtureGraphReturnsDefensiveCopies(t *testing.T) {
|
||||
previous := IsMockEnabled()
|
||||
SetEnabled(true)
|
||||
t.Cleanup(func() { SetEnabled(previous) })
|
||||
|
||||
graph := CurrentFixtureGraph()
|
||||
if len(graph.State.Nodes) == 0 {
|
||||
t.Fatal("expected legacy snapshot nodes in canonical mock graph")
|
||||
}
|
||||
if len(graph.PlatformFixtures.VMware.Hosts) == 0 {
|
||||
t.Fatal("expected VMware fixtures in canonical mock graph")
|
||||
}
|
||||
|
||||
originalNodeName := graph.State.Nodes[0].Name
|
||||
originalHostName := graph.PlatformFixtures.VMware.Hosts[0].Name
|
||||
|
||||
graph.State.Nodes[0].Name = "mutated-node"
|
||||
graph.PlatformFixtures.VMware.Hosts[0].Name = "mutated-host"
|
||||
if len(graph.AlertHistory) > 0 {
|
||||
graph.AlertHistory[0].ID = "mutated-alert"
|
||||
}
|
||||
|
||||
current := CurrentFixtureGraph()
|
||||
if current.State.Nodes[0].Name != originalNodeName {
|
||||
t.Fatalf("expected canonical graph to protect state snapshot, got %q", current.State.Nodes[0].Name)
|
||||
}
|
||||
if current.PlatformFixtures.VMware.Hosts[0].Name != originalHostName {
|
||||
t.Fatalf("expected canonical graph to protect platform fixtures, got %q", current.PlatformFixtures.VMware.Hosts[0].Name)
|
||||
}
|
||||
if len(graph.AlertHistory) > 0 && len(current.AlertHistory) > 0 && current.AlertHistory[0].ID == "mutated-alert" {
|
||||
t.Fatal("expected canonical graph to protect alert history")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMockRecoveryPointsDerivesSubjectsFromCurrentGraph(t *testing.T) {
|
||||
previous := IsMockEnabled()
|
||||
SetEnabled(true)
|
||||
t.Cleanup(func() { SetEnabled(previous) })
|
||||
|
||||
graph := CurrentFixtureGraph()
|
||||
if len(graph.State.KubernetesClusters) == 0 {
|
||||
t.Fatal("expected Kubernetes clusters in canonical mock graph")
|
||||
}
|
||||
if len(graph.State.VMs) == 0 && len(graph.State.Containers) == 0 {
|
||||
t.Fatal("expected Proxmox guests in canonical mock graph")
|
||||
}
|
||||
if len(graph.PlatformFixtures.TrueNAS.Datasets) == 0 {
|
||||
t.Fatal("expected TrueNAS datasets in canonical mock graph")
|
||||
}
|
||||
|
||||
clusterNames := make(map[string]struct{}, len(graph.State.KubernetesClusters))
|
||||
for _, cluster := range graph.State.KubernetesClusters {
|
||||
if name := firstNonEmptyTrimmed(cluster.DisplayName, cluster.CustomDisplayName, cluster.Name); name != "" {
|
||||
clusterNames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
guestNames := make(map[string]struct{}, len(graph.State.VMs)+len(graph.State.Containers))
|
||||
for _, guest := range graph.State.VMs {
|
||||
if guest.Name != "" {
|
||||
guestNames[guest.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, guest := range graph.State.Containers {
|
||||
if guest.Name != "" {
|
||||
guestNames[guest.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
datasetNames := make(map[string]struct{}, len(graph.PlatformFixtures.TrueNAS.Datasets))
|
||||
for _, dataset := range graph.PlatformFixtures.TrueNAS.Datasets {
|
||||
if dataset.Name != "" {
|
||||
datasetNames[dataset.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
points := GetMockRecoveryPoints()
|
||||
if len(points) == 0 {
|
||||
t.Fatal("expected mock recovery points")
|
||||
}
|
||||
|
||||
foundKubernetes := false
|
||||
foundProxmox := false
|
||||
foundTrueNAS := false
|
||||
|
||||
for _, point := range points {
|
||||
if point.SubjectRef == nil {
|
||||
continue
|
||||
}
|
||||
switch point.Provider {
|
||||
case recovery.ProviderKubernetes:
|
||||
if point.SubjectRef.Type == "k8s-cluster" {
|
||||
if _, ok := clusterNames[point.SubjectRef.Name]; ok {
|
||||
foundKubernetes = true
|
||||
}
|
||||
}
|
||||
case recovery.ProviderProxmoxPVE:
|
||||
if _, ok := guestNames[point.SubjectRef.Name]; ok {
|
||||
foundProxmox = true
|
||||
}
|
||||
case recovery.ProviderTrueNAS:
|
||||
if _, ok := datasetNames[point.SubjectRef.Name]; ok {
|
||||
foundTrueNAS = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundKubernetes {
|
||||
t.Fatal("expected recovery points to derive Kubernetes subjects from canonical mock graph")
|
||||
}
|
||||
if !foundProxmox {
|
||||
t.Fatal("expected recovery points to derive Proxmox subjects from canonical mock graph")
|
||||
}
|
||||
if !foundTrueNAS {
|
||||
t.Fatal("expected recovery points to derive TrueNAS subjects from canonical mock graph")
|
||||
}
|
||||
}
|
||||
@@ -875,7 +875,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
platformFixtures := mock.DefaultPlatformFixtures()
|
||||
platformFixtures := mock.GetPlatformFixtures()
|
||||
trueNASFixtures := platformFixtures.TrueNAS
|
||||
log.Debug().Int("pools", len(trueNASFixtures.Pools)).Int("datasets", len(trueNASFixtures.Datasets)).Msg("mock seeding: processing TrueNAS fixtures")
|
||||
|
||||
@@ -1007,7 +1007,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
|
||||
|
||||
// recordTrueNASFixturesMetrics records disk usage metrics for TrueNAS pools and datasets.
|
||||
func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time.Time) {
|
||||
fixtures := mock.DefaultPlatformFixtures().TrueNAS
|
||||
fixtures := mock.GetPlatformFixtures().TrueNAS
|
||||
|
||||
totalCap, totalUsed := int64(0), int64(0)
|
||||
for _, pool := range fixtures.Pools {
|
||||
@@ -1048,7 +1048,7 @@ func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time
|
||||
}
|
||||
|
||||
func recordVMwareFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time.Time) {
|
||||
snapshot := mock.DefaultPlatformFixtures().VMware
|
||||
snapshot := mock.GetPlatformFixtures().VMware
|
||||
datastoreUsage := vmwareDatastoreUsageByID(snapshot.Datastores)
|
||||
|
||||
for _, host := range snapshot.Hosts {
|
||||
@@ -1504,7 +1504,8 @@ func (m *Monitor) startMockMetricsSampler(ctx context.Context) {
|
||||
m.metricsHistory = NewMetricsHistory(maxPoints, seedDuration)
|
||||
m.mu.Unlock()
|
||||
|
||||
state := mock.GetMockState()
|
||||
graph := mock.CurrentFixtureGraph()
|
||||
state := graph.State
|
||||
log.Info().
|
||||
Int("nodes", len(state.Nodes)).
|
||||
Int("vms", len(state.VMs)).
|
||||
@@ -1532,7 +1533,7 @@ func (m *Monitor) startMockMetricsSampler(ctx context.Context) {
|
||||
if !mock.IsMockEnabled() {
|
||||
continue
|
||||
}
|
||||
recordMockStateToMetricsHistory(m.metricsHistory, nil, mock.GetMockState(), time.Now())
|
||||
recordMockStateToMetricsHistory(m.metricsHistory, nil, mock.CurrentFixtureGraph().State, time.Now())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -356,6 +356,86 @@ func TestSeedMockMetricsHistory_SeedsTrueNASMetricsStore(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeedMockMetricsHistory_UsesCanonicalMockFixtureGraphForLegacyAndProviderFixtures(t *testing.T) {
|
||||
previous := mock.IsMockEnabled()
|
||||
previousConfig := mock.GetConfig()
|
||||
t.Cleanup(func() {
|
||||
mock.SetEnabled(false)
|
||||
mock.SetMockConfig(previousConfig)
|
||||
if previous {
|
||||
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)
|
||||
|
||||
graph := mock.CurrentFixtureGraph()
|
||||
if len(graph.State.Nodes) == 0 {
|
||||
t.Fatal("expected canonical mock graph to include legacy nodes")
|
||||
}
|
||||
if strings.TrimSpace(graph.PlatformFixtures.TrueNAS.System.Hostname) == "" {
|
||||
t.Fatal("expected canonical mock graph to include TrueNAS fixtures")
|
||||
}
|
||||
if len(graph.PlatformFixtures.VMware.Hosts) == 0 {
|
||||
t.Fatal("expected canonical mock graph to include VMware fixtures")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
cfg := metrics.DefaultConfig(t.TempDir())
|
||||
cfg.RetentionRaw = 90 * 24 * time.Hour
|
||||
cfg.RetentionMinute = 90 * 24 * time.Hour
|
||||
cfg.RetentionHourly = 90 * 24 * time.Hour
|
||||
cfg.RetentionDaily = 90 * 24 * time.Hour
|
||||
cfg.WriteBufferSize = 500
|
||||
|
||||
store, err := metrics.NewStore(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create metrics store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
mh := NewMetricsHistory(1000, 7*24*time.Hour)
|
||||
seedMockMetricsHistory(mh, store, graph.State, now, 7*24*time.Hour, time.Minute)
|
||||
|
||||
nodePoints, err := store.Query("node", graph.State.Nodes[0].ID, "cpu", now.Add(-7*24*time.Hour), now, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query legacy mock node cpu metrics: %v", err)
|
||||
}
|
||||
if len(nodePoints) == 0 {
|
||||
t.Fatal("expected seeded legacy mock node cpu metrics from canonical graph state")
|
||||
}
|
||||
|
||||
truenasPoints, err := store.Query("truenas", graph.PlatformFixtures.TrueNAS.System.Hostname, "disk", now.Add(-7*24*time.Hour), now, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query TrueNAS mock disk metrics: %v", err)
|
||||
}
|
||||
if len(truenasPoints) == 0 {
|
||||
t.Fatal("expected seeded TrueNAS metrics from canonical graph fixtures")
|
||||
}
|
||||
|
||||
vmwareHostID := "vc-mock-1:host:host-101"
|
||||
vmwarePoints, err := store.Query("agent", vmwareHostID, "cpu", now.Add(-7*24*time.Hour), now, 3600)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to query VMware mock host cpu metrics: %v", err)
|
||||
}
|
||||
if len(vmwarePoints) == 0 {
|
||||
t.Fatal("expected seeded VMware metrics from canonical graph fixtures")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMockMetricsSampler_DoesNotClearExistingMetricsStoreData(t *testing.T) {
|
||||
t.Setenv("PULSE_MOCK_NODES", "1")
|
||||
t.Setenv("PULSE_MOCK_VMS_PER_NODE", "0")
|
||||
|
||||
@@ -181,6 +181,17 @@ func TestMonitorGetUnifiedReadStateOrSnapshotUsesCanonicalMockUnifiedResources(t
|
||||
mock.SetEnabled(true)
|
||||
t.Cleanup(func() { mock.SetEnabled(false) })
|
||||
|
||||
graph := mock.CurrentFixtureGraph()
|
||||
legacyName := ""
|
||||
if len(graph.State.VMs) > 0 {
|
||||
legacyName = graph.State.VMs[0].Name
|
||||
} else if len(graph.State.Containers) > 0 {
|
||||
legacyName = graph.State.Containers[0].Name
|
||||
}
|
||||
if legacyName == "" {
|
||||
t.Fatal("expected canonical mock graph to include at least one legacy resource")
|
||||
}
|
||||
|
||||
store := &resourceOnlyStore{
|
||||
resources: []unifiedresources.Resource{
|
||||
{
|
||||
@@ -217,4 +228,7 @@ func TestMonitorGetUnifiedReadStateOrSnapshotUsesCanonicalMockUnifiedResources(t
|
||||
if !hasUnifiedResourceName(resources, "esxi-01.lab.local") {
|
||||
t.Fatalf("expected mock-mode read-state to include VMware mock resources, got %#v", resources)
|
||||
}
|
||||
if !hasUnifiedResourceName(resources, legacyName) {
|
||||
t.Fatalf("expected mock-mode read-state to include legacy mock resource %q, got %#v", legacyName, resources)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { expect, test as base } from '@playwright/test';
|
||||
|
||||
import { createAuthenticatedStorageState, getMockMode, setMockMode } from './helpers';
|
||||
import { apiRequest, createAuthenticatedStorageState, getMockMode, setMockMode } from './helpers';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -11,6 +11,12 @@ type WorkerFixtures = {
|
||||
authStorageStatePath: string;
|
||||
};
|
||||
|
||||
type ResourceSummary = {
|
||||
name?: string;
|
||||
type?: string;
|
||||
sources?: string[];
|
||||
};
|
||||
|
||||
const TRUENAS_SCREENSHOT_PATH = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
@@ -61,6 +67,56 @@ async function ensureMockModeEnabled(page: import('@playwright/test').Page): Pro
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMockResourceName(
|
||||
page: import('@playwright/test').Page,
|
||||
source: string,
|
||||
queryTypes: string[] = [],
|
||||
): Promise<string> {
|
||||
const searchParams = new URLSearchParams({
|
||||
source,
|
||||
limit: '200',
|
||||
});
|
||||
if (queryTypes.length > 0) {
|
||||
searchParams.set('type', queryTypes.join(','));
|
||||
}
|
||||
const response = await apiRequest(
|
||||
page,
|
||||
`/api/resources?${searchParams.toString()}`,
|
||||
);
|
||||
expect(response.ok()).toBeTruthy();
|
||||
|
||||
const payload = (await response.json()) as { data?: ResourceSummary[] };
|
||||
const resources = Array.isArray(payload.data) ? payload.data : [];
|
||||
const chosen = resources.find((resource) => resource.name?.trim());
|
||||
if (!chosen?.name?.trim()) {
|
||||
throw new Error(`Expected at least one mock resource for source ${source}`);
|
||||
}
|
||||
return chosen.name.trim();
|
||||
}
|
||||
|
||||
async function fetchMockResourceNames(page: import('@playwright/test').Page): Promise<Record<string, string>> {
|
||||
return {
|
||||
'proxmox-pve': await fetchMockResourceName(page, 'proxmox', ['agent']),
|
||||
docker: await fetchMockResourceName(page, 'docker', ['docker-host']),
|
||||
kubernetes: await fetchMockResourceName(page, 'kubernetes', ['k8s-cluster']),
|
||||
'proxmox-pbs': await fetchMockResourceName(page, 'pbs', ['pbs']),
|
||||
'proxmox-pmg': await fetchMockResourceName(page, 'pmg', ['pmg']),
|
||||
};
|
||||
}
|
||||
|
||||
async function expectInfrastructureSource(
|
||||
page: import('@playwright/test').Page,
|
||||
source: string,
|
||||
resourceName: string,
|
||||
): Promise<void> {
|
||||
await page.goto(`/infrastructure?source=${encodeURIComponent(source)}`, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
|
||||
await expect(page.locator('#infra-source-filter')).toHaveValue(source);
|
||||
await expect(page.getByText(resourceName).first()).toBeVisible();
|
||||
}
|
||||
|
||||
test.describe.serial('Platform mock runtime', () => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
@@ -81,10 +137,11 @@ test.describe.serial('Platform mock runtime', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('renders TrueNAS and VMware mock data from the live runtime', async ({ page }, testInfo) => {
|
||||
test('renders canonical legacy and provider-backed mock data from the live runtime', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof');
|
||||
|
||||
await ensureMockModeEnabled(page);
|
||||
const resourceNames = await fetchMockResourceNames(page);
|
||||
|
||||
await page.goto('/settings/infrastructure/platforms/truenas', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
@@ -97,13 +154,7 @@ test.describe.serial('Platform mock runtime', () => {
|
||||
await expect(page.getByText('5 datasets')).toBeVisible();
|
||||
fs.mkdirSync(path.dirname(TRUENAS_SCREENSHOT_PATH), { recursive: true });
|
||||
await page.screenshot({ path: TRUENAS_SCREENSHOT_PATH, fullPage: true });
|
||||
|
||||
await page.goto('/infrastructure?source=truenas', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
|
||||
await expect(page.locator('#infra-source-filter')).toHaveValue('truenas');
|
||||
await expect(page.getByText('truenas-main').first()).toBeVisible();
|
||||
await expectInfrastructureSource(page, 'truenas', 'truenas-main');
|
||||
|
||||
await page.goto('/settings/infrastructure/platforms/vmware', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
@@ -117,12 +168,10 @@ test.describe.serial('Platform mock runtime', () => {
|
||||
await expect(page.getByText('2 datastores')).toBeVisible();
|
||||
fs.mkdirSync(path.dirname(VMWARE_SCREENSHOT_PATH), { recursive: true });
|
||||
await page.screenshot({ path: VMWARE_SCREENSHOT_PATH, fullPage: true });
|
||||
await expectInfrastructureSource(page, 'vmware-vsphere', 'esxi-01.lab.local');
|
||||
|
||||
await page.goto('/infrastructure?source=vmware-vsphere', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
});
|
||||
await expect(page.getByTestId('infrastructure-page')).toBeVisible();
|
||||
await expect(page.locator('#infra-source-filter')).toHaveValue('vmware-vsphere');
|
||||
await expect(page.getByText('esxi-01.lab.local').first()).toBeVisible();
|
||||
for (const [source, resourceName] of Object.entries(resourceNames)) {
|
||||
await expectInfrastructureSource(page, source, resourceName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user