package server import ( "context" "strings" "sync" "testing" "time" "scm.bstein.dev/bstein/soteria/internal/api" "scm.bstein.dev/bstein/soteria/internal/config" "scm.bstein.dev/bstein/soteria/internal/k8s" ) type startTestKubeClient struct { *fakeKubeClient mu sync.Mutex loadCalls int listPVCCalls int } func (k *startTestKubeClient) LoadSecretData(ctx context.Context, namespace, secretName, key string) ([]byte, error) { k.mu.Lock() k.loadCalls++ k.mu.Unlock() return k.fakeKubeClient.LoadSecretData(ctx, namespace, secretName, key) } func (k *startTestKubeClient) ListBoundPVCs(ctx context.Context) ([]k8s.PVCSummary, error) { k.mu.Lock() k.listPVCCalls++ k.mu.Unlock() return k.fakeKubeClient.ListBoundPVCs(ctx) } func (k *startTestKubeClient) counts() (int, int) { k.mu.Lock() defer k.mu.Unlock() return k.loadCalls, k.listPVCCalls } func newStartTestServer(cfg *config.Config, client kubeClient) *Server { return &Server{ cfg: cfg, client: client, longhorn: &fakeLonghornClient{}, metrics: newTelemetry(), ui: newUIRenderer(), policies: map[string]api.BackupPolicy{}, jobUsage: map[string]resticJobUsageCacheEntry{}, usageStore: map[string]resticPersistedUsageEntry{}, } } func TestStartRunsInitialLoadAndTickerLoopWithoutB2(t *testing.T) { client := &startTestKubeClient{ fakeKubeClient: &fakeKubeClient{ pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "data", VolumeName: "vol-data", Phase: "Bound"}}, }, } srv := newStartTestServer(&config.Config{ Namespace: "atlas", PolicySecretName: "soteria-policies", UsageSecretName: "soteria-usage", BackupDriver: "longhorn", BackupMaxAge: 24 * time.Hour, MetricsRefreshInterval: 10 * time.Millisecond, PolicyEvalInterval: 10 * time.Millisecond, B2Enabled: false, }, client) ctx, cancel := context.WithCancel(context.Background()) srv.Start(ctx) time.Sleep(35 * time.Millisecond) cancel() time.Sleep(20 * time.Millisecond) loadCalls, listPVCCalls := client.counts() if loadCalls < 2 { t.Fatalf("expected initial policy/usage secret loads, got %d", loadCalls) } if listPVCCalls < 2 { t.Fatalf("expected inventory refresh to run initially and on ticker, got %d", listPVCCalls) } if srv.metrics.inventoryRefreshTime == 0 { t.Fatalf("expected inventory metrics to be recorded after start loop") } } func TestStartRunsB2TickerAndStoresRefreshFailures(t *testing.T) { client := &startTestKubeClient{ fakeKubeClient: &fakeKubeClient{ pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "data", VolumeName: "vol-data", Phase: "Bound"}}, }, } srv := newStartTestServer(&config.Config{ Namespace: "atlas", PolicySecretName: "soteria-policies", UsageSecretName: "soteria-usage", BackupDriver: "longhorn", BackupMaxAge: 24 * time.Hour, MetricsRefreshInterval: 10 * time.Millisecond, PolicyEvalInterval: 10 * time.Millisecond, B2Enabled: true, B2Endpoint: "https://", B2AccessKeyID: "atlas-key", B2SecretAccessKey: "atlas-secret", B2ScanInterval: 10 * time.Millisecond, B2ScanTimeout: 10 * time.Millisecond, }, client) ctx, cancel := context.WithCancel(context.Background()) srv.Start(ctx) time.Sleep(35 * time.Millisecond) cancel() time.Sleep(20 * time.Millisecond) usage := srv.getB2Usage() if !usage.Enabled || usage.Error == "" || !strings.Contains(usage.Error, "S3 endpoint host is empty") { t.Fatalf("expected B2 ticker refresh failure snapshot, got %#v", usage) } if srv.metrics.b2ScanTimestamp == 0 { t.Fatalf("expected B2 scan metrics to be recorded during start loop") } }