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 "io"
7 "net/http"
8 "testing"
9 "time"
10
11 "github.com/carlosframework/platform/internal/appconfig"
12 "github.com/carlosframework/platform/internal/hostops"
13 "github.com/carlosframework/platform/internal/objstore"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17type callbackBrokenBody struct{}
18
19func (callbackBrokenBody) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF }
20func (callbackBrokenBody) Close() error { return nil }
21
22func TestClaimScanCallbackContentionAndSettlement(t *testing.T) {
23 for _, mode := range []string{"success", "transport failure", "accepted broken body"} {
24 t.Run(mode, func(t *testing.T) {
25 w := newScanClaimWorld(t)
26 rec, _, err := objstore.Get(context.Background(), w.cx.rel, "bab", "titogo")
27 if err != nil {
28 t.Fatal(err)
29 }
30 job := scanDeliveryJob{ref: objstore.AppRef{Account: "bab", App: "titogo"}, gen: rec.Scanning.WebhookGen}
31 calls := 0
32 p := &scanDeliveryPool{s: w.cx.srv, ctx: context.Background(), client: &http.Client{Transport: callbackRoundTripper(func(r *http.Request) (*http.Response, error) {
33 calls++
34 competingCalled := false
35 err := w.cx.srv.claimAppMutation(context.Background(), &Account{Sqid: "bab"}, "titogo", "competing-callback", func(context.Context) error { competingCalled = true; return nil })
36 if !errors.Is(err, hostops.ErrPending) || competingCalled {
37 t.Errorf("callback did not fence competing mutation: %v called=%v", err, competingCalled)
38 }
39 if mode == "transport failure" {
40 return nil, io.ErrUnexpectedEOF
41 }
42 res := callbackResponse(http.StatusOK)
43 if mode == "accepted broken body" {
44 res.Body = callbackBrokenBody{}
45 }
46 return res, nil
47 })}}
48 err = w.cx.srv.claimAppMutation(context.Background(), &Account{Sqid: "bab"}, "titogo", "busy-before-callback", func(context.Context) error {
49 retry, err := p.attempt(job, "https://titogo.bab.example/hook", []byte(`{}`))
50 if !retry || !errors.Is(err, hostops.ErrPending) || calls != 0 {
51 t.Errorf("busy callback retry=%v err=%v calls=%d", retry, err, calls)
52 }
53 return nil
54 })
55 if err != nil {
56 t.Fatal(err)
57 }
58 retry, err := p.attempt(job, "https://titogo.bab.example/hook", []byte(`{}`))
59 if mode == "transport failure" {
60 if !retry || err == nil {
61 t.Errorf("failed transport retry=%v err=%v", retry, err)
62 }
63 } else if retry || err != nil {
64 t.Errorf("accepted callback retry=%v err=%v", retry, err)
65 }
66 if calls != 1 {
67 t.Errorf("transport called %d times", calls)
68 }
69 requireOpen(t, w.admission(t), "after callback attempt")
70 })
71 }
72}
73
74func TestClaimScanMasterRotationSkipsBusyApp(t *testing.T) {
75 w := newScanClaimWorld(t)
76 w.cx.srv.cfg.CallbackSigner = callbackTestSigner(t, "review-new-master", "claim-fixture-webhook-master")
77 pub, err := appconfig.ParsePublicKey(w.cx.srv.cfg.SecretsPub, "test")
78 if err != nil {
79 t.Fatal(err)
80 }
81 err = w.cx.srv.claimAppMutation(context.Background(), &Account{Sqid: "bab"}, "titogo", "busy-before-rotation", func(context.Context) error {
82 remaining, changed, err := w.cx.srv.rotateWebhookBudget(context.Background(), pub)
83 if err != nil || remaining != 1 || changed != 0 {
84 t.Errorf("busy rotation remaining=%d changed=%d err=%v", remaining, changed, err)
85 }
86 return nil
87 })
88 if err != nil {
89 t.Fatal(err)
90 }
91 remaining, changed, err := w.cx.srv.rotateWebhookBudget(context.Background(), pub)
92 if err != nil || remaining != 0 || changed != 1 {
93 t.Fatalf("resumed rotation remaining=%d changed=%d err=%v", remaining, changed, err)
94 }
95 requireOpen(t, w.admission(t), "after rotation")
96}
97
98func TestScanMasterRotationLeavesTrashedConfigAlone(t *testing.T) {
99 fx, _, _ := rotationRaceFixture(t, "review-trash")
100 fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "review-new-master", testWebhookMaster)
101 _, before, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "titogo"))
102 if err != nil {
103 t.Fatal(err)
104 }
105 if err := release.TrashApp(context.Background(), fx.rel, fx.sqid, "titogo", "test", time.Now()); err != nil {
106 t.Fatal(err)
107 }
108 pub, err := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test")
109 if err != nil {
110 t.Fatal(err)
111 }
112 remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub)
113 if err != nil || remaining != 0 || changed != 0 {
114 t.Errorf("trashed rotation remaining=%d changed=%d err=%v", remaining, changed, err)
115 }
116 _, _, err = fx.srv.convergeCallbackKey(context.Background(), pub, &Account{Sqid: fx.sqid}, "titogo")
117 if !errors.Is(err, objstore.ErrQueueNotPermitted) {
118 t.Errorf("direct trashed convergence err=%v", err)
119 }
120 _, after, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "titogo"))
121 if err != nil {
122 t.Fatal(err)
123 }
124 if before != after {
125 t.Error("rotation rewrote a trashed app's config")
126 }
127}
128
129func TestScanRegrantInvalidatesRevokedCallbackSecret(t *testing.T) {
130 fx, token, private := rotationRaceFixture(t, "review-regrant")
131 old := callbackSecret(t, fx, private, "titogo")
132 status, body := postJSON(t, scanURL(fx, "titogo")+"/revoke", map[string]any{}, token)
133 if status != http.StatusOK {
134 t.Fatalf("revoke %d %v", status, body)
135 }
136 status, body = postJSON(t, scanURL(fx, "titogo")+"/grant", map[string]any{}, token)
137 if status != http.StatusOK {
138 t.Fatalf("regrant %d %v", status, body)
139 }
140 if got := callbackSecret(t, fx, private, "titogo"); got == old {
141 t.Fatal("regrant reactivated the revoked callback secret")
142 }
143}
144
145func TestScanMasterRotationRechecksTrashUnderPermit(t *testing.T) {
146 fx, _, _ := rotationRaceFixture(t, "review-trash-race")
147 fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "review-new-master", testWebhookMaster)
148 _, before, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "titogo"))
149 if err != nil {
150 t.Fatal(err)
151 }
152 runner := &racingRunner{Store: fx.rel, action: "scan-webhook-rotate", race: func(ctx context.Context) {
153 if err := release.TrashApp(ctx, fx.rel, fx.sqid, "titogo", "test", time.Now()); err != nil {
154 t.Fatal(err)
155 }
156 }}
157 fx.srv.cfg.Releases = runner
158 pub, err := appconfig.ParsePublicKey(fx.srv.cfg.SecretsPub, "test")
159 if err != nil {
160 t.Fatal(err)
161 }
162 remaining, changed, err := fx.srv.rotateWebhookBudget(context.Background(), pub)
163 if err != nil || remaining != 0 || changed != 0 || !runner.raced.Load() || runner.puts.Load() != 0 {
164 t.Errorf("trash race remaining=%d changed=%d raced=%v puts=%d err=%v", remaining, changed, runner.raced.Load(), runner.puts.Load(), err)
165 }
166 _, after, err := fx.rel.Get(context.Background(), appconfig.Key(fx.sqid, "titogo"))
167 if err != nil {
168 t.Fatal(err)
169 }
170 if before != after {
171 t.Error("rotation rewrote config after app was trashed before the permit")
172 }
173}
174