package api import ( "context" "encoding/json" "io" "log" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/backup" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-1271 (brief 2026-10-25 Part 4) — a future-dated off-site copy, the ONLY copy of the kept files: still offered (it // may be the household's), but the question says its date is in the future and does not recommend it. Same rig as // TestR691 (the restic runner seam; nothing reaches restic, ssh or Docker). // COMPANION RED-PROOF (REPORT.md): with keptPick ignoring the future date, use_future stays false and this fails. func TestR1271_AFutureCopyAloneIsSaidAndNotRecommended(t *testing.T) { dir := t.TempDir() drive := filepath.Join(dir, "drive") cfg := &config.Config{} cfg.Paths.StacksDir = filepath.Join(dir, "stacks") cfg.Paths.DataDir = filepath.Join(dir, "data") cfg.Stacks.ComposeCommand = "docker compose" app := filepath.Join(cfg.Paths.StacksDir, "cloudapp") for _, d := range []string{app, filepath.Join(drive, "appdata/cloudapp"), cfg.Paths.DataDir} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } _ = os.WriteFile(filepath.Join(app, "docker-compose.yml"), []byte("services:\n cloudapp:\n image: busybox\n volumes:\n - ${HDD_PATH}/appdata/cloudapp:/data\n"), 0o644) _ = os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte("display_name: Cloud App\n"), 0o644) _ = os.WriteFile(filepath.Join(drive, "appdata/cloudapp/old.txt"), []byte("old"), 0o644) m, err := stacks.NewManager(cfg, log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } if err := m.ScanStacks(); err != nil { t.Fatal(err) } sett, err := settings.Load(filepath.Join(cfg.Paths.DataDir, "settings.json"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } if err := sett.SetOffboxTarget(&settings.OffboxTarget{Enabled: true, Host: "nas.local", Port: 22, User: "felhom", RepoPath: "/srv/repo", Schedule: "daily", EscrowState: "escrowed"}); err != nil { t.Fatal(err) } bm := backup.NewManager(cfg, sett, log.New(io.Discard, "", 0)) if err := bm.WriteOffboxSecrets("KEY", "nas.local ssh-ed25519 AAAA"); err != nil { t.Fatal(err) } snapAt := time.Now().Add(4 * 24 * time.Hour) // the rehearsal box's clock: four days ahead bm.SetOffboxRunner(func(_ context.Context, _ []string, args ...string) ([]byte, error) { for _, a := range args { if a == "snapshots" { return json.Marshal([]map[string]interface{}{{"short_id": "abba81ba", "time": snapAt, "tags": []string{"cloudapp"}, "paths": []string{drive + "/backups/primary/cloudapp", drive + "/appdata/cloudapp"}}}) } } return nil, nil }) r := &Router{stackMgr: m, backupMgr: bm, logger: log.New(io.Discard, "", 0)} for lang, want := range map[string]string{"hu": "a jövőben van", "en": "is in the future"} { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/stacks/cloudapp/deploy?lang="+lang, nil) if !r.keptDataAtInstall(w, req, "cloudapp", drive, "") || w.Code != http.StatusConflict { t.Fatalf("%s: the choice was not asked (code %d)", lang, w.Code) } var body map[string]interface{} _ = json.Unmarshal(w.Body.Bytes(), &body) data, _ := body["data"].(map[string]interface{}) warn, _ := data["use_warn"].(string) if data["use_offered"] != true || data["use_future"] != true || !strings.Contains(warn, want) { t.Fatalf("%s: a future-dated only copy is offered, marked and said: offered=%v future=%v warn=%q", lang, data["use_offered"], data["use_future"], warn) } } }