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 "net/http"
5 "net/url"
6 "strings"
7 "testing"
8 "time"
9)
10
11func TestAPIMintDeploy(t *testing.T) {
12 fx := newConsoleFixture(t)
13 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
14 t.Fatal(err)
15 }
16 token := cliToken(t, fx, "paul@keymail.dev", "code-deploy-mint")
17 u := fx.ts.URL + "/api/cli/services/bab"
18 status, out := postJSON(t, u, map[string]any{"name": "ci", "role": "deploy", "app": "hello", "rungs": []string{"edge", "canary"}}, token)
19 if status != http.StatusCreated {
20 t.Fatalf("mint deploy = %d %v", status, out)
21 }
22 if got, _ := out["rungs"].([]any); len(got) != 2 || got[0] != "canary" || got[1] != "edge" {
23 t.Fatalf("rungs in response = %v", out["rungs"])
24 }
25 for _, tc := range []struct {
26 body map[string]any
27 want string // D-text fragment from the approved Copy table
28 }{
29 {map[string]any{"name": "a1", "role": "deploy", "rungs": []string{"edge"}}, "must name one app"},
30 {map[string]any{"name": "a2", "role": "deploy", "app": "hello"}, "must name its rungs"},
31 {map[string]any{"name": "a3", "role": "deploy", "app": "hello", "rungs": []string{"stable"}}, "stable cannot be delegated"},
32 {map[string]any{"name": "a4", "role": "deploy", "app": "hello", "rungs": []string{"prod"}}, `"prod"`},
33 {map[string]any{"name": "a5", "role": "publish", "app": "hello", "rungs": []string{"edge"}}, "only a deploy credential"},
34 } {
35 status, out := postJSON(t, u, tc.body, token)
36 if status != http.StatusBadRequest || !strings.Contains(str(t, out, "error"), tc.want) {
37 t.Errorf("%v = %d %v; want 400 containing %q", tc.body, status, out, tc.want)
38 }
39 }
40 status, out = getJSON(t, u, token)
41 if status != http.StatusOK {
42 t.Fatalf("list = %d %v", status, out)
43 }
44 rows, _ := out["services"].([]any)
45 if len(rows) != 1 {
46 t.Fatalf("listing = %v", rows)
47 }
48 if r, _ := rows[0].(map[string]any)["rungs"].([]any); len(r) != 2 {
49 t.Fatalf("listing rungs = %v", rows[0])
50 }
51}
52
53// A non-deploy row lists "rungs": [] (an empty array, never null), so a
54// client can range over it without a nil check.
55func TestAPIListNonDeployRungsAreAnEmptyArray(t *testing.T) {
56 fx := newConsoleFixture(t)
57 if _, err := fx.st.CreateAppIn(OpsAccountID, "hello", "paul@keymail.dev", time.Now()); err != nil {
58 t.Fatal(err)
59 }
60 token := cliToken(t, fx, "paul@keymail.dev", "code-deploy-list")
61 u := fx.ts.URL + "/api/cli/services/bab"
62 if status, out := postJSON(t, u, map[string]any{"name": "pub", "role": "publish", "app": "hello"}, token); status != http.StatusCreated {
63 t.Fatalf("mint publish = %d %v", status, out)
64 } else if r, ok := out["rungs"].([]any); !ok || len(r) != 0 {
65 t.Fatalf("mint response rungs = %#v, want []", out["rungs"])
66 }
67 status, out := getJSON(t, u, token)
68 if status != http.StatusOK {
69 t.Fatalf("list = %d %v", status, out)
70 }
71 rows, _ := out["services"].([]any)
72 if len(rows) != 1 {
73 t.Fatalf("listing = %v", rows)
74 }
75 if r, ok := rows[0].(map[string]any)["rungs"].([]any); !ok || len(r) != 0 {
76 t.Fatalf("listing rungs = %#v, want []", rows[0].(map[string]any)["rungs"])
77 }
78}
79
80func TestFormMintDeploy(t *testing.T) {
81 fx := newConsoleFixture(t)
82 c := signInPaul(t, fx)
83 csrf := sessionToken(t, fx, c)
84
85 before, err := fx.st.ServicePrincipals(OpsAccountID)
86 if err != nil {
87 t.Fatal(err)
88 }
89 // Each refusal is a flash on the settings page, before any passkey trip
90 // and with no row created.
91 for _, bad := range []struct {
92 why string
93 form url.Values
94 want string
95 }{
96 {"no app", url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"deploy"}, "rungs": {"edge"}}, "A deploy credential must name one app. Nothing was minted."},
97 {"no rungs", url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"deploy"}, "app": {"kass"}}, "A deploy credential must name at least one rung: canary, edge or beta. Nothing was minted."},
98 {"stable", url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"deploy"}, "app": {"kass"}, "rungs": {"edge", "stable"}}, "Stable cannot be delegated. Moving it needs a passkey in the console. Nothing was minted."},
99 {"an unknown rung", url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"deploy"}, "app": {"kass"}, "rungs": {"prod"}}, "is not a rung. Choose canary, edge or beta. Nothing was minted."},
100 {"rungs on publish", url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"publish"}, "app": {"kass"}, "rungs": {"edge"}}, "Only a deploy credential takes rungs. Nothing was minted."},
101 } {
102 status, loc, body := postForm(t, c, fx.u("/services/new"), bad.form)
103 if strings.HasPrefix(loc, fx.fk.srv.URL) {
104 t.Errorf("%s cost a passkey trip (-> %q)", bad.why, loc)
105 }
106 if status != http.StatusSeeOther || loc != fx.path("/settings") {
107 t.Errorf("%s = %d -> %q, want 303 back to the page:\n%s", bad.why, status, loc, body)
108 continue
109 }
110 _, _, page := get(t, c, fx.u("/settings"))
111 if !strings.Contains(page, bad.want) {
112 t.Errorf("%s: the settings page does not say %q:\n%s", bad.why, bad.want, page)
113 }
114 rows, err := fx.st.ServicePrincipals(OpsAccountID)
115 if err != nil || len(rows) != len(before) {
116 t.Errorf("%s: ServicePrincipals = %d rows %v, want %d (nothing minted)", bad.why, len(rows), err, len(before))
117 }
118 }
119
120 status, _, body := postGated(t, fx, c, fx.u("/services/new"),
121 url.Values{"csrf": {csrf}, "name": {"ci"}, "role": {"deploy"}, "app": {"kass"}, "rungs": {"edge", "canary"}})
122 if status != http.StatusOK {
123 t.Fatalf("deploy mint = %d:\n%s", status, body)
124 }
125 if !strings.Contains(body, "rungs canary, edge") {
126 t.Fatalf("the secret banner does not name the rungs:\n%s", body)
127 }
128 secret := paneSecret(t, body)
129 p, ok := fx.st.CLIPrincipal(secret, time.Now())
130 if !ok || strings.Join(p.Rungs, ",") != "canary,edge" {
131 t.Fatalf("minted credential resolves to %+v, %v; want rungs canary,edge", p, ok)
132 }
133 // The listing shows the rungs too.
134 _, _, page := get(t, c, fx.u("/settings"))
135 if !strings.Contains(page, "canary, edge") {
136 t.Fatalf("the credential listing does not show the rungs:\n%s", page)
137 }
138}
139