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 "errors"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/carlosframework/platform/internal/objstore"
11)
12
13func storeNotifyFixture(t *testing.T) *alertFixture {
14 t.Helper()
15 return newCapturedFixture(t)
16}
17
18func TestStoreApplicationNotifiesCanonicalConfiguredFleetOperators(t *testing.T) {
19 fx := storeNotifyFixture(t)
20 fx.srv.cfg.StoreOperators = []string{" OPS@Keymail.dev ", "ops@keymail.dev", "listed-only@keymail.dev"}
21 for _, address := range []string{"ops@keymail.dev", "fleet-only@keymail.dev"} {
22 if err := fx.st.SetOperator(address, true); err != nil {
23 t.Fatal(err)
24 }
25 if err := fx.st.SetGrantToken(address, "grant-"+address, time.Now()); err != nil {
26 t.Fatal(err)
27 }
28 }
29
30 fx.srv.notifyStoreEntitlement("bab", &objstore.Entitlement{Applied: &objstore.Applied{By: "owner@example.test"}})
31
32 if len(fx.sent) != 1 {
33 t.Fatalf("sent %d messages, want one canonical, deduplicated intersection: %+v", len(fx.sent), fx.sent)
34 }
35 if got := fx.sent[0]; got.to != "ops@keymail.dev" || got.token != "grant-ops@keymail.dev" {
36 t.Fatalf("sent to %q with %q, want the configured fleet operator's own token", got.to, got.token)
37 }
38}
39
40func TestStoreApplicationSkipsAnOperatorWithoutAGrant(t *testing.T) {
41 fx := storeNotifyFixture(t)
42 fx.srv.cfg.StoreOperators = []string{"ready@keymail.dev", "missing@keymail.dev"}
43 for _, address := range fx.srv.cfg.StoreOperators {
44 if err := fx.st.SetOperator(address, true); err != nil {
45 t.Fatal(err)
46 }
47 }
48 if err := fx.st.SetGrantToken("ready@keymail.dev", "ready-token", time.Now()); err != nil {
49 t.Fatal(err)
50 }
51
52 fx.srv.notifyStoreEntitlement("bab", &objstore.Entitlement{Applied: &objstore.Applied{By: "owner@example.test"}})
53 if len(fx.sent) != 1 || fx.sent[0].to != "ready@keymail.dev" {
54 t.Fatalf("sent %+v, want only the operator with a grant", fx.sent)
55 }
56}
57
58func TestStoreDecisionTellsApplicantWithoutLeakingOperatorFields(t *testing.T) {
59 fx := storeNotifyFixture(t)
60 if err := fx.st.SetGrantToken("owner@example.test", "owner-token", time.Now()); err != nil {
61 t.Fatal(err)
62 }
63 e := &objstore.Entitlement{
64 Applied: &objstore.Applied{By: "owner@example.test", Note: "secret applicant note"},
65 Decision: &objstore.Decision{Approved: false, By: "decider@example.test", Reason: "secret decline reason"},
66 }
67
68 fx.srv.notifyStoreEntitlement("bab", e)
69 if len(fx.sent) != 1 {
70 t.Fatalf("sent %d messages, want one: %+v", len(fx.sent), fx.sent)
71 }
72 got := fx.sent[0]
73 if got.to != "owner@example.test" || got.token != "owner-token" {
74 t.Fatalf("sent to %q with %q, want the applicant's own grant", got.to, got.token)
75 }
76 for _, secret := range []string{"secret applicant note", "secret decline reason", "decider@example.test", "owner-token"} {
77 if strings.Contains(got.subject+got.body, secret) {
78 t.Fatalf("tenant mail leaked %q: %q %q", secret, got.subject, got.body)
79 }
80 }
81 for _, want := range []string{"declined", "bab", fx.srv.cfg.Origin + "/a/bab/settings"} {
82 if !strings.Contains(got.subject+got.body, want) {
83 t.Fatalf("tenant mail is missing %q: %q %q", want, got.subject, got.body)
84 }
85 }
86}
87
88// An applicant with no Keymail grant gets the decision by email; one with a
89// grant still gets it through Keymail and not by email.
90func TestStoreDecisionNoticeByEmail(t *testing.T) {
91 fx := storeNotifyFixture(t)
92 m := &fakeConsoleMail{ready: true}
93 fx.srv.consoleMail = m
94 e := &objstore.Entitlement{
95 Applied: &objstore.Applied{By: "Owner@Example.test", Note: "secret applicant note"},
96 Decision: &objstore.Decision{Approved: true, By: "decider@example.test", Reason: "secret reason"},
97 }
98
99 fx.srv.notifyStoreEntitlement("bab", e)
100 if len(fx.sent) != 0 {
101 t.Fatalf("keymail sends without a grant: %+v", fx.sent)
102 }
103 sent := m.sentCopy()
104 if len(sent) != 1 {
105 t.Fatalf("console mailer got %d messages, want 1: %q", len(sent), sent)
106 }
107 to, body, _ := strings.Cut(sent[0], "|")
108 if to != "owner@example.test" {
109 t.Fatalf("notice went to %q", to)
110 }
111 for _, want := range []string{"approved", "bab", fx.srv.cfg.Origin + "/a/bab/settings"} {
112 if !strings.Contains(body, want) {
113 t.Fatalf("notice missing %q:\n%s", want, body)
114 }
115 }
116 for _, secret := range []string{"secret applicant note", "secret reason", "decider@example.test"} {
117 if strings.Contains(body, secret) {
118 t.Fatalf("notice leaked %q:\n%s", secret, body)
119 }
120 }
121
122 if err := fx.st.SetGrantToken("owner@example.test", "owner-token", time.Now()); err != nil {
123 t.Fatal(err)
124 }
125 fx.srv.notifyStoreEntitlement("bab", e)
126 if len(fx.sent) != 1 || fx.sent[0].token != "owner-token" {
127 t.Fatalf("grant holder not sent through keymail: %+v", fx.sent)
128 }
129 if got := m.sentCopy(); len(got) != 1 {
130 t.Fatalf("grant holder was also emailed: %q", got)
131 }
132}
133
134func TestStoreNotificationIsBestEffortAndNilSafe(t *testing.T) {
135 fx := storeNotifyFixture(t)
136 fx.srv.cfg.StoreOperators = []string{"ops@keymail.dev"}
137 if err := fx.st.SetOperator("ops@keymail.dev", true); err != nil {
138 t.Fatal(err)
139 }
140 if err := fx.st.SetGrantToken("ops@keymail.dev", "ops-token", time.Now()); err != nil {
141 t.Fatal(err)
142 }
143 fx.err = errors.New("send failed")
144
145 fx.srv.notifyStoreEntitlement("bab", &objstore.Entitlement{Applied: &objstore.Applied{By: "owner@example.test"}})
146 if len(fx.sent) != 1 {
147 t.Fatalf("send failure attempted %d messages, want one", len(fx.sent))
148 }
149 fx.srv.sendApp = nil
150 fx.srv.notifyStoreEntitlement("bab", &objstore.Entitlement{Applied: &objstore.Applied{By: "owner@example.test"}})
151}
152
153func TestStoreNotificationUsesABackgroundTimeout(t *testing.T) {
154 fx := storeNotifyFixture(t)
155 fx.srv.cfg.StoreOperators = []string{"ops@keymail.dev"}
156 if err := fx.st.SetOperator("ops@keymail.dev", true); err != nil {
157 t.Fatal(err)
158 }
159 if err := fx.st.SetGrantToken("ops@keymail.dev", "ops-token", time.Now()); err != nil {
160 t.Fatal(err)
161 }
162 fx.srv.sendApp = func(ctx context.Context, _, _, _, _ string) (string, error) {
163 deadline, ok := ctx.Deadline()
164 if !ok || time.Until(deadline) > 15*time.Second || time.Until(deadline) < 14*time.Second {
165 t.Errorf("send context deadline = %v, want about 15 seconds", deadline)
166 }
167 return "message", nil
168 }
169
170 fx.srv.notifyStoreEntitlement("bab", &objstore.Entitlement{Applied: &objstore.Applied{By: "owner@example.test"}})
171}
172
173func TestStoreWritesFinishWhileNotificationWaits(t *testing.T) {
174 fx := newConsoleFixture(t)
175 token := entitlementOperatorToken(t, fx, "paul@keymail.dev", "notify-async")
176 if err := fx.st.SetGrantToken("paul@keymail.dev", "notification-grant", time.Now()); err != nil {
177 t.Fatal(err)
178 }
179 entered := make(chan struct{}, 2)
180 releaseSend := make(chan struct{})
181 finished := make(chan struct{}, 2)
182 fx.srv.sendApp = func(ctx context.Context, _, _, _, _ string) (string, error) {
183 entered <- struct{}{}
184 select {
185 case <-releaseSend:
186 case <-ctx.Done():
187 }
188 finished <- struct{}{}
189 return "", errors.New("delivery failed")
190 }
191 defer close(releaseSend)
192 status, _ := postJSON(t, entitlementApplyURL(fx, fx.sqid), map[string]any{"note": "uploads"}, token)
193 if status != 200 || !storedEntitlement(t, fx, fx.sqid).Pending() {
194 t.Fatalf("apply=%d", status)
195 }
196 select {
197 case <-entered:
198 case <-time.After(time.Second):
199 t.Fatal("apply notification never started")
200 }
201 status, _ = postJSON(t, entitlementDecisionURL(fx, fx.sqid, "decline"), map[string]any{"reason": "private reason"}, token)
202 if status != 200 || storedEntitlement(t, fx, fx.sqid).Decision == nil {
203 t.Fatalf("decision=%d", status)
204 }
205 select {
206 case <-entered:
207 case <-time.After(time.Second):
208 t.Fatal("decision notification never started")
209 }
210 // Both writes returned while their sends were blocked. Release the callbacks
211 // before the fixture closes its database and listener.
212 releaseSend <- struct{}{}
213 releaseSend <- struct{}{}
214 <-finished
215 <-finished
216}
217