| 1 | package hostactors |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "reflect" |
| 6 | "slices" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | func bindingRoster() Roster { |
| 13 | key := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)).Public().(ed25519.PublicKey) |
| 14 | row := Box{EnrolledAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), CensusPublicKey: key, Enrollment: strings.Repeat("a", 32), Namespace: "platform", BoxID: "box", Label: "label", Region: "ie", Pool: "default", Cloud: Cloud{Provider: "aws", Account: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active", QuarantineRevision: 2, LossIDs: []string{"loss_1"}, Group: "group", Role: ""} |
| 15 | sibling := row |
| 16 | sibling.BoxID, sibling.Enrollment, sibling.Cloud.InstanceID = "sibling", strings.Repeat("b", 32), "i-0def" |
| 17 | return Roster{ |
| 18 | HomeRegion: "ie", RequestLimitNS: 30_000_000_000, Publication: "published", FloorSHA256: strings.Repeat("d", 64), |
| 19 | EnrollmentAttempts: map[string]EnrollmentAttempt{}, Regions: map[string]Region{"ie": {Namespaces: []string{"platform"}, Pools: []string{"default"}}}, |
| 20 | V: 1, DeploymentBucket: "deployment", Revision: 7, |
| 21 | Boxes: map[string]Box{"box": row, "sibling": sibling}, |
| 22 | Consoles: map[string]Console{}, Holds: map[string]Reservation{}, Activations: map[string]Reservation{}, |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | // The v1 tuple is frozen: the golden digest pins its order and encoding, every |
| 27 | // bound fact moves it, and every excluded fact leaves it alone. The field |
| 28 | // lists below must name every Roster and Box field, so a field added later |
| 29 | // fails here until someone decides which side it is on (and, if bound, writes |
| 30 | // a v2 encoder). |
| 31 | func TestBoxBindingTuple(t *testing.T) { |
| 32 | // The encoded tuple and its sha256 (checked by hand with sha256sum). |
| 33 | const tuple = `["carlos-box-binding-v1","deployment","ie",30000000000,"published","dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","platform","box","ie","default",["aws","111122223333","eu-west-1","i-0abc"],"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","active",2,"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"]` |
| 34 | const golden = "878c8d6e101fac0431418841b065a199c0796a2b83d30ee86e64400e44f94dca" |
| 35 | base := bindingRoster() |
| 36 | if raw := string(boxBindingV1(base, base.Boxes["box"])); raw != tuple { |
| 37 | t.Fatalf("v1 tuple encoding changed:\n got %s\nwant %s", raw, tuple) |
| 38 | } |
| 39 | got, ok := BoxBinding(base, "box") |
| 40 | if !ok || !ValidBinding(got) { |
| 41 | t.Fatalf("BoxBinding = %q, %v", got, ok) |
| 42 | } |
| 43 | if got != golden { |
| 44 | t.Fatalf("golden binding changed: got %s want %s (the v1 tuple is frozen)", got, golden) |
| 45 | } |
| 46 | if _, ok := BoxBinding(base, "unknown"); ok { |
| 47 | t.Fatal("unknown box returned a binding") |
| 48 | } |
| 49 | if _, ok := BoxBinding(Roster{}, "box"); ok { |
| 50 | t.Fatal("empty roster returned a binding") |
| 51 | } |
| 52 | |
| 53 | bound := map[string]func(*Roster, *Box){ |
| 54 | "Roster.DeploymentBucket": func(r *Roster, _ *Box) { r.DeploymentBucket = "other" }, |
| 55 | "Roster.HomeRegion": func(r *Roster, _ *Box) { r.HomeRegion = "uk" }, |
| 56 | "Roster.RequestLimitNS": func(r *Roster, _ *Box) { r.RequestLimitNS++ }, |
| 57 | "Roster.Publication": func(r *Roster, _ *Box) { r.Publication = "preparing" }, |
| 58 | "Roster.FloorSHA256": func(r *Roster, _ *Box) { r.FloorSHA256 = strings.Repeat("e", 64) }, |
| 59 | "Box.Enrollment": func(_ *Roster, b *Box) { b.Enrollment = strings.Repeat("f", 32) }, |
| 60 | "Box.Namespace": func(_ *Roster, b *Box) { b.Namespace = "other" }, |
| 61 | "Box.BoxID": func(_ *Roster, b *Box) { b.BoxID = "other" }, |
| 62 | "Box.Region": func(_ *Roster, b *Box) { b.Region = "uk" }, |
| 63 | "Box.Pool": func(_ *Roster, b *Box) { b.Pool = "other" }, |
| 64 | "Box.Cloud.Provider": func(_ *Roster, b *Box) { b.Cloud.Provider = "other" }, |
| 65 | "Box.Cloud.Account": func(_ *Roster, b *Box) { b.Cloud.Account = "444455556666" }, |
| 66 | "Box.Cloud.Region": func(_ *Roster, b *Box) { b.Cloud.Region = "eu-west-2" }, |
| 67 | "Box.Cloud.InstanceID": func(_ *Roster, b *Box) { b.Cloud.InstanceID = "i-0fff" }, |
| 68 | "Box.EvidenceSHA256": func(_ *Roster, b *Box) { b.EvidenceSHA256 = strings.Repeat("9", 64) }, |
| 69 | "Box.State": func(_ *Roster, b *Box) { b.State = "quarantined" }, |
| 70 | "Box.QuarantineRevision": func(_ *Roster, b *Box) { b.QuarantineRevision++ }, |
| 71 | "Box.CensusPublicKey": func(_ *Roster, b *Box) { |
| 72 | b.CensusPublicKey = ed25519.NewKeyFromSeed(append(make([]byte, ed25519.SeedSize-1), 1)).Public().(ed25519.PublicKey) |
| 73 | }, |
| 74 | } |
| 75 | excluded := map[string]func(*Roster, *Box){ |
| 76 | "Roster.V": func(r *Roster, _ *Box) { r.V = 2 }, |
| 77 | "Roster.Revision": func(r *Roster, _ *Box) { r.Revision++ }, |
| 78 | "Roster.Holds": func(r *Roster, _ *Box) { |
| 79 | r.Holds["hold_1"] = Reservation{ID: "hold_1", Host: "h", OpID: "op", Actors: map[string]string{"box": "x"}} |
| 80 | }, |
| 81 | "Roster.Activations": func(r *Roster, _ *Box) { |
| 82 | r.Activations["act_1"] = Reservation{ID: "act_1", Actors: map[string]string{"box": "x"}} |
| 83 | }, |
| 84 | "Roster.EnrollmentAttempts": func(r *Roster, _ *Box) { |
| 85 | r.EnrollmentAttempts["attempt"] = EnrollmentAttempt{AttemptID: "attempt", InstanceID: "i-0new"} |
| 86 | }, |
| 87 | "Roster.Consoles": func(r *Roster, _ *Box) { |
| 88 | r.Consoles["i-console"] = Console{Enrollment: strings.Repeat("e", 32), InstanceID: "i-console", Namespaces: []string{"platform"}, DeploymentBucket: "deployment"} |
| 89 | }, |
| 90 | "Roster.Regions": func(r *Roster, _ *Box) { r.Regions["uk"] = Region{Namespaces: []string{"platform"}} }, |
| 91 | "Roster.Boxes": func(r *Roster, _ *Box) { |
| 92 | s := r.Boxes["sibling"] |
| 93 | s.State, s.QuarantineRevision, s.LossIDs = "quarantined", 1, []string{"loss"} |
| 94 | r.Boxes["sibling"] = s |
| 95 | r.Boxes["third"] = Box{BoxID: "third"} |
| 96 | }, |
| 97 | "Box.Role": func(_ *Roster, b *Box) { b.Role = "platform" }, |
| 98 | "Box.Label": func(_ *Roster, b *Box) { b.Label = "renamed" }, |
| 99 | "Box.Group": func(_ *Roster, b *Box) { b.Group = "other-group" }, |
| 100 | "Box.LossIDs": func(_ *Roster, b *Box) { b.LossIDs = append(b.LossIDs, "loss_2") }, |
| 101 | "Box.EnrolledAt": func(_ *Roster, b *Box) { b.EnrolledAt = b.EnrolledAt.Add(time.Hour) }, |
| 102 | } |
| 103 | |
| 104 | // Every field is classified, exactly once. |
| 105 | var fields []string |
| 106 | for _, ty := range []struct { |
| 107 | prefix string |
| 108 | t reflect.Type |
| 109 | }{{"Roster.", reflect.TypeOf(Roster{})}, {"Box.", reflect.TypeOf(Box{})}, {"Box.Cloud.", reflect.TypeOf(Cloud{})}} { |
| 110 | for i := 0; i < ty.t.NumField(); i++ { |
| 111 | name := ty.prefix + ty.t.Field(i).Name |
| 112 | if name == "Box.Cloud" { |
| 113 | continue |
| 114 | } |
| 115 | fields = append(fields, name) |
| 116 | } |
| 117 | } |
| 118 | for _, name := range fields { |
| 119 | _, in := bound[name] |
| 120 | _, out := excluded[name] |
| 121 | if in == out { |
| 122 | t.Errorf("%s: bound=%v excluded=%v; classify every field exactly once", name, in, out) |
| 123 | } |
| 124 | } |
| 125 | if len(bound)+len(excluded) != len(fields) { |
| 126 | t.Errorf("classified %d fields, struct has %d", len(bound)+len(excluded), len(fields)) |
| 127 | } |
| 128 | |
| 129 | mutate := func(edit func(*Roster, *Box)) string { |
| 130 | r := bindingRoster() |
| 131 | b := r.Boxes["box"] |
| 132 | edit(&r, &b) |
| 133 | ref := "box" |
| 134 | delete(r.Boxes, "box") |
| 135 | r.Boxes[ref] = b |
| 136 | out, ok := BoxBinding(r, ref) |
| 137 | if !ok { |
| 138 | t.Fatal("box vanished") |
| 139 | } |
| 140 | return out |
| 141 | } |
| 142 | names := func(m map[string]func(*Roster, *Box)) []string { |
| 143 | var out []string |
| 144 | for k := range m { |
| 145 | out = append(out, k) |
| 146 | } |
| 147 | slices.Sort(out) |
| 148 | return out |
| 149 | } |
| 150 | seen := map[string]string{got: "base"} |
| 151 | for _, name := range names(bound) { |
| 152 | d := mutate(bound[name]) |
| 153 | if d == got { |
| 154 | t.Errorf("%s changed but the binding did not", name) |
| 155 | } |
| 156 | if prior, dup := seen[d]; dup { |
| 157 | t.Errorf("%s and %s gave the same binding", name, prior) |
| 158 | } |
| 159 | seen[d] = name |
| 160 | } |
| 161 | for _, name := range names(excluded) { |
| 162 | if d := mutate(excluded[name]); d != got { |
| 163 | t.Errorf("%s changed and the binding moved; it is excluded", name) |
| 164 | } |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | func TestValidBinding(t *testing.T) { |
| 169 | for s, want := range map[string]bool{ |
| 170 | strings.Repeat("a", 64): true, |
| 171 | strings.Repeat("0", 64): true, |
| 172 | strings.Repeat("A", 64): false, |
| 173 | strings.Repeat("a", 63): false, |
| 174 | strings.Repeat("a", 65): false, |
| 175 | strings.Repeat("g", 64): false, |
| 176 | "": false, |
| 177 | "7": false, |
| 178 | strings.Repeat("a", 63) + " ": false, |
| 179 | } { |
| 180 | if got := ValidBinding(s); got != want { |
| 181 | t.Errorf("ValidBinding(%q) = %v, want %v", s, got, want) |
| 182 | } |
| 183 | } |
| 184 | } |
| 185 | |