test(soteria): lift b2 and job helper coverage

This commit is contained in:
codex 2026-04-20 17:32:43 -03:00
parent 2ec2edae4c
commit 0cda32777f
2 changed files with 386 additions and 0 deletions

213
internal/k8s/jobs_test.go Normal file
View File

@ -0,0 +1,213 @@
package k8s
import (
"strings"
"testing"
"time"
"scm.bstein.dev/bstein/soteria/internal/api"
"scm.bstein.dev/bstein/soteria/internal/config"
batchv1 "k8s.io/api/batch/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestJobNameSanitizesAndTruncates(t *testing.T) {
name := jobName("backup", "PVC.With Spaces_and_symbols________________________________")
if len(name) > 63 {
t.Fatalf("expected kubernetes-safe name length, got %d for %q", len(name), name)
}
if strings.ContainsAny(name, " _.") {
t.Fatalf("expected sanitized name, got %q", name)
}
if !strings.HasPrefix(name, "soteria-backup-") {
t.Fatalf("expected soteria backup prefix, got %q", name)
}
}
func TestHelperDefaultsAndParsers(t *testing.T) {
if !dedupeEnabled(nil) {
t.Fatalf("expected nil dedupe to default true")
}
falseValue := false
if dedupeEnabled(&falseValue) {
t.Fatalf("expected explicit false dedupe to remain false")
}
if keepLastWithDefault(nil) != 0 {
t.Fatalf("expected nil keep_last to default to zero")
}
negative := -5
if keepLastWithDefault(&negative) != 0 {
t.Fatalf("expected negative keep_last to clamp to zero")
}
if !parseBoolWithDefault("yes", false) || parseBoolWithDefault("off", true) {
t.Fatalf("unexpected bool parsing")
}
if parseIntWithDefault("12", 0) != 12 || parseIntWithDefault("-1", 7) != 7 || parseIntWithDefault("bad", 9) != 9 {
t.Fatalf("unexpected int parsing")
}
}
func TestRepositoryHelpers(t *testing.T) {
base := "s3:https://b2.example.invalid/atlas"
if repo := resticRepositoryForBackup(base, "apps", "data", true); repo != base {
t.Fatalf("expected dedupe-enabled repository to stay shared, got %q", repo)
}
isolated := resticRepositoryForBackup(base, "Apps", "Data.Volume", false)
if !strings.Contains(isolated, "/isolated/apps/data-volume") {
t.Fatalf("expected isolated repository suffix, got %q", isolated)
}
if sanitizeRepositorySegment(" ") != "unknown" {
t.Fatalf("expected blank repository segment to default to unknown")
}
if appended := appendRepositoryPath("https://b2.example.invalid/root/", "/child/"); appended != "https://b2.example.invalid/root/child" {
t.Fatalf("unexpected appended repository path %q", appended)
}
}
func TestBuildBackupJobIncludesResticMetadata(t *testing.T) {
cfg := &config.Config{
ResticImage: "restic/restic:test",
ResticRepository: "s3:https://b2.example.invalid/atlas",
ResticBackupArgs: []string{"--exclude", "/tmp"},
ResticForgetArgs: []string{"--keep-last", "7"},
S3Endpoint: "https://b2.example.invalid",
S3Region: "us-west-000",
JobTTLSeconds: 900,
JobNodeSelector: map[string]string{"hardware": "rpi5"},
WorkerServiceAccount: "soteria-worker",
}
req := api.BackupRequest{
Namespace: "apps",
PVC: "data",
Tags: []string{"manual", "drill"},
}
job := buildBackupJob(cfg, req, "backup-job", "restic-secret", cfg.ResticRepository, true, 3)
if job.Name != "backup-job" || job.Namespace != "apps" {
t.Fatalf("unexpected job metadata: %#v", job.ObjectMeta)
}
if job.Labels[labelPVC] != "data" || job.Annotations[annotationKeepLast] != "3" {
t.Fatalf("unexpected backup annotations/labels: %#v %#v", job.Labels, job.Annotations)
}
container := job.Spec.Template.Spec.Containers[0]
if container.Image != "restic/restic:test" || container.Args[0] == "" {
t.Fatalf("unexpected restic container: %#v", container)
}
if !strings.Contains(container.Args[0], "--tag pvc=data") || !strings.Contains(container.Args[0], "--exclude /tmp") {
t.Fatalf("expected generated backup command to include request metadata, got %q", container.Args[0])
}
if job.Spec.Template.Spec.ServiceAccountName != "soteria-worker" {
t.Fatalf("expected worker service account, got %#v", job.Spec.Template.Spec.ServiceAccountName)
}
if job.Spec.Template.Spec.NodeSelector["hardware"] != "rpi5" {
t.Fatalf("expected node selector to be copied, got %#v", job.Spec.Template.Spec.NodeSelector)
}
}
func TestBuildRestoreJobUsesTargetPVC(t *testing.T) {
cfg := &config.Config{
ResticImage: "restic/restic:test",
ResticRepository: "s3:https://b2.example.invalid/atlas",
JobTTLSeconds: 600,
}
req := api.RestoreTestRequest{
Namespace: "apps",
TargetPVC: "restore-data",
}
job := buildRestoreJob(cfg, req, "restore-job", "restore-secret", "latest", cfg.ResticRepository)
if job.Labels[labelPVC] != "restore-data" {
t.Fatalf("expected target pvc label, got %#v", job.Labels)
}
if claim := job.Spec.Template.Spec.Volumes[0].PersistentVolumeClaim; claim == nil || claim.ClaimName != "restore-data" {
t.Fatalf("expected restore volume pvc claim, got %#v", job.Spec.Template.Spec.Volumes[0])
}
}
func TestGeneratedResticCommandsAndEnv(t *testing.T) {
cfg := &config.Config{
ResticRepository: "s3:https://b2.example.invalid/atlas",
ResticBackupArgs: []string{"--exclude", "/cache"},
S3Endpoint: "https://b2.example.invalid",
S3Region: "us-west-000",
}
keepLast := 2
command := backupCommand(cfg, api.BackupRequest{
PVC: "data",
Dedupe: boolPtr(false),
KeepLast: &keepLast,
Tags: []string{"nightly"},
})
if !strings.Contains(command, "restic init") || !strings.Contains(command, "--keep-last 2") || !strings.Contains(command, "dedupe=off") {
t.Fatalf("unexpected backup command %q", command)
}
restore := restoreCommand("latest")
if !strings.Contains(restore, "restic restore latest") || !strings.Contains(restore, "/restore/") {
t.Fatalf("unexpected restore command %q", restore)
}
env := resticEnv(cfg, "restic-secret", "")
values := map[string]string{}
for _, item := range env {
values[item.Name] = item.Value
}
if values["RESTIC_REPOSITORY"] != cfg.ResticRepository || values["AWS_REGION"] != "us-west-000" {
t.Fatalf("unexpected env values %#v", values)
}
if int32Ptr(7) == nil || *int32Ptr(7) != 7 {
t.Fatalf("expected int32 pointer helper to round-trip")
}
}
func TestSummarizeAndSortBackupJobs(t *testing.T) {
now := time.Now().UTC()
completed := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-complete",
Namespace: "apps",
CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)),
Annotations: map[string]string{
annotationResticRepository: "s3:https://b2.example.invalid/atlas",
annotationDedupeEnabled: "false",
annotationKeepLast: "5",
},
},
Status: batchv1.JobStatus{
Succeeded: 1,
CompletionTime: &metav1.Time{Time: now.Add(-time.Hour)},
},
}
running := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "job-running",
Namespace: "apps",
CreationTimestamp: metav1.NewTime(now.Add(-30 * time.Minute)),
},
Status: batchv1.JobStatus{
Active: 1,
},
}
completedSummary := summarizeBackupJob(completed, "data")
if completedSummary.State != "Completed" || completedSummary.DedupeEnabled || completedSummary.KeepLast != 5 {
t.Fatalf("unexpected completed summary %#v", completedSummary)
}
runningSummary := summarizeBackupJob(running, "data")
if runningSummary.State != "Running" {
t.Fatalf("unexpected running summary %#v", runningSummary)
}
items := []BackupJobSummary{completedSummary, runningSummary}
sortBackupJobSummaries(items)
if items[0].Name != "job-running" {
t.Fatalf("expected newer running job first, got %#v", items)
}
}
func boolPtr(value bool) *bool {
return &value
}

