| 1 | package registry |
| 2 | |
| 3 | import ( |
| 4 | "path/filepath" |
| 5 | "testing" |
| 6 | ) |
| 7 | |
| 8 | func TestUpdateSettingsColumn(t *testing.T) { |
| 9 | reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| 10 | if err != nil { |
| 11 | t.Fatal(err) |
| 12 | } |
| 13 | if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil { |
| 14 | t.Fatal(err) |
| 15 | } |
| 16 | rt, _ := reg.Get("a.example.com") |
| 17 | if rt.UpdateSettings != "" { |
| 18 | t.Fatalf("a new row has settings %q", rt.UpdateSettings) |
| 19 | } |
| 20 | before, err := reg.Fingerprint() |
| 21 | if err != nil { |
| 22 | t.Fatal(err) |
| 23 | } |
| 24 | col := `{"updates":"manual","channel":"stable"}` |
| 25 | if err := reg.SetUpdateSettings("a.example.com", col); err != nil { |
| 26 | t.Fatal(err) |
| 27 | } |
| 28 | if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col { |
| 29 | t.Fatalf("column = %q", rt.UpdateSettings) |
| 30 | } |
| 31 | after, err := reg.Fingerprint() |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | if after != before { |
| 36 | t.Fatal("update settings changed the edge's fingerprint") |
| 37 | } |
| 38 | // A full-row Upsert that does not carry the column keeps it. |
| 39 | if err := reg.Upsert(Route{Host: "a.example.com", Kind: "instance", App: "km"}); err != nil { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != col { |
| 43 | t.Fatalf("Upsert cleared the column: %q", rt.UpdateSettings) |
| 44 | } |
| 45 | if err := reg.SetUpdateSettings("a.example.com", ""); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | if rt, _ = reg.Get("a.example.com"); rt.UpdateSettings != "" { |
| 49 | t.Fatal("the narrow setter could not clear the column") |
| 50 | } |
| 51 | if err := reg.SetUpdateSettings("nobody.example.com", col); err == nil { |
| 52 | t.Fatal("an unknown host was not an error") |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // A hostname that changes tenant on an Upsert takes the new tenant's update |
| 57 | // settings and build state, empty included: A's manual hold or major hold, |
| 58 | // and A's build state, never carry into B's row (task 8, ruling R25). A |
| 59 | // same-tenant Upsert that does not carry the columns keeps them. |
| 60 | func TestATenantChangeUpsertTakesTheNewTenantsSettings(t *testing.T) { |
| 61 | reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | const host = "a.example.com" |
| 66 | settings := `{"updates":"manual","channel":"stable"}` |
| 67 | state := `{"known":true,"replica":true}` |
| 68 | tenantA := Route{Host: host, Kind: "instance", Account: "bab", App: "km", UpdateSettings: settings, BuildState: state} |
| 69 | if err := reg.Upsert(tenantA); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | rt, _ := reg.Get(host) |
| 73 | if rt.UpdateSettings != settings || rt.BuildState != state { |
| 74 | t.Fatalf("A's row: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) |
| 75 | } |
| 76 | |
| 77 | // The same tenant, columns not carried: kept (today's behaviour). |
| 78 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bab", App: "km"}); err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | if rt, _ = reg.Get(host); rt.UpdateSettings != settings || rt.BuildState != state { |
| 82 | t.Fatalf("a same-tenant Upsert lost the columns: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) |
| 83 | } |
| 84 | |
| 85 | // Another app on the same account: B's empty columns win. |
| 86 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bab", App: "other"}); err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | if rt, _ = reg.Get(host); rt.UpdateSettings != "" || rt.BuildState != "" { |
| 90 | t.Fatalf("another app kept A's columns: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) |
| 91 | } |
| 92 | |
| 93 | // Another account with the same app name, and B's own values: B's win. |
| 94 | if err := reg.Upsert(tenantA); err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | bSettings := `{"updates":"auto","major":2}` |
| 98 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: "bes", App: "km", UpdateSettings: bSettings}); err != nil { |
| 99 | t.Fatal(err) |
| 100 | } |
| 101 | if rt, _ = reg.Get(host); rt.UpdateSettings != bSettings || rt.BuildState != "" { |
| 102 | t.Fatalf("another account: settings %q, build state %q", rt.UpdateSettings, rt.BuildState) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | // Astra r4 finding 2: the tenant is the EFFECTIVE account, as |
| 107 | // release.RouteAccount reads it (an empty or invalid account is the ops |
| 108 | // account, "bab"). A legacy row with account "" re-added as "bab" (or the |
| 109 | // reverse, or an invalid spelling) is the same tenant, so its columns are |
| 110 | // kept as today. A genuine account change with empty incoming columns |
| 111 | // clears both. |
| 112 | func TestAnEquivalentAccountSpellingIsTheSameTenant(t *testing.T) { |
| 113 | reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| 114 | if err != nil { |
| 115 | t.Fatal(err) |
| 116 | } |
| 117 | const host = "a.example.com" |
| 118 | settings := `{"updates":"manual","channel":"stable"}` |
| 119 | state := `{"known":true,"replica":true}` |
| 120 | for _, tc := range []struct{ from, to string }{ |
| 121 | {"", "bab"}, {"bab", ""}, {"", ""}, {"NOT-VALID", "bab"}, {"bab", "x"}, |
| 122 | // Astra r5 N1: an embedded NUL is invalid (SQLite's length and |
| 123 | // GLOB stop at it), so these are the ops account too. |
| 124 | {"abc\x00x", "bab"}, {"bab", "abc\x00x"}, {"abc\x00x", ""}, {"abc\x00", "bab"}, {"\x00", "bab"}, {"abc\x00x", "abc\x00y"}, |
| 125 | } { |
| 126 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil { |
| 130 | t.Fatal(err) |
| 131 | } |
| 132 | if rt, _ := reg.Get(host); rt.Account != tc.to || rt.UpdateSettings != settings || rt.BuildState != state { |
| 133 | t.Fatalf("%q -> %q (one effective account): account %q, settings %q, build state %q; want both columns kept", |
| 134 | tc.from, tc.to, rt.Account, rt.UpdateSettings, rt.BuildState) |
| 135 | } |
| 136 | } |
| 137 | for _, tc := range []struct{ from, to string }{{"", "bes"}, {"bes", ""}, {"bes", "cdf"}} { |
| 138 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.from, App: "km", UpdateSettings: settings, BuildState: state}); err != nil { |
| 139 | t.Fatal(err) |
| 140 | } |
| 141 | if err := reg.Upsert(Route{Host: host, Kind: "instance", Account: tc.to, App: "km"}); err != nil { |
| 142 | t.Fatal(err) |
| 143 | } |
| 144 | if rt, _ := reg.Get(host); rt.UpdateSettings != "" || rt.BuildState != "" { |
| 145 | t.Fatalf("%q -> %q (another account): settings %q, build state %q; want both cleared", |
| 146 | tc.from, tc.to, rt.UpdateSettings, rt.BuildState) |
| 147 | } |
| 148 | } |
| 149 | } |
| 150 | |
| 151 | // Upsert's tenant compare runs in SQLite (effectiveAccountSQL); it must |
| 152 | // say exactly what EffectiveAccount (controlname.ValidAccount) says. |
| 153 | func TestEffectiveAccountSQLIsEffectiveAccount(t *testing.T) { |
| 154 | reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| 155 | if err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | for _, a := range []string{"", "bab", "bes", "abc", "ab", "a", "abcdefghijklmnop", "abcdefghijklmnopq", |
| 159 | "Abc", "ABC", "ab1", "a-b", "a_b", "a b", " abc", "abc ", "abc\n", "\u00e1bc", "ab\u00e1", "ab]", "ab^", "ab*", "ab?", "ab[", |
| 160 | "abc\x00x", "abc\x00", "\x00abc", "\x00", "abc\x00\x00", "ab\x00", "abcdefghijklmnop\x00x"} { |
| 161 | var got string |
| 162 | if err := reg.sql.QueryRow(`SELECT `+effectiveAccountSQL("?1"), a).Scan(&got); err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | if want := EffectiveAccount(a); got != want { |
| 166 | t.Fatalf("account %q: SQL says %q, EffectiveAccount says %q", a, got, want) |
| 167 | } |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | func TestUpdateSettingsMovesSnapshotFingerprint(t *testing.T) { |
| 172 | reg, err := Open(filepath.Join(t.TempDir(), "r.db")) |
| 173 | if err != nil { |
| 174 | t.Fatal(err) |
| 175 | } |
| 176 | if err := reg.Upsert(Route{Host: "s.example.com", Kind: "instance", App: "km"}); err != nil { |
| 177 | t.Fatal(err) |
| 178 | } |
| 179 | // SnapshotFingerprint with unset update_settings |
| 180 | before, err := reg.SnapshotFingerprint() |
| 181 | if err != nil { |
| 182 | t.Fatal(err) |
| 183 | } |
| 184 | col := `{"updates":"manual","channel":"stable"}` |
| 185 | if err := reg.SetUpdateSettings("s.example.com", col); err != nil { |
| 186 | t.Fatal(err) |
| 187 | } |
| 188 | // SnapshotFingerprint with set update_settings must be different |
| 189 | // (restores must reproduce the column value) |
| 190 | after, err := reg.SnapshotFingerprint() |
| 191 | if err != nil { |
| 192 | t.Fatal(err) |
| 193 | } |
| 194 | if after == before { |
| 195 | t.Fatal("setting update_settings did not move SnapshotFingerprint, so a restore would miss the column") |
| 196 | } |
| 197 | } |
| 198 | |