test(soteria): cover B2 scan and remaining policy/k8s edges
This commit is contained in:
parent
5c9cca4420
commit
4a2febe148
@ -2,6 +2,7 @@ package k8s
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -12,7 +13,9 @@ import (
|
|||||||
batchv1 "k8s.io/api/batch/v1"
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
k8sfake "k8s.io/client-go/kubernetes/fake"
|
k8sfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
k8stesting "k8s.io/client-go/testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestListBackupJobsAndListBackupJobsForPVCCoverFilteringAndSorting(t *testing.T) {
|
func TestListBackupJobsAndListBackupJobsForPVCCoverFilteringAndSorting(t *testing.T) {
|
||||||
@ -166,6 +169,48 @@ func TestResolvePVCMountedNodeIgnoresDeadPodsAndFindsMountedClaim(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadBackupJobLogCoversSuccessAndListFailures(t *testing.T) {
|
||||||
|
client := &Client{Clientset: k8sfake.NewSimpleClientset(
|
||||||
|
&corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "job-pod-old",
|
||||||
|
Namespace: "apps",
|
||||||
|
CreationTimestamp: metav1.NewTime(time.Now().UTC().Add(-2 * time.Hour)),
|
||||||
|
Labels: map[string]string{"job-name": "backup-job"},
|
||||||
|
},
|
||||||
|
Status: corev1.PodStatus{StartTime: ptrTime(metav1.NewTime(time.Now().UTC().Add(-90 * time.Minute)))},
|
||||||
|
},
|
||||||
|
&corev1.Pod{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "job-pod-new",
|
||||||
|
Namespace: "apps",
|
||||||
|
CreationTimestamp: metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour)),
|
||||||
|
Labels: map[string]string{"job-name": "backup-job"},
|
||||||
|
},
|
||||||
|
Status: corev1.PodStatus{StartTime: ptrTime(metav1.NewTime(time.Now().UTC().Add(-30 * time.Minute)))},
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
|
||||||
|
logs, err := client.ReadBackupJobLog(context.Background(), "apps", "backup-job")
|
||||||
|
if err != nil || logs != "fake logs" {
|
||||||
|
t.Fatalf("expected fake pod logs response, got %q %v", logs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyClient := &Client{Clientset: k8sfake.NewSimpleClientset()}
|
||||||
|
if _, err := emptyClient.ReadBackupJobLog(context.Background(), "apps", "backup-job"); err == nil || !strings.Contains(err.Error(), "no pod found") {
|
||||||
|
t.Fatalf("expected missing pod error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset := k8sfake.NewSimpleClientset()
|
||||||
|
clientset.PrependReactor("list", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("list pods exploded")
|
||||||
|
})
|
||||||
|
listFailClient := &Client{Clientset: clientset}
|
||||||
|
if _, err := listFailClient.ReadBackupJobLog(context.Background(), "apps", "backup-job"); err == nil || !strings.Contains(err.Error(), "list pods exploded") {
|
||||||
|
t.Fatalf("expected wrapped pod list error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateBackupJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
func TestCreateBackupJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
||||||
clientset := k8sfake.NewSimpleClientset(
|
clientset := k8sfake.NewSimpleClientset(
|
||||||
&corev1.Secret{
|
&corev1.Secret{
|
||||||
@ -252,6 +297,73 @@ func TestCreateBackupJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateBackupJobCleansUpSecretOnJobCreateFailureAndSurfacesBindFailure(t *testing.T) {
|
||||||
|
clientset := k8sfake.NewSimpleClientset(
|
||||||
|
&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "restic-src", Namespace: "shared"},
|
||||||
|
Type: corev1.SecretTypeOpaque,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"AWS_ACCESS_KEY_ID": []byte("abc"),
|
||||||
|
"AWS_SECRET_ACCESS_KEY": []byte("def"),
|
||||||
|
"RESTIC_PASSWORD": []byte("ghi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cfg := &config.Config{
|
||||||
|
SecretNamespace: "shared",
|
||||||
|
ResticSecretName: "restic-src",
|
||||||
|
ResticRepository: "s3:https://repo/root",
|
||||||
|
ResticImage: "restic/restic:latest",
|
||||||
|
JobTTLSeconds: 3600,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset.PrependReactor("create", "jobs", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("create job exploded")
|
||||||
|
})
|
||||||
|
client := &Client{Clientset: clientset}
|
||||||
|
if _, secretName, err := client.CreateBackupJob(context.Background(), cfg, api.BackupRequest{
|
||||||
|
Namespace: "apps",
|
||||||
|
PVC: "data",
|
||||||
|
}); err == nil || !strings.Contains(err.Error(), "create job exploded") {
|
||||||
|
t.Fatalf("expected job create error, got secret=%q err=%v", secretName, err)
|
||||||
|
}
|
||||||
|
secrets, err := client.Clientset.CoreV1().Secrets("apps").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list secrets after failed backup create: %v", err)
|
||||||
|
}
|
||||||
|
if len(secrets.Items) != 0 {
|
||||||
|
t.Fatalf("expected copied secret cleanup on create failure, got %#v", secrets.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindFailClientset := k8sfake.NewSimpleClientset(
|
||||||
|
&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "restic-src", Namespace: "shared"},
|
||||||
|
Type: corev1.SecretTypeOpaque,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"AWS_ACCESS_KEY_ID": []byte("abc"),
|
||||||
|
"AWS_SECRET_ACCESS_KEY": []byte("def"),
|
||||||
|
"RESTIC_PASSWORD": []byte("ghi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bindFailClientset.PrependReactor("update", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
update := action.(k8stesting.UpdateAction)
|
||||||
|
secret := update.GetObject().(*corev1.Secret)
|
||||||
|
if secret.Namespace == "apps" {
|
||||||
|
return true, nil, errors.New("bind secret exploded")
|
||||||
|
}
|
||||||
|
return false, nil, nil
|
||||||
|
})
|
||||||
|
bindFailClient := &Client{Clientset: bindFailClientset}
|
||||||
|
jobName, secretName, err := bindFailClient.CreateBackupJob(context.Background(), cfg, api.BackupRequest{
|
||||||
|
Namespace: "apps",
|
||||||
|
PVC: "data",
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "bind secret exploded") || jobName == "" || secretName == "" {
|
||||||
|
t.Fatalf("expected bind failure after backup job create, got job=%q secret=%q err=%v", jobName, secretName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateRestoreJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
func TestCreateRestoreJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
||||||
clientset := k8sfake.NewSimpleClientset(
|
clientset := k8sfake.NewSimpleClientset(
|
||||||
&corev1.Secret{
|
&corev1.Secret{
|
||||||
@ -317,3 +429,67 @@ func TestCreateRestoreJobCoversValidationDryRunAndLiveCreation(t *testing.T) {
|
|||||||
t.Fatalf("expected restore job owner reference on copied secret, got %#v", secret.OwnerReferences)
|
t.Fatalf("expected restore job owner reference on copied secret, got %#v", secret.OwnerReferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateRestoreJobCleansUpSecretOnJobCreateFailureAndSurfacesBindFailure(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
SecretNamespace: "shared",
|
||||||
|
ResticSecretName: "restic-src",
|
||||||
|
ResticRepository: "s3:https://repo/root",
|
||||||
|
ResticImage: "restic/restic:latest",
|
||||||
|
JobTTLSeconds: 3600,
|
||||||
|
}
|
||||||
|
clientset := k8sfake.NewSimpleClientset(
|
||||||
|
&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "restic-src", Namespace: "shared"},
|
||||||
|
Type: corev1.SecretTypeOpaque,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"AWS_ACCESS_KEY_ID": []byte("abc"),
|
||||||
|
"AWS_SECRET_ACCESS_KEY": []byte("def"),
|
||||||
|
"RESTIC_PASSWORD": []byte("ghi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
clientset.PrependReactor("create", "jobs", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
return true, nil, errors.New("create restore job exploded")
|
||||||
|
})
|
||||||
|
client := &Client{Clientset: clientset}
|
||||||
|
if _, _, err := client.CreateRestoreJob(context.Background(), cfg, api.RestoreTestRequest{Namespace: "apps"}); err == nil || !strings.Contains(err.Error(), "create restore job exploded") {
|
||||||
|
t.Fatalf("expected restore job create error, got %v", err)
|
||||||
|
}
|
||||||
|
secrets, err := client.Clientset.CoreV1().Secrets("apps").List(context.Background(), metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list secrets after failed restore create: %v", err)
|
||||||
|
}
|
||||||
|
if len(secrets.Items) != 0 {
|
||||||
|
t.Fatalf("expected copied secret cleanup on restore create failure, got %#v", secrets.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindFailClientset := k8sfake.NewSimpleClientset(
|
||||||
|
&corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "restic-src", Namespace: "shared"},
|
||||||
|
Type: corev1.SecretTypeOpaque,
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"AWS_ACCESS_KEY_ID": []byte("abc"),
|
||||||
|
"AWS_SECRET_ACCESS_KEY": []byte("def"),
|
||||||
|
"RESTIC_PASSWORD": []byte("ghi"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bindFailClientset.PrependReactor("update", "secrets", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||||
|
update := action.(k8stesting.UpdateAction)
|
||||||
|
secret := update.GetObject().(*corev1.Secret)
|
||||||
|
if secret.Namespace == "apps" {
|
||||||
|
return true, nil, errors.New("bind restore secret exploded")
|
||||||
|
}
|
||||||
|
return false, nil, nil
|
||||||
|
})
|
||||||
|
bindFailClient := &Client{Clientset: bindFailClientset}
|
||||||
|
jobName, secretName, err := bindFailClient.CreateRestoreJob(context.Background(), cfg, api.RestoreTestRequest{Namespace: "apps"})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "bind restore secret exploded") || jobName == "" || secretName == "" {
|
||||||
|
t.Fatalf("expected restore bind failure after job create, got job=%q secret=%q err=%v", jobName, secretName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrTime(value metav1.Time) *metav1.Time {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|||||||
144
internal/server/b2_scan_test.go
Normal file
144
internal/server/b2_scan_test.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeS3Object struct {
|
||||||
|
key string
|
||||||
|
size int64
|
||||||
|
lastModified time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFakeS3Server(t *testing.T, buckets []string, objects map[string][]fakeS3Object, missing map[string]bool) *httptest.Server {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
sort.Strings(buckets)
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
switch {
|
||||||
|
case r.URL.Path == "/" && r.URL.RawQuery == "":
|
||||||
|
fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?><ListAllMyBucketsResult><Buckets>`)
|
||||||
|
for _, bucket := range buckets {
|
||||||
|
fmt.Fprintf(w, `<Bucket><Name>%s</Name><CreationDate>2026-04-20T00:00:00Z</CreationDate></Bucket>`, bucket)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `</Buckets></ListAllMyBucketsResult>`)
|
||||||
|
case strings.HasSuffix(r.URL.Path, "/") && r.URL.Query().Has("location"):
|
||||||
|
fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?><LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">us-west-001</LocationConstraint>`)
|
||||||
|
case strings.HasSuffix(r.URL.Path, "/") && r.URL.Query().Get("list-type") == "2":
|
||||||
|
bucket := strings.Trim(strings.TrimSuffix(r.URL.Path, "/"), "/")
|
||||||
|
if missing[bucket] {
|
||||||
|
http.Error(w, "no such bucket", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?><ListBucketResult><Name>%s</Name><IsTruncated>false</IsTruncated>`, bucket)
|
||||||
|
for _, object := range objects[bucket] {
|
||||||
|
fmt.Fprintf(w, `<Contents><Key>%s</Key><LastModified>%s</LastModified><Size>%d</Size></Contents>`,
|
||||||
|
object.key,
|
||||||
|
object.lastModified.UTC().Format(time.RFC3339),
|
||||||
|
object.size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `</ListBucketResult>`)
|
||||||
|
default:
|
||||||
|
http.Error(w, "unexpected request", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanB2UsageAutoDiscoversBucketsAndAggregatesObjects(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
server := newFakeS3Server(t,
|
||||||
|
[]string{"zeta", "alpha"},
|
||||||
|
map[string][]fakeS3Object{
|
||||||
|
"alpha": {
|
||||||
|
{key: "alpha-new", size: 12, lastModified: now.Add(-2 * time.Hour)},
|
||||||
|
{key: "alpha-old", size: 30, lastModified: now.Add(-48 * time.Hour)},
|
||||||
|
},
|
||||||
|
"zeta": {
|
||||||
|
{key: "zeta-new", size: 8, lastModified: now.Add(-1 * time.Hour)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := scanB2Usage(context.Background(), b2Credentials{
|
||||||
|
Endpoint: server.URL,
|
||||||
|
Region: "us-west-001",
|
||||||
|
AccessKeyID: "atlas-key",
|
||||||
|
SecretAccessKey: "atlas-secret",
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scan b2 usage autodiscovery: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Enabled || !result.Available {
|
||||||
|
t.Fatalf("expected available B2 usage result, got %#v", result)
|
||||||
|
}
|
||||||
|
if len(result.Buckets) != 2 || result.Buckets[0].Name != "alpha" || result.Buckets[1].Name != "zeta" {
|
||||||
|
t.Fatalf("expected sorted bucket results, got %#v", result.Buckets)
|
||||||
|
}
|
||||||
|
if result.TotalObjects != 3 || result.TotalBytes != 50 {
|
||||||
|
t.Fatalf("expected aggregated object totals, got %#v", result)
|
||||||
|
}
|
||||||
|
if result.RecentObjects24h != 2 || result.RecentBytes24h != 20 {
|
||||||
|
t.Fatalf("expected recent object totals, got %#v", result)
|
||||||
|
}
|
||||||
|
if result.Buckets[0].RecentObjects24h != 1 || result.Buckets[0].RecentBytes24h != 12 || result.Buckets[0].LastModifiedAt == "" {
|
||||||
|
t.Fatalf("expected alpha bucket recent stats, got %#v", result.Buckets[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScanB2UsageConfiguredBucketsAndErrorBranches(t *testing.T) {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
server := newFakeS3Server(t,
|
||||||
|
[]string{},
|
||||||
|
map[string][]fakeS3Object{
|
||||||
|
"alpha": {{key: "alpha", size: 1, lastModified: now.Add(-1 * time.Hour)}},
|
||||||
|
"beta": {{key: "beta", size: 2, lastModified: now.Add(-2 * time.Hour)}},
|
||||||
|
},
|
||||||
|
map[string]bool{"missing": true},
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := scanB2Usage(context.Background(), b2Credentials{
|
||||||
|
Endpoint: server.URL,
|
||||||
|
Region: "us-west-001",
|
||||||
|
AccessKeyID: "atlas-key",
|
||||||
|
SecretAccessKey: "atlas-secret",
|
||||||
|
}, []string{" beta ", "", "alpha"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scan b2 usage configured buckets: %v", err)
|
||||||
|
}
|
||||||
|
if len(result.Buckets) != 2 || result.Buckets[0].Name != "alpha" || result.Buckets[1].Name != "beta" {
|
||||||
|
t.Fatalf("expected configured buckets to be trimmed and sorted, got %#v", result.Buckets)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyServer := newFakeS3Server(t, nil, nil, nil)
|
||||||
|
defer emptyServer.Close()
|
||||||
|
if _, err := scanB2Usage(context.Background(), b2Credentials{
|
||||||
|
Endpoint: emptyServer.URL,
|
||||||
|
Region: "us-west-001",
|
||||||
|
AccessKeyID: "atlas-key",
|
||||||
|
SecretAccessKey: "atlas-secret",
|
||||||
|
}, nil); err == nil || !strings.Contains(err.Error(), "no B2 buckets available for scan") {
|
||||||
|
t.Fatalf("expected no-buckets error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := scanB2Usage(context.Background(), b2Credentials{
|
||||||
|
Endpoint: server.URL,
|
||||||
|
Region: "us-west-001",
|
||||||
|
AccessKeyID: "atlas-key",
|
||||||
|
SecretAccessKey: "atlas-secret",
|
||||||
|
}, []string{"missing"}); err == nil || !strings.Contains(err.Error(), "scan B2 bucket missing") {
|
||||||
|
t.Fatalf("expected bucket scan error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -183,3 +184,70 @@ func TestLoadPoliciesRejectsInvalidDocuments(t *testing.T) {
|
|||||||
t.Fatalf("expected invalid document error, got %v", err)
|
t.Fatalf("expected invalid document error, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadPoliciesAppliesDefaultsAndSkipsInvalidEntries(t *testing.T) {
|
||||||
|
srv := &Server{
|
||||||
|
cfg: &config.Config{
|
||||||
|
Namespace: "maintenance",
|
||||||
|
PolicySecretName: "soteria-policies",
|
||||||
|
},
|
||||||
|
client: &fakeKubeClient{
|
||||||
|
secretData: map[string][]byte{
|
||||||
|
policySecretKey: []byte(`{
|
||||||
|
"policies": [
|
||||||
|
{"namespace":"apps","pvc":"data","interval_hours":0,"enabled":true},
|
||||||
|
{"namespace":" ","pvc":"skip","interval_hours":4,"enabled":true},
|
||||||
|
{"namespace":"ops","interval_hours":12,"enabled":false,"dedupe":false,"keep_last":3,"created_at":"2026-04-20T00:00:00Z","updated_at":"2026-04-20T01:00:00Z"}
|
||||||
|
]
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policies: map[string]api.BackupPolicy{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := srv.loadPolicies(context.Background()); err != nil {
|
||||||
|
t.Fatalf("load valid policies: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(srv.policies) != 2 {
|
||||||
|
t.Fatalf("expected two valid policies after filtering, got %#v", srv.policies)
|
||||||
|
}
|
||||||
|
|
||||||
|
apps := srv.policies["apps__data"]
|
||||||
|
if apps.IntervalHours != defaultPolicyHours || !apps.Dedupe || apps.KeepLast != 0 || !apps.Enabled {
|
||||||
|
t.Fatalf("expected defaults for apps policy, got %#v", apps)
|
||||||
|
}
|
||||||
|
if apps.CreatedAt == "" || apps.UpdatedAt == "" {
|
||||||
|
t.Fatalf("expected timestamps to default, got %#v", apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops := srv.policies["ops___all"]
|
||||||
|
if ops.IntervalHours != 12 || ops.Dedupe || ops.KeepLast != 3 || ops.Enabled {
|
||||||
|
t.Fatalf("expected explicit ops policy values to persist, got %#v", ops)
|
||||||
|
}
|
||||||
|
if ops.CreatedAt != "2026-04-20T00:00:00Z" || ops.UpdatedAt != "2026-04-20T01:00:00Z" {
|
||||||
|
t.Fatalf("expected explicit timestamps to persist, got %#v", ops)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPersistPoliciesRejectsUnsupportedValues(t *testing.T) {
|
||||||
|
srv := &Server{
|
||||||
|
cfg: &config.Config{
|
||||||
|
Namespace: "maintenance",
|
||||||
|
PolicySecretName: "soteria-policies",
|
||||||
|
},
|
||||||
|
client: &policyTestKubeClient{fakeKubeClient: &fakeKubeClient{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := srv.persistPolicies(context.Background(), []api.BackupPolicy{
|
||||||
|
{
|
||||||
|
ID: "apps__data",
|
||||||
|
Namespace: "apps",
|
||||||
|
PVC: "data",
|
||||||
|
IntervalHours: math.NaN(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "encode policy document") {
|
||||||
|
t.Fatalf("expected persist encode error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -291,15 +291,15 @@ func TestRootFailsWhenUIRendererUnavailable(t *testing.T) {
|
|||||||
func TestStartSeedsInitialBackgroundState(t *testing.T) {
|
func TestStartSeedsInitialBackgroundState(t *testing.T) {
|
||||||
srv := &Server{
|
srv := &Server{
|
||||||
cfg: &config.Config{
|
cfg: &config.Config{
|
||||||
AuthRequired: false,
|
AuthRequired: false,
|
||||||
BackupDriver: "longhorn",
|
BackupDriver: "longhorn",
|
||||||
BackupMaxAge: 24 * time.Hour,
|
BackupMaxAge: 24 * time.Hour,
|
||||||
MetricsRefreshInterval: time.Hour,
|
MetricsRefreshInterval: time.Hour,
|
||||||
PolicyEvalInterval: time.Hour,
|
PolicyEvalInterval: time.Hour,
|
||||||
B2Enabled: false,
|
B2Enabled: false,
|
||||||
Namespace: "maintenance",
|
Namespace: "maintenance",
|
||||||
PolicySecretName: "soteria-policies",
|
PolicySecretName: "soteria-policies",
|
||||||
UsageSecretName: "",
|
UsageSecretName: "",
|
||||||
},
|
},
|
||||||
client: &fakeKubeClient{},
|
client: &fakeKubeClient{},
|
||||||
longhorn: &fakeLonghornClient{},
|
longhorn: &fakeLonghornClient{},
|
||||||
@ -437,13 +437,42 @@ func TestResolveB2CredentialsLoadsSecretValues(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveB2CredentialsRejectsMissingValues(t *testing.T) {
|
func TestResolveB2CredentialsRejectsMissingValues(t *testing.T) {
|
||||||
srv := &Server{
|
testCases := []struct {
|
||||||
cfg: &config.Config{},
|
name string
|
||||||
client: &fakeKubeClient{},
|
cfg config.Config
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing endpoint",
|
||||||
|
cfg: config.Config{},
|
||||||
|
want: "B2 endpoint is not configured",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing access key",
|
||||||
|
cfg: config.Config{
|
||||||
|
B2Endpoint: "https://s3.us-west-000.backblazeb2.com",
|
||||||
|
B2SecretAccessKey: "def",
|
||||||
|
},
|
||||||
|
want: "B2 access key ID is not configured",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing secret key",
|
||||||
|
cfg: config.Config{
|
||||||
|
B2Endpoint: "https://s3.us-west-000.backblazeb2.com",
|
||||||
|
B2AccessKeyID: "abc",
|
||||||
|
},
|
||||||
|
want: "B2 secret access key is not configured",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := srv.resolveB2Credentials(context.Background()); err == nil || !strings.Contains(err.Error(), "B2 endpoint is not configured") {
|
for _, tc := range testCases {
|
||||||
t.Fatalf("expected missing endpoint error, got %v", err)
|
srv := &Server{
|
||||||
|
cfg: &tc.cfg,
|
||||||
|
client: &fakeKubeClient{},
|
||||||
|
}
|
||||||
|
if _, err := srv.resolveB2Credentials(context.Background()); err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||||
|
t.Fatalf("%s: expected error containing %q, got %v", tc.name, tc.want, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user