| new file mode 100644 |
| index 000000000..18713ecc6 |
| --- /dev/null |
| +++ b/internal/console/boxchoice_test.go |
| @@ -0,0 +1,163 @@ |
| +package console |
| + |
| +import ( |
| + "encoding/json" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/boxid" |
| +) |
| + |
| +// haBox is an eligible box's edges file in placement "uk". |
| +func haBox(id string, mut func(m map[string]any)) string { |
| + m := map[string]any{ |
| + "v": 1, "box": id, "box_id": id, "eip": "1.2.3.4", "region": "eu-west-2", "placement": "uk", |
| + "hosts": map[string]any{}, "hibernation_configured": true, "serves_shared_pool": true, |
| + "accepts_inbound": true, "secrets_key_fp": testKeyFP, "capabilities": []string{"region-ha-1"}, |
| + "headroom": map[string]any{"mem_free_mib": 1000, "disk_free_mib": 1000, "awake": 1}, |
| + } |
| + if mut != nil { |
| + mut(m) |
| + } |
| + b, _ := json.Marshal(m) |
| + return string(b) |
| +} |
| + |
| +func ids(cs []boxCandidate) []string { |
| + out := make([]string, len(cs)) |
| + for i, c := range cs { |
| + out[i] = c.BoxID |
| + } |
| + return out |
| +} |
| + |
| +func verifiedFor(boxes ...string) map[string]boxid.Identity { |
| + out := map[string]boxid.Identity{} |
| + for _, b := range boxes { |
| + out[b] = boxid.Identity{AccountID: "1", Region: "eu-west-2", InstanceID: b} |
| + } |
| + return out |
| +} |
| + |
| +func TestEligibleBoxesAppliesThePredicateOneBoxAtATime(t *testing.T) { |
| + del := func(k string) func(map[string]any) { return func(m map[string]any) { delete(m, k) } } |
| + set := func(k string, v any) func(map[string]any) { return func(m map[string]any) { m[k] = v } } |
| + cases := []struct { |
| + name string |
| + mut func(map[string]any) |
| + want bool |
| + }{ |
| + {"all present", nil, true}, |
| + {"no dial address", set("eip", ""), false}, |
| + {"hibernation unconfigured", set("hibernation_configured", false), false}, |
| + {"hibernation unknown", del("hibernation_configured"), false}, |
| + {"refuses the shared pool", set("serves_shared_pool", false), false}, |
| + {"refuses inbound", set("accepts_inbound", false), false}, |
| + {"wrong secrets key", set("secrets_key_fp", "sha256:other"), false}, |
| + {"no capability", del("capabilities"), false}, |
| + {"draining", set("draining", true), false}, |
| + {"another placement", set("placement", "us"), false}, |
| + {"box_id disagrees with its record", set("box_id", "i-0zzz"), false}, |
| + } |
| + for _, tc := range cases { |
| + t.Run(tc.name, func(t *testing.T) { |
| + snap := views(t, haBox("i-0aaa", tc.mut)) |
| + got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil) |
| + if (len(got) == 1) != tc.want { |
| + t.Fatalf("eligible = %v, want %v", ids(got), tc.want) |
| + } |
| + }) |
| + } |
| +} |
| + |
| +func TestEligibleBoxesNeedsAVerifiedRecordFreshnessAndNotLost(t *testing.T) { |
| + snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil), haBox("i-0ccc", nil)) |
| + snap = markStale(snap, "i-0bbb") |
| + got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil) |
| + if len(got) != 1 || got[0].BoxID != "i-0aaa" { |
| + t.Fatalf("got %v, want only i-0aaa (i-0bbb is stale, i-0ccc has no verified record)", ids(got)) |
| + } |
| + got = eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, map[string]bool{"i-0aaa": true}) |
| + if len(got) != 0 { |
| + t.Fatalf("a lost box is eligible: %v", ids(got)) |
| + } |
| +} |
| + |
| +// TestEligibilityNeverCombinesBoxes is nearest-region's rule for boxes: one |
| +// box holding the key and another accepting inbound do not add up to one. |
| +func TestEligibleBoxesNeverCombinesTwoHalfBoxes(t *testing.T) { |
| + snap := views(t, |
| + haBox("i-0aaa", func(m map[string]any) { m["secrets_key_fp"] = "" }), |
| + haBox("i-0bbb", func(m map[string]any) { m["accepts_inbound"] = false })) |
| + if got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 { |
| + t.Fatalf("got %v, want none", ids(got)) |
| + } |
| +} |
| + |
| +func TestEligibleBoxesAnIncompleteSnapshotOffersNothing(t *testing.T) { |
| + snap := views(t, haBox("i-0aaa", nil)) |
| + snap.Complete = false |
| + if got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 { |
| + t.Fatalf("got %v from an incomplete snapshot", ids(got)) |
| + } |
| +} |
| + |
| +func TestEligibleBoxesHomePlacementMatchesTheEmptyCode(t *testing.T) { |
| + home := haBox("i-0aaa", func(m map[string]any) { m["placement"] = "" }) |
| + named := haBox("i-0bbb", func(m map[string]any) { m["placement"] = "ie" }) |
| + snap := views(t, home, named) |
| + got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "", "eu-west-2", "ie", testKeyFP, nil) |
| + if len(got) != 2 { |
| + t.Fatalf("got %v, want both home boxes", ids(got)) |
| + } |
| +} |
| + |
| +func TestChooseBox(t *testing.T) { |
| + a, b, c := boxCandidate{BoxID: "i-0aaa"}, boxCandidate{BoxID: "i-0bbb"}, boxCandidate{BoxID: "i-0ccc"} |
| + load := func(hosts int, apps ...string) boxLoad { |
| + l := boxLoad{Hosts: hosts, Apps: map[string]int{}} |
| + for _, x := range apps { |
| + l.Apps[x]++ |
| + } |
| + return l |
| + } |
| + if _, ok := chooseBox(nil, nil, "bab/jam"); ok { |
| + t.Fatal("chose from nothing") |
| + } |
| + got, _ := chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5), "i-0bbb": load(2)}, "bab/jam") |
| + if got.BoxID != "i-0bbb" { |
| + t.Fatalf("fewest hosts: got %s", got.BoxID) |
| + } |
| + got, _ = chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5), "i-0bbb": load(2, "bab/jam")}, "bab/jam") |
| + if got.BoxID != "i-0aaa" { |
| + t.Fatalf("spread beats load: got %s", got.BoxID) |
| + } |
| + got, ok := chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5, "bab/jam"), "i-0bbb": load(2, "bab/jam", "bab/jam")}, "bab/jam") |
| + if !ok || got.BoxID != "i-0bbb" { |
| + t.Fatalf("every box runs the app: fewest hosts overall, never a refusal; got %s ok=%v", got.BoxID, ok) |
| + } |
| + got, _ = chooseBox([]boxCandidate{c, b, a}, nil, "bab/jam") |
| + if got.BoxID != "i-0aaa" { |
| + t.Fatalf("a tie breaks by box id: got %s", got.BoxID) |
| + } |
| +} |
| + |
| +// A box whose SIGNED region is not the placement's AWS region is not in |
| +// that placement, even when its own file says it is and both regions have |
| +// certificates. |
| +func TestEligibleBoxesRefusesABoxSignedInAnotherRegion(t *testing.T) { |
| + snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil)) |
| + verified := verifiedFor("i-0aaa", "i-0bbb") |
| + verified["i-0bbb"] = boxid.Identity{AccountID: "1", Region: "us-east-1", InstanceID: "i-0bbb"} |
| + got := eligibleBoxes(snap, verified, "uk", "eu-west-2", "ie", testKeyFP, nil) |
| + if len(got) != 1 || got[0].BoxID != "i-0aaa" { |
| + t.Fatalf("got %v, want only i-0aaa", ids(got)) |
| + } |
| + // And a file that advertises another AWS region than the one expected. |
| + snap = views(t, haBox("i-0aaa", func(m map[string]any) { m["region"] = "us-east-1" })) |
| + if got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 { |
| + t.Fatalf("got %v, want none", ids(got)) |
| + } |
| + if got := eligibleBoxes(views(t, haBox("i-0aaa", nil)), verifiedFor("i-0aaa"), "uk", "", "ie", testKeyFP, nil); len(got) != 0 { |
| + t.Fatalf("got %v with no expected region", ids(got)) |
| + } |
| +} |