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 "encoding/json"
6 "io"
7 "net/http"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14func retentionFixture(t *testing.T, code string) (*consoleFixture, string) {
15 t.Helper()
16 fx := newConsoleFixture(t)
17 now := time.Now()
18 if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", now); err != nil {
19 t.Fatal(err)
20 }
21 // A MEMBER: retention is app config, like env set (spec §7 ruling).
22 if err := fx.st.AddMember(OpsAccountID, "alice@keymail.dev", RoleMember, now); err != nil {
23 t.Fatal(err)
24 }
25 return fx, cliToken(t, fx, "alice@keymail.dev", code)
26}
27
28func getRetention(t *testing.T, u, bearer string) (int, []byte) {
29 t.Helper()
30 req, err := http.NewRequest(http.MethodGet, u, nil)
31 if err != nil {
32 t.Fatal(err)
33 }
34 req.Header.Set("Authorization", "Bearer "+bearer)
35 res, err := http.DefaultClient.Do(req)
36 if err != nil {
37 t.Fatal(err)
38 }
39 defer res.Body.Close()
40 body, _ := io.ReadAll(res.Body)
41 return res.StatusCode, body
42}
43
44func TestAPIRetentionSetWritesAuditsAndAnnounces(t *testing.T) {
45 fx, token := retentionFixture(t, "code-ret-set")
46 status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token,
47 `{"keep": 50}`)
48 if status != http.StatusOK {
49 t.Fatalf("set = %d %s, want 200", status, out)
50 }
51 var res struct {
52 Account string `json:"account"`
53 App string `json:"app"`
54 Keep int `json:"keep"`
55 }
56 if err := json.Unmarshal(out, &res); err != nil {
57 t.Fatal(err)
58 }
59 if res.Account != "bab" || res.App != "titogo" || res.Keep != 50 {
60 t.Fatalf("set body = %+v", res)
61 }
62 ret, err := release.GetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo")
63 if err != nil || ret == nil || ret.Keep != 50 {
64 t.Fatalf("record after set = %+v, %v; want keep 50", ret, err)
65 }
66 if ret.SetBy != "alice@keymail.dev" {
67 t.Fatalf("SetBy = %q, want the caller's address", ret.SetBy)
68 }
69 if got := serial(t, fx); got != 1 {
70 t.Fatalf("serial = %d, want 1", got)
71 }
72}
73
74func TestAPIRetentionFloorIs400(t *testing.T) {
75 fx, token := retentionFixture(t, "code-ret-floor")
76 status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token,
77 `{"keep": 5}`)
78 if status != http.StatusBadRequest {
79 t.Fatalf("keep 5 = %d %s, want 400", status, out)
80 }
81 if ret, _ := release.GetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo"); ret != nil {
82 t.Fatal("a refused set still wrote a record")
83 }
84}
85
86func TestAPIRetentionOffClearsTheRecord(t *testing.T) {
87 fx, token := retentionFixture(t, "code-ret-off")
88 if err := release.SetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo", 50, "seed", time.Now()); err != nil {
89 t.Fatal(err)
90 }
91 status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token, `{"off": true}`)
92 if status != http.StatusOK {
93 t.Fatalf("off = %d %s, want 200", status, out)
94 }
95 if ret, _ := release.GetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo"); ret != nil {
96 t.Fatalf("record after off = %+v, want nil", ret)
97 }
98}
99
100func TestAPIRetentionDryRunWritesNothing(t *testing.T) {
101 fx, token := retentionFixture(t, "code-ret-dry")
102 status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token,
103 `{"keep": 20, "dry_run": true}`)
104 if status != http.StatusOK {
105 t.Fatalf("dry-run = %d %s, want 200", status, out)
106 }
107 var res struct {
108 DryRun bool `json:"dry_run"`
109 EligibleVersions []string `json:"eligible_versions"`
110 }
111 if err := json.Unmarshal(out, &res); err != nil {
112 t.Fatal(err)
113 }
114 if !res.DryRun {
115 t.Fatalf("dry-run body = %s, want dry_run true", out)
116 }
117 if ret, _ := release.GetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo"); ret != nil {
118 t.Fatal("dry-run wrote a record")
119 }
120 if got := serial(t, fx); got != 0 {
121 t.Fatalf("serial after dry-run = %d, want 0 (nothing to announce)", got)
122 }
123}
124
125func TestAPIRetentionGetShowsPolicyAndOffState(t *testing.T) {
126 fx, token := retentionFixture(t, "code-ret-get")
127 status, out := getRetention(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token)
128 if status != http.StatusOK {
129 t.Fatalf("get (off) = %d %s, want 200", status, out)
130 }
131 var res struct {
132 Keep int `json:"keep"`
133 }
134 if err := json.Unmarshal(out, &res); err != nil {
135 t.Fatal(err)
136 }
137 if res.Keep != 0 {
138 t.Fatalf("keep with no policy = %d, want 0", res.Keep)
139 }
140 if err := release.SetRetention(context.Background(), fx.rel, release.OpsAccount, "titogo", 25, "seed", time.Now()); err != nil {
141 t.Fatal(err)
142 }
143 status, out = getRetention(t, fx.ts.URL+"/api/cli/apps/bab/titogo/retention", token)
144 if status != http.StatusOK {
145 t.Fatalf("get = %d %s, want 200", status, out)
146 }
147 if err := json.Unmarshal(out, &res); err != nil {
148 t.Fatal(err)
149 }
150 if res.Keep != 25 {
151 t.Fatalf("keep = %d, want 25", res.Keep)
152 }
153}
154
155func TestAPIRetentionUnknownAppIsPlain404(t *testing.T) {
156 fx, token := retentionFixture(t, "code-ret-404")
157 status, out := postBody(t, fx.ts.URL+"/api/cli/apps/bab/nothere/retention", token, `{"keep": 50}`)
158 if status != http.StatusNotFound {
159 t.Fatalf("unknown app = %d %s, want the plain 404", status, out)
160 }
161}
162