diff --git a/internal/k8s/job_helpers_test.go b/internal/k8s/job_helpers_test.go index b732499..306bbd5 100644 --- a/internal/k8s/job_helpers_test.go +++ b/internal/k8s/job_helpers_test.go @@ -2,6 +2,7 @@ package k8s import ( "context" + "errors" "strings" "testing" @@ -11,7 +12,9 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" k8sfake "k8s.io/client-go/kubernetes/fake" + k8stesting "k8s.io/client-go/testing" ) func TestJobSupportHelpersCoverFallbackAndFormattingBranches(t *testing.T) { @@ -130,4 +133,19 @@ func TestCopySecretAndBindSecretToJobCoverErrorBranches(t *testing.T) { if secret.Namespace != "apps" || secret.Name != "copy" || secret.Labels["app"] != "soteria" { t.Fatalf("expected copied secret in destination namespace, got %#v", secret) } + + clientset := k8sfake.NewSimpleClientset( + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "src", Namespace: "shared"}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"token": []byte("atlas")}, + }, + ) + clientset.PrependReactor("create", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("create secret exploded") + }) + client = &Client{Clientset: clientset} + if _, err := client.copySecret(context.Background(), "shared", "src", "apps", "copy", nil); err == nil || !strings.Contains(err.Error(), "create secret exploded") { + t.Fatalf("expected wrapped secret create error, got %v", err) + } } diff --git a/internal/k8s/jobs_test.go b/internal/k8s/jobs_test.go index 1d4bd93..9ad0861 100644 --- a/internal/k8s/jobs_test.go +++ b/internal/k8s/jobs_test.go @@ -114,6 +114,18 @@ func TestListBackupJobsAndListBackupJobsForPVCCoverFilteringAndSorting(t *testin if len(items) != 2 || items[0].Name != "backup-alpha" || items[1].Name != "backup-zeta" { t.Fatalf("expected filtered pvc job list, got %#v", items) } + + listFailClientset := k8sfake.NewSimpleClientset() + listFailClientset.PrependReactor("list", "jobs", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.New("list jobs exploded") + }) + listFailClient := &Client{Clientset: listFailClientset} + if _, err := listFailClient.ListBackupJobs(context.Background(), "apps"); err == nil || !strings.Contains(err.Error(), "list jobs exploded") { + t.Fatalf("expected list backup jobs error, got %v", err) + } + if _, err := listFailClient.ListBackupJobsForPVC(context.Background(), "apps", "data"); err == nil || !strings.Contains(err.Error(), "list jobs exploded") { + t.Fatalf("expected pvc-scoped list backup jobs error, got %v", err) + } } func TestResolvePVCMountedNodeIgnoresDeadPodsAndFindsMountedClaim(t *testing.T) { diff --git a/internal/longhorn/client_test.go b/internal/longhorn/client_test.go index 62f2897..389f8a1 100644 --- a/internal/longhorn/client_test.go +++ b/internal/longhorn/client_test.go @@ -181,6 +181,20 @@ func TestVolumeOperationsPropagateAPIErrorsAndOptionalBranches(t *testing.T) { } }) + t.Run("snapshot backup api error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "snapshot failed", http.StatusBadGateway) + })) + defer server.Close() + + client := New(server.URL) + _, err := client.SnapshotBackup(ctx, "pv/name", "snap-1", map[string]string{"env": "dev"}, "incremental") + apiErr, ok := err.(*APIError) + if !ok || apiErr.Status != http.StatusBadGateway { + t.Fatalf("expected APIError 502 for SnapshotBackup, got %#v", err) + } + }) + t.Run("create volume from backup error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "create failed", http.StatusBadGateway) @@ -344,6 +358,14 @@ func TestListBackupsAndFindBackupSelections(t *testing.T) { t.Fatalf("expected snapshot lookup to return backup-old, got %#v", bySnapshot) } + byName, err := client.FindBackup(ctx, "vol-a", "backup-new") + if err != nil { + t.Fatalf("FindBackup by name: %v", err) + } + if byName.Name != "backup-new" { + t.Fatalf("expected name lookup to return backup-new, got %#v", byName) + } + byURL, err := client.FindBackup(ctx, "vol-a", "s3://bucket/invalid") if err != nil { t.Fatalf("FindBackup by URL: %v", err) @@ -369,6 +391,20 @@ func TestListBackupsAndFindBackupErrors(t *testing.T) { } }) + t.Run("backup volume lookup error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "volume lookup failed", http.StatusBadGateway) + })) + defer server.Close() + + client := New(server.URL) + _, err := client.ListBackups(ctx, "vol-a") + apiErr, ok := err.(*APIError) + if !ok || apiErr.Status != http.StatusBadGateway { + t.Fatalf("expected backup volume lookup APIError 502, got %#v", err) + } + }) + t.Run("backup list request fails", func(t *testing.T) { serverURL := "" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/server/restore_handlers.go b/internal/server/restore_handlers.go index d95814e..d918773 100644 --- a/internal/server/restore_handlers.go +++ b/internal/server/restore_handlers.go @@ -309,8 +309,5 @@ func targetPVCName(prefix, sourcePVC string) string { if len(name) > 63 { name = strings.Trim(name[:63], "-") } - if name == "" { - name = "restore" - } return name } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 0eeaa4a..2cf3aa7 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -31,6 +31,11 @@ type fakeKubeClient struct { secretData map[string][]byte } +type secretErrorKubeClient struct { + *fakeKubeClient + err error +} + func (f *fakeKubeClient) ResolvePVCVolume(_ context.Context, namespace, pvcName string) (string, *corev1.PersistentVolumeClaim, *corev1.PersistentVolume, error) { return namespace + "-" + pvcName + "-pv", nil, nil, nil } @@ -135,6 +140,10 @@ func (f *fakeKubeClient) SaveSecretData(_ context.Context, _, _, key string, val return nil } +func (f *secretErrorKubeClient) LoadSecretData(_ context.Context, _, _, _ string) ([]byte, error) { + return nil, f.err +} + type fakeLonghornClient struct { backups []longhorn.Backup createSnapshotName string @@ -493,6 +502,42 @@ func TestLoadB2SecretValueValidatesInputs(t *testing.T) { } } +func TestLoadB2SecretValueWrapsClientErrors(t *testing.T) { + srv := &Server{ + cfg: &config.Config{ + B2SecretNamespace: "atlas", + B2SecretName: "b2-creds", + }, + client: &secretErrorKubeClient{ + fakeKubeClient: &fakeKubeClient{}, + err: fmt.Errorf("vault bridge exploded"), + }, + } + + if _, err := srv.loadB2SecretValue(context.Background(), "AWS_ACCESS_KEY_ID"); err == nil || !strings.Contains(err.Error(), "vault bridge exploded") { + t.Fatalf("expected wrapped secret load error, got %v", err) + } +} + +func TestResolveB2CredentialsWrapsSecretLoadErrors(t *testing.T) { + srv := &Server{ + cfg: &config.Config{ + B2SecretNamespace: "atlas", + B2SecretName: "b2-creds", + B2AccessKeyField: "AWS_ACCESS_KEY_ID", + B2SecretKeyField: "AWS_SECRET_ACCESS_KEY", + }, + client: &secretErrorKubeClient{ + fakeKubeClient: &fakeKubeClient{}, + err: fmt.Errorf("secret load exploded"), + }, + } + + if _, err := srv.resolveB2Credentials(context.Background()); err == nil || !strings.Contains(err.Error(), "secret load exploded") { + t.Fatalf("expected wrapped secret load error during credential resolution, 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 {