| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "reflect" |
| 6 | "testing" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/edgesfile" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | "github.com/carlosframework/platform/internal/status" |
| 12 | ) |
| 13 | |
| 14 | // The guidance a member is shown must name EVERY armed edge, not just the |
| 15 | // ones this console's own PLATFORM_EDGE_IPS happens to list — otherwise a |
| 16 | // multi-region deployment tells people to point at one region and then grades |
| 17 | // them against all of them. Union, configured addresses first, no duplicates. |
| 18 | func TestDNSTargetsUnionsTheConfiguredListWithTheFold(t *testing.T) { |
| 19 | s := &Server{ |
| 20 | cfg: Config{EdgeIPs: []string{"203.0.113.7"}}, |
| 21 | foldEdgeIPs: func(context.Context) []string { return []string{"198.51.100.9", "203.0.113.7"} }, |
| 22 | } |
| 23 | got := s.dnsTargets() |
| 24 | if !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7", "198.51.100.9"}) { |
| 25 | t.Fatalf("EdgeIPs = %v, want the configured list first, then the fold's extras, deduped", got.EdgeIPs) |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // dnsTargets is called once PER CLAIM inside the domains-list loop. An |
| 30 | // uncached fold would put a box-snapshot sweep plus one Get per edges file |
| 31 | // behind every row of every listing — the cost discipline |
| 32 | // domainListCNAMELookupCap already established for this door. |
| 33 | func TestEdgeFoldIsCachedForTheTTL(t *testing.T) { |
| 34 | folds := 0 |
| 35 | s := &Server{foldEdgeIPs: func(context.Context) []string { |
| 36 | folds++ |
| 37 | return []string{"198.51.100.9"} |
| 38 | }} |
| 39 | for i := 0; i < 5; i++ { |
| 40 | s.dnsTargets() |
| 41 | } |
| 42 | if folds != 1 { |
| 43 | t.Fatalf("folded %d times inside the TTL, want 1", folds) |
| 44 | } |
| 45 | |
| 46 | // Past the TTL the next reader refolds — an edge that armed a minute ago |
| 47 | // has to become visible without a console restart. |
| 48 | s.edgeFold.mu.Lock() |
| 49 | s.edgeFold.at = time.Now().Add(-edgeFoldTTL - time.Second) |
| 50 | s.edgeFold.mu.Unlock() |
| 51 | if got := s.dnsTargets(); folds != 2 || len(got.EdgeIPs) != 1 { |
| 52 | t.Fatalf("after the TTL: folds = %d, EdgeIPs = %v, want a refold", folds, got.EdgeIPs) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // A fold that comes back empty is a bucket that could not be read this |
| 57 | // minute far more often than it is a deployment that lost every edge, so the |
| 58 | // previous answer stands rather than being emptied — narrowing the guidance |
| 59 | // every member is shown on the strength of one failed read is the worse |
| 60 | // error. The timestamp still moves, so a persistent outage costs one bounded |
| 61 | // attempt per TTL, not one per request. |
| 62 | func TestEdgeFoldKeepsTheLastAnswerWhenAFoldFails(t *testing.T) { |
| 63 | folds := 0 |
| 64 | s := &Server{foldEdgeIPs: func(context.Context) []string { |
| 65 | folds++ |
| 66 | if folds == 1 { |
| 67 | return []string{"198.51.100.9"} |
| 68 | } |
| 69 | return nil // the store is down |
| 70 | }} |
| 71 | if got := s.dnsTargets(); len(got.EdgeIPs) != 1 { |
| 72 | t.Fatalf("first fold: EdgeIPs = %v", got.EdgeIPs) |
| 73 | } |
| 74 | s.edgeFold.mu.Lock() |
| 75 | s.edgeFold.at = time.Now().Add(-edgeFoldTTL - time.Second) |
| 76 | s.edgeFold.mu.Unlock() |
| 77 | got := s.dnsTargets() |
| 78 | if folds != 2 { |
| 79 | t.Fatalf("folds = %d, want the stale entry to have been retried", folds) |
| 80 | } |
| 81 | if !reflect.DeepEqual(got.EdgeIPs, []string{"198.51.100.9"}) { |
| 82 | t.Fatalf("EdgeIPs = %v, want the previous answer left standing", got.EdgeIPs) |
| 83 | } |
| 84 | // And the failed attempt still moved the clock: a console whose bucket is |
| 85 | // down must not refold on every request of every listing. |
| 86 | before := folds |
| 87 | s.dnsTargets() |
| 88 | if folds != before { |
| 89 | t.Fatalf("folds = %d, want the failed attempt to still hold off for the TTL", folds) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | // The real fold, end to end over a bucket: a live box's published EIP reaches |
| 94 | // the guidance even though nothing ever set it in this console's env. |
| 95 | func TestRealEdgeFoldReadsTheDeploymentsEdgesFiles(t *testing.T) { |
| 96 | ctx := context.Background() |
| 97 | store := release.NewDirStore(t.TempDir()) |
| 98 | if _, err := store.Put(ctx, edgesfile.Key("annie"), |
| 99 | []byte(`{"v":1,"box":"annie","eip":"198.51.100.9","hosts":{}}`), ""); err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | if err := status.WriteBox(ctx, store, status.Box{Label: "annie", GeneratedAt: time.Now()}); err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | s := &Server{cfg: Config{EdgeIPs: []string{"203.0.113.7"}, Releases: store}} |
| 106 | if got := s.dnsTargets(); !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7", "198.51.100.9"}) { |
| 107 | t.Fatalf("EdgeIPs = %v, want the configured address plus the folded one", got.EdgeIPs) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // A console with no deployment bucket at all (Releases nil) is a legal |
| 112 | // deployment: it degrades to the configured list rather than panicking or |
| 113 | // emptying the guidance. |
| 114 | func TestRealEdgeFoldWithNoStoreDegradesToTheConfiguredList(t *testing.T) { |
| 115 | s := &Server{cfg: Config{EdgeIPs: []string{"203.0.113.7"}}} |
| 116 | if got := s.dnsTargets(); !reflect.DeepEqual(got.EdgeIPs, []string{"203.0.113.7"}) { |
| 117 | t.Fatalf("EdgeIPs = %v, want the configured list", got.EdgeIPs) |
| 118 | } |
| 119 | } |
| 120 | |