package registry import ( "path/filepath" "testing" ) func TestUpdateSettingsColumn(t *testing.T) { reg, err := Open(filepath.Join(t.TempDir(), "r.db")) if err != nil { t.Fatal(err) } if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil { t.Fatal(err) } rt, _ := reg.Get("a.example.com") if rt.UpdateSettings != "" { t.Fatalf("a new row has settings %q", rt.UpdateSettings) } before, err := reg.Fingerprint() if err != nil { t.Fatal(err) } col := `{"updates":"manual","channel":"stable"}` if err := reg.SetUpdateSettings("a.example.com", col); err != nil { t.Fatal(err) } if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col { t.Fatalf("column = %q", rt.UpdateSettings) } after, err := reg.Fingerprint() if err != nil { t.Fatal(err) } if after != before { t.Fatal("update settings changed the edge's fingerprint") } // A full-row Upsert that does not carry the column keeps it. if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil { t.Fatal(err) } if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col { t.Fatalf("Upsert cleared the column: %q", rt.UpdateSettings) } if err := reg.SetUpdateSettings("a.example.com", ""); err != nil { t.Fatal(err) } if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != "" { t.Fatal("the narrow setter could not clear the column") } if err := reg.SetUpdateSettings("nobody.example.com", col); err == nil { t.Fatal("an unknown host was not an error") } } // A hostname that changes tenant on an Upsert takes the new tenant's update // settings and build state, empty included: A's manual hold or major hold, // and A's build state, never carry into B's row (task 8, ruling R25). A // same-tenant Upsert that does not carry the columns keeps them. func TestATenantChangeUpsertTakesTheNewTenantsSettings(t *testing.T) { reg, err := Open(filepath.Join(t.TempDir(), "r.db")) if err != nil { t.Fatal(err) } const host = "a.example.com" settings := `{"updates":"manual","channel":"stable"}` state := `{"known":true,"replica":true}` tenantA := Route{Host: host, Kind: "instance", Account: "bab", App: "km", UpdateSettings: settings, BuildState: state} if err := reg.Upsert(tenantA); err != nil { t.Fatal(err) } rt, _ := reg.Get(host) if rt.UpdateSettings != settings || rt.BuildState != state { t.Fatalf("A's row: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) } // The same tenant, columns not carried: kept (today's behaviour). if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bab", App: "km"}); err != nil { t.Fatal(err) } if rt, _ = reg.Get(host); rt.UpdateSettings != settings || rt.BuildState != state { t.Fatalf("a same-tenant Upsert lost the columns: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) } // Another app on the same account: B's empty columns win. if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bab", App: "other"}); err != nil { t.Fatal(err) } if rt, _ = reg.Get(host); rt.UpdateSettings != "" || rt.BuildState != "" { t.Fatalf("another app kept A's columns: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) } // Another account with the same app name, and B's own values: B's win. if err := reg.Upsert(tenantA); err != nil { t.Fatal(err) } bSettings := `{"updates":"auto","major":2}` if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bes", App: "km", UpdateSettings: bSettings}); err != nil { t.Fatal(err) } if rt, _ = reg.Get(host); rt.UpdateSettings != bSettings || rt.BuildState != "" { t.Fatalf("another account: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) } } // Astra r4 finding 2: the tenant is the EFFECTIVE account, as // release.RouteAccount reads it (an empty or invalid account is the ops // account, "bab"). A legacy row with account "" re-added as "bab" (or the // reverse, or an invalid spelling) is the same tenant, so its columns are // kept as today. A genuine account change with empty incoming columns // clears both. func TestAnEquivalentAccountSpellingIsTheSameTenant(t *testing.T) { reg, err := Open(filepath.Join(t.TempDir(), "r.db")) if err != nil { t.Fatal(err) } const host = "a.example.com" settings := `{"updates":"manual","channel":"stable"}` state := `{"known":true,"replica":true}` for _, tc := range []struct{ from, to string }{ {"", "bab"}, {"bab", ""}, {"", ""}, {"NOT-VALID", "bab"}, {"bab", "x"}, // Astra r5 N1: an embedded NUL is invalid (SQLite's length and // GLOB stop at it), so these are the ops account too. {"abc\x00x", "bab"}, {"bab", "abc\x00x"}, {"abc\x00x", ""}, {"abc\x00", "bab"}, {"\x00", "bab"}, {"abc\x00x", "abc\x00y"}, } { if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil { t.Fatal(err) } if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil { t.Fatal(err) } if rt, _ := reg.Get(host); rt.Account != tc.to || rt.UpdateSettings != settings || rt.BuildState != state { t.Fatalf("%q -> %q (one effective account): account %q, settings %q, build state %q; want both columns kept", tc.from, tc.to, rt.Account, rt.UpdateSettings, rt.BuildState) } } for _, tc := range []struct{ from, to string }{{"", "bes"}, {"bes", ""}, {"bes", "cdf"}} { if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil { t.Fatal(err) } if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil { t.Fatal(err) } if rt, _ := reg.Get(host); rt.UpdateSettings != "" || rt.BuildState != "" { t.Fatalf("%q -> %q (another account): settings %q, build state %q; want both cleared", tc.from, tc.to, rt.UpdateSettings, rt.BuildState) } } } // Upsert's tenant compare runs in SQLite (effectiveAccountSQL); it must // say exactly what EffectiveAccount (controlname.ValidAccount) says. func TestEffectiveAccountSQLIsEffectiveAccount(t *testing.T) { reg, err := Open(filepath.Join(t.TempDir(), "r.db")) if err != nil { t.Fatal(err) } for _, a := range []string{"", "bab", "bes", "abc", "ab", "a", "abcdefghijklmnop", "abcdefghijklmnopq", "Abc", "ABC", "ab1", "a-b", "a_b", "a b", " abc", "abc ", "abc\n", "\u00e1bc", "ab\u00e1", "ab]", "ab^", "ab*", "ab?", "ab[", "abc\x00x", "abc\x00", "\x00abc", "\x00", "abc\x00\x00", "ab\x00", "abcdefghijklmnop\x00x"} { var got string if err := reg.sql.QueryRow(`SELECT `+effectiveAccountSQL("?1"), a).Scan(&got); err != nil { t.Fatal(err) } if want := EffectiveAccount(a); got != want { t.Fatalf("account %q: SQL says %q, EffectiveAccount says %q", a, got, want) } } } func TestUpdateSettingsMovesSnapshotFingerprint(t *testing.T) { reg, err := Open(filepath.Join(t.TempDir(), "r.db")) if err != nil { t.Fatal(err) } if err := reg.Upsert(Route{Host: "s.example.com", Kind: "instance", App: "km"}); err != nil { t.Fatal(err) } // SnapshotFingerprint with unset update_settings before, err := reg.SnapshotFingerprint() if err != nil { t.Fatal(err) } col := `{"updates":"manual","channel":"stable"}` if err := reg.SetUpdateSettings("s.example.com", col); err != nil { t.Fatal(err) } // SnapshotFingerprint with set update_settings must be different // (restores must reproduce the column value) after, err := reg.SnapshotFingerprint() if err != nil { t.Fatal(err) } if after == before { t.Fatal("setting update_settings did not move SnapshotFingerprint, so a restore would miss the column") } }