| 1 | package release |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "os" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func testSystemRelease(v string) SystemRelease { |
| 14 | return SystemRelease{ |
| 15 | Version: v, |
| 16 | Artifacts: map[string]SystemArtifact{ |
| 17 | "linux-arm64": {Key: SystemArtifactKey(v, "linux-arm64"), SHA256: strings.Repeat("ab", 32)}, |
| 18 | }, |
| 19 | Source: "release:" + v, |
| 20 | ReleasedAt: time.Date(2026, 9, 6, 8, 0, 0, 0, time.UTC), |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | func TestSystemPointerRoundTrip(t *testing.T) { |
| 25 | priv, pub := testKeys(t) |
| 26 | s := NewDirStore(t.TempDir()) |
| 27 | ctx := context.Background() |
| 28 | if _, _, err := GetSystemPointer(ctx, s, pub, SystemComponentRouter, "stable"); !errors.Is(err, os.ErrNotExist) { |
| 29 | t.Fatalf("missing pointer: err = %v, want ErrNotExist", err) |
| 30 | } |
| 31 | p, err := PromoteSystem(ctx, s, RootSigner(priv), SystemComponentRouter, "stable", testSystemRelease("v0.18.0"), |
| 32 | SystemPromoteOptions{Actor: "paul", Spread: 5 * time.Minute}) |
| 33 | if err != nil { |
| 34 | t.Fatal(err) |
| 35 | } |
| 36 | if p.Current.Version != "v0.18.0" || p.Spread != "5m0s" || p.Schema != 1 || p.PromotedAt.IsZero() { |
| 37 | t.Fatalf("promoted pointer = %+v", p) |
| 38 | } |
| 39 | got, etag, err := GetSystemPointer(ctx, s, pub, SystemComponentRouter, "stable") |
| 40 | if err != nil || etag == "" { |
| 41 | t.Fatalf("get: %v etag=%q", err, etag) |
| 42 | } |
| 43 | if got.Current.Artifacts["linux-arm64"].SHA256 != strings.Repeat("ab", 32) { |
| 44 | t.Fatalf("artifact lost: %+v", got.Current) |
| 45 | } |
| 46 | if got.SpreadDuration() != 5*time.Minute { |
| 47 | t.Fatalf("spread = %v", got.SpreadDuration()) |
| 48 | } |
| 49 | if len(got.Audit) != 1 || got.Audit[0].Actor != "paul" || got.Audit[0].Version != "v0.18.0" { |
| 50 | t.Fatalf("audit = %+v", got.Audit) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func TestSystemPromoteKeepsWholeReleaseInHistoryAndRollbackRestoresIt(t *testing.T) { |
| 55 | priv, pub := testKeys(t) |
| 56 | s := NewDirStore(t.TempDir()) |
| 57 | ctx := context.Background() |
| 58 | sg := RootSigner(priv) |
| 59 | if _, err := PromoteSystem(ctx, s, sg, "router", "canary", testSystemRelease("v0.18.0"), SystemPromoteOptions{}); err != nil { |
| 60 | t.Fatal(err) |
| 61 | } |
| 62 | first, _, _ := GetSystemPointer(ctx, s, pub, "router", "canary") |
| 63 | time.Sleep(1100 * time.Millisecond) // PromotedAt is second-truncated; the rollback must be NEWER |
| 64 | if _, err := PromoteSystem(ctx, s, sg, "router", "canary", testSystemRelease("v0.19.0"), SystemPromoteOptions{}); err != nil { |
| 65 | t.Fatal(err) |
| 66 | } |
| 67 | p, _, _ := GetSystemPointer(ctx, s, pub, "router", "canary") |
| 68 | if len(p.History) != 1 || p.History[0].Version != "v0.18.0" || p.History[0].Artifacts["linux-arm64"].Key == "" { |
| 69 | t.Fatalf("history must carry the whole release: %+v", p.History) |
| 70 | } |
| 71 | time.Sleep(1100 * time.Millisecond) |
| 72 | rb, err := RollbackSystem(ctx, s, sg, "router", "canary", "paul") |
| 73 | if err != nil { |
| 74 | t.Fatal(err) |
| 75 | } |
| 76 | if rb.Current.Version != "v0.18.0" || rb.Current.Artifacts["linux-arm64"].Key != SystemArtifactKey("v0.18.0", "linux-arm64") { |
| 77 | t.Fatalf("rollback current = %+v", rb.Current) |
| 78 | } |
| 79 | if !rb.PromotedAt.After(first.PromotedAt) || !rb.PromotedAt.After(p.PromotedAt) { |
| 80 | t.Fatalf("a rollback is a NEW promote: promoted_at %v must be newer than %v and %v", rb.PromotedAt, first.PromotedAt, p.PromotedAt) |
| 81 | } |
| 82 | if len(rb.History) != 0 { |
| 83 | t.Fatalf("history after rollback = %+v", rb.History) |
| 84 | } |
| 85 | if _, err := RollbackSystem(ctx, s, sg, "router", "canary", "paul"); !errors.Is(err, ErrNoSystemHistory) { |
| 86 | t.Fatalf("second rollback err = %v", err) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | func TestSystemPromoteSameVersionDoesNotGrowHistory(t *testing.T) { |
| 91 | priv, pub := testKeys(t) |
| 92 | s := NewDirStore(t.TempDir()) |
| 93 | ctx := context.Background() |
| 94 | sg := RootSigner(priv) |
| 95 | for i := 0; i < 2; i++ { |
| 96 | if _, err := PromoteSystem(ctx, s, sg, "router", "stable", testSystemRelease("v1.0.0"), SystemPromoteOptions{}); err != nil { |
| 97 | t.Fatal(err) |
| 98 | } |
| 99 | } |
| 100 | p, _, _ := GetSystemPointer(ctx, s, pub, "router", "stable") |
| 101 | if len(p.History) != 0 { |
| 102 | t.Fatalf("re-promoting the running version must not push it into history: %+v", p.History) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestSystemPointerRefusesScopedSigner(t *testing.T) { |
| 107 | _, _, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 30*24*time.Hour) |
| 108 | s := NewDirStore(t.TempDir()) |
| 109 | _, err := PromoteSystem(context.Background(), s, sg, "router", "canary", testSystemRelease("v1.0.0"), SystemPromoteOptions{}) |
| 110 | if err == nil || !strings.Contains(err.Error(), "root key") { |
| 111 | t.Fatalf("scoped signer signed a system pointer: err = %v", err) |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func TestSystemPointerRefusesAGrantOnTheWire(t *testing.T) { |
| 116 | // A wrapper carrying a grant is refused BEFORE the signature is checked, |
| 117 | // like the production flag (production.go) and the pipeline. |
| 118 | priv, pub := testKeys(t) |
| 119 | s := NewDirStore(t.TempDir()) |
| 120 | ctx := context.Background() |
| 121 | if _, err := PromoteSystem(ctx, s, RootSigner(priv), "router", "stable", testSystemRelease("v1.0.0"), SystemPromoteOptions{}); err != nil { |
| 122 | t.Fatal(err) |
| 123 | } |
| 124 | body, etag, _ := s.Get(ctx, SystemPointerKey("router", "stable")) |
| 125 | var sw signedWrapper |
| 126 | if err := json.Unmarshal(body, &sw); err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | sw.Grant = &signedGrant{Payload: "x", Sig: "y"} |
| 130 | tampered, _ := json.Marshal(sw) |
| 131 | if _, err := s.Put(ctx, SystemPointerKey("router", "stable"), tampered, etag); err != nil { |
| 132 | t.Fatal(err) |
| 133 | } |
| 134 | if _, _, err := GetSystemPointer(ctx, s, pub, "router", "stable"); err == nil || !strings.Contains(err.Error(), "root key") { |
| 135 | t.Fatalf("grant-bearing wrapper accepted: err = %v", err) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | func TestSystemPointerTamperAndCrossChannelRefused(t *testing.T) { |
| 140 | priv, pub := testKeys(t) |
| 141 | s := NewDirStore(t.TempDir()) |
| 142 | ctx := context.Background() |
| 143 | if _, err := PromoteSystem(ctx, s, RootSigner(priv), "router", "canary", testSystemRelease("v1.0.0"), SystemPromoteOptions{}); err != nil { |
| 144 | t.Fatal(err) |
| 145 | } |
| 146 | body, _, _ := s.Get(ctx, SystemPointerKey("router", "canary")) |
| 147 | // Cross-channel replay: the canary wrapper planted at the stable key. |
| 148 | if _, err := s.Put(ctx, SystemPointerKey("router", "stable"), body, ""); err != nil { |
| 149 | t.Fatal(err) |
| 150 | } |
| 151 | // Refusals are typed, not stringly: a box tells "not authority" (refuse) |
| 152 | // from "the bucket is unreachable" (defer) with errors.Is. |
| 153 | if _, _, err := GetSystemPointer(ctx, s, pub, "router", "stable"); err == nil || !errors.Is(err, ErrBadSystemPointer) { |
| 154 | t.Fatalf("a canary pointer verified as stable: err = %v", err) |
| 155 | } |
| 156 | // Tampered signature. |
| 157 | var sw signedWrapper |
| 158 | _ = json.Unmarshal(body, &sw) |
| 159 | sw.Sig = "AA" + sw.Sig[2:] |
| 160 | bad, _ := json.Marshal(sw) |
| 161 | _, etag, _ := s.Get(ctx, SystemPointerKey("router", "canary")) |
| 162 | if _, err := s.Put(ctx, SystemPointerKey("router", "canary"), bad, etag); err != nil { |
| 163 | t.Fatal(err) |
| 164 | } |
| 165 | if _, _, err := GetSystemPointer(ctx, s, pub, "router", "canary"); err == nil || !errors.Is(err, ErrBadSystemPointer) { |
| 166 | t.Fatalf("tampered signature verified: err = %v", err) |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | func TestSystemPointerValidation(t *testing.T) { |
| 171 | priv, _ := testKeys(t) |
| 172 | sg := RootSigner(priv) |
| 173 | ctx := context.Background() |
| 174 | cases := map[string]SystemRelease{ |
| 175 | "dev version": {Version: "dev", Artifacts: testSystemRelease("x").Artifacts}, |
| 176 | "no artifacts": {Version: "v1.0.0"}, |
| 177 | "bad platform": {Version: "v1.0.0", Artifacts: map[string]SystemArtifact{"linux_arm64": {Key: "bootstrap/router/v1.0.0/carlos-linux_arm64", SHA256: strings.Repeat("a", 64)}}}, |
| 178 | "key not derived": {Version: "v1.0.0", Artifacts: map[string]SystemArtifact{"linux-arm64": {Key: "bootstrap/carlos", SHA256: strings.Repeat("a", 64)}}}, |
| 179 | "short sha": {Version: "v1.0.0", Artifacts: map[string]SystemArtifact{"linux-arm64": {Key: SystemArtifactKey("v1.0.0", "linux-arm64"), SHA256: "abc"}}}, |
| 180 | "version traversal": {Version: "../x", Artifacts: testSystemRelease("../x").Artifacts}, |
| 181 | } |
| 182 | for name, rel := range cases { |
| 183 | s := NewDirStore(t.TempDir()) |
| 184 | if _, err := PromoteSystem(ctx, s, sg, "router", "stable", rel, SystemPromoteOptions{}); err == nil { |
| 185 | t.Errorf("%s: accepted", name) |
| 186 | } |
| 187 | } |
| 188 | s := NewDirStore(t.TempDir()) |
| 189 | if _, err := PromoteSystem(ctx, s, sg, "Router", "stable", testSystemRelease("v1.0.0"), SystemPromoteOptions{}); err == nil { |
| 190 | t.Error("bad component accepted") |
| 191 | } |
| 192 | if _, err := PromoteSystem(ctx, s, sg, "router", "stable", testSystemRelease("v1.0.0"), SystemPromoteOptions{Spread: 48 * time.Hour}); err == nil { |
| 193 | t.Error("48h spread accepted") |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | func TestSystemPromoteCASConflict(t *testing.T) { |
| 198 | priv, _ := testKeys(t) |
| 199 | ctx := context.Background() |
| 200 | inner := NewDirStore(t.TempDir()) |
| 201 | // A store whose Put always says "someone else moved it". |
| 202 | s := conflictingStore{Store: inner} |
| 203 | if _, err := PromoteSystem(ctx, s, RootSigner(priv), "router", "stable", testSystemRelease("v1.0.0"), SystemPromoteOptions{}); !errors.Is(err, ErrCASConflict) { |
| 204 | t.Fatalf("err = %v, want ErrCASConflict", err) |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | type conflictingStore struct{ Store } |
| 209 | |
| 210 | func (c conflictingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { |
| 211 | return "", ErrCASConflict |
| 212 | } |
| 213 | |