From 7b592edb6dea42e7bbb442e5e23bedcaa5836067 Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 20 Apr 2026 18:42:10 -0300 Subject: [PATCH] test(soteria): lift helper and B2 refresh coverage --- internal/k8s/client_test.go | 18 +++++ internal/k8s/job_helpers_test.go | 121 +++++++++++++++++++++++++++++ internal/server/b2_refresh_test.go | 64 +++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 internal/k8s/client_test.go create mode 100644 internal/k8s/job_helpers_test.go create mode 100644 internal/server/b2_refresh_test.go diff --git a/internal/k8s/client_test.go b/internal/k8s/client_test.go new file mode 100644 index 0000000..c3b90ac --- /dev/null +++ b/internal/k8s/client_test.go @@ -0,0 +1,18 @@ +package k8s + +import ( + "strings" + "testing" +) + +func TestNewReturnsErrorWhenNoClusterConfigOrValidKubeconfigExists(t *testing.T) { + t.Setenv("KUBECONFIG", "/definitely/missing/kubeconfig") + + client, err := New() + if err == nil || client != nil { + t.Fatalf("expected New to fail without usable Kubernetes config, got client=%#v err=%v", client, err) + } + if !strings.Contains(err.Error(), "load kubeconfig") { + t.Fatalf("expected kubeconfig load error, got %v", err) + } +} diff --git a/internal/k8s/job_helpers_test.go b/internal/k8s/job_helpers_test.go new file mode 100644 index 0000000..0bb73cf --- /dev/null +++ b/internal/k8s/job_helpers_test.go @@ -0,0 +1,121 @@ +package k8s + +import ( + "context" + "strings" + "testing" + + "scm.bstein.dev/bstein/soteria/internal/api" + "scm.bstein.dev/bstein/soteria/internal/config" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sfake "k8s.io/client-go/kubernetes/fake" +) + +func TestJobSupportHelpersCoverFallbackAndFormattingBranches(t *testing.T) { + if got := sanitizeRepositorySegment(" "); got != "unknown" { + t.Fatalf("expected empty repository segment to fall back to unknown, got %q", got) + } + if got := keepLastWithDefault(nil); got != 0 { + t.Fatalf("expected nil keep-last to default to zero, got %d", got) + } + negative := -1 + if got := keepLastWithDefault(&negative); got != 0 { + t.Fatalf("expected negative keep-last to clamp to zero, got %d", got) + } + if got := appendRepositoryPath("", "isolated/apps/data"); got != "" { + t.Fatalf("expected empty base repository to stay empty, got %q", got) + } + if got := appendRepositoryPath("s3:https://repo/root", ""); got != "s3:https://repo/root" { + t.Fatalf("expected empty suffix to preserve base repository, got %q", got) + } + if got := parseIntWithDefault("7", 1); got != 7 { + t.Fatalf("expected valid integer parse, got %d", got) + } + if got := parseIntWithDefault("-1", 1); got != 1 { + t.Fatalf("expected negative integer to fall back, got %d", got) + } + if got := parseBoolWithDefault("off", true); got != false { + t.Fatalf("expected explicit false parse, got %v", got) + } +} + +func TestJobNameBackupCommandAndResticEnvCoverRemainingBranches(t *testing.T) { + cfg := &config.Config{ + 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", + } + + name := jobName("backup", "Data_Set.With Spaces.and.extra-long-segment-to-force-truncation-beyond-sixty-three-characters") + if len(name) > 63 || strings.Contains(name, "_") || strings.Contains(name, " ") || strings.Contains(name, ".") { + t.Fatalf("expected sanitized Kubernetes-safe job name, got %q", name) + } + if !strings.HasPrefix(name, "soteria-backup-data-set-with-spaces-and-extra") { + t.Fatalf("expected job name prefix to be sanitized, got %q", name) + } + + dedupe := false + keepLast := 3 + cmd := backupCommand(cfg, api.BackupRequest{ + PVC: "data", + Tags: []string{" nightly ", "", "prod"}, + Dedupe: &dedupe, + KeepLast: &keepLast, + }) + if !strings.Contains(cmd, "restic init") || !strings.Contains(cmd, "--tag dedupe=off") || !strings.Contains(cmd, "--keep-last 3") { + t.Fatalf("expected backup command to include bootstrap/dedupe/keep-last logic, got %q", cmd) + } + if !strings.Contains(cmd, "--exclude *.tmp") || !strings.Contains(cmd, "--tag nightly") || !strings.Contains(cmd, "--tag prod") { + t.Fatalf("expected backup command to include trimmed tags and extra args, got %q", cmd) + } + + cmd = backupCommand(cfg, api.BackupRequest{PVC: "data"}) + if !strings.Contains(cmd, "restic forget --keep-daily 7 --prune") { + t.Fatalf("expected backup command to use configured forget args, got %q", cmd) + } + + env := resticEnv(cfg, "restic-secret", "") + values := map[string]string{} + for _, item := range env { + if item.Value != "" { + values[item.Name] = item.Value + } + } + if values["RESTIC_REPOSITORY"] != "s3:https://repo/root" || values["RESTIC_S3_ENDPOINT"] != cfg.S3Endpoint || values["AWS_REGION"] != cfg.S3Region { + t.Fatalf("expected repository/endpoint/region env vars, got %#v", values) + } +} + +func TestCopySecretAndBindSecretToJobCoverErrorBranches(t *testing.T) { + client := &Client{Clientset: k8sfake.NewSimpleClientset()} + + if _, err := client.copySecret(context.Background(), "shared", "missing", "apps", "copy", nil); err == nil { + t.Fatalf("expected missing source secret error") + } + + if err := client.bindSecretToJob(context.Background(), "apps", "missing", &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "job-a"}, + }); err == nil { + t.Fatalf("expected missing secret bind error") + } + + client = &Client{Clientset: k8sfake.NewSimpleClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "src", Namespace: "shared"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"token": []byte("atlas")}, + }, + )} + secret, err := client.copySecret(context.Background(), "shared", "src", "apps", "copy", map[string]string{"app": "soteria"}) + if err != nil { + t.Fatalf("copy secret: %v", err) + } + if secret.Namespace != "apps" || secret.Name != "copy" || secret.Labels["app"] != "soteria" { + t.Fatalf("expected copied secret in destination namespace, got %#v", secret) + } +} diff --git a/internal/server/b2_refresh_test.go b/internal/server/b2_refresh_test.go new file mode 100644 index 0000000..c2dda95 --- /dev/null +++ b/internal/server/b2_refresh_test.go @@ -0,0 +1,64 @@ +package server + +import ( + "context" + "strings" + "testing" + "time" + + "scm.bstein.dev/bstein/soteria/internal/config" +) + +func TestRefreshB2UsageRecordsCredentialResolutionErrors(t *testing.T) { + srv := &Server{ + cfg: &config.Config{ + B2Enabled: true, + B2ScanTimeout: time.Second, + }, + client: &fakeKubeClient{}, + metrics: newTelemetry(), + } + + srv.refreshB2Usage(context.Background()) + + usage := srv.getB2Usage() + if !usage.Enabled || usage.Error == "" || !strings.Contains(usage.Error, "B2 endpoint is not configured") { + t.Fatalf("expected B2 credential resolution error snapshot, got %#v", usage) + } + if usage.ScannedAt == "" { + t.Fatalf("expected failed refresh to record scanned timestamp, got %#v", usage) + } + if srv.metrics.b2ScanSuccess != 0 || srv.metrics.b2ScanTimestamp == 0 { + t.Fatalf("expected failed B2 scan metrics to be recorded, got success=%f timestamp=%f", srv.metrics.b2ScanSuccess, srv.metrics.b2ScanTimestamp) + } +} + +func TestRefreshB2UsageRecordsScanErrorsAfterCredentialsResolve(t *testing.T) { + srv := &Server{ + cfg: &config.Config{ + B2Enabled: true, + B2Endpoint: "https://", + B2AccessKeyID: "atlas-key", + B2SecretAccessKey: "atlas-secret", + B2ScanTimeout: time.Second, + }, + client: &fakeKubeClient{}, + metrics: newTelemetry(), + } + + srv.refreshB2Usage(context.Background()) + + usage := srv.getB2Usage() + if usage.Endpoint != "https://" { + t.Fatalf("expected resolved endpoint to be preserved in failed snapshot, got %#v", usage) + } + if usage.Error == "" || !strings.Contains(usage.Error, "S3 endpoint host is empty") { + t.Fatalf("expected B2 scan error snapshot, got %#v", usage) + } + if usage.ScannedAt == "" || usage.ScanDurationMS < 0 { + t.Fatalf("expected scan metadata on failure, got %#v", usage) + } + if srv.metrics.b2ScanSuccess != 0 || srv.metrics.b2ScanDurationSeconds < 0 { + t.Fatalf("expected failure metrics after scan error, got success=%f duration=%f", srv.metrics.b2ScanSuccess, srv.metrics.b2ScanDurationSeconds) + } +}