package server import ( "context" "strings" "testing" "time" "scm.bstein.dev/bstein/soteria/internal/api" "scm.bstein.dev/bstein/soteria/internal/config" ) func TestListPoliciesSortsByNamespacePVCAndID(t *testing.T) { srv := &Server{ policies: map[string]api.BackupPolicy{ "ops__db": {ID: "ops__db", Namespace: "ops", PVC: "db"}, "apps__cache": {ID: "apps__cache", Namespace: "apps", PVC: "cache"}, "apps__all": {ID: "apps__all", Namespace: "apps", PVC: ""}, "apps__z-extra": {ID: "apps__z-extra", Namespace: "apps", PVC: "z-extra"}, }, } policies := srv.listPolicies() if len(policies) != 4 { t.Fatalf("expected four policies, got %#v", policies) } gotOrder := []string{policies[0].ID, policies[1].ID, policies[2].ID, policies[3].ID} wantOrder := []string{"apps__all", "apps__cache", "apps__z-extra", "ops__db"} for i := range wantOrder { if gotOrder[i] != wantOrder[i] { t.Fatalf("expected sorted order %v, got %v", wantOrder, gotOrder) } } } func TestUpsertPolicyAppliesDefaultsAndPreservesCreatedAt(t *testing.T) { srv := newPolicyHandlerServer(&policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}}) created, err := srv.upsertPolicy(context.Background(), api.BackupPolicyUpsertRequest{ Namespace: "apps", PVC: "data", IntervalHours: 0, }) if err != nil { t.Fatalf("upsertPolicy create: %v", err) } if created.IntervalHours != defaultPolicyHours { t.Fatalf("expected default interval %.0f, got %#v", defaultPolicyHours, created) } if !created.Dedupe || created.KeepLast != 0 || !created.Enabled { t.Fatalf("expected default dedupe/keep_last/enabled values, got %#v", created) } if created.CreatedAt == "" || created.UpdatedAt == "" { t.Fatalf("expected timestamps to be populated, got %#v", created) } disabled := false dedupe := false keepLast := 4 updated, err := srv.upsertPolicy(context.Background(), api.BackupPolicyUpsertRequest{ Namespace: "apps", PVC: "data", IntervalHours: 12, Enabled: &disabled, Dedupe: &dedupe, KeepLast: &keepLast, }) if err != nil { t.Fatalf("upsertPolicy update: %v", err) } if updated.CreatedAt != created.CreatedAt { t.Fatalf("expected CreatedAt to be preserved, create=%q update=%q", created.CreatedAt, updated.CreatedAt) } if updated.IntervalHours != 12 || updated.Enabled || updated.Dedupe || updated.KeepLast != 4 { t.Fatalf("expected updated policy values to stick, got %#v", updated) } } func TestUpsertPolicyRejectsInvalidInputs(t *testing.T) { srv := newPolicyHandlerServer(&policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}}) testCases := []struct { name string req api.BackupPolicyUpsertRequest want string }{ { name: "invalid namespace", req: api.BackupPolicyUpsertRequest{Namespace: "Apps", IntervalHours: 1}, want: "namespace must be a valid Kubernetes DNS-1123 label", }, { name: "invalid pvc", req: api.BackupPolicyUpsertRequest{Namespace: "apps", PVC: "Bad_PVC", IntervalHours: 1}, want: "pvc must be a valid Kubernetes DNS-1123 label", }, { name: "interval too high", req: api.BackupPolicyUpsertRequest{Namespace: "apps", IntervalHours: maxPolicyIntervalHrs + 1}, want: "interval_hours must be <=", }, { name: "negative keep last", req: api.BackupPolicyUpsertRequest{Namespace: "apps", IntervalHours: 1, KeepLast: intPtr(-1)}, want: "keep_last must be >=", }, } for _, tc := range testCases { if _, err := srv.upsertPolicy(context.Background(), tc.req); err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("%s: expected error containing %q, got %v", tc.name, tc.want, err) } } } func TestDeletePolicyHandlesEmptyMissingAndSuccess(t *testing.T) { srv := &Server{ policies: map[string]api.BackupPolicy{ "apps__data": {ID: "apps__data", Namespace: "apps", PVC: "data"}, }, } removed, err := srv.deletePolicy(context.Background(), "") if err != nil || removed { t.Fatalf("expected empty id delete to no-op, removed=%v err=%v", removed, err) } removed, err = srv.deletePolicy(context.Background(), "missing") if err != nil || removed { t.Fatalf("expected missing id delete to no-op, removed=%v err=%v", removed, err) } srv.client = &policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}} srv.cfg = &config.Config{Namespace: "maintenance", PolicySecretName: "soteria-policies"} removed, err = srv.deletePolicy(context.Background(), "apps__data") if err != nil || !removed { t.Fatalf("expected existing id delete to succeed, removed=%v err=%v", removed, err) } } func TestPolicySliceFromMapAndBackupDueHelpers(t *testing.T) { slice := policySliceFromMap(map[string]api.BackupPolicy{ "b": {ID: "b"}, "a": {ID: "a"}, }) if len(slice) != 2 || slice[0].ID != "a" || slice[1].ID != "b" { t.Fatalf("expected sorted policy slice by id, got %#v", slice) } if !backupDue("", 1) { t.Fatalf("expected empty backup timestamp to be due") } if !backupDue("not-a-time", 1) { t.Fatalf("expected invalid backup timestamp to be due") } if !backupDue(time.Now().UTC().Add(-48*time.Hour).Format(time.RFC3339), 1) { t.Fatalf("expected old backup timestamp to be due") } if backupDue(time.Now().UTC().Add(-30*time.Minute).Format(time.RFC3339), 2) { t.Fatalf("expected recent backup timestamp to not be due") } if backupDue(time.Now().UTC().Add(-30*time.Minute).Format(time.RFC3339), 0) { t.Fatalf("expected default interval fallback to keep recent backup not due") } } func TestLoadPoliciesRejectsInvalidDocuments(t *testing.T) { srv := &Server{ cfg: &config.Config{ Namespace: "maintenance", PolicySecretName: "soteria-policies", }, client: &fakeKubeClient{ secretData: map[string][]byte{ policySecretKey: []byte(`{"policies":[`), }, }, policies: map[string]api.BackupPolicy{}, } err := srv.loadPolicies(context.Background()) if err == nil || !strings.Contains(err.Error(), "decode policy document") { t.Fatalf("expected invalid document error, got %v", err) } }