package api import ( "context" "encoding/json" "fmt" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestRestoreSnapshotCopyFailureDoesNotConsumeRetention(t *testing.T) { if os.Geteuid() != 0 { t.Skip("requires chown") } a, i, home := configTestAPI(t, false) configWrite(t, filepath.Join(home, ".credentials.yaml"), "archive") w := configCall(a, i, "POST", "backups", `{"password":"original-password"}`) var result struct{ ID string } if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &result) != nil { t.Fatal(w.Code, w.Body.String()) } configWrite(t, filepath.Join(home, ".credentials.yaml"), "live") base := filepath.Dir(home) for _, name := range []string{"pre-restore-valuable", "failed-restore-valuable"} { if err := os.Mkdir(filepath.Join(base, name), 0700); err != nil { t.Fatal(err) } configWrite(t, filepath.Join(base, name, "keep"), name) } if err := os.Symlink("missing-target", filepath.Join(home, "dangling")); err != nil { t.Fatal(err) } body := fmt.Sprintf(`{"backup_id":%q,"password":"original-password"}`, result.ID) for attempt := 0; attempt < 3; attempt++ { w = configCall(a, i, "POST", "restore", body) if w.Code != 400 || !strings.Contains(w.Body.String(), "cannot snapshot home") { t.Fatalf("attempt %d: %d %s", attempt, w.Code, w.Body.String()) } entries, err := os.ReadDir(base) if err != nil || len(entries) != 3 { t.Fatalf("partial snapshot leaked: %v %v", entries, err) } for _, name := range []string{"pre-restore-valuable", "failed-restore-valuable"} { data, err := os.ReadFile(filepath.Join(base, name, "keep")) if err != nil || string(data) != name { t.Fatalf("valuable snapshot lost: %s %v", name, err) } } data, err := os.ReadFile(filepath.Join(home, ".credentials.yaml")) if err != nil || string(data) != "live" { t.Fatalf("live home changed: %q %v", data, err) } } if err := os.Remove(filepath.Join(home, "dangling")); err != nil { t.Fatal(err) } if w = configCall(a, i, "POST", "restore", body); w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } entries, err := os.ReadDir(base) if err != nil || len(entries) != 4 { t.Fatalf("completed snapshot not retained: %v %v", entries, err) } if w = configCall(a, i, "POST", "restore", body); w.Code != 409 { t.Fatal("retention not enforced", w.Code, w.Body.String()) } } func TestWorkspaceLeaseBlocksConfigAndUpgrade(t *testing.T) { a, i, _ := configTestAPI(t, false) a.workspaceReaders = map[string]int{i.ID: 1} for _, action := range []string{"profile", "restore", "backups"} { w := configCall(a, i, "POST", action, `{}`) if w.Code != 409 || !strings.Contains(w.Body.String(), "instance files busy") { t.Fatal(w.Code, w.Body.String()) } } w := configCall(a, i, "GET", "backups", "") if w.Code != 200 { t.Fatal(w.Code, w.Body.String()) } w = httptest.NewRecorder() body, _ := json.Marshal(map[string]string{"image_id": "sha256:" + strings.Repeat("a", 64), "password": "original-password"}) r := httptest.NewRequest("POST", "/", strings.NewReader(string(body))) a.upgrade(w, r, i) if w.Code != 409 || !strings.Contains(w.Body.String(), "instance files busy") { t.Fatal(w.Code, w.Body.String()) } } func TestUpgradeCopyMultipleDirectoryBatches(t *testing.T) { if os.Geteuid() != 0 { t.Skip("requires chown") } root, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatal(err) } defer root.Close() if err = root.Mkdir("home", 0700); err != nil { t.Fatal(err) } for n := 0; n < 257; n++ { configWrite(t, filepath.Join(root.Name(), "home", fmt.Sprint(n)), "x") } if err = upgradeCopy(context.Background(), root, "home", "snapshot", 257); err != nil { t.Fatal(err) } entries, err := os.ReadDir(filepath.Join(root.Name(), "snapshot")) if err != nil || len(entries) != 257 { t.Fatal(len(entries), err) } }