Files
felhom-agent/internal/config/config_test.go
T
admin dc70e15d28 reconcile: tier-aware restore-task deadline (S4.1 unattended offsite restore-test)
A WAN (pbs-tier) restore of a large guest exceeds the restore-task wait's 10m
default → the wait expired mid-restore, teardown fired against a still-restoring
(not-yet-pool-associated) scratch guest → leak + a phantom VM.Allocate 403.

- RestoreTestSpec.RestoreTaskTimeout (0→10m default); the restore WaitTask passes
  it. Local tier unchanged (10m).
- config RestoreTestPBSRestoreTimeoutSeconds + accessor (default 120m).
- main restoreTaskTimeout(cfg,tier): configured PBS timeout only when tier==pbs,
  else 0. Both scheduler + selftest spec builds.
- Tests + WaitOptions red-proof + accessor contract.

The "grant scratch-band VM.Allocate" follow-up is diagnosed not blind-applied:
the scratch is restored INTO /pool/felhom (ACL already grants VM.Allocate), so
the earlier 403 was a consequence of the timeout. No ACL/host-install change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-04 19:45:26 +02:00

136 lines
4.2 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// TestRestoreTestPBSRestoreTimeout mirrors the BackupCadence accessor contract: positive as-is,
// 0 → default (120m), negative → default.
func TestRestoreTestPBSRestoreTimeout(t *testing.T) {
cases := []struct {
secs int
want time.Duration
}{
{0, 120 * time.Minute},
{-5, 120 * time.Minute},
{1800, 30 * time.Minute},
{7200, 120 * time.Minute},
}
for _, c := range cases {
got := BackupConfig{RestoreTestPBSRestoreTimeoutSeconds: c.secs}.RestoreTestPBSRestoreTimeout()
if got != c.want {
t.Errorf("RestoreTestPBSRestoreTimeout(secs=%d) = %v, want %v", c.secs, got, c.want)
}
}
}
func TestRedactedMasksSecret(t *testing.T) {
c := Default()
c.Proxmox.Token = "felhom-agent@pve!agent=b6547d9d-08ec-4f22-beb8-a551dc2cd69d"
got := c.Redacted().Proxmox.Token
if strings.Contains(got, "b6547d9d") {
t.Fatalf("secret leaked in redacted token: %q", got)
}
if !strings.HasPrefix(got, "felhom-agent@pve!agent=") {
t.Errorf("redacted token lost its public prefix: %q", got)
}
// The original must be untouched (Redacted returns a copy).
if !strings.Contains(c.Proxmox.Token, "b6547d9d") {
t.Errorf("Redacted mutated the original config")
}
}
func TestValidate(t *testing.T) {
c := Default()
c.Proxmox.Node = "demo-felhom"
c.Proxmox.Token = "felhom-agent@pve!agent=secret"
if err := c.Validate(); err != nil {
t.Fatalf("valid config rejected: %v", err)
}
c.Proxmox.Token = "no-bang-no-eq"
if err := c.Validate(); err == nil {
t.Errorf("malformed token accepted")
}
}
func TestRedactedMasksHubKey(t *testing.T) {
c := Default()
c.Hub.APIKey = "hub-secret-abcdef"
if got := c.Redacted().Hub.APIKey; got == "hub-secret-abcdef" || got == "" {
t.Fatalf("hub key not masked: %q", got)
}
if !strings.Contains(c.Hub.APIKey, "abcdef") {
t.Error("Redacted mutated the original hub key")
}
}
func TestHubConfigValidate(t *testing.T) {
base := HubConfig{URL: "https://hub.felhom.eu", HostID: "h1", APIKey: "k"}
if err := base.Validate(); err != nil {
t.Fatalf("valid hub config rejected: %v", err)
}
bad := []HubConfig{
{HostID: "h", APIKey: "k"}, // no URL
{URL: "https://x", APIKey: "k"}, // no host
{URL: "https://x", HostID: "h"}, // no key
{URL: "http://hub.felhom.eu", HostID: "h", APIKey: "k"}, // http non-loopback
{URL: "ftp://x", HostID: "h", APIKey: "k"}, // bad scheme
}
for i, h := range bad {
if err := h.Validate(); err == nil {
t.Errorf("case %d: expected validation error for %+v", i, h)
}
}
// http is allowed for loopback (tests).
if err := (HubConfig{URL: "http://127.0.0.1:8443", HostID: "h", APIKey: "k"}).Validate(); err != nil {
t.Errorf("http loopback should be allowed: %v", err)
}
}
func TestHubEnvOverlayAndDefaults(t *testing.T) {
t.Setenv("FELHOM_AGENT_HUB_URL", "https://hub.example")
t.Setenv("FELHOM_AGENT_HUB_HOST_ID", "env-host")
t.Setenv("FELHOM_AGENT_HUB_API_KEY", "env-key")
t.Setenv("FELHOM_AGENT_HUB_POLL_SECONDS", "120")
cfg, err := Load("")
if err != nil {
t.Fatal(err)
}
if cfg.Hub.URL != "https://hub.example" || cfg.Hub.HostID != "env-host" || cfg.Hub.APIKey != "env-key" {
t.Errorf("hub env overlay failed: %+v", cfg.Hub)
}
if cfg.Hub.PollSeconds != 120 {
t.Errorf("poll seconds = %d, want 120", cfg.Hub.PollSeconds)
}
// withDefaults fills zero timeout.
if (HubConfig{}).WithDefaults().TimeoutSeconds != 30 {
t.Error("WithDefaults should set TimeoutSeconds=30")
}
}
func TestLoadFileThenEnvOverride(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "agent.json")
if err := os.WriteFile(path, []byte(`{"proxmox":{"node":"file-node","token":"u@pve!t=filesecret"}}`), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("FELHOM_AGENT_PROXMOX_NODE", "env-node")
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Proxmox.Node != "env-node" {
t.Errorf("env did not override node: %q", cfg.Proxmox.Node)
}
if cfg.Proxmox.Token != "u@pve!t=filesecret" {
t.Errorf("token from file lost: %q", cfg.Proxmox.Token)
}
if cfg.Proxmox.Endpoint != "https://127.0.0.1:8006" {
t.Errorf("default endpoint lost: %q", cfg.Proxmox.Endpoint)
}
}