| 1 | package edge |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "log" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "os" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | |
| 13 | "github.com/carlosframework/platform/internal/registry" |
| 14 | ) |
| 15 | |
| 16 | // --------------------------------------------------------------------------- |
| 17 | // Carlos-Purge-Host as a LIST: one response reaching every host of its own |
| 18 | // app and account (amadan carlos/platform#173). |
| 19 | // --------------------------------------------------------------------------- |
| 20 | |
| 21 | // purgeHostsEdge is purgeOverrideEdge for any number of routes. The first is |
| 22 | // the one requests are served on in these tests. |
| 23 | func purgeHostsEdge(t *testing.T, routes []registry.Route, h http.HandlerFunc) *Edge { |
| 24 | t.Helper() |
| 25 | backend := httptest.NewServer(h) |
| 26 | t.Cleanup(backend.Close) |
| 27 | reg := openReg(t) |
| 28 | for _, rt := range routes { |
| 29 | rt.Kind, rt.Addr, rt.Status = "instance", backend.Listener.Addr().String(), "ready" |
| 30 | if err := reg.Upsert(rt); err != nil { |
| 31 | t.Fatalf("upsert %s: %v", rt.Host, err) |
| 32 | } |
| 33 | } |
| 34 | e, err := New(reg, nil) |
| 35 | if err != nil { |
| 36 | t.Fatalf("New: %v", err) |
| 37 | } |
| 38 | e.EnableCache(NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes)) |
| 39 | return e |
| 40 | } |
| 41 | |
| 42 | // titoAccount is the #173 topology: a canonical account host, a custom domain |
| 43 | // wired to one event, another wired to a series, all one app and one account. |
| 44 | func titoAccount() []registry.Route { |
| 45 | return []registry.Route{ |
| 46 | route("go.tito.example", "tito", "tenanta"), |
| 47 | route("acme.go.tito.example", "tito", "tenanta"), |
| 48 | route("tickets.acme.example", "tito", "tenanta"), |
| 49 | route("series.acme.example", "tito", "tenanta"), |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func servePOST(e *Edge, url string) *httptest.ResponseRecorder { |
| 54 | rec := httptest.NewRecorder() |
| 55 | e.ServeHTTP(rec, httptest.NewRequest("POST", url, nil)) |
| 56 | return rec |
| 57 | } |
| 58 | |
| 59 | // TestPurgeHostListPurgesEveryListedHost is the headline of #173: an admin |
| 60 | // write on the root domain purges the same page under the canonical host and |
| 61 | // every custom domain at once, whichever list spelling the app uses, and the |
| 62 | // serving host is still left alone because the list did not name it. |
| 63 | func TestPurgeHostListPurgesEveryListedHost(t *testing.T) { |
| 64 | listed := []string{"acme.go.tito.example", "tickets.acme.example", "series.acme.example"} |
| 65 | for _, tc := range []struct { |
| 66 | name string |
| 67 | set func(http.Header) |
| 68 | }{ |
| 69 | {"comma joined", func(h http.Header) { |
| 70 | h.Set("Carlos-Purge-Host", strings.Join(listed, ", ")) |
| 71 | }}, |
| 72 | {"several lines", func(h http.Header) { |
| 73 | for _, host := range listed { |
| 74 | h.Add("Carlos-Purge-Host", host) |
| 75 | } |
| 76 | }}, |
| 77 | {"mixed, with a duplicate", func(h http.Header) { |
| 78 | h.Add("Carlos-Purge-Host", listed[0]+", "+listed[1]) |
| 79 | h.Add("Carlos-Purge-Host", listed[2]+","+listed[0]) |
| 80 | }}, |
| 81 | } { |
| 82 | t.Run(tc.name, func(t *testing.T) { |
| 83 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 84 | tc.set(w.Header()) |
| 85 | w.Header().Set("Carlos-Purge", "/events/42") |
| 86 | w.WriteHeader(http.StatusFound) |
| 87 | }) |
| 88 | for _, host := range listed { |
| 89 | seed(t, e.cache, host, "/events/42", "", "old") |
| 90 | seed(t, e.cache, host, "/events/43", "", "untouched") |
| 91 | } |
| 92 | seed(t, e.cache, "go.tito.example", "/events/42", "", "serving host") |
| 93 | |
| 94 | rec := servePOST(e, "http://go.tito.example/admin/events/42") |
| 95 | |
| 96 | for _, host := range listed { |
| 97 | if cached(e.cache, host, "/events/42", "") { |
| 98 | t.Fatalf("%s /events/42 survived a list that named it", host) |
| 99 | } |
| 100 | if !cached(e.cache, host, "/events/43", "") { |
| 101 | t.Fatalf("%s /events/43 was purged but nothing named it", host) |
| 102 | } |
| 103 | } |
| 104 | if !cached(e.cache, "go.tito.example", "/events/42", "") { |
| 105 | t.Fatal("the SERVING host was purged — the list replaces it, and did not name it") |
| 106 | } |
| 107 | if got := rec.Header().Values("Carlos-Purge-Host"); len(got) != 0 { |
| 108 | t.Fatalf("Carlos-Purge-Host reached the client: %q", got) |
| 109 | } |
| 110 | }) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // TestPurgeHostListNamingTheServingHostPurgesItToo — the list replaces the |
| 115 | // serving host rather than adding to it, so an app that wants both says so. |
| 116 | func TestPurgeHostListNamingTheServingHostPurgesItToo(t *testing.T) { |
| 117 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 118 | w.Header().Set("Carlos-Purge-Host", "go.tito.example, tickets.acme.example") |
| 119 | w.Header().Set("Carlos-Purge", "/x") |
| 120 | w.WriteHeader(http.StatusOK) |
| 121 | }) |
| 122 | seed(t, e.cache, "go.tito.example", "/x", "", "serving") |
| 123 | seed(t, e.cache, "tickets.acme.example", "/x", "", "custom") |
| 124 | seed(t, e.cache, "series.acme.example", "/x", "", "unnamed") |
| 125 | |
| 126 | servePOST(e, "http://go.tito.example/write") |
| 127 | |
| 128 | if cached(e.cache, "go.tito.example", "/x", "") { |
| 129 | t.Fatal("the serving host survived a list that named it") |
| 130 | } |
| 131 | if cached(e.cache, "tickets.acme.example", "/x", "") { |
| 132 | t.Fatal("the listed custom domain survived") |
| 133 | } |
| 134 | if !cached(e.cache, "series.acme.example", "/x", "") { |
| 135 | t.Fatal("a host the list did not name was purged") |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | // TestPurgeHostListStarPurgesEveryListedHost — Tito's "every organiser write |
| 140 | // purges the whole host" rule, reaching every host of the account in one |
| 141 | // response. |
| 142 | func TestPurgeHostListStarPurgesEveryListedHost(t *testing.T) { |
| 143 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 144 | w.Header().Set("Carlos-Purge-Host", "acme.go.tito.example, tickets.acme.example") |
| 145 | w.Header().Set("Carlos-Purge", "/ignored, *") |
| 146 | w.WriteHeader(http.StatusOK) |
| 147 | }) |
| 148 | for _, host := range []string{"acme.go.tito.example", "tickets.acme.example"} { |
| 149 | seed(t, e.cache, host, "/one", "", "1") |
| 150 | seed(t, e.cache, host, "/two", "q=1", "2") |
| 151 | } |
| 152 | seed(t, e.cache, "series.acme.example", "/one", "", "unnamed") |
| 153 | |
| 154 | servePOST(e, "http://go.tito.example/write") |
| 155 | |
| 156 | for _, host := range []string{"acme.go.tito.example", "tickets.acme.example"} { |
| 157 | if cached(e.cache, host, "/one", "") || cached(e.cache, host, "/two", "q=1") { |
| 158 | t.Fatalf("* did not purge the whole of %s", host) |
| 159 | } |
| 160 | } |
| 161 | if !cached(e.cache, "series.acme.example", "/one", "") { |
| 162 | t.Fatal("* reached a host the list did not name") |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | // TestPurgeHostListCapDegradeHitsEveryListedHost — the target-count degrade |
| 167 | // follows the list, not the serving host. |
| 168 | func TestPurgeHostListCapDegradeHitsEveryListedHost(t *testing.T) { |
| 169 | targets := make([]string, maxPurgeTargets+1) |
| 170 | for i := range targets { |
| 171 | targets[i] = fmt.Sprintf("/p/%d", i) |
| 172 | } |
| 173 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 174 | w.Header().Set("Carlos-Purge-Host", "acme.go.tito.example, tickets.acme.example") |
| 175 | w.Header().Set("Carlos-Purge", strings.Join(targets, ", ")) |
| 176 | w.WriteHeader(http.StatusOK) |
| 177 | }) |
| 178 | for _, host := range []string{"go.tito.example", "acme.go.tito.example", "tickets.acme.example"} { |
| 179 | seed(t, e.cache, host, "/unnamed", "", host) |
| 180 | } |
| 181 | |
| 182 | servePOST(e, "http://go.tito.example/write") |
| 183 | |
| 184 | for _, host := range []string{"acme.go.tito.example", "tickets.acme.example"} { |
| 185 | if cached(e.cache, host, "/unnamed", "") { |
| 186 | t.Fatalf("the cap degrade did not purge listed host %s", host) |
| 187 | } |
| 188 | } |
| 189 | if !cached(e.cache, "go.tito.example", "/unnamed", "") { |
| 190 | t.Fatal("the cap degrade purged the SERVING host, which the list did not name") |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | // TestPurgeHostListOneBadEntryRefusesTheWholeList — all-or-nothing survives |
| 195 | // the widening. Wherever the bad entry sits in the list, and whatever is |
| 196 | // wrong with it, no host is purged: a list that clears the good entries and |
| 197 | // drops the bad one looks like it worked. |
| 198 | func TestPurgeHostListOneBadEntryRefusesTheWholeList(t *testing.T) { |
| 199 | routes := append(titoAccount(), |
| 200 | route("other-app.example", "other", "tenanta"), |
| 201 | route("other-account.example", "tito", "tenantb"), |
| 202 | ) |
| 203 | for _, tc := range []struct{ name, list string }{ |
| 204 | {"different app, last", "acme.go.tito.example, tickets.acme.example, other-app.example"}, |
| 205 | {"different app, first", "other-app.example, acme.go.tito.example"}, |
| 206 | {"different account", "acme.go.tito.example, other-account.example"}, |
| 207 | {"malformed, last", "acme.go.tito.example, tickets.acme.example:443"}, |
| 208 | {"malformed, first", "HTTPS://acme.go.tito.example, tickets.acme.example"}, |
| 209 | {"malformed beside an unserved one", "ghost.example, tickets.acme.example/x"}, |
| 210 | } { |
| 211 | t.Run(tc.name, func(t *testing.T) { |
| 212 | e := purgeHostsEdge(t, routes, func(w http.ResponseWriter, r *http.Request) { |
| 213 | w.Header().Set("Carlos-Purge-Host", tc.list) |
| 214 | w.Header().Set("Carlos-Purge", "/x") |
| 215 | w.WriteHeader(http.StatusOK) |
| 216 | }) |
| 217 | all := []string{"go.tito.example", "acme.go.tito.example", "tickets.acme.example", |
| 218 | "series.acme.example", "other-app.example", "other-account.example"} |
| 219 | for _, host := range all { |
| 220 | seed(t, e.cache, host, "/x", "", host) |
| 221 | } |
| 222 | |
| 223 | servePOST(e, "http://go.tito.example/write") |
| 224 | |
| 225 | for _, host := range all { |
| 226 | if !cached(e.cache, host, "/x", "") { |
| 227 | t.Fatalf("%s was purged by a list carrying a bad entry (%q)", host, tc.list) |
| 228 | } |
| 229 | } |
| 230 | }) |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | // TestPurgeHostListSkipsUnservedHosts — the one exception to all-or-nothing. |
| 235 | // An app lists its domains from its own records, which do not know which box |
| 236 | // serves which host; a domain waiting on DNS, or placed elsewhere, must not |
| 237 | // cost the purge of the hosts this edge does serve. The unserved host's own |
| 238 | // (orphaned) entry is left alone: skipped means not judged, never purged. |
| 239 | func TestPurgeHostListSkipsUnservedHosts(t *testing.T) { |
| 240 | var buf bytes.Buffer |
| 241 | log.SetOutput(&buf) |
| 242 | t.Cleanup(func() { log.SetOutput(os.Stderr) }) |
| 243 | |
| 244 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 245 | w.Header().Set("Carlos-Purge-Host", "ghost.example, tickets.acme.example") |
| 246 | w.Header().Set("Carlos-Purge", "/x") |
| 247 | w.WriteHeader(http.StatusOK) |
| 248 | }) |
| 249 | seed(t, e.cache, "ghost.example", "/x", "", "orphan") |
| 250 | seed(t, e.cache, "tickets.acme.example", "/x", "", "custom") |
| 251 | seed(t, e.cache, "go.tito.example", "/x", "", "serving") |
| 252 | |
| 253 | servePOST(e, "http://go.tito.example/write") |
| 254 | |
| 255 | if cached(e.cache, "tickets.acme.example", "/x", "") { |
| 256 | t.Fatal("an unserved entry cost the purge of a served, in-tenancy host") |
| 257 | } |
| 258 | if !cached(e.cache, "ghost.example", "/x", "") { |
| 259 | t.Fatal("an unserved host's entries were purged without any tenancy check") |
| 260 | } |
| 261 | if !cached(e.cache, "go.tito.example", "/x", "") { |
| 262 | t.Fatal("the serving host was purged, but the list did not name it") |
| 263 | } |
| 264 | if !strings.Contains(buf.String(), "ghost.example") { |
| 265 | t.Fatalf("the skipped host was not logged; log:\n%s", buf.String()) |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | // TestPurgeHostListOverTheCapIsRefused — the host cap cannot degrade the way |
| 270 | // the target cap does, because over-purging past it would mean acting on |
| 271 | // hosts nobody validated. Distinct hosts are what count: duplicates collapse |
| 272 | // before the cap is applied. |
| 273 | func TestPurgeHostListOverTheCapIsRefused(t *testing.T) { |
| 274 | names := make([]string, maxPurgeHosts) |
| 275 | names[0] = "acme.go.tito.example" |
| 276 | for i := 1; i < len(names); i++ { |
| 277 | names[i] = fmt.Sprintf("unserved-%d.example", i) |
| 278 | } |
| 279 | |
| 280 | t.Run("at the cap, with duplicates", func(t *testing.T) { |
| 281 | list := strings.Join(append(names, names...), ", ") |
| 282 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 283 | w.Header().Set("Carlos-Purge-Host", list) |
| 284 | w.Header().Set("Carlos-Purge", "/x") |
| 285 | w.WriteHeader(http.StatusOK) |
| 286 | }) |
| 287 | seed(t, e.cache, "acme.go.tito.example", "/x", "", "A") |
| 288 | servePOST(e, "http://go.tito.example/write") |
| 289 | if cached(e.cache, "acme.go.tito.example", "/x", "") { |
| 290 | t.Fatalf("a list of exactly %d distinct hosts was refused", maxPurgeHosts) |
| 291 | } |
| 292 | }) |
| 293 | |
| 294 | t.Run("one over", func(t *testing.T) { |
| 295 | list := strings.Join(append(names, "one-too-many.example"), ", ") |
| 296 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 297 | w.Header().Set("Carlos-Purge-Host", list) |
| 298 | w.Header().Set("Carlos-Purge", "/x") |
| 299 | w.WriteHeader(http.StatusOK) |
| 300 | }) |
| 301 | seed(t, e.cache, "acme.go.tito.example", "/x", "", "A") |
| 302 | servePOST(e, "http://go.tito.example/write") |
| 303 | if !cached(e.cache, "acme.go.tito.example", "/x", "") { |
| 304 | t.Fatalf("a list of %d distinct hosts was honoured", maxPurgeHosts+1) |
| 305 | } |
| 306 | }) |
| 307 | } |
| 308 | |
| 309 | // TestPurgeHostListOversizeIsRefused — the byte cap bounds the header before |
| 310 | // any splitting happens, so a huge header of duplicates cannot buy work by |
| 311 | // staying under the distinct-host cap. |
| 312 | func TestPurgeHostListOversizeIsRefused(t *testing.T) { |
| 313 | list := strings.Repeat("acme.go.tito.example,", maxPurgeHeaderBytes/len("acme.go.tito.example,")+1) |
| 314 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 315 | w.Header().Set("Carlos-Purge-Host", list) |
| 316 | w.Header().Set("Carlos-Purge", "/x") |
| 317 | w.WriteHeader(http.StatusOK) |
| 318 | }) |
| 319 | seed(t, e.cache, "acme.go.tito.example", "/x", "", "A") |
| 320 | servePOST(e, "http://go.tito.example/write") |
| 321 | if !cached(e.cache, "acme.go.tito.example", "/x", "") { |
| 322 | t.Fatalf("a %d-byte Carlos-Purge-Host was honoured", len(list)) |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | // TestPurgeHostListEveryEntryUnservedPurgesNothing — the lone-unknown-host |
| 327 | // case generalised: nothing served, nothing purged, no fallback. |
| 328 | func TestPurgeHostListEveryEntryUnservedPurgesNothing(t *testing.T) { |
| 329 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 330 | w.Header().Set("Carlos-Purge-Host", "ghost-1.example, ghost-2.example") |
| 331 | w.Header().Set("Carlos-Purge", "*") |
| 332 | w.WriteHeader(http.StatusOK) |
| 333 | }) |
| 334 | seed(t, e.cache, "go.tito.example", "/x", "", "serving") |
| 335 | servePOST(e, "http://go.tito.example/write") |
| 336 | if !cached(e.cache, "go.tito.example", "/x", "") { |
| 337 | t.Fatal("a list of unserved hosts fell back to the serving host") |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | // TestPurgeHostListReachesAHostServedThroughAWildcard — a catch-all alias |
| 342 | // ("*.acme.example") routes every subdomain to one route, and the cache keys |
| 343 | // each page on the name the browser asked for. Listing that name must purge |
| 344 | // it: the edge serves it, so skipping it as "unserved" would leave a stale |
| 345 | // page behind a purge that cleared the rest of the list. |
| 346 | func TestPurgeHostListReachesAHostServedThroughAWildcard(t *testing.T) { |
| 347 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 348 | w.Header().Set("Carlos-Purge-Host", "tickets.acme.example, foo.wild.example") |
| 349 | w.Header().Set("Carlos-Purge", "/x") |
| 350 | w.WriteHeader(http.StatusOK) |
| 351 | }) |
| 352 | upsertAlias(t, e.reg, "*.wild.example", "acme.go.tito.example") |
| 353 | if err := e.Reload(); err != nil { |
| 354 | t.Fatalf("Reload: %v", err) |
| 355 | } |
| 356 | if _, ok := e.current()["*.wild.example"]; !ok { |
| 357 | t.Fatal("fixture: the wildcard alias is not in the served table, so this proves nothing") |
| 358 | } |
| 359 | seed(t, e.cache, "foo.wild.example", "/x", "", "wild") |
| 360 | seed(t, e.cache, "tickets.acme.example", "/x", "", "custom") |
| 361 | |
| 362 | servePOST(e, "http://go.tito.example/write") |
| 363 | |
| 364 | if cached(e.cache, "foo.wild.example", "/x", "") { |
| 365 | t.Fatal("a host served through a wildcard alias was skipped as unserved") |
| 366 | } |
| 367 | if cached(e.cache, "tickets.acme.example", "/x", "") { |
| 368 | t.Fatal("the rest of the list was not honoured") |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | // TestPurgeHostListWildcardOfAnotherTenantIsRefused — resolving through the |
| 373 | // wildcard is still a tenancy check on the route it lands on: a name under |
| 374 | // another account's catch-all refuses the whole list. |
| 375 | func TestPurgeHostListWildcardOfAnotherTenantIsRefused(t *testing.T) { |
| 376 | routes := append(titoAccount(), route("other-account.example", "tito", "tenantb")) |
| 377 | e := purgeHostsEdge(t, routes, func(w http.ResponseWriter, r *http.Request) { |
| 378 | w.Header().Set("Carlos-Purge-Host", "tickets.acme.example, foo.theirs.example") |
| 379 | w.Header().Set("Carlos-Purge", "/x") |
| 380 | w.WriteHeader(http.StatusOK) |
| 381 | }) |
| 382 | upsertAlias(t, e.reg, "*.theirs.example", "other-account.example") |
| 383 | if err := e.Reload(); err != nil { |
| 384 | t.Fatalf("Reload: %v", err) |
| 385 | } |
| 386 | if _, ok := e.current()["*.theirs.example"]; !ok { |
| 387 | t.Fatal("fixture: the wildcard alias is not in the served table, so this proves nothing") |
| 388 | } |
| 389 | seed(t, e.cache, "foo.theirs.example", "/x", "", "theirs") |
| 390 | seed(t, e.cache, "tickets.acme.example", "/x", "", "custom") |
| 391 | |
| 392 | servePOST(e, "http://go.tito.example/write") |
| 393 | |
| 394 | if !cached(e.cache, "foo.theirs.example", "/x", "") { |
| 395 | t.Fatal("a host under ANOTHER ACCOUNT's wildcard was purged") |
| 396 | } |
| 397 | if !cached(e.cache, "tickets.acme.example", "/x", "") { |
| 398 | t.Fatal("a list carrying a cross-tenant entry was partly honoured") |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | // TestPurgeHostListNamingAWildcardKeyIsRefused — "*.acme.example" is a |
| 403 | // routing pattern, not a name anything is cached under, so naming it is a |
| 404 | // malformed entry and refuses the list rather than purging nothing quietly. |
| 405 | func TestPurgeHostListNamingAWildcardKeyIsRefused(t *testing.T) { |
| 406 | e := purgeHostsEdge(t, titoAccount(), func(w http.ResponseWriter, r *http.Request) { |
| 407 | w.Header().Set("Carlos-Purge-Host", "tickets.acme.example, *.wild.example") |
| 408 | w.Header().Set("Carlos-Purge", "/x") |
| 409 | w.WriteHeader(http.StatusOK) |
| 410 | }) |
| 411 | upsertAlias(t, e.reg, "*.wild.example", "acme.go.tito.example") |
| 412 | if err := e.Reload(); err != nil { |
| 413 | t.Fatalf("Reload: %v", err) |
| 414 | } |
| 415 | seed(t, e.cache, "tickets.acme.example", "/x", "", "custom") |
| 416 | |
| 417 | servePOST(e, "http://go.tito.example/write") |
| 418 | |
| 419 | if !cached(e.cache, "tickets.acme.example", "/x", "") { |
| 420 | t.Fatal("a list naming a wildcard key was honoured") |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | // TestPurgeHostListRefusedWhenTheServingRouteChangedSinceCapture — the |
| 425 | // staleness re-read runs for the first SERVED cross-host entry wherever it |
| 426 | // sits, not only when it leads the list. Own host first and an unserved host |
| 427 | // first are the two orders that reach a served sibling without it being |
| 428 | // first. |
| 429 | func TestPurgeHostListRefusedWhenTheServingRouteChangedSinceCapture(t *testing.T) { |
| 430 | for _, list := range []string{ |
| 431 | "a.example.com, b.example.com", |
| 432 | "ghost.example.com, b.example.com", |
| 433 | } { |
| 434 | t.Run(list, func(t *testing.T) { |
| 435 | var ( |
| 436 | e *Edge |
| 437 | reg *registry.Registry |
| 438 | addr string |
| 439 | ) |
| 440 | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 441 | // The route table moves under the in-flight request. |
| 442 | if err := reg.Upsert(registry.Route{ |
| 443 | Host: "a.example.com", App: "tito", Account: "tenantb", |
| 444 | Kind: "instance", Addr: addr, Status: "ready", |
| 445 | }); err != nil { |
| 446 | t.Errorf("upsert during flight: %v", err) |
| 447 | } |
| 448 | if err := e.Reload(); err != nil { |
| 449 | t.Errorf("reload during flight: %v", err) |
| 450 | } |
| 451 | w.Header().Set("Carlos-Purge-Host", list) |
| 452 | w.Header().Set("Carlos-Purge", "/x") |
| 453 | w.WriteHeader(http.StatusOK) |
| 454 | })) |
| 455 | t.Cleanup(backend.Close) |
| 456 | addr = backend.Listener.Addr().String() |
| 457 | reg = openReg(t) |
| 458 | for _, h := range []string{"a.example.com", "b.example.com"} { |
| 459 | if err := reg.Upsert(registry.Route{ |
| 460 | Host: h, App: "tito", Account: "tenanta", |
| 461 | Kind: "instance", Addr: addr, Status: "ready", |
| 462 | }); err != nil { |
| 463 | t.Fatalf("upsert %s: %v", h, err) |
| 464 | } |
| 465 | } |
| 466 | var err error |
| 467 | if e, err = New(reg, nil); err != nil { |
| 468 | t.Fatalf("New: %v", err) |
| 469 | } |
| 470 | e.EnableCache(NewCache(DefaultCacheMaxEntries, DefaultCacheMaxBodyBytes)) |
| 471 | seed(t, e.cache, "a.example.com", "/x", "", "A") |
| 472 | seed(t, e.cache, "b.example.com", "/x", "", "B") |
| 473 | |
| 474 | servePOST(e, "http://a.example.com/write") |
| 475 | |
| 476 | if !cached(e.cache, "b.example.com", "/x", "") { |
| 477 | t.Fatal("the sibling was purged on the strength of a STALE captured identity") |
| 478 | } |
| 479 | if !cached(e.cache, "a.example.com", "/x", "") { |
| 480 | t.Fatal("a refused list was partly honoured on the serving host") |
| 481 | } |
| 482 | }) |
| 483 | } |
| 484 | } |
| 485 | |