| 1 | package steering |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "log" |
| 6 | "reflect" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/registry" |
| 11 | ) |
| 12 | |
| 13 | func captureLog(t *testing.T) *bytes.Buffer { |
| 14 | t.Helper() |
| 15 | var buf bytes.Buffer |
| 16 | prev := log.Writer() |
| 17 | flags := log.Flags() |
| 18 | log.SetOutput(&buf) |
| 19 | log.SetFlags(0) |
| 20 | t.Cleanup(func() { log.SetOutput(prev); log.SetFlags(flags) }) |
| 21 | return &buf |
| 22 | } |
| 23 | |
| 24 | func steeredRoute(host string) registry.Route { |
| 25 | return registry.Route{Host: host, Steering: "latency"} |
| 26 | } |
| 27 | |
| 28 | // TestDesiredStateReplicasDoNotSteer pins the #162 half of the fold: an |
| 29 | // account whose replica box publishes the host marked replica gets records |
| 30 | // from EVERY armed edge EXCEPT that one — the pre-#162 rule is any-edge- |
| 31 | // any-app (far edges backhaul), so the ONLY entries a replica mark removes |
| 32 | // are its own. The synthetic edge.<apps-domain> records keep ALL armed |
| 33 | // edges regardless (every edge is a cache front). |
| 34 | func TestDesiredStateReplicasDoNotSteer(t *testing.T) { |
| 35 | dublin := edgeHomes("dublin", "1.2.3.4", "eu-west-1", "app.example.com") |
| 36 | oregon := edge("oregon", "5.6.7.8", "us-west-2") |
| 37 | oregon.Hosts = map[string]edgeHostView{ |
| 38 | "app.example.com": {Replica: true}, |
| 39 | } |
| 40 | |
| 41 | got := desiredState([]registry.Route{steeredRoute("app.example.com")}, |
| 42 | []edgesFileView{dublin, oregon}, "example.com") |
| 43 | |
| 44 | want := []desiredRecord{ |
| 45 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 46 | {Name: "edge.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"}, |
| 47 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 48 | } |
| 49 | if !reflect.DeepEqual(got, want) { |
| 50 | t.Fatalf("desiredState() =\n%#v\nwant\n%#v", got, want) |
| 51 | } |
| 52 | |
| 53 | // A host named by NO box still earns records from every armed edge: |
| 54 | // steering is any-edge-any-app, and the far edge backhauls to the home |
| 55 | // whether or not this box holds a row for the host. |
| 56 | got = desiredState([]registry.Route{steeredRoute("never-published.example.com")}, |
| 57 | []edgesFileView{dublin, oregon}, "example.com") |
| 58 | want = []desiredRecord{ |
| 59 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 60 | {Name: "edge.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"}, |
| 61 | {Name: "never-published.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 62 | {Name: "never-published.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"}, |
| 63 | } |
| 64 | if !reflect.DeepEqual(got, want) { |
| 65 | t.Fatalf("unpublished-host desiredState() =\n%#v\nwant\n%#v", got, want) |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | func edge(box, eip, region string) edgesFileView { |
| 70 | return edgesFileView{V: 1, Box: box, EIP: eip, Region: region} |
| 71 | } |
| 72 | |
| 73 | // edgeHomes is edge() with the homed-host set filled in: the tests that |
| 74 | // drive desiredState's per-host fold need Hosts, which pre-#162 was |
| 75 | // deliberately absent (the fold never asked who served what) and which the |
| 76 | // replica-aware fold now reads. Hosts uses the writer's own key shape; |
| 77 | // every entry is home-marked (Replica clear) unless a test says otherwise. |
| 78 | func edgeHomes(box, eip, region string, homes ...string) edgesFileView { |
| 79 | e := edge(box, eip, region) |
| 80 | e.Hosts = map[string]edgeHostView{} |
| 81 | for _, h := range homes { |
| 82 | e.Hosts[h] = edgeHostView{} |
| 83 | } |
| 84 | return e |
| 85 | } |
| 86 | |
| 87 | // TestDesiredState is the brief's table: one steered host x two armed |
| 88 | // edges = two records; unsteered host absent; edge.<apps-domain> present |
| 89 | // whenever >=1 edge is armed, absent at zero. |
| 90 | func TestDesiredState(t *testing.T) { |
| 91 | dublin := edgeHomes("dublin", "1.2.3.4", "eu-west-1", |
| 92 | "app.example.com", "steered.example.com", "example.com") |
| 93 | oregon := edgeHomes("oregon", "5.6.7.8", "us-west-2", |
| 94 | "app.example.com", "steered.example.com") |
| 95 | |
| 96 | tests := []struct { |
| 97 | name string |
| 98 | routes []registry.Route |
| 99 | edges []edgesFileView |
| 100 | appsDomain string |
| 101 | want []desiredRecord |
| 102 | }{ |
| 103 | { |
| 104 | name: "one steered host x two armed edges", |
| 105 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 106 | edges: []edgesFileView{dublin, oregon}, |
| 107 | appsDomain: "example.com", |
| 108 | want: []desiredRecord{ |
| 109 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 110 | {Name: "edge.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"}, |
| 111 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 112 | {Name: "app.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"}, |
| 113 | }, |
| 114 | }, |
| 115 | { |
| 116 | name: "unsteered host absent", |
| 117 | routes: []registry.Route{ |
| 118 | {Host: "app.example.com", Steering: ""}, |
| 119 | steeredRoute("steered.example.com"), |
| 120 | }, |
| 121 | edges: []edgesFileView{dublin}, |
| 122 | appsDomain: "example.com", |
| 123 | want: []desiredRecord{ |
| 124 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 125 | {Name: "steered.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 126 | }, |
| 127 | }, |
| 128 | { |
| 129 | name: "zero armed edges yields nothing, no edge.<apps-domain>", |
| 130 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 131 | edges: nil, |
| 132 | appsDomain: "example.com", |
| 133 | want: nil, |
| 134 | }, |
| 135 | { |
| 136 | name: "edge with empty EIP skipped", |
| 137 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 138 | edges: []edgesFileView{dublin, edge("noeip", "", "us-west-2")}, |
| 139 | appsDomain: "example.com", |
| 140 | want: []desiredRecord{ |
| 141 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 142 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 143 | }, |
| 144 | }, |
| 145 | { |
| 146 | name: "edge with empty region skipped", |
| 147 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 148 | edges: []edgesFileView{dublin, edge("noregion", "9.9.9.9", "")}, |
| 149 | appsDomain: "example.com", |
| 150 | want: []desiredRecord{ |
| 151 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 152 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 153 | }, |
| 154 | }, |
| 155 | { |
| 156 | name: "edge region carrying a placement code (not an AWS region) skipped", |
| 157 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 158 | edges: []edgesFileView{dublin, edge("tito", "9.9.9.9", "ie")}, |
| 159 | appsDomain: "example.com", |
| 160 | want: []desiredRecord{ |
| 161 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 162 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 163 | }, |
| 164 | }, |
| 165 | { |
| 166 | name: "pool-carrying steered route excluded", |
| 167 | routes: []registry.Route{ |
| 168 | {Host: "pooled.example.com", Steering: "latency", Pool: "reserved"}, |
| 169 | }, |
| 170 | edges: []edgesFileView{dublin}, |
| 171 | appsDomain: "example.com", |
| 172 | want: []desiredRecord{ |
| 173 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 174 | }, |
| 175 | }, |
| 176 | { |
| 177 | name: "wildcard registry row with steering set never produces a record", |
| 178 | routes: []registry.Route{ |
| 179 | {Host: "*.example.com", Steering: "latency"}, |
| 180 | }, |
| 181 | edges: []edgesFileView{dublin}, |
| 182 | appsDomain: "example.com", |
| 183 | want: []desiredRecord{ |
| 184 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 185 | }, |
| 186 | }, |
| 187 | { |
| 188 | name: "invalid steering value skipped, never guessed", |
| 189 | routes: []registry.Route{ |
| 190 | {Host: "hand-written.example.com", Steering: "geo"}, |
| 191 | }, |
| 192 | edges: []edgesFileView{dublin}, |
| 193 | appsDomain: "example.com", |
| 194 | want: []desiredRecord{ |
| 195 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 196 | }, |
| 197 | }, |
| 198 | { |
| 199 | name: "trailing-dot input normalizes to a single trailing dot", |
| 200 | routes: []registry.Route{steeredRoute("app.example.com.")}, |
| 201 | edges: []edgesFileView{dublin}, |
| 202 | appsDomain: "example.com.", |
| 203 | want: []desiredRecord{ |
| 204 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 205 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 206 | }, |
| 207 | }, |
| 208 | { |
| 209 | name: "mixed-case host folds to one lowercase Name", |
| 210 | routes: []registry.Route{steeredRoute("App.Example.COM")}, |
| 211 | edges: []edgesFileView{dublin}, |
| 212 | appsDomain: "example.com", |
| 213 | want: []desiredRecord{ |
| 214 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 215 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 216 | }, |
| 217 | }, |
| 218 | { |
| 219 | name: "blank apps domain skips the synthetic edge hostname AND every per-host record", |
| 220 | routes: []registry.Route{steeredRoute("app.example.com")}, |
| 221 | edges: []edgesFileView{dublin}, |
| 222 | appsDomain: "", |
| 223 | want: nil, |
| 224 | }, |
| 225 | { |
| 226 | name: "out-of-zone steered host excluded, in-zone host still lands", |
| 227 | routes: []registry.Route{ |
| 228 | steeredRoute("app.example.com"), |
| 229 | steeredRoute("console.carloku.com"), |
| 230 | }, |
| 231 | edges: []edgesFileView{dublin}, |
| 232 | appsDomain: "example.com", |
| 233 | want: []desiredRecord{ |
| 234 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 235 | {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 236 | }, |
| 237 | }, |
| 238 | { |
| 239 | name: "steered host equal to the apps domain itself (apex) lands", |
| 240 | routes: []registry.Route{steeredRoute("example.com")}, |
| 241 | edges: []edgesFileView{dublin}, |
| 242 | appsDomain: "example.com", |
| 243 | want: []desiredRecord{ |
| 244 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 245 | {Name: "example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 246 | }, |
| 247 | }, |
| 248 | { |
| 249 | name: "steered host merely sharing a suffix (not a subdomain) is excluded", |
| 250 | routes: []registry.Route{steeredRoute("notexample.com")}, |
| 251 | edges: []edgesFileView{dublin}, |
| 252 | appsDomain: "example.com", |
| 253 | want: []desiredRecord{ |
| 254 | {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"}, |
| 255 | }, |
| 256 | }, |
| 257 | } |
| 258 | |
| 259 | for _, tt := range tests { |
| 260 | t.Run(tt.name, func(t *testing.T) { |
| 261 | captureLog(t) // silence + isolate log output from test noise |
| 262 | got := desiredState(tt.routes, tt.edges, tt.appsDomain) |
| 263 | if !reflect.DeepEqual(got, tt.want) { |
| 264 | t.Fatalf("desiredState() =\n%#v\nwant\n%#v", got, tt.want) |
| 265 | } |
| 266 | }) |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | func TestDesiredStateLogsEachSkipReason(t *testing.T) { |
| 271 | dublin := edge("dublin", "1.2.3.4", "eu-west-1") |
| 272 | |
| 273 | tests := []struct { |
| 274 | name string |
| 275 | routes []registry.Route |
| 276 | edges []edgesFileView |
| 277 | wantLog string |
| 278 | }{ |
| 279 | { |
| 280 | name: "edge with no EIP is named in the log", |
| 281 | edges: []edgesFileView{edge("noeip", "", "eu-west-1")}, |
| 282 | wantLog: "steering: edge noeip unarmed: no EIP", |
| 283 | }, |
| 284 | { |
| 285 | name: "edge with a placement code names the bad value", |
| 286 | edges: []edgesFileView{edge("tito", "9.9.9.9", "ie")}, |
| 287 | wantLog: `steering: edge tito unarmed: region "ie" is not an AWS region`, |
| 288 | }, |
| 289 | { |
| 290 | name: "pool-carrying route is named", |
| 291 | edges: []edgesFileView{dublin}, |
| 292 | routes: []registry.Route{{Host: "pooled.example.com", Steering: "latency", Pool: "reserved"}}, |
| 293 | wantLog: `steering: route pooled.example.com excluded: pool "reserved" set`, |
| 294 | }, |
| 295 | { |
| 296 | name: "wildcard route is named", |
| 297 | edges: []edgesFileView{dublin}, |
| 298 | routes: []registry.Route{{Host: "*.example.com", Steering: "latency"}}, |
| 299 | wantLog: "steering: route *.example.com excluded: wildcard host", |
| 300 | }, |
| 301 | { |
| 302 | name: "invalid steering value is named verbatim", |
| 303 | edges: []edgesFileView{dublin}, |
| 304 | routes: []registry.Route{{Host: "hand.example.com", Steering: "geo"}}, |
| 305 | wantLog: `steering: route hand.example.com excluded: invalid steering value "geo"`, |
| 306 | }, |
| 307 | { |
| 308 | name: "out-of-zone steered host is named, with the CNAME guidance", |
| 309 | edges: []edgesFileView{dublin}, |
| 310 | routes: []registry.Route{steeredRoute("console.carloku.com")}, |
| 311 | wantLog: `steering: route console.carloku.com excluded: host is not under the apps domain "example.com" — custom domains steer via CNAME to edge.<apps-domain>`, |
| 312 | }, |
| 313 | } |
| 314 | |
| 315 | for _, tt := range tests { |
| 316 | t.Run(tt.name, func(t *testing.T) { |
| 317 | buf := captureLog(t) |
| 318 | desiredState(tt.routes, tt.edges, "example.com") |
| 319 | if !strings.Contains(buf.String(), tt.wantLog) { |
| 320 | t.Fatalf("log output =\n%s\nwant substring %q", buf.String(), tt.wantLog) |
| 321 | } |
| 322 | }) |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | // TestDesiredStateBlankAppsDomainExcludesEveryPerHostRecord pins the second |
| 327 | // half of the Important review finding: appsDomain == "" is not just "no |
| 328 | // synthetic edge.<apps-domain> record" — with no domain there is no zone a |
| 329 | // per-host route can be proven to lie under, so EVERY steered route is |
| 330 | // excluded too (steeredHosts' own appsDomain == "" branch), loudly, one |
| 331 | // line per route, same log family as the out-of-zone case. |
| 332 | func TestDesiredStateBlankAppsDomainExcludesEveryPerHostRecord(t *testing.T) { |
| 333 | dublin := edge("dublin", "1.2.3.4", "eu-west-1") |
| 334 | buf := captureLog(t) |
| 335 | |
| 336 | got := desiredState( |
| 337 | []registry.Route{steeredRoute("app.example.com"), steeredRoute("other.example.com")}, |
| 338 | []edgesFileView{dublin}, |
| 339 | "", |
| 340 | ) |
| 341 | if got != nil { |
| 342 | t.Fatalf("desiredState() = %#v, want nil (no domain, no synthetic hostname, no per-host record)", got) |
| 343 | } |
| 344 | |
| 345 | logged := buf.String() |
| 346 | for _, want := range []string{ |
| 347 | `steering: route app.example.com excluded: no apps domain configured`, |
| 348 | `steering: route other.example.com excluded: no apps domain configured`, |
| 349 | "custom domains steer via CNAME to edge.<apps-domain>", |
| 350 | } { |
| 351 | if !strings.Contains(logged, want) { |
| 352 | t.Fatalf("log output =\n%s\nwant substring %q", logged, want) |
| 353 | } |
| 354 | } |
| 355 | } |
| 356 | |
| 357 | // TestDesiredStateCaseFoldDedupe pins the case-collision half of the |
| 358 | // review's Important finding: two registry rows differing only by case |
| 359 | // are two legitimate primary-key rows right up until fqdn folds them onto |
| 360 | // one Route53 name, at which point desiredState must produce exactly ONE |
| 361 | // record per armed edge for that name, not two — and the log must name |
| 362 | // BOTH original (still mixed-case) Host values, not the already-folded |
| 363 | // Name, because that is the only information an operator can act on: |
| 364 | // which two registry rows to go fix. That naming has to happen where the |
| 365 | // originals still exist — steeredHosts, via dedupeCaseCollisions — not |
| 366 | // later at the desiredRecord level, where a same-edge collision has |
| 367 | // identical Box/EIP on both sides and the log would just repeat the one |
| 368 | // folded Name. |
| 369 | func TestDesiredStateCaseFoldDedupe(t *testing.T) { |
| 370 | dublin := edgeHomes("dublin", "1.2.3.4", "eu-west-1", "app.example.com") |
| 371 | buf := captureLog(t) |
| 372 | |
| 373 | routes := []registry.Route{ |
| 374 | steeredRoute("App.example.com"), |
| 375 | steeredRoute("app.example.com"), |
| 376 | } |
| 377 | got := desiredState(routes, []edgesFileView{dublin}, "example.com") |
| 378 | |
| 379 | var matches int |
| 380 | for _, rec := range got { |
| 381 | if rec.Name == "app.example.com." { |
| 382 | matches++ |
| 383 | } |
| 384 | } |
| 385 | if matches != 1 { |
| 386 | t.Fatalf("desiredState() produced %d records for the case-colliding name, want exactly 1 (got %#v)", matches, got) |
| 387 | } |
| 388 | // Total should be the edge.<apps-domain> record plus the ONE surviving |
| 389 | // per-host record — not two. |
| 390 | if len(got) != 2 { |
| 391 | t.Fatalf("desiredState() = %#v, want 2 records (1 synthetic + 1 deduped)", got) |
| 392 | } |
| 393 | |
| 394 | logged := buf.String() |
| 395 | // Sorted-Host order (ASCII, uppercase < lowercase) keeps |
| 396 | // "App.example.com" and drops "app.example.com" — both original |
| 397 | // strings, plus the folded DNS name, must appear verbatim. |
| 398 | for _, want := range []string{`"App.example.com"`, `"app.example.com"`, `"app.example.com."`} { |
| 399 | if !strings.Contains(logged, want) { |
| 400 | t.Fatalf("log output =\n%s\nwant substring %s naming the original colliding host/DNS name", logged, want) |
| 401 | } |
| 402 | } |
| 403 | if !strings.Contains(logged, "steering: case-collision:") { |
| 404 | t.Fatalf("log output =\n%s\nwant a case-collision line", logged) |
| 405 | } |
| 406 | // The record-level backstop (dedupeRecords) must NOT be the one that |
| 407 | // fired — the collision was already resolved at the route level, one |
| 408 | // layer earlier, where the log could still name both original hosts. |
| 409 | if strings.Contains(logged, "steering: record ") { |
| 410 | t.Fatalf("log output =\n%s\nwant no dedupeRecords line — the collision should be caught earlier, in steeredHosts", logged) |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | func TestDesiredStateBlankAppsDomainLogged(t *testing.T) { |
| 415 | dublin := edge("dublin", "1.2.3.4", "eu-west-1") |
| 416 | buf := captureLog(t) |
| 417 | desiredState([]registry.Route{steeredRoute("app.example.com")}, []edgesFileView{dublin}, "") |
| 418 | want := "steering: no apps domain configured" |
| 419 | if !strings.Contains(buf.String(), want) { |
| 420 | t.Fatalf("log output =\n%s\nwant substring %q", buf.String(), want) |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | func TestParseEdgesFileView(t *testing.T) { |
| 425 | tests := []struct { |
| 426 | name string |
| 427 | body string |
| 428 | want edgesFileView |
| 429 | wantErr bool |
| 430 | }{ |
| 431 | { |
| 432 | name: "v1 body with region", |
| 433 | body: `{"v":1,"box":"dublin","eip":"1.2.3.4","region":"eu-west-1","generation":"2026-08-22T09:00:00Z","hosts":{"app.example.com":{"role":"home"}}}`, |
| 434 | want: edgesFileView{V: 1, Box: "dublin", EIP: "1.2.3.4", Region: "eu-west-1"}, |
| 435 | }, |
| 436 | { |
| 437 | name: "v1 body without region (pre-region writer)", |
| 438 | body: `{"v":1,"box":"dublin","eip":"1.2.3.4","generation":"2026-08-22T09:00:00Z","hosts":{}}`, |
| 439 | want: edgesFileView{V: 1, Box: "dublin", EIP: "1.2.3.4", Region: ""}, |
| 440 | }, |
| 441 | { |
| 442 | name: "unversioned body refused", |
| 443 | body: `{"box":"dublin","eip":"1.2.3.4"}`, |
| 444 | wantErr: true, |
| 445 | }, |
| 446 | { |
| 447 | name: "garbage body refused", |
| 448 | body: `not json`, |
| 449 | wantErr: true, |
| 450 | }, |
| 451 | } |
| 452 | |
| 453 | for _, tt := range tests { |
| 454 | t.Run(tt.name, func(t *testing.T) { |
| 455 | got, err := parseEdgesFileView([]byte(tt.body)) |
| 456 | if tt.wantErr { |
| 457 | if err == nil { |
| 458 | t.Fatalf("parseEdgesFileView(%q) = %+v, nil; want error", tt.body, got) |
| 459 | } |
| 460 | return |
| 461 | } |
| 462 | if err != nil { |
| 463 | t.Fatalf("parseEdgesFileView(%q) unexpected error: %v", tt.body, err) |
| 464 | } |
| 465 | // Field-by-field, not struct equality: Hosts is a map, and a |
| 466 | // comparable-struct check died the day #162 added it. The |
| 467 | // scalar fields are the ones this test pins; Hosts is read |
| 468 | // only for the replica mark in desiredState, pinned there. |
| 469 | if got.V != tt.want.V || got.Box != tt.want.Box || got.EIP != tt.want.EIP || |
| 470 | got.Region != tt.want.Region { |
| 471 | t.Fatalf("parseEdgesFileView(%q) = %+v, want %+v", tt.body, got, tt.want) |
| 472 | } |
| 473 | }) |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | func TestArmedEdgesSortedByBox(t *testing.T) { |
| 478 | edges := []edgesFileView{ |
| 479 | edge("oregon", "5.6.7.8", "us-west-2"), |
| 480 | edge("dublin", "1.2.3.4", "eu-west-1"), |
| 481 | } |
| 482 | got := armedEdges(edges) |
| 483 | if len(got) != 2 || got[0].Box != "dublin" || got[1].Box != "oregon" { |
| 484 | t.Fatalf("armedEdges() = %+v, want dublin then oregon", got) |
| 485 | } |
| 486 | } |
| 487 | |