package scheduler import ( "context" "os" "path/filepath" "testing" "time" "github.com/dtoro/oikos/internal/db/sqlcgen" ) func backupCheckDef(t *testing.T, config string) sqlcgen.ListEnabledCheckDefsRow { t.Helper() return sqlcgen.ListEnabledCheckDefsRow{ Kind: "backup-freshness", Config: []byte(config), TimeoutS: 15, } } // A misconfigured check must say so rather than quietly reporting healthy — // "no path configured" and "backup ran fine" must never look the same. func TestBackupFreshnessRejectsIncompleteConfig(t *testing.T) { for _, c := range []struct{ desc, config string }{ {"no path", `{"host":"localhost"}`}, {"no host", `{"path":"/tmp"}`}, {"empty", `{}`}, } { got := checkBackupFreshness(context.Background(), backupCheckDef(t, c.config)) if got.health != "unknown" || got.signalKind != "backup-misconfigured" { t.Errorf("%s: got health=%q kind=%q, want unknown/backup-misconfigured", c.desc, got.health, got.signalKind) } } } // Live probe against a real SSH endpoint. Guarded by OIKOS_SSH_TEST_HOST: // // OIKOS_SSH_TEST_HOST=localhost OIKOS_SSH_USER=$USER \ // OIKOS_SSH_KEY_PATH=~/.ssh/id_ed25519 go test ./internal/scheduler/ -run TestBackupFreshnessLive // // The probe shell has to work on both GNU and BSD find — the first real target // is the pre-deploy pg_dump on the macOS mac-mini, so a GNU-only construct // would fail exactly where it matters. func TestBackupFreshnessLiveDistinguishesTheFourStates(t *testing.T) { host := os.Getenv("OIKOS_SSH_TEST_HOST") if host == "" { t.Skip("OIKOS_SSH_TEST_HOST not set — skipping live backup probe") } sshKeyPath = os.Getenv("OIKOS_SSH_KEY_PATH") sshUser = os.Getenv("OIKOS_SSH_USER") dir := t.TempDir() fresh := filepath.Join(dir, "fresh") stale := filepath.Join(dir, "stale") empty := filepath.Join(dir, "empty") for _, d := range []string{fresh, stale, empty} { if err := os.Mkdir(d, 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(fresh, "dump.sql"), []byte("x"), 0o644); err != nil { t.Fatal(err) } oldFile := filepath.Join(stale, "dump.sql") if err := os.WriteFile(oldFile, []byte("x"), 0o644); err != nil { t.Fatal(err) } old := time.Now().Add(-72 * time.Hour) if err := os.Chtimes(oldFile, old, old); err != nil { t.Fatal(err) } cases := []struct { desc, path, wantHealth, wantKind string }{ {"recent artifact", fresh, "healthy", ""}, {"artifact older than max_age", stale, "degraded", "backup-stale"}, {"directory exists but is empty", empty, "down", "backup-missing"}, {"directory does not exist", filepath.Join(dir, "nope"), "down", "backup-missing"}, } for _, c := range cases { cfg := `{"host":"` + host + `","path":"` + c.path + `","max_age_s":86400}` got := checkBackupFreshness(context.Background(), backupCheckDef(t, cfg)) if got.health != c.wantHealth || got.signalKind != c.wantKind { t.Errorf("%s: got health=%q kind=%q evidence=%q, want %q/%q", c.desc, got.health, got.signalKind, got.evidence, c.wantHealth, c.wantKind) } } } // A path with a quote in it must not break out of the remote sh command. func TestShellSingleQuoteEscapes(t *testing.T) { got := shellSingleQuote(`/tmp/it's; rm -rf /`) want := `'/tmp/it'\''s; rm -rf /'` if got != want { t.Errorf("shellSingleQuote = %s, want %s", got, want) } }