View File

@ -321,6 +321,179 @@ func TestStartSeedsInitialBackgroundState(t *testing.T) {
}
}
func TestQueryBoolParsesTruthyValues(t *testing.T) {
if !queryBool("true") || !queryBool(" yes ") || !queryBool("1") || !queryBool("ON") {
t.Fatalf("expected truthy values to parse as true")
}
if queryBool("false") || queryBool("") || queryBool("0") {
t.Fatalf("expected falsey values to parse as false")
}
}
func TestHandleB2UsageRejectsUnsupportedMethod(t *testing.T) {
srv := &Server{cfg: &config.Config{}, metrics: newTelemetry()}
srv.handler = http.HandlerFunc(srv.route)
req := httptest.NewRequest(http.MethodPost, "/v1/b2", nil)
res := httptest.NewRecorder()
srv.Handler().ServeHTTP(res, req)
if res.Code != http.StatusMethodNotAllowed {
t.Fatalf("expected 405, got %d: %s", res.Code, res.Body.String())
}
}
func TestHandleB2UsageReturnsCachedSnapshot(t *testing.T) {
srv := &Server{
cfg: &config.Config{B2Enabled: false},
metrics: newTelemetry(),
b2Usage: api.B2UsageResponse{Enabled: false, Endpoint: "cached-endpoint"},
}
srv.handler = http.HandlerFunc(srv.route)
req := httptest.NewRequest(http.MethodGet, "/v1/b2", nil)
res := httptest.NewRecorder()
srv.Handler().ServeHTTP(res, req)
if res.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", res.Code, res.Body.String())
}
var payload api.B2UsageResponse
if err := json.Unmarshal(res.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode b2 response: %v", err)
}
if payload.Endpoint != "cached-endpoint" {
t.Fatalf("expected cached snapshot, got %#v", payload)
}
}
func TestRefreshB2UsageDisabledStoresDisabledSnapshot(t *testing.T) {
srv := &Server{
cfg: &config.Config{B2Enabled: false},
metrics: newTelemetry(),
}
srv.refreshB2Usage(context.Background())
if usage := srv.getB2Usage(); usage.Enabled {
t.Fatalf("expected disabled usage snapshot, got %#v", usage)
}
}
func TestResolveB2CredentialsFromConfigValues(t *testing.T) {
srv := &Server{
cfg: &config.Config{
B2Endpoint: "https://s3.us-west-000.backblazeb2.com",
B2Region: "us-west-000",
B2AccessKeyID: "abc",
B2SecretAccessKey: "def",
},
client: &fakeKubeClient{},
}
creds, err := srv.resolveB2Credentials(context.Background())
if err != nil {
t.Fatalf("resolve b2 credentials: %v", err)
}
if creds.Endpoint != "https://s3.us-west-000.backblazeb2.com" || creds.Region != "us-west-000" {
t.Fatalf("unexpected endpoint/region: %#v", creds)
}
if creds.AccessKeyID != "abc" || creds.SecretAccessKey != "def" {
t.Fatalf("unexpected keys: %#v", creds)
}
}
func TestResolveB2CredentialsLoadsSecretValues(t *testing.T) {
kube := &fakeKubeClient{
secretData: map[string][]byte{
"AWS_ACCESS_KEY_ID": []byte("abc"),
"AWS_SECRET_ACCESS_KEY": []byte("def"),
"AWS_ENDPOINT": []byte("https://s3.us-west-123.backblazeb2.com"),
},
}
srv := &Server{
cfg: &config.Config{
Namespace: "atlas",
B2SecretNamespace: "atlas",
B2SecretName: "b2-creds",
B2AccessKeyField: "AWS_ACCESS_KEY_ID",
B2SecretKeyField: "AWS_SECRET_ACCESS_KEY",
B2EndpointField: "AWS_ENDPOINT",
},
client: kube,
}
creds, err := srv.resolveB2Credentials(context.Background())
if err != nil {
t.Fatalf("resolve b2 credentials from secret: %v", err)
}
if creds.Region != "us-west-123" {
t.Fatalf("expected inferred region, got %#v", creds)
}
if creds.AccessKeyID != "abc" || creds.SecretAccessKey != "def" {
t.Fatalf("unexpected secret credentials: %#v", creds)
}
}
func TestResolveB2CredentialsRejectsMissingValues(t *testing.T) {
srv := &Server{
cfg: &config.Config{},
client: &fakeKubeClient{},
}
if _, err := srv.resolveB2Credentials(context.Background()); err == nil || !strings.Contains(err.Error(), "B2 endpoint is not configured") {
t.Fatalf("expected missing endpoint error, got %v", err)
}
}
func TestLoadB2SecretValueValidatesInputs(t *testing.T) {
srv := &Server{
cfg: &config.Config{
B2SecretNamespace: "atlas",
B2SecretName: "b2-creds",
},
client: &fakeKubeClient{},
}
if _, err := srv.loadB2SecretValue(context.Background(), " "); err == nil || !strings.Contains(err.Error(), "empty") {
t.Fatalf("expected empty key error, got %v", err)
}
if _, err := srv.loadB2SecretValue(context.Background(), "AWS_ACCESS_KEY_ID"); err == nil || !strings.Contains(err.Error(), "is empty") {
t.Fatalf("expected empty secret value error, got %v", err)
}
}
func TestNormalizeS3Endpoint(t *testing.T) {
host, secure, err := normalizeS3Endpoint("https://s3.us-west-000.backblazeb2.com")
if err != nil || host != "s3.us-west-000.backblazeb2.com" || !secure {
t.Fatalf("unexpected https endpoint parse: %q %v %v", host, secure, err)
}
host, secure, err = normalizeS3Endpoint("http://minio.internal:9000")
if err != nil || host != "minio.internal:9000" || secure {
t.Fatalf("unexpected http endpoint parse: %q %v %v", host, secure, err)
}
host, secure, err = normalizeS3Endpoint("minio.internal:9000/")
if err != nil || host != "minio.internal:9000" || !secure {
t.Fatalf("unexpected bare endpoint parse: %q %v %v", host, secure, err)
}
if _, _, err := normalizeS3Endpoint(""); err == nil {
t.Fatalf("expected empty endpoint error")
}
if _, _, err := normalizeS3Endpoint("https://"); err == nil {
t.Fatalf("expected missing host error")
}
}
func TestInferB2RegionFallsBackWhenNeeded(t *testing.T) {
if region := inferB2Region("https://s3.us-west-009.backblazeb2.com", "fallback"); region != "us-west-009" {
t.Fatalf("expected inferred region, got %q", region)
}
if region := inferB2Region("http://minio.internal:9000", "fallback"); region != "fallback" {
t.Fatalf("expected fallback region, got %q", region)
}
}
func TestProtectedInventoryRequiresAuth(t *testing.T) {
srv := &Server{
cfg: &config.Config{AuthRequired: true, AllowedGroups: []string{"admin", "maintenance"}, BackupDriver: "longhorn"},