@@ -22,8 +22,10 @@ import (
22
22
"net/http"
23
23
"net/http/httptest"
24
24
"testing"
25
+ "time"
25
26
26
27
"gotest.tools/v3/assert"
28
+ cmpopt "gotest.tools/v3/assert/opt"
27
29
corev1 "k8s.io/api/core/v1"
28
30
corev1apply "k8s.io/client-go/applyconfigurations/core/v1"
29
31
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -127,8 +129,9 @@ func TestInstallationReconcile(t *testing.T) {
127
129
}
128
130
129
131
ctx := context .Background ()
130
- err := reconciler .reconcile (ctx , secret )
132
+ next , err := reconciler .reconcile (ctx , secret )
131
133
assert .NilError (t , err )
134
+ assert .Assert (t , next == 0 )
132
135
133
136
// It calls the API.
134
137
assert .Equal (t , len (requests ), 1 )
@@ -178,7 +181,7 @@ func TestInstallationReconcile(t *testing.T) {
178
181
}
179
182
180
183
ctx := context .Background ()
181
- err := reconciler .reconcile (ctx , secret )
184
+ _ , err := reconciler .reconcile (ctx , secret )
182
185
assert .Equal (t , err , expected , "expected a Kubernetes error" )
183
186
184
187
// It stores the API result in memory.
@@ -227,8 +230,9 @@ func TestInstallationReconcile(t *testing.T) {
227
230
}
228
231
229
232
ctx := context .Background ()
230
- err := reconciler .reconcile (ctx , secret )
233
+ next , err := reconciler .reconcile (ctx , secret )
231
234
assert .NilError (t , err )
235
+ assert .Assert (t , next == 0 )
232
236
233
237
assert .Equal (t , self .ID , "asdf" , "expected no change to memory" )
234
238
@@ -254,15 +258,15 @@ func TestInstallationReconcile(t *testing.T) {
254
258
}
255
259
256
260
ctx := context .Background ()
257
- err := reconciler .reconcile (ctx , secret )
261
+ _ , err := reconciler .reconcile (ctx , secret )
258
262
assert .Equal (t , err , expected , "expected a Kubernetes error" )
259
263
assert .Equal (t , self .ID , "asdf" , "expected no change to memory" )
260
264
})
261
265
})
262
266
263
267
// Scenario:
264
268
// When there is a Secret but no Installation in memory,
265
- // Then Reconcile should store it in memory.
269
+ // Then Reconcile should verify it in the API and store it in memory.
266
270
//
267
271
t .Run ("Restart" , func (t * testing.T ) {
268
272
var reconciler * InstallationReconciler
@@ -271,18 +275,228 @@ func TestInstallationReconcile(t *testing.T) {
271
275
beforeEach := func () {
272
276
reconciler = new (InstallationReconciler )
273
277
secret = new (corev1.Secret )
274
- secret .Data = map [string ][]byte {KeyBridgeToken : []byte (`{"id":"xyz"}` )}
278
+ secret .Data = map [string ][]byte {
279
+ KeyBridgeToken : []byte (`{
280
+ "id":"xyz", "auth_object":{
281
+ "secret":"abc",
282
+ "expires_at":"2020-10-28T05:06:07Z"
283
+ }
284
+ }` ),
285
+ }
275
286
self .Installation = Installation {}
276
287
}
277
288
278
- t .Run ("ItLoads " , func (t * testing.T ) {
289
+ t .Run ("ItVerifies " , func (t * testing.T ) {
279
290
beforeEach ()
280
291
292
+ // API double; spy on requests.
293
+ var requests []http.Request
294
+ {
295
+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
296
+ requests = append (requests , * r )
297
+ _ = json .NewEncoder (w ).Encode (map [string ]any {"secret" : "def" })
298
+ }))
299
+ t .Cleanup (server .Close )
300
+
301
+ reconciler .NewClient = func () * Client {
302
+ c := NewClient (server .URL , "" )
303
+ c .Backoff .Steps = 1
304
+ assert .Equal (t , c .BaseURL .String (), server .URL )
305
+ return c
306
+ }
307
+ }
308
+
309
+ // Kubernetes double; spy on SSA patches.
310
+ var applies []string
311
+ {
312
+ reconciler .Writer = runtime .ClientPatch (func (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
313
+ assert .Equal (t , string (patch .Type ()), "application/apply-patch+yaml" )
314
+
315
+ data , err := patch .Data (obj )
316
+ applies = append (applies , string (data ))
317
+ return err
318
+ })
319
+ }
320
+
281
321
ctx := context .Background ()
282
- err := reconciler .reconcile (ctx , secret )
322
+ next , err := reconciler .reconcile (ctx , secret )
283
323
assert .NilError (t , err )
324
+ assert .Assert (t , next == 0 )
284
325
326
+ assert .Equal (t , len (requests ), 1 )
327
+ assert .Equal (t , requests [0 ].Header .Get ("Authorization" ), "Bearer abc" )
328
+ assert .Equal (t , requests [0 ].Method , "POST" )
329
+ assert .Equal (t , requests [0 ].URL .Path , "/vendor/operator/auth-objects" )
330
+
331
+ // It stores the result in memory.
285
332
assert .Equal (t , self .ID , "xyz" )
333
+ assert .Equal (t , self .AuthObject .Secret , "def" )
334
+
335
+ // It stores the memory in Kubernetes.
336
+ assert .Equal (t , len (applies ), 1 )
337
+ assert .Assert (t , cmp .Contains (applies [0 ], `"kind":"Secret"` ))
338
+
339
+ var decoded corev1.Secret
340
+ assert .NilError (t , yaml .Unmarshal ([]byte (applies [0 ]), & decoded ))
341
+ assert .Assert (t , cmp .Contains (string (decoded .Data ["bridge-token" ]), `"id":"xyz"` ))
342
+ assert .Assert (t , cmp .Contains (string (decoded .Data ["bridge-token" ]), `"secret":"def"` ))
343
+ })
344
+
345
+ t .Run ("Expired" , func (t * testing.T ) {
346
+ beforeEach ()
347
+
348
+ // API double; authentication error.
349
+ {
350
+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
351
+ w .WriteHeader (http .StatusUnauthorized )
352
+ }))
353
+ t .Cleanup (server .Close )
354
+
355
+ reconciler .NewClient = func () * Client {
356
+ c := NewClient (server .URL , "" )
357
+ c .Backoff .Steps = 1
358
+ assert .Equal (t , c .BaseURL .String (), server .URL )
359
+ return c
360
+ }
361
+ }
362
+
363
+ // Kubernetes double; spy on SSA patches.
364
+ var applies []string
365
+ {
366
+ reconciler .Writer = runtime .ClientPatch (func (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
367
+ assert .Equal (t , string (patch .Type ()), "application/apply-patch+yaml" )
368
+
369
+ data , err := patch .Data (obj )
370
+ applies = append (applies , string (data ))
371
+ return err
372
+ })
373
+ }
374
+
375
+ ctx := context .Background ()
376
+ next , err := reconciler .reconcile (ctx , secret )
377
+ assert .NilError (t , err )
378
+ assert .Assert (t , next == 0 )
379
+
380
+ assert .DeepEqual (t , self .Installation , Installation {})
381
+
382
+ // It archives the expired one.
383
+ assert .Equal (t , len (applies ), 1 )
384
+ assert .Assert (t , cmp .Contains (applies [0 ], `"kind":"Secret"` ))
385
+
386
+ var decoded corev1.Secret
387
+ assert .NilError (t , yaml .Unmarshal ([]byte (applies [0 ]), & decoded ))
388
+ assert .Equal (t , len (decoded .Data ["bridge-token" ]), 0 )
389
+
390
+ archived := string (decoded .Data ["bridge-token--2020-10-28" ])
391
+ assert .Assert (t , cmp .Contains (archived , `"id":"xyz"` ))
392
+ assert .Assert (t , cmp .Contains (archived , `"secret":"abc"` ))
393
+ })
394
+ })
395
+
396
+ // Scenario:
397
+ // When there is an Installation in the Secret and in memory,
398
+ // Then Reconcile should refresh it periodically.
399
+ //
400
+ t .Run ("Refresh" , func (t * testing.T ) {
401
+ var reconciler * InstallationReconciler
402
+ var secret * corev1.Secret
403
+
404
+ beforeEach := func (timestamp []byte ) {
405
+ reconciler = new (InstallationReconciler )
406
+ reconciler .Refresh = time .Minute
407
+
408
+ secret = new (corev1.Secret )
409
+ secret .Data = map [string ][]byte {
410
+ KeyBridgeToken : []byte (`{"id":"ddd", "auth_object":{"secret":"eee"}}` ),
411
+ KeyBridgeLocalTime : timestamp ,
412
+ }
413
+
414
+ self .Installation = Installation {ID : "ddd" }
415
+ }
416
+
417
+ for _ , tt := range []struct {
418
+ Name string
419
+ Timestamp []byte
420
+ }{
421
+ {Name : "NoTimestamp" , Timestamp : nil },
422
+ {Name : "BadTimestamp" , Timestamp : []byte (`asdf` )},
423
+ {Name : "OldTimestamp" , Timestamp : []byte (`"2020-10-10T20:20:20Z"` )},
424
+ {Name : "FutureTimestamp" , Timestamp : []byte (`"2030-10-10T20:20:20Z"` )},
425
+ } {
426
+ t .Run (tt .Name , func (t * testing.T ) {
427
+ beforeEach (tt .Timestamp )
428
+
429
+ // API double; spy on requests.
430
+ var requests []http.Request
431
+ {
432
+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
433
+ requests = append (requests , * r )
434
+ _ = json .NewEncoder (w ).Encode (map [string ]any {"secret" : "fresh" })
435
+ }))
436
+ t .Cleanup (server .Close )
437
+
438
+ reconciler .NewClient = func () * Client {
439
+ c := NewClient (server .URL , "" )
440
+ c .Backoff .Steps = 1
441
+ assert .Equal (t , c .BaseURL .String (), server .URL )
442
+ return c
443
+ }
444
+ }
445
+
446
+ // Kubernetes double; spy on SSA patches.
447
+ var applies []string
448
+ {
449
+ reconciler .Writer = runtime .ClientPatch (func (ctx context.Context , obj client.Object , patch client.Patch , opts ... client.PatchOption ) error {
450
+ assert .Equal (t , string (patch .Type ()), "application/apply-patch+yaml" )
451
+
452
+ data , err := patch .Data (obj )
453
+ applies = append (applies , string (data ))
454
+ return err
455
+ })
456
+ }
457
+
458
+ ctx := context .Background ()
459
+ next , err := reconciler .reconcile (ctx , secret )
460
+ assert .NilError (t , err )
461
+ assert .Assert (t , next == 0 )
462
+
463
+ assert .Equal (t , len (requests ), 1 )
464
+ assert .Equal (t , requests [0 ].Header .Get ("Authorization" ), "Bearer eee" )
465
+ assert .Equal (t , requests [0 ].Method , "POST" )
466
+ assert .Equal (t , requests [0 ].URL .Path , "/vendor/operator/auth-objects" )
467
+
468
+ // It stores the result in memory.
469
+ assert .Equal (t , self .ID , "ddd" )
470
+ assert .Equal (t , self .AuthObject .Secret , "fresh" )
471
+
472
+ // It stores the memory in Kubernetes.
473
+ assert .Equal (t , len (applies ), 1 )
474
+ assert .Assert (t , cmp .Contains (applies [0 ], `"kind":"Secret"` ))
475
+
476
+ var decoded corev1.Secret
477
+ assert .NilError (t , yaml .Unmarshal ([]byte (applies [0 ]), & decoded ))
478
+ assert .Assert (t , cmp .Contains (string (decoded .Data ["bridge-token" ]), `"id":"ddd"` ))
479
+ assert .Assert (t , cmp .Contains (string (decoded .Data ["bridge-token" ]), `"secret":"fresh"` ))
480
+ })
481
+ }
482
+
483
+ t .Run ("CurrentTimestamp" , func (t * testing.T ) {
484
+ current := time .Now ().Add (- 15 * time .Minute )
485
+ currentJSON , _ := current .UTC ().MarshalJSON ()
486
+
487
+ beforeEach (currentJSON )
488
+ reconciler .Refresh = time .Hour
489
+
490
+ // Any API calls would panic because no spies are configured here.
491
+
492
+ ctx := context .Background ()
493
+ next , err := reconciler .reconcile (ctx , secret )
494
+ assert .NilError (t , err )
495
+
496
+ // The next reconcile is scheduled around (60 - 15 =) 45 minutes
497
+ // from now, plus or minus (60 * 10% =) 6 minutes of jitter.
498
+ assert .DeepEqual (t , next , 45 * time .Minute ,
499
+ cmpopt .DurationWithThreshold (6 * time .Minute ))
286
500
})
287
501
})
288
502
}
0 commit comments