carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/doorkey"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// testClock is the mirror's injectable clock.
17type testClock struct {
18 mu sync.Mutex
19 t time.Time
20}
21
22func (c *testClock) now() time.Time {
23 c.mu.Lock()
24 defer c.mu.Unlock()
25 return c.t
26}
27
28func (c *testClock) advance(d time.Duration) {
29 c.mu.Lock()
30 c.t = c.t.Add(d)
31 c.mu.Unlock()
32}
33
34type doorKeyFixture struct {
35 pub ed25519.PublicKey
36 priv ed25519.PrivateKey
37 clk *testClock
38}
39
40func newDoorKeyFixture(t *testing.T) *doorKeyFixture {
41 t.Helper()
42 pub, priv, err := ed25519.GenerateKey(rand.Reader)
43 if err != nil {
44 t.Fatal(err)
45 }
46 return &doorKeyFixture{pub: pub, priv: priv, clk: &testClock{t: time.Now().UTC()}}
47}
48
49func (fx *doorKeyFixture) mirror(s release.Store) *doorKeyMirror {
50 d := newDoorKeyMirror(s, fx.pub)
51 d.now = fx.clk.now
52 return d
53}
54
55// signed is a record for account/app at revision rev, signed now on the
56// fixture's clock and good for the whole lifetime.
57func (fx *doorKeyFixture) signed(t *testing.T, priv ed25519.PrivateKey, account, app string, rev uint64) []byte {
58 t.Helper()
59 now := fx.clk.now()
60 return fx.signedAt(t, priv, account, app, rev, now, now.Add(doorkey.Lifetime))
61}
62
63func (fx *doorKeyFixture) signedAt(t *testing.T, priv ed25519.PrivateKey, account, app string, rev uint64, signedAt, expiresAt time.Time) []byte {
64 t.Helper()
65 _, entry, err := doorkey.NewToken(account, app)
66 if err != nil {
67 t.Fatal(err)
68 }
69 wire, err := doorkey.Sign(priv, doorkey.Record{V: 1, Account: account, App: app, Revision: rev,
70 Keys: []doorkey.Entry{entry}, SignedAt: signedAt, ExpiresAt: expiresAt})
71 if err != nil {
72 t.Fatal(err)
73 }
74 return wire
75}
76
77func refreshDoorKeys(t *testing.T, d *doorKeyMirror) {
78 t.Helper()
79 if err := d.Refresh(context.Background()); err != nil {
80 t.Fatalf("refresh: %v", err)
81 }
82}
83
84func wantDoorRecord(t *testing.T, d *doorKeyMirror, account, app string, want bool) doorkey.Record {
85 t.Helper()
86 rec, ok := d.Record(account, app)
87 if ok != want {
88 t.Fatalf("Record(%s, %s) ok = %v, want %v", account, app, ok, want)
89 }
90 return rec
91}
92
93// A record listed under its own path, validly signed and unexpired, is
94// admitted; one that leaves the list is dropped on the next refresh.
95func TestDoorKeyMirrorDropsARecordAbsentFromTheList(t *testing.T) {
96 fx := newDoorKeyFixture(t)
97 s := meshTestStore(t)
98 putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
99 putJSON(t, s, doorkey.RecordKey("bae", "classic"), string(fx.signed(t, fx.priv, "bae", "classic", 1)))
100 // Not a record path: ignored, not an error.
101 putJSON(t, s, doorkey.Prefix+"stray.json", "{}")
102 d := fx.mirror(s)
103 refreshDoorKeys(t, d)
104 if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 1 || rec.App != "titogo" {
105 t.Fatalf("record = %+v", rec)
106 }
107 wantDoorRecord(t, d, "bae", "classic", true)
108
109 if err := s.Delete(context.Background(), doorkey.RecordKey("bae", "titogo")); err != nil {
110 t.Fatal(err)
111 }
112 refreshDoorKeys(t, d)
113 wantDoorRecord(t, d, "bae", "titogo", false)
114 wantDoorRecord(t, d, "bae", "classic", true)
115}
116
117// A failed list keeps the last map, but only for ten minutes after the last
118// successful list: then the whole map is cleared.
119func TestDoorKeyMirrorClearsTenMinutesAfterTheLastSuccessfulList(t *testing.T) {
120 fx := newDoorKeyFixture(t)
121 s := &listFailingStore{Store: meshTestStore(t)}
122 putJSON(t, s.Store, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
123 d := fx.mirror(s)
124 refreshDoorKeys(t, d)
125
126 s.setFail(true)
127 fx.clk.advance(9 * time.Minute)
128 if err := d.Refresh(context.Background()); err == nil {
129 t.Fatal("a failed list must be reported")
130 }
131 wantDoorRecord(t, d, "bae", "titogo", true)
132
133 fx.clk.advance(time.Minute)
134 if err := d.Refresh(context.Background()); err == nil {
135 t.Fatal("a failed list must be reported")
136 }
137 wantDoorRecord(t, d, "bae", "titogo", false)
138
139 // The bucket comes back: the record is admitted again.
140 s.setFail(false)
141 refreshDoorKeys(t, d)
142 wantDoorRecord(t, d, "bae", "titogo", true)
143}
144
145// The bound holds at read time too: a mirror whose loop has stopped
146// refreshing does not keep admitting a record for longer than ten minutes.
147func TestDoorKeyMirrorRecordRefusesPastTheBoundWithoutARefresh(t *testing.T) {
148 fx := newDoorKeyFixture(t)
149 s := meshTestStore(t)
150 putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
151 d := fx.mirror(s)
152 refreshDoorKeys(t, d)
153 fx.clk.advance(10 * time.Minute)
154 wantDoorRecord(t, d, "bae", "titogo", false)
155}
156
157// One record's GET failing keeps that record under the same ten-minute bound,
158// measured from its own last successful read. Its neighbours are unaffected.
159func TestDoorKeyMirrorKeepsARecordThroughAGetFailureForTenMinutes(t *testing.T) {
160 fx := newDoorKeyFixture(t)
161 s := &oneKeyFailingStore{Store: meshTestStore(t)}
162 putJSON(t, s.Store, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
163 putJSON(t, s.Store, doorkey.RecordKey("bae", "classic"), string(fx.signed(t, fx.priv, "bae", "classic", 1)))
164 d := fx.mirror(s)
165 refreshDoorKeys(t, d)
166
167 s.failKey(doorkey.RecordKey("bae", "titogo"))
168 fx.clk.advance(5 * time.Minute)
169 if err := d.Refresh(context.Background()); err == nil {
170 t.Fatal("a failed GET must be reported")
171 }
172 wantDoorRecord(t, d, "bae", "titogo", true)
173 wantDoorRecord(t, d, "bae", "classic", true)
174
175 fx.clk.advance(5 * time.Minute)
176 if err := d.Refresh(context.Background()); err == nil {
177 t.Fatal("a failed GET must be reported")
178 }
179 wantDoorRecord(t, d, "bae", "titogo", false)
180 wantDoorRecord(t, d, "bae", "classic", true)
181}
182
183// A record that fails verification (signature, shape, expiry) or whose
184// payload names another account or app than its path is dropped at once,
185// not carried forward like an unreadable one.
186func TestDoorKeyMirrorDropsAnUnverifiableRecordAtOnce(t *testing.T) {
187 fx := newDoorKeyFixture(t)
188 _, otherPriv, _ := ed25519.GenerateKey(rand.Reader)
189 now := fx.clk.now()
190 for _, c := range []struct {
191 name string
192 wire func() []byte
193 }{
194 {"signed by another key", func() []byte { return fx.signed(t, otherPriv, "bae", "titogo", 2) }},
195 {"not a signed record", func() []byte { return []byte(`{"payload":"e30=","sig":"AAAA"}`) }},
196 {"expired", func() []byte {
197 return fx.signedAt(t, fx.priv, "bae", "titogo", 2, now.Add(-2*time.Hour), now.Add(-time.Hour))
198 }},
199 {"another app's record", func() []byte { return fx.signed(t, fx.priv, "bae", "other", 2) }},
200 {"another account's record", func() []byte { return fx.signed(t, fx.priv, "zed", "titogo", 2) }},
201 } {
202 t.Run(c.name, func(t *testing.T) {
203 captureLog(t)
204 s := meshTestStore(t)
205 key := doorkey.RecordKey("bae", "titogo")
206 putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
207 d := fx.mirror(s)
208 refreshDoorKeys(t, d)
209 wantDoorRecord(t, d, "bae", "titogo", true)
210 putJSON(t, s, key, string(c.wire()))
211 refreshDoorKeys(t, d)
212 wantDoorRecord(t, d, "bae", "titogo", false)
213 if c.name == "another app's record" {
214 wantDoorRecord(t, d, "bae", "other", false)
215 }
216 })
217 }
218}
219
220// Record refuses a record whose expiry has passed at read time, between
221// refreshes.
222func TestDoorKeyMirrorRecordRefusesAnExpiredRecordAtReadTime(t *testing.T) {
223 fx := newDoorKeyFixture(t)
224 s := meshTestStore(t)
225 now := fx.clk.now()
226 putJSON(t, s, doorkey.RecordKey("bae", "titogo"),
227 string(fx.signedAt(t, fx.priv, "bae", "titogo", 1, now, now.Add(time.Minute))))
228 d := fx.mirror(s)
229 refreshDoorKeys(t, d)
230 wantDoorRecord(t, d, "bae", "titogo", true)
231 fx.clk.advance(time.Minute)
232 wantDoorRecord(t, d, "bae", "titogo", false)
233}
234
235// A lower revision than this process last admitted is kept out: the newer
236// record stays while it is unexpired, the lower one is logged once, and it is
237// never admitted for the life of the process, even after the newer expires.
238func TestDoorKeyMirrorKeepsTheNewerRevisionAndLogsTheLowerOnce(t *testing.T) {
239 buf := captureLog(t)
240 fx := newDoorKeyFixture(t)
241 s := meshTestStore(t)
242 key := doorkey.RecordKey("bae", "titogo")
243 now := fx.clk.now()
244 putJSON(t, s, key, string(fx.signedAt(t, fx.priv, "bae", "titogo", 5, now, now.Add(time.Hour))))
245 d := fx.mirror(s)
246 refreshDoorKeys(t, d)
247
248 putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 4)))
249 for i := 0; i < 3; i++ {
250 refreshDoorKeys(t, d)
251 fx.clk.advance(30 * time.Second)
252 }
253 if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 5 {
254 t.Fatalf("revision %d admitted, want the newer 5 kept", rec.Revision)
255 }
256 if n := strings.Count(buf.String(), "revision 4"); n != 1 {
257 t.Fatalf("the lower revision was logged %d times, want once:\n%s", n, buf.String())
258 }
259
260 // The kept record expires; the lower one is still refused.
261 fx.clk.advance(time.Hour)
262 refreshDoorKeys(t, d)
263 wantDoorRecord(t, d, "bae", "titogo", false)
264
265 // An equal revision (a renewal) and a higher one are admitted.
266 putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 5)))
267 refreshDoorKeys(t, d)
268 wantDoorRecord(t, d, "bae", "titogo", true)
269 putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 6)))
270 refreshDoorKeys(t, d)
271 if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 6 {
272 t.Fatalf("revision %d, want 6", rec.Revision)
273 }
274}
275
276func TestDoorKeyMirrorNilIsInert(t *testing.T) {
277 var d *doorKeyMirror
278 if _, ok := d.Record("bae", "titogo"); ok {
279 t.Fatal("a nil mirror admitted a record")
280 }
281}
282
283// run polls on its own loop until stop closes.
284func TestDoorKeyMirrorRunPollsUntilStopped(t *testing.T) {
285 captureLog(t)
286 old := doorKeyInterval
287 doorKeyInterval = 10 * time.Millisecond
288 t.Cleanup(func() { doorKeyInterval = old })
289 fx := newDoorKeyFixture(t)
290 s := meshTestStore(t)
291 d := fx.mirror(s)
292 stop := make(chan struct{})
293 done := make(chan struct{})
294 go func() { d.run(stop); close(done) }()
295 putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1)))
296 deadline := time.Now().Add(5 * time.Second)
297 for {
298 if _, ok := d.Record("bae", "titogo"); ok {
299 break
300 }
301 if time.Now().After(deadline) {
302 t.Fatal("run never admitted the record")
303 }
304 time.Sleep(5 * time.Millisecond)
305 }
306 close(stop)
307 select {
308 case <-done:
309 case <-time.After(5 * time.Second):
310 t.Fatal("run did not return after stop")
311 }
312}
313