carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "net/http"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10 "github.com/carlosframework/platform/internal/svcrole"
11)
12
13func TestDeployScopeAllows(t *testing.T) {
14 ce := Principal{Kind: PrincipalService, Role: svcrole.Deploy, Name: "ci", App: "hello", Rungs: []string{"canary", "edge"}}
15 beta := Principal{Kind: PrincipalService, Role: svcrole.Deploy, Name: "ci", App: "hello", Rungs: []string{"beta"}}
16 pub := Principal{Kind: PrincipalService, Role: svcrole.Publish, Name: "ci", App: "hello"}
17 person := Principal{Kind: PrincipalUser, Address: "paul@keymail.dev"}
18 for _, tc := range []struct {
19 name string
20 p Principal
21 pipelined bool
22 channel string
23 hotfix bool
24 ok bool
25 }{
26 {"edge in scope", ce, false, "edge", false, true},
27 {"canary branch in scope", ce, false, "canary/feature-x", false, true},
28 {"bare canary refused", ce, false, "canary", false, false},
29 {"bare canary refused on a pipelined app", ce, true, "canary", false, false},
30 {"beta outside rungs", ce, false, "beta", false, false},
31 {"stable refused", ce, false, "stable", false, false},
32 {"stable refused even when granted beta", beta, false, "stable", false, false},
33 {"not a rung", ce, false, "staging", false, false},
34 {"hotfix refused", ce, false, "edge", true, false},
35 {"pipelined: edge refused", ce, true, "edge", false, false},
36 {"pipelined: canary branch allowed", ce, true, "canary/x", false, true},
37 {"beta credential on beta", beta, false, "beta", false, true},
38 {"publish untouched", pub, true, "stable", true, true},
39 {"person untouched", person, true, "stable", true, true},
40 } {
41 err := deployScopeAllows(tc.p, tc.pipelined, tc.channel, tc.hotfix)
42 if (err == nil) != tc.ok {
43 t.Errorf("%s: err = %v, want ok=%v", tc.name, err, tc.ok)
44 }
45 }
46}
47
48// TestDeployScopeRefusalsAtTheDoors: promote and rollback by a deploy
49// credential refuse out of scope moves with the approved text (D21 to D24)
50// and leave every channel exactly as it was.
51func TestDeployScopeRefusalsAtTheDoors(t *testing.T) {
52 fx := newConsoleFixture(t)
53 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
54 t.Fatal(err)
55 }
56 person := cliToken(t, fx, "paul@keymail.dev", "code-deploy-scope")
57 liftShipBudget(fx)
58 if status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/hello/releases", person,
59 []shipPart{metaPart(t, "v1", "binary"), {name: "linux-arm64", body: []byte("not an ELF at all")}}); status != http.StatusCreated {
60 t.Fatalf("person ship = %d %v", status, out)
61 }
62 tok, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "ci", "hello", svcrole.Deploy, []string{"canary", "edge"}, nil, "paul@keymail.dev", time.Now())
63 if err != nil {
64 t.Fatal(err)
65 }
66
67 ctx := context.Background()
68 channelVersion := func(channel string) string {
69 t.Helper()
70 cp, err := release.Channel(ctx, fx.rel, fx.pub, "bab", "hello", channel)
71 if err != nil {
72 return "" // absent
73 }
74 return cp.Version
75 }
76 promote := func(body map[string]any) (int, map[string]any) {
77 t.Helper()
78 return postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote", body, tok)
79 }
80 refused := func(name string, status int, out map[string]any, want string) {
81 t.Helper()
82 if status != http.StatusForbidden {
83 t.Errorf("%s: status = %d %v, want 403", name, status, out)
84 return
85 }
86 if got, _ := out["error"].(string); got != want {
87 t.Errorf("%s: error = %q, want %q", name, got, want)
88 }
89 }
90
91 type attempt struct {
92 name string
93 body map[string]any
94 channel string
95 want string
96 }
97 d22 := func(channel string) string {
98 return "deploy credential ci may move canary/<name>, edge only, and " + channel + " is not one of them."
99 }
100 for _, a := range []attempt{
101 {"beta", map[string]any{"version": "v1", "channel": "beta"}, "beta", d22("beta")},
102 {"stable", map[string]any{"version": "v1", "channel": "stable"}, "stable", d22("stable")},
103 {"bare canary", map[string]any{"version": "v1", "channel": "canary"}, "canary",
104 "deploy credential ci may move canary/<name> channels, not canary itself."},
105 {"hotfix", map[string]any{"version": "v1", "channel": "edge", "hotfix": true}, "edge",
106 "deploy credential ci cannot hotfix. Promote in ladder order, or ask a member to hotfix."},
107 } {
108 before := channelVersion(a.channel)
109 status, out := promote(a.body)
110 refused(a.name, status, out, a.want)
111 if after := channelVersion(a.channel); after != before {
112 t.Errorf("%s: channel %s moved %q -> %q", a.name, a.channel, before, after)
113 }
114 }
115
116 // A pipeline names edge and stable. Edge is then refused (D24), and a
117 // pipeline naming stable with no passkey rule still refuses deploy.
118 if _, err := release.SetPipeline(ctx, fx.rel, fx.priv, "bab", "hello",
119 []release.PipelineChannel{{Name: "edge"}, {Name: "stable"}}, "paul@keymail.dev"); err != nil {
120 t.Fatal(err)
121 }
122 for _, a := range []attempt{
123 {"pipelined edge", map[string]any{"version": "v1", "channel": "edge"}, "edge",
124 "hello has a pipeline, so deploy credential ci may move canary/<name> channels only."},
125 {"pipelined stable", map[string]any{"version": "v1", "channel": "stable"}, "stable", d22("stable")},
126 } {
127 before := channelVersion(a.channel)
128 status, out := promote(a.body)
129 refused(a.name, status, out, a.want)
130 if after := channelVersion(a.channel); after != before {
131 t.Errorf("%s: channel %s moved %q -> %q", a.name, a.channel, before, after)
132 }
133 }
134
135 // Rollback: beta is outside the credential's rungs.
136 before := channelVersion("beta")
137 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback", map[string]any{"channel": "beta"}, tok)
138 refused("rollback beta", status, out, d22("beta"))
139 if after := channelVersion("beta"); after != before {
140 t.Errorf("rollback beta: channel moved %q -> %q", before, after)
141 }
142}
143
144// TestDeployRollbackOfStableIsD22: a deploy credential rolling back stable
145// gets the D22 refusal naming it and its rungs, not the legacy passkey text,
146// and stable is left alone. A person's rollback of stable keeps the legacy
147// answer (the early deploy check returns nil for every other principal).
148func TestDeployRollbackOfStableIsD22(t *testing.T) {
149 fx, person, deploy := deployFixture(t)
150 stableAbsent := func() {
151 t.Helper()
152 if _, _, err := release.GetQualified(context.Background(), fx.rel, release.ChannelKey("bab", "hello", "stable")); err == nil {
153 t.Fatal("stable pointer exists, want it untouched (absent)")
154 }
155 }
156
157 status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback", map[string]any{"channel": "stable"}, deploy)
158 want := "deploy credential ci may move " + deployHeldWords([]string{"canary", "edge"}) + " only, and stable is not one of them."
159 if status != http.StatusForbidden {
160 t.Fatalf("deploy rollback stable = %d %v, want 403", status, out)
161 }
162 if got, _ := out["error"].(string); got != want {
163 t.Errorf("deploy rollback stable error = %q, want %q", got, want)
164 }
165 if _, has := out["console_url"]; has {
166 t.Errorf("deploy refusal carries the legacy console_url: %v", out)
167 }
168 stableAbsent()
169
170 status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback", map[string]any{"channel": "stable"}, person)
171 if status != http.StatusForbidden {
172 t.Fatalf("person rollback stable = %d %v, want 403", status, out)
173 }
174 if got, _ := out["error"].(string); got != "stable needs a passkey — finish in the console" {
175 t.Errorf("person rollback stable error = %q, want the legacy text", got)
176 }
177 if _, has := out["console_url"]; !has {
178 t.Errorf("person refusal lost its console_url: %v", out)
179 }
180 stableAbsent()
181}
182