package console import ( "context" "reflect" "testing" "time" "github.com/carlosframework/platform/internal/edgesfile" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/status" ) // The guidance a member is shown must name EVERY armed edge, not just the // ones this console's own PLATFORM_EDGE_IPS happens to list — otherwise a // multi-region deployment tells people to point at one region and then grades // them against all of them. Union, configured addresses first, no duplicates. func TestDNSTargetsUnionsTheConfiguredListWithTheFold(t *testing.T) { s := &Server{ cfg: Config{EdgeIPs: []string{"203.0.113.7"}}, foldEdgeIPs: func(context.Context) []string { return []string{"198.51.100.9", "203.0.113.7"} }, } got := s.dnsTargets() if !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7", "198.51.100.9"}) { t.Fatalf("EdgeIPs = %v, want the configured list first, then the fold's extras, deduped", got.EdgeIPs) } } // dnsTargets is called once PER CLAIM inside the domains-list loop. An // uncached fold would put a box-snapshot sweep plus one Get per edges file // behind every row of every listing — the cost discipline // domainListCNAMELookupCap already established for this door. func TestEdgeFoldIsCachedForTheTTL(t *testing.T) { folds := 0 s := &Server{foldEdgeIPs: func(context.Context) []string { folds++ return []string{"198.51.100.9"} }} for i := 0; i < 5; i++ { s.dnsTargets() } if folds != 1 { t.Fatalf("folded %d times inside the TTL, want 1", folds) } // Past the TTL the next reader refolds — an edge that armed a minute ago // has to become visible without a console restart. s.edgeFold.mu.Lock() s.edgeFold.at = time.Now().Add(-edgeFoldTTL - time.Second) s.edgeFold.mu.Unlock() if got := s.dnsTargets(); folds != 2 || len(got.EdgeIPs) != 1 { t.Fatalf("after the TTL: folds = %d, EdgeIPs = %v, want a refold", folds, got.EdgeIPs) } } // A fold that comes back empty is a bucket that could not be read this // minute far more often than it is a deployment that lost every edge, so the // previous answer stands rather than being emptied — narrowing the guidance // every member is shown on the strength of one failed read is the worse // error. The timestamp still moves, so a persistent outage costs one bounded // attempt per TTL, not one per request. func TestEdgeFoldKeepsTheLastAnswerWhenAFoldFails(t *testing.T) { folds := 0 s := &Server{foldEdgeIPs: func(context.Context) []string { folds++ if folds == 1 { return []string{"198.51.100.9"} } return nil // the store is down }} if got := s.dnsTargets(); len(got.EdgeIPs) != 1 { t.Fatalf("first fold: EdgeIPs = %v", got.EdgeIPs) } s.edgeFold.mu.Lock() s.edgeFold.at = time.Now().Add(-edgeFoldTTL - time.Second) s.edgeFold.mu.Unlock() got := s.dnsTargets() if folds != 2 { t.Fatalf("folds = %d, want the stale entry to have been retried", folds) } if !reflect.DeepEqual(got.EdgeIPs, []string{"198.51.100.9"}) { t.Fatalf("EdgeIPs = %v, want the previous answer left standing", got.EdgeIPs) } // And the failed attempt still moved the clock: a console whose bucket is // down must not refold on every request of every listing. before := folds s.dnsTargets() if folds != before { t.Fatalf("folds = %d, want the failed attempt to still hold off for the TTL", folds) } } // The real fold, end to end over a bucket: a live box's published EIP reaches // the guidance even though nothing ever set it in this console's env. func TestRealEdgeFoldReadsTheDeploymentsEdgesFiles(t *testing.T) { ctx := context.Background() store := release.NewDirStore(t.TempDir()) if _, err := store.Put(ctx, edgesfile.Key("annie"), []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{}}`), ""); err != nil { t.Fatal(err) } if err := status.WriteBox(ctx, store, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil { t.Fatal(err) } s := &Server{cfg: Config{EdgeIPs: []string{"203.0.113.7"}, Releases: store}} if got := s.dnsTargets(); !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7", "198.51.100.9"}) { t.Fatalf("EdgeIPs = %v, want the configured address plus the folded one", got.EdgeIPs) } } // A console with no deployment bucket at all (Releases nil) is a legal // deployment: it degrades to the configured list rather than panicking or // emptying the guidance. func TestRealEdgeFoldWithNoStoreDegradesToTheConfiguredList(t *testing.T) { s := &Server{cfg: Config{EdgeIPs: []string{"203.0.113.7"}}} if got := s.dnsTargets(); !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7"}) { t.Fatalf("EdgeIPs = %v, want the configured list", got.EdgeIPs) } }