| 1 | package release_test |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | // liveChannels is what production is running on RIGHT NOW. |
| 12 | var liveChannels = []string{"canary/platform-adoption", "canary/keymail", "canary/console", "canary/rehearsal"} |
| 13 | |
| 14 | // The two channel names production is running on RIGHT NOW must never be |
| 15 | // refused by the ladder — go.tito.io follows canary/platform-adoption and |
| 16 | // keymail.dev follows canary/keymail. If these fail, production cannot deploy. |
| 17 | func TestLiveProductionChannelsAreNeverRefused(t *testing.T) { |
| 18 | for _, ch := range liveChannels { |
| 19 | rung, ok := release.Rung(ch) |
| 20 | if !ok || rung != release.RungCanary { |
| 21 | t.Fatalf("Rung(%q) = %q,%v — want canary,true", ch, rung, ok) |
| 22 | } |
| 23 | // Promotion INTO a canary channel from anywhere must be allowed. |
| 24 | for _, from := range []string{"", "canary", "edge", "beta", "stable"} { |
| 25 | if err := release.PromotionAllowed(from, release.RungCanary); err != nil { |
| 26 | t.Errorf("PromotionAllowed(%q -> canary) refused: %v", from, err) |
| 27 | } |
| 28 | } |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // The skip that matters: nothing reaches stable without beta, and the refusal |
| 33 | // tells the operator the way out. |
| 34 | func TestEdgeToStableIsRefusedAndNamesTheBypass(t *testing.T) { |
| 35 | err := release.PromotionAllowed(release.RungEdge, release.RungStable) |
| 36 | if err == nil { |
| 37 | t.Fatal("edge -> stable was allowed; the ladder means nothing") |
| 38 | } |
| 39 | if got := err.Error(); !contains(got, "-hotfix") { |
| 40 | t.Errorf("refusal %q does not name --hotfix", got) |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | // The other half of "production can still deploy": every live channel is |
| 45 | // promoted with the ROOT key, and what lands is a wrapper with NO grant in |
| 46 | // it, verifying against the root public key. Scoped signing is opt-in from |
| 47 | // the first byte — nothing about the fleet's existing pointers changes until |
| 48 | // someone deliberately mints a scoped key. |
| 49 | // |
| 50 | // Mutation-mindedness: make the grant field non-optional, attach an empty |
| 51 | // grant to root-signed pointers, or route root signing through grant |
| 52 | // handling, and this fails on the key list — before any signature check, and |
| 53 | // for every channel production actually runs on. |
| 54 | func TestLiveProductionChannelsStayRootSignedWithNoGrant(t *testing.T) { |
| 55 | ctx := context.Background() |
| 56 | s := release.NewDirStore(t.TempDir()) |
| 57 | seedB64, pubB64, err := release.GenerateKey() |
| 58 | if err != nil { |
| 59 | t.Fatalf("GenerateKey: %v", err) |
| 60 | } |
| 61 | priv, err := release.ParseSigningKey(seedB64, "test") |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | pub, err := release.ParsePublicKey(pubB64, "test") |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | m := release.Manifest{ |
| 70 | App: "kass", Version: "9f3c1ad", Kind: "binary", |
| 71 | Artifacts: map[string]release.Artifact{"linux-arm64": {}}, |
| 72 | } |
| 73 | if err := release.Ship(ctx, s, release.OpsAccount, m, map[string][]byte{"linux-arm64": []byte("build")}); err != nil { |
| 74 | t.Fatalf("Ship: %v", err) |
| 75 | } |
| 76 | |
| 77 | for _, ch := range liveChannels { |
| 78 | if err := release.Promote(ctx, s, priv, release.OpsAccount, "kass", ch, "9f3c1ad"); err != nil { |
| 79 | t.Fatalf("Promote %s: %v", ch, err) |
| 80 | } |
| 81 | raw, _, err := s.Get(ctx, release.ChannelKey(release.OpsAccount, "kass", ch)) |
| 82 | if err != nil { |
| 83 | t.Fatalf("read %s: %v", ch, err) |
| 84 | } |
| 85 | var keys map[string]json.RawMessage |
| 86 | if err := json.Unmarshal(raw, &keys); err != nil { |
| 87 | t.Fatalf("%s: %v", ch, err) |
| 88 | } |
| 89 | if len(keys) != 2 || keys["payload"] == nil || keys["sig"] == nil { |
| 90 | t.Errorf("%s wrapper has keys %v, want exactly payload and sig", ch, keysOf(keys)) |
| 91 | } |
| 92 | if _, err := release.Channel(ctx, s, pub, release.OpsAccount, "kass", ch); err != nil { |
| 93 | t.Errorf("%s does not verify against the root pubkey: %v", ch, err) |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | func keysOf(m map[string]json.RawMessage) []string { |
| 99 | out := make([]string, 0, len(m)) |
| 100 | for k := range m { |
| 101 | out = append(out, k) |
| 102 | } |
| 103 | return out |
| 104 | } |
| 105 | |
| 106 | func contains(s, sub string) bool { |
| 107 | for i := 0; i+len(sub) <= len(s); i++ { |
| 108 | if s[i:i+len(sub)] == sub { |
| 109 | return true |
| 110 | } |
| 111 | } |
| 112 | return false |
| 113 | } |
| 114 | |