package k8s import ( "strings" "testing" "scm.bstein.dev/bstein/soteria/internal/api" "scm.bstein.dev/bstein/soteria/internal/config" ) func TestBuildBackupJobAppliesSelectorsServiceAccountAndMetadata(t *testing.T) { cfg := &config.Config{ ResticImage: "restic/restic:latest", ResticRepository: "s3:https://repo/root", ResticBackupArgs: []string{"--exclude", "*.tmp"}, ResticForgetArgs: []string{"--keep-daily", "7", "--prune"}, S3Endpoint: "https://s3.us-west-001.backblazeb2.com", S3Region: "us-west-001", JobTTLSeconds: 3600, JobNodeSelector: map[string]string{"hardware": "rpi5"}, WorkerServiceAccount: "soteria-worker", } keepLast := 3 req := api.BackupRequest{ Namespace: "apps", PVC: "data", Tags: []string{"nightly"}, KeepLast: &keepLast, } job := buildBackupJob(cfg, req, "backup-job", "restic-secret", "s3:https://repo/root/isolated/apps/data", false, keepLast) if job.Name != "backup-job" || job.Namespace != "apps" { t.Fatalf("expected backup job identity, got %#v", job.ObjectMeta) } if job.Labels[labelPVC] != "data" || job.Annotations[annotationDedupeEnabled] != "false" || job.Annotations[annotationKeepLast] != "3" { t.Fatalf("expected pvc/dedupe/keep-last metadata, got labels=%#v annotations=%#v", job.Labels, job.Annotations) } if got := job.Spec.Template.Spec.NodeSelector["hardware"]; got != "rpi5" { t.Fatalf("expected node selector to be applied, got %#v", job.Spec.Template.Spec.NodeSelector) } if got := job.Spec.Template.Spec.ServiceAccountName; got != "soteria-worker" { t.Fatalf("expected worker service account, got %q", got) } if len(job.Spec.Template.Spec.Containers) != 1 { t.Fatalf("expected one backup container, got %#v", job.Spec.Template.Spec.Containers) } container := job.Spec.Template.Spec.Containers[0] if container.Name != "restic" || container.Image != "restic/restic:latest" { t.Fatalf("expected restic container, got %#v", container) } if len(container.Args) != 1 || !strings.Contains(container.Args[0], "restic backup /data") || !strings.Contains(container.Args[0], "--keep-last 3") { t.Fatalf("expected backup command payload, got %#v", container.Args) } if len(job.Spec.Template.Spec.Volumes) != 2 || job.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim == nil { t.Fatalf("expected pvc + cache volumes, got %#v", job.Spec.Template.Spec.Volumes) } if got := *job.Spec.BackoffLimit; got != 0 { t.Fatalf("expected zero backoff limit, got %d", got) } if got := *job.Spec.TTLSecondsAfterFinished; got != 3600 { t.Fatalf("expected ttl from config, got %d", got) } } func TestBuildRestoreJobCoversDefaultAndTargetPVCRestoreShapes(t *testing.T) { cfg := &config.Config{ ResticImage: "restic/restic:latest", ResticRepository: "s3:https://repo/root", S3Endpoint: "https://s3.us-west-001.backblazeb2.com", S3Region: "us-west-001", JobTTLSeconds: 1800, JobNodeSelector: map[string]string{"hardware": "rpi5"}, WorkerServiceAccount: "soteria-worker", } defaultJob := buildRestoreJob(cfg, api.RestoreTestRequest{ Namespace: "apps", }, "restore-job", "restic-secret", "latest", "s3:https://repo/root") if got := defaultJob.Spec.Template.Spec.Volumes[0].EmptyDir; got == nil { t.Fatalf("expected default restore target to use emptyDir, got %#v", defaultJob.Spec.Template.Spec.Volumes[0]) } if got := defaultJob.Spec.Template.Spec.ServiceAccountName; got != "soteria-worker" { t.Fatalf("expected worker service account, got %q", got) } if got := defaultJob.Spec.Template.Spec.NodeSelector["hardware"]; got != "rpi5" { t.Fatalf("expected node selector to be applied, got %#v", defaultJob.Spec.Template.Spec.NodeSelector) } targetPVCJob := buildRestoreJob(cfg, api.RestoreTestRequest{ Namespace: "apps", TargetPVC: "restore-data", }, "restore-job", "restic-secret", "snapshot-1", "s3:https://repo/root") if targetPVCJob.Labels[labelPVC] != "restore-data" { t.Fatalf("expected restore pvc label, got %#v", targetPVCJob.Labels) } if claim := targetPVCJob.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim; claim == nil || claim.ClaimName != "restore-data" { t.Fatalf("expected target pvc volume mount, got %#v", targetPVCJob.Spec.Template.Spec.Volumes[0]) } if len(targetPVCJob.Spec.Template.Spec.Containers) != 1 || !strings.Contains(targetPVCJob.Spec.Template.Spec.Containers[0].Args[0], "restic restore snapshot-1") { t.Fatalf("expected restore command payload, got %#v", targetPVCJob.Spec.Template.Spec.Containers) } } func TestRestoreCommandAndInt32PtrHelpers(t *testing.T) { command := restoreCommand("snapshot-42") if !strings.Contains(command, "restic restore snapshot-42") || !strings.Contains(command, "cp -a /cache/restore") { t.Fatalf("expected restore shell command, got %q", command) } value := int32Ptr(17) if value == nil || *value != 17 { t.Fatalf("expected int32 pointer helper to preserve value, got %#v", value) } }