Files
felhom.eu/hub/internal/store/drtier_backfill_test.go
T

113 lines
4.1 KiB
Go

package store
// v0.51.0 dr_tier one-time legacy backfill — "initialize from reality": on the migration that
// ADDS the column, a customer whose host already carries an ENABLED pbs_dr descriptor flips ON;
// everyone else stays OFF (never auto-cascade a legacy box). Simulated against a genuine
// pre-v0.51.0 database file (tables without dr_tier), then opened through store.New so the REAL
// migrate() path runs. Red-proof partner: make the backfill ignore `"enabled":false` vs true
// (set every pbs_dr customer ON) → the disabled-descriptor case fails.
import (
"database/sql"
"io"
"log"
"path/filepath"
"testing"
_ "modernc.org/sqlite"
)
func TestDRTierBackfill_InitializesFromReality(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "legacy.db")
// 1. Build a PRE-v0.51.0 database: customer_configs + hosts WITHOUT dr_tier.
raw, err := sql.Open("sqlite", dbPath)
if err != nil {
t.Fatal(err)
}
mustExec := func(q string, args ...any) {
t.Helper()
if _, err := raw.Exec(q, args...); err != nil {
t.Fatalf("legacy seed: %v (%s)", err, q)
}
}
mustExec(`CREATE TABLE customer_configs (
customer_id TEXT PRIMARY KEY,
customer_name TEXT NOT NULL DEFAULT '',
domain TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
retrieval_password TEXT NOT NULL,
api_key TEXT NOT NULL,
config_json TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
)`)
mustExec(`CREATE TABLE hosts (
host_id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
api_key TEXT NOT NULL,
agent_version TEXT NOT NULL DEFAULT '',
last_report_at DATETIME,
desired_json TEXT NOT NULL DEFAULT '{}',
desired_generation INTEGER NOT NULL DEFAULT 0,
dr_record_json TEXT NOT NULL DEFAULT '{}',
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
)`)
seedCustomer := func(id string) {
mustExec(`INSERT INTO customer_configs (customer_id, retrieval_password, api_key) VALUES (?, ?, ?)`,
id, "pw", "key-"+id)
}
seedCustomer("applied") // host carries an ENABLED descriptor → must flip ON
seedCustomer("disabled") // descriptor present but enabled:false → must stay OFF
seedCustomer("plain") // no descriptor at all → must stay OFF
seedCustomer("hostless") // no host row → must stay OFF
mustExec(`INSERT INTO hosts (host_id, customer_id, api_key, desired_json) VALUES (?, ?, ?, ?)`,
"applied-01", "applied", "h1",
`{"pbs_dr":{"enabled":true,"storage_id":"felhom-pbs","namespace":"applied"}}`)
mustExec(`INSERT INTO hosts (host_id, customer_id, api_key, desired_json) VALUES (?, ?, ?, ?)`,
"disabled-01", "disabled", "h2",
`{"pbs_dr":{"enabled":false,"storage_id":"felhom-pbs","namespace":"disabled"}}`)
mustExec(`INSERT INTO hosts (host_id, customer_id, api_key, desired_json) VALUES (?, ?, ?, ?)`,
"plain-01", "plain", "h3", `{}`)
if err := raw.Close(); err != nil {
t.Fatal(err)
}
// 2. Open through the real store — migrate() adds dr_tier and runs the one-time backfill.
st, err := New(dbPath, log.New(io.Discard, "", 0))
if err != nil {
t.Fatalf("store.New over the legacy db: %v", err)
}
defer st.Close()
want := map[string]bool{"applied": true, "disabled": false, "plain": false, "hostless": false}
for id, wantOn := range want {
cfg, err := st.GetCustomerConfig(id)
if err != nil || cfg == nil {
t.Fatalf("read %s: %v", id, err)
}
if cfg.DRTier != wantOn {
t.Errorf("customer %s: dr_tier=%v, want %v (initialize from reality)", id, cfg.DRTier, wantOn)
}
}
// 3. The backfill is ONE-TIME: a later flag change must survive a re-open (the ALTER now
// fails → no re-backfill stomping operator decisions).
cfg, _ := st.GetCustomerConfig("applied")
cfg.DRTier = false // operator opts the customer out
if err := st.SaveCustomerConfig(cfg); err != nil {
t.Fatal(err)
}
st.Close()
st2, err := New(dbPath, log.New(io.Discard, "", 0))
if err != nil {
t.Fatal(err)
}
defer st2.Close()
cfg2, _ := st2.GetCustomerConfig("applied")
if cfg2.DRTier {
t.Fatal("re-open re-ran the backfill and stomped the operator's opt-out")
}
}