package edgesfile import ( "bytes" "context" "errors" "log" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // The fold can only WIDEN: a box the deployment can see adds its EIP, and the // operator's env list is always included. That is the safe direction — // nothing here deletes, and a short fold must degrade to today's behaviour // (env only), never below it. func TestEdgeIPsUnionsTheFoldWithTheEnvList(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`) putEdgesFile(t, s, "betty", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{}}`) putFreshBox(t, s, "annie", now) putFreshBox(t, s, "betty", now) got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now) slices.Sort(got) want := []string{"1.1.1.1", "2.2.2.2", "9.9.9.9"} if !slices.Equal(got, want) { t.Fatalf("EdgeIPs = %v, want %v", got, want) } } // A box whose STATUS SNAPSHOT has gone stale drops out — that is what keeps a // retired box's lingering edges file from holding a dead customer domain at // "pointing" forever (a lying green). Generation is NOT the signal: an // unchanged file is never republished, so a healthy box's Generation is // arbitrarily old. func TestEdgeIPsDropsABoxWhoseSnapshotIsStale(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`) putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{}}`) putFreshBox(t, s, "annie", now) putFreshBox(t, s, "retired", now.Add(-72*time.Hour)) got := EdgeIPs(context.Background(), s, nil, now) if slices.Contains(got, "3.3.3.3") { t.Fatalf("EdgeIPs = %v, want the retired box's EIP dropped", got) } if !slices.Contains(got, "1.1.1.1") { t.Fatalf("EdgeIPs = %v, want the live box kept", got) } } // An edges file with no matching status snapshot at all is not evidence of // liveness. Same treatment as a stale one. func TestEdgeIPsDropsABoxWithNoSnapshot(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "ghost", `{"v":1,"box":"ghost","eip":"4.4.4.4","hosts":{}}`) if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) { t.Fatalf("EdgeIPs = %v, want env only", got) } } // A store that cannot be listed degrades to the env list — never to empty. An // empty edge set would stamp every custom domain "elsewhere" at once. func TestEdgeIPsFallsBackToEnvWhenTheStoreFails(t *testing.T) { s := brokenStore{} now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) { t.Fatalf("EdgeIPs = %v, want the env list on a store failure", got) } } // The fixtures above call now "fresh" and now-72h "stale" in their own // words; this asserts the RULE agrees, so a change to status.SnapshotPeriod // or to BoxStale's factor turns this test red rather than quietly turning // the two tests above into assertions about nothing. func TestFoldFixtureAgreesWithTheStalenessRule(t *testing.T) { now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) if status.BoxStale(status.Box{GeneratedAt: now}, now, status.SnapshotPeriod) { t.Error("a snapshot written this instant must read as fresh") } if !status.BoxStale(status.Box{GeneratedAt: now.Add(-72 * time.Hour)}, now, status.SnapshotPeriod) { t.Error("a snapshot 72h old must read as stale — the retired-box fixture depends on it") } } // A box publishing no EIP at all (readPublicIP never answered) contributes // nothing rather than an empty string, which would match no resolution and // only pollute the guidance strings the console builds from the same set. func TestEdgeIPsSkipsABoxWithNoAddress(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"","hosts":{}}`) putFreshBox(t, s, "annie", now) if got := EdgeIPs(context.Background(), s, nil, now); len(got) != 0 { t.Fatalf("EdgeIPs = %v, want nothing — a blank EIP is not an address", got) } } // ---- fixtures --------------------------------------------------------------- // foldTestStore is a real DirStore over a temp dir: the fold reads through // release.Store and status.LoadBoxes, and a hand-rolled fake would have to // re-implement both prefixes' listing semantics to say anything true. func foldTestStore(t *testing.T) release.Store { t.Helper() return release.NewDirStore(t.TempDir()) } func putEdgesFile(t *testing.T, s release.Store, box, body string) { t.Helper() if _, err := s.Put(context.Background(), Key(box), []byte(body), ""); err != nil { t.Fatalf("put edges file for %s: %v", box, err) } } // putFreshBox writes one box snapshot through status.WriteBox — the SAME // writer statusTick uses — so the key derivation and the Label field the // fold joins on cannot drift from production. at is the snapshot's // GeneratedAt: the caller decides whether that reads as fresh or stale, and // TestFoldFixtureAgreesWithTheStalenessRule pins both readings. func putFreshBox(t *testing.T, s release.Store, label string, at time.Time) { t.Helper() if err := status.WriteBox(context.Background(), s, status.Box{Label: label, GeneratedAt: at}); err != nil { t.Fatalf("write box snapshot for %s: %v", label, err) } } // brokenStore is a store whose every read fails — the bucket-is-down case. type brokenStore struct{} var errBrokenStore = errors.New("store is down") func (brokenStore) Get(context.Context, string) ([]byte, string, error) { return nil, "", errBrokenStore } func (brokenStore) Put(context.Context, string, []byte, string) (string, error) { return "", errBrokenStore } func (brokenStore) List(context.Context, string) ([]string, error) { return nil, errBrokenStore } func (brokenStore) ListDirs(context.Context, string) ([]string, error) { return nil, errBrokenStore } func (brokenStore) Delete(context.Context, string) error { return errBrokenStore } // Edges files exist and NONE of them proved live: the answer degrades to the // env list with no error and nothing red, so the log line is the only thing // an operator can see. The likeliest cause is configuration rather than a // dead fleet — edges files are written through openDeployStore while box // snapshots go through openStatusStore, which honours CARLOS_STATUS_BUCKET // and CARLOS_STATUS_PREFIX, so the two halves of this join are the same // store by default but not by guarantee. Operational, not contractual: what // is pinned is that the line appears at all and names where to look. func TestEdgeIPsSaysSoWhenNothingJoined(t *testing.T) { buf := captureLog(t) s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`) putEdgesFile(t, s, "betty", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{}}`) // No snapshots at all — exactly what a split-bucket deployment looks // like from in here. if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) { t.Fatalf("EdgeIPs = %v, want env only", got) } line := buf.String() if !strings.Contains(line, "no fresh box snapshot matched") { t.Errorf("log = %q, want the total-miss tripwire", line) } if !strings.Contains(line, "CARLOS_STATUS_BUCKET") { t.Errorf("log = %q, want it to name where to look", line) } } // A deployment that has published no edges files yet is an ordinary state, // not a fault — and so is a partial miss, where one retired box drops out and // the rest join. Neither may cry wolf, or the line stops being read. func TestEdgeIPsStaysQuietWhenThereIsNothingToJoinOrSomethingDid(t *testing.T) { now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) buf := captureLog(t) empty := foldTestStore(t) EdgeIPs(context.Background(), empty, []string{"9.9.9.9"}, now) if strings.Contains(buf.String(), "no fresh box snapshot matched") { t.Errorf("log = %q, want silence: no edges files is not a fault", buf.String()) } buf2 := captureLog(t) s := foldTestStore(t) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`) putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{}}`) putFreshBox(t, s, "annie", now) putFreshBox(t, s, "retired", now.Add(-72*time.Hour)) EdgeIPs(context.Background(), s, nil, now) if strings.Contains(buf2.String(), "no fresh box snapshot matched") { t.Errorf("log = %q, want silence: one box joined, which is the rule working", buf2.String()) } } // captureLog redirects the standard logger for one test — the same // save/restore-under-Cleanup shape internal/steering's tests use. func captureLog(t *testing.T) *bytes.Buffer { t.Helper() var buf bytes.Buffer prev, flags := log.Writer(), log.Flags() log.SetOutput(&buf) log.SetFlags(0) t.Cleanup(func() { log.SetOutput(prev); log.SetFlags(flags) }) return &buf } // DeploymentRoutes is the evidence a hostname claim is checked against // before it may materialize as an alias, so what it counts and what it // ignores is the whole rule: NON-alias home entries on LIVE boxes, and // nothing else. // // The alias exclusion is the load-bearing half. An alias IS a materialized // claim, so counting one would make every claim reserve its own hostname // against itself and no re-derivation would ever succeed again — the // self-poisoning failure this exclusion exists to prevent. func TestDeploymentRoutesCountsRealRoutesAndIgnoresAliases(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{ "go.tito.io":{"role":"home"}, "tickets.example.com":{"role":"home","alias":true}, "app.bab.oncarlos.com":{"role":"home","platform":true}}}`) putFreshBox(t, s, "annie", now) got := DeploymentRoutes(context.Background(), s, now) if !got["go.tito.io"] { t.Errorf("routes = %v, want the operator-minted route counted", got) } if got["tickets.example.com"] { t.Errorf("routes = %v, want the ALIAS entry ignored — counting it would make every claim reserve its own hostname against itself", got) } // A platform mint is still a real route: its hostname is genuinely // served and is not a claim's own output. if !got["app.bab.oncarlos.com"] { t.Errorf("routes = %v, want a platform-minted route counted", got) } } // A retired box's lingering edges file must not go on reserving hostnames // forever — the same staleness rule EdgeIPs uses, for the same reason, and // with sharper consequences: here a phantom entry is a REFUSAL, so a dead // box would permanently block a hostname nobody serves. func TestDeploymentRoutesDropsABoxWhoseSnapshotIsStale(t *testing.T) { s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"live.example":{"role":"home"}}}`) putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{"dead.example":{"role":"home"}}}`) putFreshBox(t, s, "annie", now) putFreshBox(t, s, "retired", now.Add(-72*time.Hour)) got := DeploymentRoutes(context.Background(), s, now) if got["dead.example"] { t.Errorf("routes = %v, want the retired box's hostnames dropped", got) } if !got["live.example"] { t.Errorf("routes = %v, want the live box's hostname kept", got) } } // Best-effort by contract: a store that cannot be read, and no store at all, // both answer nil — "nothing known", which callers treat as permissive. // Fail-OPEN is deliberate here even though this is a security guard: a // bucket that cannot be listed must not take every tenant's custom domain // down with it, and the per-box route check plus the fold's own route // preference are the layers underneath. func TestDeploymentRoutesIsNilWhenNothingCanBeRead(t *testing.T) { now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) if got := DeploymentRoutes(context.Background(), nil, now); got != nil { t.Errorf("nil store: routes = %v, want nil", got) } if got := DeploymentRoutes(context.Background(), brokenStore{}, now); got != nil { t.Errorf("broken store: routes = %v, want nil", got) } } // The total-miss tripwire, EdgeIPs' line and then some: there a total miss // degrades to the operator's configured edge list, which is merely the // pre-fold behaviour. Here it degrades to "no hostname is anyone's", which // silently DISARMS the guard — an empty map, no error, nothing red, and // every claim materializing exactly as it did before the rule existed. The // same three causes (split buckets, every box dark, box-name drift) and the // same invisibility, so the log line is all an operator gets. func TestDeploymentRoutesSaysSoWhenNothingJoined(t *testing.T) { buf := captureLog(t) s := foldTestStore(t) now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"go.tito.io":{"role":"home"}}}`) // No snapshots at all — what a split-bucket deployment looks like here. if got := DeploymentRoutes(context.Background(), s, now); len(got) != 0 { t.Fatalf("routes = %v, want empty", got) } line := buf.String() if !strings.Contains(line, "no fresh box snapshot matched") { t.Errorf("log = %q, want the total-miss tripwire", line) } if !strings.Contains(line, "CARLOS_STATUS_BUCKET") { t.Errorf("log = %q, want it to name where to look", line) } // And silence when there is nothing to join: an empty deployment is an // ordinary state, not a fault, or the line stops being read. buf2 := captureLog(t) DeploymentRoutes(context.Background(), foldTestStore(t), now) if strings.Contains(buf2.String(), "no fresh box snapshot matched") { t.Errorf("log = %q, want silence: no edges files is not a fault", buf2.String()) } }