test(soteria): finish per-file coverage floor
This commit is contained in:
parent
683014443d
commit
f7a08eb420
@ -3,9 +3,11 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"scm.bstein.dev/bstein/soteria/internal/config"
|
||||
"scm.bstein.dev/bstein/soteria/internal/k8s"
|
||||
@ -228,6 +230,63 @@ func TestRunLogsShutdownAndLateServerErrorsWithoutFailing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultMainHooksExecute(t *testing.T) {
|
||||
restore := swapMainTestHooks()
|
||||
defer restore()
|
||||
|
||||
cfg := &config.Config{
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
MetricsRefreshInterval: time.Hour,
|
||||
PolicyEvalInterval: time.Hour,
|
||||
BackupMaxAge: time.Hour,
|
||||
}
|
||||
|
||||
app := newServerFn(cfg, &k8s.Client{}, &longhorn.Client{})
|
||||
if app == nil || app.Handler() == nil {
|
||||
t.Fatalf("expected default server hook to build an application server")
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("reserve local address: %v", err)
|
||||
}
|
||||
addr := ln.Addr().String()
|
||||
_ = ln.Close()
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- listenAndServeFn(httpServer)
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for {
|
||||
conn, dialErr := net.DialTimeout("tcp", addr, 50*time.Millisecond)
|
||||
if dialErr == nil {
|
||||
_ = conn.Close()
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("default listen hook did not start server: %v", dialErr)
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := shutdownServerFn(httpServer, ctx); err != nil {
|
||||
t.Fatalf("expected default shutdown hook to succeed, got %v", err)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
t.Fatalf("expected server closed or nil from default hooks, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func swapMainTestHooks() func() {
|
||||
originalLoad := loadConfigFn
|
||||
originalK8s := newK8sClientFn
|
||||
|
||||
@ -175,6 +175,25 @@ func TestLoadSupportsResticAndB2Overrides(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInfersB2EnablementAndSecretNamespaceDefault(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
withEnv(t, "SOTERIA_NAMESPACE", "atlas")
|
||||
withEnv(t, "SOTERIA_B2_SECRET_NAME", "b2-creds")
|
||||
withEnv(t, "SOTERIA_B2_ACCESS_KEY_ID", "abc")
|
||||
withEnv(t, "SOTERIA_B2_SECRET_ACCESS_KEY", "def")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("load config with inferred b2 enablement: %v", err)
|
||||
}
|
||||
if !cfg.B2Enabled {
|
||||
t.Fatalf("expected B2 enablement to be inferred from secret config")
|
||||
}
|
||||
if cfg.B2SecretNamespace != "atlas" {
|
||||
t.Fatalf("expected B2 secret namespace to default to service namespace, got %#v", cfg.B2SecretNamespace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidConfigurations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -184,7 +203,7 @@ func TestLoadRejectsInvalidConfigurations(t *testing.T) {
|
||||
{
|
||||
name: "restic missing repository",
|
||||
env: map[string]string{
|
||||
"SOTERIA_BACKUP_DRIVER": "restic",
|
||||
"SOTERIA_BACKUP_DRIVER": "restic",
|
||||
"SOTERIA_RESTIC_REPOSITORY": "",
|
||||
},
|
||||
substr: "SOTERIA_RESTIC_REPOSITORY is required",
|
||||
@ -196,6 +215,15 @@ func TestLoadRejectsInvalidConfigurations(t *testing.T) {
|
||||
},
|
||||
substr: "SOTERIA_JOB_NODE_SELECTOR is invalid",
|
||||
},
|
||||
{
|
||||
name: "restic invalid repository path",
|
||||
env: map[string]string{
|
||||
"SOTERIA_BACKUP_DRIVER": "restic",
|
||||
"SOTERIA_RESTIC_REPOSITORY": "s3:https://repo/../bad",
|
||||
"SOTERIA_RESTIC_SECRET_NAME": "restic-creds",
|
||||
},
|
||||
substr: "SOTERIA_RESTIC_REPOSITORY contains invalid path segments",
|
||||
},
|
||||
{
|
||||
name: "unsupported driver",
|
||||
env: map[string]string{
|
||||
@ -234,12 +262,21 @@ func TestLoadRejectsInvalidConfigurations(t *testing.T) {
|
||||
{
|
||||
name: "invalid b2 scan interval",
|
||||
env: map[string]string{
|
||||
"SOTERIA_B2_ENABLED": "true",
|
||||
"SOTERIA_B2_ENDPOINT": "https://s3.example.invalid",
|
||||
"SOTERIA_B2_ENABLED": "true",
|
||||
"SOTERIA_B2_ENDPOINT": "https://s3.example.invalid",
|
||||
"SOTERIA_B2_SCAN_INTERVAL_SECONDS": "0",
|
||||
},
|
||||
substr: "SOTERIA_B2_SCAN_INTERVAL_SECONDS must be greater than zero",
|
||||
},
|
||||
{
|
||||
name: "invalid b2 scan timeout",
|
||||
env: map[string]string{
|
||||
"SOTERIA_B2_ENABLED": "true",
|
||||
"SOTERIA_B2_ENDPOINT": "https://s3.example.invalid",
|
||||
"SOTERIA_B2_SCAN_TIMEOUT_SECONDS": "0",
|
||||
},
|
||||
substr: "SOTERIA_B2_SCAN_TIMEOUT_SECONDS must be greater than zero",
|
||||
},
|
||||
{
|
||||
name: "invalid policy eval interval",
|
||||
env: map[string]string{
|
||||
@ -296,6 +333,9 @@ func TestEnvAndParsingHelpers(t *testing.T) {
|
||||
if selector := parseNodeSelector("role=worker, hardware=rpi5"); selector["role"] != "worker" || selector["hardware"] != "rpi5" {
|
||||
t.Fatalf("unexpected selector parse: %#v", selector)
|
||||
}
|
||||
if selector := parseNodeSelector("role=worker, , hardware=rpi5"); selector["role"] != "worker" || selector["hardware"] != "rpi5" {
|
||||
t.Fatalf("expected empty selector segments to be ignored, got %#v", selector)
|
||||
}
|
||||
if parseNodeSelector("role=") != nil {
|
||||
t.Fatalf("expected invalid selector to return nil")
|
||||
}
|
||||
|
||||
115
internal/k8s/job_manifests_test.go
Normal file
115
internal/k8s/job_manifests_test.go
Normal file
@ -0,0 +1,115 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -19,6 +19,14 @@ func TestLoadSecretDataCoversMissingSecretValueAndCopy(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "filled", Namespace: "atlas"},
|
||||
Data: map[string][]byte{"token": []byte("atlas-secret")},
|
||||
},
|
||||
&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "missing-key", Namespace: "atlas"},
|
||||
Data: map[string][]byte{"other": []byte("atlas-secret")},
|
||||
},
|
||||
&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "empty-value", Namespace: "atlas"},
|
||||
Data: map[string][]byte{"token": {}},
|
||||
},
|
||||
&corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "empty", Namespace: "atlas"},
|
||||
},
|
||||
@ -34,6 +42,16 @@ func TestLoadSecretDataCoversMissingSecretValueAndCopy(t *testing.T) {
|
||||
t.Fatalf("expected empty secret key to return nil, got %q %v", string(value), err)
|
||||
}
|
||||
|
||||
value, err = client.LoadSecretData(context.Background(), "atlas", "missing-key", "token")
|
||||
if err != nil || value != nil {
|
||||
t.Fatalf("expected missing data key to return nil, got %q %v", string(value), err)
|
||||
}
|
||||
|
||||
value, err = client.LoadSecretData(context.Background(), "atlas", "empty-value", "token")
|
||||
if err != nil || value != nil {
|
||||
t.Fatalf("expected empty data value to return nil, got %q %v", string(value), err)
|
||||
}
|
||||
|
||||
value, err = client.LoadSecretData(context.Background(), "atlas", "filled", "token")
|
||||
if err != nil || string(value) != "atlas-secret" {
|
||||
t.Fatalf("expected copied secret value, got %q %v", string(value), err)
|
||||
@ -96,6 +114,38 @@ func TestSaveSecretDataCreatesAndUpdatesSecrets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveSecretDataInitializesNilDataAndLabelsOnExistingSecret(t *testing.T) {
|
||||
client := &Client{Clientset: k8sfake.NewSimpleClientset()}
|
||||
if _, err := client.Clientset.CoreV1().Secrets("atlas").Create(context.Background(), &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "restic-usage", Namespace: "atlas"},
|
||||
}, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatalf("seed secret: %v", err)
|
||||
}
|
||||
seeded, err := client.Clientset.CoreV1().Secrets("atlas").Get(context.Background(), "restic-usage", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("get seeded secret: %v", err)
|
||||
}
|
||||
seeded.ResourceVersion = "1"
|
||||
if _, err := client.Clientset.CoreV1().Secrets("atlas").Update(context.Background(), seeded, metav1.UpdateOptions{}); err != nil {
|
||||
t.Fatalf("prime seeded secret resource version: %v", err)
|
||||
}
|
||||
|
||||
if err := client.SaveSecretData(context.Background(), "atlas", "restic-usage", "usage.json", []byte("value"), map[string]string{"app": "soteria"}); err != nil {
|
||||
t.Fatalf("save secret data with nil data/labels: %v", err)
|
||||
}
|
||||
|
||||
updated, err := client.Clientset.CoreV1().Secrets("atlas").Get(context.Background(), "restic-usage", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("get updated secret: %v", err)
|
||||
}
|
||||
if string(updated.Data["usage.json"]) != "value" {
|
||||
t.Fatalf("expected updated secret payload, got %#v", updated.Data)
|
||||
}
|
||||
if updated.Labels["app"] != "soteria" {
|
||||
t.Fatalf("expected labels to be initialized, got %#v", updated.Labels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveSecretDataWrapsGetAndWriteErrors(t *testing.T) {
|
||||
t.Run("get error", func(t *testing.T) {
|
||||
clientset := k8sfake.NewSimpleClientset()
|
||||
|
||||
@ -112,6 +112,15 @@ func TestListBoundPVCsAndExistsCoversFilteringSortingAndCapacityFallback(t *test
|
||||
if err != nil || exists {
|
||||
t.Fatalf("expected pvc to be missing, got %v %v", exists, err)
|
||||
}
|
||||
|
||||
clientset := k8sfake.NewSimpleClientset()
|
||||
clientset.PrependReactor("list", "persistentvolumeclaims", func(action k8stesting.Action) (bool, runtime.Object, error) {
|
||||
return true, nil, apierrors.NewForbidden(schema.GroupResource{Resource: "persistentvolumeclaims"}, "", nil)
|
||||
})
|
||||
client = &Client{Clientset: clientset}
|
||||
if _, err := client.ListBoundPVCs(context.Background()); err == nil {
|
||||
t.Fatalf("expected wrapped pvc list error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersistentVolumeClaimExistsWrapsUnexpectedErrors(t *testing.T) {
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"scm.bstein.dev/bstein/soteria/internal/api"
|
||||
"scm.bstein.dev/bstein/soteria/internal/config"
|
||||
@ -191,6 +192,100 @@ func TestHandleBackupsRejectsInvalidRequestsAndBackendErrors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBackupHandlersSuccessPaths(t *testing.T) {
|
||||
t.Run("inventory success", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
&config.Config{AuthRequired: false, BackupDriver: "longhorn", BackupMaxAge: 24 * time.Hour},
|
||||
&backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{
|
||||
fakeKubeClient: &fakeKubeClient{
|
||||
pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "data", VolumeName: "pv-data", Phase: "Bound"}},
|
||||
},
|
||||
}},
|
||||
&backupTestLonghornClient{
|
||||
restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}},
|
||||
listBackups: []longhorn.Backup{{Name: "backup-a", Created: time.Now().UTC().Format(time.RFC3339), State: "Completed", Size: "1Gi"}},
|
||||
},
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/inventory", nil)
|
||||
res := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), `"namespaces"`) {
|
||||
t.Fatalf("expected inventory success, got %d %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backups success longhorn", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
&config.Config{AuthRequired: false, BackupDriver: "longhorn"},
|
||||
&backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{}}},
|
||||
&backupTestLonghornClient{
|
||||
restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}},
|
||||
listBackups: []longhorn.Backup{{Name: "backup-a", SnapshotName: "snap-a", Created: "2026-04-20T00:00:00Z", State: "Completed", URL: "s3://bucket/a", Size: "1Gi"}},
|
||||
},
|
||||
)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/backups?namespace=apps&pvc=data", nil)
|
||||
res := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), `"backup-a"`) {
|
||||
t.Fatalf("expected longhorn backups success, got %d %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backups success restic", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
&config.Config{AuthRequired: false, BackupDriver: "restic", ResticRepository: "s3:https://repo/root"},
|
||||
&backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{
|
||||
backupJobs: map[string][]k8s.BackupJobSummary{
|
||||
"apps/data": {{Name: "restic-job", Namespace: "apps", PVC: "data", State: "Completed", Repository: "s3:https://repo/root", CreatedAt: time.Now().UTC()}},
|
||||
},
|
||||
}}},
|
||||
&backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}},
|
||||
)
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/backups?namespace=apps&pvc=data", nil)
|
||||
res := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), `"restic-job"`) {
|
||||
t.Fatalf("expected restic backups success, got %d %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handle backup success", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
&config.Config{AuthRequired: false, BackupDriver: "restic"},
|
||||
&backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{}}},
|
||||
&backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}},
|
||||
)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/backup", strings.NewReader(`{"namespace":"apps","pvc":"data","dry_run":true}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), `"dry_run":true`) {
|
||||
t.Fatalf("expected backup success, got %d %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("namespace backup success", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
&config.Config{AuthRequired: false, BackupDriver: "restic"},
|
||||
&backupTestKubeClient{restoreTestKubeClient: &restoreTestKubeClient{fakeKubeClient: &fakeKubeClient{
|
||||
pvcs: []k8s.PVCSummary{
|
||||
{Namespace: "apps", Name: "alpha", VolumeName: "pv-alpha", Phase: "Bound"},
|
||||
{Namespace: "apps", Name: "beta", VolumeName: "pv-beta", Phase: "Bound"},
|
||||
},
|
||||
}}},
|
||||
&backupTestLonghornClient{restoreTestLonghornClient: &restoreTestLonghornClient{fakeLonghornClient: &fakeLonghornClient{}}},
|
||||
)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/backup/namespace", strings.NewReader(`{"namespace":"apps","dry_run":true}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
srv.Handler().ServeHTTP(res, req)
|
||||
if res.Code != http.StatusOK || !strings.Contains(res.Body.String(), `"total":2`) || !strings.Contains(res.Body.String(), `"dry_run":true`) {
|
||||
t.Fatalf("expected namespace backup success, got %d %s", res.Code, res.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleBackupAndNamespaceBackupValidationPaths(t *testing.T) {
|
||||
t.Run("backup request validation", func(t *testing.T) {
|
||||
srv := newBackupTestServer(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user