carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package domains
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// aliasRegWith opens a fresh registry and seeds it with routes, the same
17// pattern reconcile_test.go's regWith uses (that helper lives in the
18// domains_test package, unreachable from here).
19func aliasRegWith(t *testing.T, routes ...registry.Route) *registry.Registry {
20 t.Helper()
21 reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
22 if err != nil {
23 t.Fatal(err)
24 }
25 for _, rt := range routes {
26 if err := reg.Upsert(rt); err != nil {
27 t.Fatal(err)
28 }
29 }
30 return reg
31}
32
33// putClaim writes c's JSON straight to its ClaimKey, bypassing AttachClaim's
34// CAS dance — the reconcile pass reads whatever is in the bucket, not just
35// what AttachClaim would have produced.
36func putClaim(t *testing.T, s release.Store, c Claim) {
37 t.Helper()
38 c.Schema = 1
39 body, err := json.Marshal(c)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if _, err := s.Put(context.Background(), ClaimKey(c.Host), body, ""); err != nil {
44 t.Fatal(err)
45 }
46}
47
48// deleteClaim removes host's claim from the bucket, so a re-run of
49// ReconcileAliases sees it gone (pass-owned removal tests).
50func deleteClaim(t *testing.T, s release.Store, host string) {
51 t.Helper()
52 if err := s.Delete(context.Background(), ClaimKey(host)); err != nil {
53 t.Fatal(err)
54 }
55}
56
57// addRoute upserts one route into reg, same shape the other tests in this
58// file inline into aliasRegWith's constructor call.
59func addRoute(t *testing.T, reg *registry.Registry, host, account, app string) {
60 t.Helper()
61 if err := reg.Upsert(registry.Route{
62 Host: host, App: app, Account: account, Kind: "instance", Status: "ready",
63 }); err != nil {
64 t.Fatal(err)
65 }
66}
67
68func aliasNow() func() time.Time {
69 return func() time.Time { return time.Unix(1755000000, 0) }
70}
71
72func TestReconcileAliasesMaterializes(t *testing.T) {
73 s := release.NewDirStore(t.TempDir())
74 reg := aliasRegWith(t, registry.Route{
75 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
76 })
77 putClaim(t, s, Claim{
78 Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live",
79 })
80 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
81 t.Fatal(err)
82 }
83 aliases, err := reg.ListAliases()
84 if err != nil {
85 t.Fatal(err)
86 }
87 if len(aliases) != 1 {
88 t.Fatalf("got %d aliases, want 1: %+v", len(aliases), aliases)
89 }
90 a := aliases[0]
91 if a.Alias != "tickets.example.com" || a.Host != "canary/live" ||
92 a.Account != "bab" || a.App != "kass" || a.CreatedAt != 1755000000 {
93 t.Fatalf("alias row mismatch: %+v", a)
94 }
95}
96
97func TestReconcileAliasesOwnershipGate(t *testing.T) {
98 s := release.NewDirStore(t.TempDir())
99 reg := aliasRegWith(t, registry.Route{
100 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
101 })
102 // Claim names the right route but the wrong app.
103 putClaim(t, s, Claim{
104 Host: "tickets.example.com", Account: "bab", App: "somethingelse", Route: "canary/live",
105 })
106 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
107 t.Fatal(err)
108 }
109 aliases, err := reg.ListAliases()
110 if err != nil {
111 t.Fatal(err)
112 }
113 if len(aliases) != 0 {
114 t.Fatalf("got %d aliases, want 0: %+v", len(aliases), aliases)
115 }
116}
117
118func TestReconcileAliasesOwnershipGateAccountMismatch(t *testing.T) {
119 s := release.NewDirStore(t.TempDir())
120 // Route's account is a VALID sqid ("bad") so release.RouteAccount
121 // returns it verbatim rather than falling back to OpsAccount — the
122 // mismatch must come from the comparison itself, not an invalid-account
123 // fallback masking it.
124 reg := aliasRegWith(t, registry.Route{
125 Host: "canary/live", App: "kass", Account: "bad", Kind: "instance", Status: "ready",
126 })
127 // Claim names the right route and the right app, but a different
128 // (also valid) account.
129 putClaim(t, s, Claim{
130 Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live",
131 })
132 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
133 t.Fatal(err)
134 }
135 aliases, err := reg.ListAliases()
136 if err != nil {
137 t.Fatal(err)
138 }
139 if len(aliases) != 0 {
140 t.Fatalf("got %d aliases, want 0 (account mismatch must refuse): %+v", len(aliases), aliases)
141 }
142}
143
144func TestReconcileAliasesRoutesWin(t *testing.T) {
145 s := release.NewDirStore(t.TempDir())
146 reg := aliasRegWith(t,
147 registry.Route{Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready"},
148 registry.Route{Host: "tickets.example.com", App: "other", Account: "bab", Kind: "instance", Status: "ready"},
149 )
150 putClaim(t, s, Claim{
151 Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live",
152 })
153 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
154 t.Fatal(err)
155 }
156 aliases, err := reg.ListAliases()
157 if err != nil {
158 t.Fatal(err)
159 }
160 if len(aliases) != 0 {
161 t.Fatalf("got %d aliases, want 0 (a real route owns that host): %+v", len(aliases), aliases)
162 }
163}
164
165func TestReconcileAliasesMissingRouteSkips(t *testing.T) {
166 s := release.NewDirStore(t.TempDir())
167 reg := aliasRegWith(t) // no routes at all — this box doesn't have the route
168 putClaim(t, s, Claim{
169 Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live",
170 })
171 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
172 t.Fatal(err)
173 }
174 aliases, err := reg.ListAliases()
175 if err != nil {
176 t.Fatal(err)
177 }
178 if len(aliases) != 0 {
179 t.Fatalf("got %d aliases, want 0: %+v", len(aliases), aliases)
180 }
181}
182
183func TestReconcileAliasesRemovesOrphans(t *testing.T) {
184 s := release.NewDirStore(t.TempDir())
185 reg := aliasRegWith(t, registry.Route{
186 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
187 })
188 if err := reg.UpsertAlias(registry.RouteAlias{
189 Alias: "orphan.example.com", Host: "canary/live", Account: "bab", App: "kass",
190 CreatedAt: 1000,
191 }); err != nil {
192 t.Fatal(err)
193 }
194 // No claim backs orphan.example.com.
195 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
196 t.Fatal(err)
197 }
198 aliases, err := reg.ListAliases()
199 if err != nil {
200 t.Fatal(err)
201 }
202 if len(aliases) != 0 {
203 t.Fatalf("got %d aliases, want 0 (orphan should have been removed): %+v", len(aliases), aliases)
204 }
205}
206
207func TestReconcileAliasesRouteless(t *testing.T) {
208 s := release.NewDirStore(t.TempDir())
209 reg := aliasRegWith(t, registry.Route{
210 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
211 })
212 // A first-route claim (#82's case): no Route set yet.
213 putClaim(t, s, Claim{
214 Host: "tickets.example.com", Account: "bab", App: "kass",
215 })
216 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
217 t.Fatal(err)
218 }
219 aliases, err := reg.ListAliases()
220 if err != nil {
221 t.Fatal(err)
222 }
223 if len(aliases) != 0 {
224 t.Fatalf("got %d aliases, want 0 (routeless claim must be ignored): %+v", len(aliases), aliases)
225 }
226}
227
228func TestReconcileAliasesIdempotent(t *testing.T) {
229 s := release.NewDirStore(t.TempDir())
230 reg := aliasRegWith(t, registry.Route{
231 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
232 })
233 putClaim(t, s, Claim{
234 Host: "tickets.example.com", Account: "bab", App: "kass", Route: "canary/live",
235 })
236 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
237 t.Fatal(err)
238 }
239 first, err := reg.ListAliases()
240 if err != nil {
241 t.Fatal(err)
242 }
243 if len(first) != 1 {
244 t.Fatalf("got %d aliases after first pass, want 1: %+v", len(first), first)
245 }
246 firstCreatedAt := first[0].CreatedAt
247
248 // Second pass, later clock: CreatedAt must be preserved, not bumped.
249 later := func() time.Time { return time.Unix(1755000000+3600, 0) }
250 if err := ReconcileAliases(context.Background(), s, reg, later, nil, nil); err != nil {
251 t.Fatal(err)
252 }
253 second, err := reg.ListAliases()
254 if err != nil {
255 t.Fatal(err)
256 }
257 if len(second) != 1 {
258 t.Fatalf("got %d aliases after second pass, want 1: %+v", len(second), second)
259 }
260 if second[0].CreatedAt != firstCreatedAt {
261 t.Fatalf("CreatedAt changed on re-run: first=%d second=%d", firstCreatedAt, second[0].CreatedAt)
262 }
263 if second[0].Alias != first[0].Alias || second[0].Host != first[0].Host ||
264 second[0].Account != first[0].Account || second[0].App != first[0].App {
265 t.Fatalf("alias row changed on re-run: first=%+v second=%+v", first[0], second[0])
266 }
267}
268
269// TestReconcileWildcardCreatedAtStable is the final-review pin: unlike
270// route_aliases (UpsertAlias's ON CONFLICT deliberately leaves created_at out
271// of its SET list, so the DB itself preserves it), UpsertWildcardDomain DOES
272// overwrite created_at on conflict — wildcard_domains.created_at is only kept
273// stable by the reconcile pass's own skip-upsert-when-unchanged guard (an
274// unchanged host/account/app row is never re-upserted). That guard matters
275// because Fingerprint hashes wildcard_domains.created_at (unlike
276// route_aliases, which excludes it): a created_at that bumped on every pass
277// would move the fingerprint on every 2s poll and force a needless edge
278// reload forever, even with a completely unchanged claim set.
279func TestReconcileWildcardCreatedAtStable(t *testing.T) {
280 s := release.NewDirStore(t.TempDir())
281 reg := aliasRegWith(t)
282 addRoute(t, reg, "woodstar-relay.bab.oncarlos.com", "bab", "woodstar-relay")
283 putClaim(t, s, Claim{Host: "woodstar.app", Account: "bab", App: "woodstar-relay",
284 Route: "woodstar-relay.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z",
285 Wildcard: true, Catchall: true})
286
287 if err := ReconcileAliases(context.Background(), s, reg, func() time.Time { return time.Unix(1000, 0) }, nil, nil); err != nil {
288 t.Fatal(err)
289 }
290 first, err := reg.ListWildcardDomains()
291 if err != nil {
292 t.Fatal(err)
293 }
294 if len(first) != 1 || first[0].Host != "woodstar.app" {
295 t.Fatalf("wildcard_domains after first pass = %+v, want one row for woodstar.app", first)
296 }
297 firstCreatedAt := first[0].CreatedAt
298
299 // Second pass, later clock, same claim set: CreatedAt must be preserved,
300 // not bumped — a bump here would move Fingerprint's wildcard_domains
301 // section every poll and force a reload→refresh loop with nothing
302 // actually changed.
303 if err := ReconcileAliases(context.Background(), s, reg, func() time.Time { return time.Unix(1000+3600, 0) }, nil, nil); err != nil {
304 t.Fatal(err)
305 }
306 second, err := reg.ListWildcardDomains()
307 if err != nil {
308 t.Fatal(err)
309 }
310 if len(second) != 1 || second[0].Host != "woodstar.app" {
311 t.Fatalf("wildcard_domains after second pass = %+v, want one row for woodstar.app", second)
312 }
313 if second[0].CreatedAt != firstCreatedAt {
314 t.Fatalf("wildcard_domains CreatedAt changed on re-run: first=%d second=%d", firstCreatedAt, second[0].CreatedAt)
315 }
316}
317
318func TestReconcileDerivesWildcardState(t *testing.T) {
319 ctx := context.Background()
320 s := release.NewDirStore(t.TempDir())
321 reg := aliasRegWith(t)
322 // Local primary route, tenancy bab/woodstar-relay.
323 addRoute(t, reg, "woodstar-relay.bab.oncarlos.com", "bab", "woodstar-relay")
324
325 putClaim(t, s, Claim{Host: "woodstar.app", Account: "bab", App: "woodstar-relay",
326 Route: "woodstar-relay.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z",
327 Wildcard: true, Catchall: true})
328 // Wildcard-only (no catchall): row yes, *. alias no.
329 putClaim(t, s, Claim{Host: "certonly.example", Account: "bab", App: "woodstar-relay",
330 Route: "woodstar-relay.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z", Wildcard: true})
331 // Wildcard claim whose primary is NOT on this box: no row.
332 putClaim(t, s, Claim{Host: "elsewhere.example", Account: "bab", App: "woodstar-relay",
333 Route: "not-here.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z", Wildcard: true})
334 // Invalid host shape: no row, no alias.
335 putClaim(t, s, Claim{Host: "bad host.example", Account: "bab", App: "woodstar-relay",
336 Route: "woodstar-relay.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z", Wildcard: true, Catchall: true})
337
338 if err := ReconcileAliases(ctx, s, reg, func() time.Time { return time.Unix(1000, 0) }, nil, nil); err != nil {
339 t.Fatal(err)
340 }
341
342 wds, err := reg.ListWildcardDomains()
343 if err != nil {
344 t.Fatal(err)
345 }
346 if len(wds) != 2 || wds[0].Host != "certonly.example" || wds[1].Host != "woodstar.app" {
347 t.Fatalf("wildcard_domains = %+v", wds)
348 }
349 aliases, _ := reg.ListAliases()
350 var stars []string
351 for _, a := range aliases {
352 if strings.HasPrefix(a.Alias, "*.") {
353 stars = append(stars, a.Alias+"->"+a.Host)
354 }
355 }
356 if len(stars) != 1 || stars[0] != "*.woodstar.app->woodstar-relay.bab.oncarlos.com" {
357 t.Fatalf("catch-all aliases = %v", stars)
358 }
359
360 // Claim removed => derived state removed (pass-owned).
361 deleteClaim(t, s, "woodstar.app")
362 if err := ReconcileAliases(ctx, s, reg, func() time.Time { return time.Unix(2000, 0) }, nil, nil); err != nil {
363 t.Fatal(err)
364 }
365 wds, _ = reg.ListWildcardDomains()
366 if len(wds) != 1 || wds[0].Host != "certonly.example" {
367 t.Fatalf("after claim delete: %+v", wds)
368 }
369 aliases, _ = reg.ListAliases()
370 for _, a := range aliases {
371 if strings.HasPrefix(a.Alias, "*.") {
372 t.Fatalf("catch-all alias must be removed with its claim: %+v", a)
373 }
374 }
375}
376
377func TestReconcileWildcardHostCollisionMismatchedTenant(t *testing.T) {
378 // A named-Route wildcard claim whose own Host is itself a REAL local
379 // route owned by a DIFFERENT tenant: the wildcard row (and any
380 // catch-all) must be refused, same spirit as the plain-alias half
381 // already refusing to shadow a real route. The claim's Route names a
382 // primary that DOES belong to the claim's own tenant, so the
383 // primary-tenancy check alone would pass — the collision on c.Host
384 // itself is the only gate that can catch this case.
385 ctx := context.Background()
386 s := release.NewDirStore(t.TempDir())
387 reg := aliasRegWith(t)
388 addRoute(t, reg, "primary.bab.oncarlos.com", "bab", "appA")
389 addRoute(t, reg, "hostx.example", "bad", "appB") // real route, different tenant
390
391 putClaim(t, s, Claim{Host: "hostx.example", Account: "bab", App: "appA",
392 Route: "primary.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z",
393 Wildcard: true, Catchall: true})
394
395 if err := ReconcileAliases(ctx, s, reg, func() time.Time { return time.Unix(1000, 0) }, nil, nil); err != nil {
396 t.Fatal(err)
397 }
398 wds, err := reg.ListWildcardDomains()
399 if err != nil {
400 t.Fatal(err)
401 }
402 if len(wds) != 0 {
403 t.Fatalf("wildcard_domains = %+v, want none (real route under a different tenant owns the host)", wds)
404 }
405 aliases, err := reg.ListAliases()
406 if err != nil {
407 t.Fatal(err)
408 }
409 for _, a := range aliases {
410 if strings.HasPrefix(a.Alias, "*.") {
411 t.Fatalf("no catch-all should derive from a refused wildcard claim: %+v", a)
412 }
413 }
414}
415
416func TestReconcileWildcardCap(t *testing.T) {
417 ctx := context.Background()
418 s := release.NewDirStore(t.TempDir())
419 reg := aliasRegWith(t)
420 addRoute(t, reg, "primary.bab.oncarlos.com", "bab", "appA")
421
422 // 33 valid wildcard claims sharing one local primary, with deterministic
423 // zero-padded hosts so bucket-list (lexicographic) order matches numeric
424 // order: w00.example .. w32.example.
425 for i := 0; i < 33; i++ {
426 putClaim(t, s, Claim{Host: fmt.Sprintf("w%02d.example", i), Account: "bab", App: "appA",
427 Route: "primary.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z", Wildcard: true})
428 }
429
430 if err := ReconcileAliases(ctx, s, reg, func() time.Time { return time.Unix(1000, 0) }, nil, nil); err != nil {
431 t.Fatal(err)
432 }
433 wds, err := reg.ListWildcardDomains()
434 if err != nil {
435 t.Fatal(err)
436 }
437 if len(wds) != 32 {
438 t.Fatalf("got %d wildcard_domains, want 32 (per-box cap): %+v", len(wds), wds)
439 }
440 for _, w := range wds {
441 if w.Host == "w32.example" {
442 t.Fatalf("w32.example (33rd claim, lexicographically last) should have been dropped over the cap: %+v", wds)
443 }
444 }
445}
446
447func TestReconcileWildcardEmptyRouteClaim(t *testing.T) {
448 // First-route claim (#82's shape): empty Route, the claim's own host IS
449 // the primary once materialized. Catchall aliases that primary.
450 ctx := context.Background()
451 s := release.NewDirStore(t.TempDir())
452 reg := aliasRegWith(t)
453 addRoute(t, reg, "story.example", "bab", "story")
454 putClaim(t, s, Claim{Host: "story.example", Account: "bab", App: "story",
455 CreatedAt: "2026-08-13T00:00:00Z", Wildcard: true, Catchall: true})
456
457 if err := ReconcileAliases(ctx, s, reg, func() time.Time { return time.Unix(1000, 0) }, nil, nil); err != nil {
458 t.Fatal(err)
459 }
460 wds, _ := reg.ListWildcardDomains()
461 if len(wds) != 1 || wds[0].Host != "story.example" {
462 t.Fatalf("wildcard_domains = %+v", wds)
463 }
464 aliases, _ := reg.ListAliases()
465 found := false
466 for _, a := range aliases {
467 if a.Alias == "*.story.example" && a.Host == "story.example" {
468 found = true
469 }
470 }
471 if !found {
472 t.Fatalf("want *.story.example -> story.example, got %+v", aliases)
473 }
474}
475
476// --- Task 4: the reserved sidecar suffix, enforced at the alias door -----
477//
478// Task 3 already refuses to SERVE a Kind=="sidecar" row (edge.go's Reload).
479// ReconcileAliases must never mint a route_aliases row that names a
480// reserved-suffix host on EITHER end: as the primary (Host), which would
481// keep an unroutable sidecar name "alive" as somebody's alias target, and —
482// the direction the Task 3 review caught, and the one most likely to be
483// missed — as the alias's OWN name (Alias), which would insert a sidecar
484// hostname into the edge's routing map serving an ORDINARY route's upstream,
485// making Allowed() return true for it (an ACME order on a sidecar hostname).
486
487// TestReconcileAliasesRefusesReservedSuffixPrimary: the claim's Route names
488// a host under the reserved suffix. Even though such a route can exist in
489// the registry (a real provisioned sidecar's own row, Kind=="sidecar"),
490// ReconcileAliases must never let anything alias it.
491func TestReconcileAliasesRefusesReservedSuffixPrimary(t *testing.T) {
492 s := release.NewDirStore(t.TempDir())
493 reg := aliasRegWith(t, registry.Route{
494 Host: "bot.eleven.bdm.sidecar.internal", App: "eleven", Account: "bdm", Kind: "sidecar", Status: "ready",
495 })
496 putClaim(t, s, Claim{
497 Host: "tickets.example.com", Account: "bdm", App: "eleven", Route: "bot.eleven.bdm.sidecar.internal",
498 })
499 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
500 t.Fatal(err)
501 }
502 aliases, err := reg.ListAliases()
503 if err != nil {
504 t.Fatal(err)
505 }
506 if len(aliases) != 0 {
507 t.Fatalf("got %d aliases, want 0 (a reserved-suffix primary must never be aliased): %+v", len(aliases), aliases)
508 }
509}
510
511// TestReconcileAliasesRefusesReservedSuffixAliasName is the end the brief's
512// wording alone would miss: the PRIMARY is an ordinary route, but the
513// ALIAS's own name is under the reserved suffix. Nothing about tenancy or
514// route existence would refuse this — only a check on the alias name
515// itself does, and this is the exact row the Task 3 review named: inserted
516// into edge's routing map, it would serve the primary's upstream under a
517// sidecar hostname and make Allowed() true for it.
518func TestReconcileAliasesRefusesReservedSuffixAliasName(t *testing.T) {
519 s := release.NewDirStore(t.TempDir())
520 reg := aliasRegWith(t, registry.Route{
521 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
522 })
523 putClaim(t, s, Claim{
524 Host: "bot.eleven.bdm.sidecar.internal", Account: "bab", App: "kass", Route: "canary/live",
525 })
526 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
527 t.Fatal(err)
528 }
529 aliases, err := reg.ListAliases()
530 if err != nil {
531 t.Fatal(err)
532 }
533 if len(aliases) != 0 {
534 t.Fatalf("got %d aliases, want 0 (an alias name under the reserved suffix must never be minted): %+v", len(aliases), aliases)
535 }
536}
537
538// TestReconcileAliasesRefusesReservedSuffixCatchall: the same alias-name-end
539// danger, through the OTHER route_aliases writer (the catch-all "*.<host>"
540// derivation off a Wildcard+Catchall claim) — a second call site, so it
541// needs its own guard, not just the plain-alias one above.
542func TestReconcileAliasesRefusesReservedSuffixCatchall(t *testing.T) {
543 ctx := context.Background()
544 s := release.NewDirStore(t.TempDir())
545 reg := aliasRegWith(t)
546 addRoute(t, reg, "app.bab.oncarlos.com", "bab", "app")
547 putClaim(t, s, Claim{
548 Host: "sub.eleven.bdm.sidecar.internal", Account: "bab", App: "app",
549 Route: "app.bab.oncarlos.com", CreatedAt: "2026-08-13T00:00:00Z",
550 Wildcard: true, Catchall: true,
551 })
552 if err := ReconcileAliases(ctx, s, reg, aliasNow(), nil, nil); err != nil {
553 t.Fatal(err)
554 }
555 aliases, err := reg.ListAliases()
556 if err != nil {
557 t.Fatal(err)
558 }
559 for _, a := range aliases {
560 if strings.HasPrefix(a.Alias, "*.") {
561 t.Fatalf("a catch-all alias under the reserved suffix must never be minted: %+v", a)
562 }
563 }
564}
565
566// A claim naming a hostname that ANOTHER box serves as a real route must not
567// materialize. The byHost check above it is per-box (byHost is THIS box's
568// routes), and the attach door cannot verify a route exists at all —
569// operator-minted rows have no instance record. So without this, an account
570// can claim someone else's operator route and, now that aliases publish
571// fleet-wide, capture its traffic on every remote edge.
572func TestReconcileAliasesRefusesAHostAnotherBoxServesAsARoute(t *testing.T) {
573 s := release.NewDirStore(t.TempDir())
574 reg := aliasRegWith(t, registry.Route{
575 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
576 })
577 putClaim(t, s, Claim{
578 Host: "go.tito.io", Account: "bab", App: "kass", Route: "canary/live",
579 })
580
581 // go.tito.io is a real, non-alias route on some OTHER box in the fleet.
582 deploymentRoutes := map[string]bool{"go.tito.io": true}
583
584 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), deploymentRoutes, nil); err != nil {
585 t.Fatal(err)
586 }
587 aliases, err := reg.ListAliases()
588 if err != nil {
589 t.Fatal(err)
590 }
591 for _, a := range aliases {
592 if a.Alias == "go.tito.io" {
593 t.Fatalf("an alias must not materialize for a hostname another box serves as a real route: %+v", aliases)
594 }
595 }
596}
597
598// Retro-clean: a claim that materialized before this rule existed is removed
599// on the next pass, which is the half neither the fold rule nor a door check
600// can deliver — both of those only ever act on NEW traffic and NEW attaches.
601func TestReconcileAliasesRemovesAnAliasThatNowCollidesWithADeploymentRoute(t *testing.T) {
602 s := release.NewDirStore(t.TempDir())
603 reg := aliasRegWith(t, registry.Route{
604 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
605 })
606 if err := reg.UpsertAlias(registry.RouteAlias{
607 Alias: "go.tito.io", Host: "canary/live", Account: "bab", App: "kass", CreatedAt: 1000,
608 }); err != nil {
609 t.Fatal(err)
610 }
611 putClaim(t, s, Claim{
612 Host: "go.tito.io", Account: "bab", App: "kass", Route: "canary/live",
613 })
614
615 deploymentRoutes := map[string]bool{"go.tito.io": true}
616
617 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), deploymentRoutes, nil); err != nil {
618 t.Fatal(err)
619 }
620 aliases, err := reg.ListAliases()
621 if err != nil {
622 t.Fatal(err)
623 }
624 for _, a := range aliases {
625 if a.Alias == "go.tito.io" {
626 t.Fatalf("a pre-existing colliding alias must be removed on the next pass: %+v", aliases)
627 }
628 }
629}
630
631// An empty/unknown deployment-route set must not break ordinary attach: the
632// fold degrades to "nothing known", and an unrelated custom domain still
633// materializes. Fail-OPEN here is the deliberate choice — the fold is
634// best-effort evidence read out of a bucket, and a bucket that cannot be
635// listed must not take every tenant's custom domain down with it. The local
636// byHost check and the remote fold's route preference both still stand.
637func TestReconcileAliasesStillMaterializesWhenTheRouteSetIsUnknown(t *testing.T) {
638 s := release.NewDirStore(t.TempDir())
639 reg := aliasRegWith(t, registry.Route{
640 Host: "canary/live", App: "kass", Account: "bab", Kind: "instance", Status: "ready",
641 })
642 putClaim(t, s, Claim{
643 Host: "shop.customer.com", Account: "bab", App: "kass", Route: "canary/live",
644 })
645
646 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), nil, nil); err != nil {
647 t.Fatal(err)
648 }
649 aliases, err := reg.ListAliases()
650 if err != nil {
651 t.Fatal(err)
652 }
653 if len(aliases) != 1 || aliases[0].Alias != "shop.customer.com" {
654 t.Fatalf("aliases = %+v, want the ordinary custom domain materialized", aliases)
655 }
656}
657
658// The pin for the one mistake this task is most likely to make: the
659// catch-all guard must ask about "*."+host, NEVER host.
660//
661// A first-route wildcard claim's own host IS a real, non-alias route on the
662// claimant's OWN box — that is precisely what "first route" means — so it
663// appears in the deployment route set by construction. A guard keyed on
664// c.Host would therefore refuse the catch-all for every wildcard first-route
665// claim in the fleet, on the very deployment that owns it. That is the
666// woodstar.app class, live in production today.
667//
668// This test fails with deploymentRoutes[c.Host] and passes with
669// deploymentRoutes[star]. Nothing else in this file distinguishes them.
670func TestReconcileAliasesStillDerivesTheCatchAllForAFirstRouteClaim(t *testing.T) {
671 s := release.NewDirStore(t.TempDir())
672 // The route carries the claim's OWN account and app on purpose: the
673 // ownership gate above the new guard would short-circuit on a mismatched
674 // row and the test would pass for the wrong reason.
675 reg := aliasRegWith(t, registry.Route{
676 Host: "woodstar.app", App: "site", Account: "bab", Kind: "static", Status: "ready",
677 })
678 putClaim(t, s, Claim{
679 Host: "woodstar.app", Account: "bab", App: "site",
680 Route: "", Wildcard: true, Catchall: true, // first-route: its own primary
681 })
682
683 // The claimant's own box publishes woodstar.app as a REAL route.
684 deploymentRoutes := map[string]bool{"woodstar.app": true}
685
686 if err := ReconcileAliases(context.Background(), s, reg, aliasNow(), deploymentRoutes, nil); err != nil {
687 t.Fatal(err)
688 }
689 aliases, err := reg.ListAliases()
690 if err != nil {
691 t.Fatal(err)
692 }
693 found := false
694 for _, a := range aliases {
695 if a.Alias == "*.woodstar.app" && a.Host == "woodstar.app" {
696 found = true
697 }
698 }
699 if !found {
700 t.Fatalf("catch-all missing: %+v — the guard keyed on the claim host "+
701 "instead of \"*.\"+host, refusing every first-route wildcard claim", aliases)
702 }
703}
704