448a68237a
Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
113 lines
4.1 KiB
Go
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")
|
|
}
|
|
}
|