package edge import ( "context" "crypto/ed25519" "crypto/rand" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/doorkey" "github.com/carlosframework/platform/internal/release" ) // testClock is the mirror's injectable clock. type testClock struct { mu sync.Mutex t time.Time } func (c *testClock) now() time.Time { c.mu.Lock() defer c.mu.Unlock() return c.t } func (c *testClock) advance(d time.Duration) { c.mu.Lock() c.t = c.t.Add(d) c.mu.Unlock() } type doorKeyFixture struct { pub ed25519.PublicKey priv ed25519.PrivateKey clk *testClock } func newDoorKeyFixture(t *testing.T) *doorKeyFixture { t.Helper() pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } return &doorKeyFixture{pub: pub, priv: priv, clk: &testClock{t: time.Now().UTC()}} } func (fx *doorKeyFixture) mirror(s release.Store) *doorKeyMirror { d := newDoorKeyMirror(s, fx.pub) d.now = fx.clk.now return d } // signed is a record for account/app at revision rev, signed now on the // fixture's clock and good for the whole lifetime. func (fx *doorKeyFixture) signed(t *testing.T, priv ed25519.PrivateKey, account, app string, rev uint64) []byte { t.Helper() now := fx.clk.now() return fx.signedAt(t, priv, account, app, rev, now, now.Add(doorkey.Lifetime)) } func (fx *doorKeyFixture) signedAt(t *testing.T, priv ed25519.PrivateKey, account, app string, rev uint64, signedAt, expiresAt time.Time) []byte { t.Helper() _, entry, err := doorkey.NewToken(account, app) if err != nil { t.Fatal(err) } wire, err := doorkey.Sign(priv, doorkey.Record{V: 1, Account: account, App: app, Revision: rev, Keys: []doorkey.Entry{entry}, SignedAt: signedAt, ExpiresAt: expiresAt}) if err != nil { t.Fatal(err) } return wire } func refreshDoorKeys(t *testing.T, d *doorKeyMirror) { t.Helper() if err := d.Refresh(context.Background()); err != nil { t.Fatalf("refresh: %v", err) } } func wantDoorRecord(t *testing.T, d *doorKeyMirror, account, app string, want bool) doorkey.Record { t.Helper() rec, ok := d.Record(account, app) if ok != want { t.Fatalf("Record(%s, %s) ok = %v, want %v", account, app, ok, want) } return rec } // A record listed under its own path, validly signed and unexpired, is // admitted; one that leaves the list is dropped on the next refresh. func TestDoorKeyMirrorDropsARecordAbsentFromTheList(t *testing.T) { fx := newDoorKeyFixture(t) s := meshTestStore(t) putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1))) putJSON(t, s, doorkey.RecordKey("bae", "classic"), string(fx.signed(t, fx.priv, "bae", "classic", 1))) // Not a record path: ignored, not an error. putJSON(t, s, doorkey.Prefix+"stray.json", "{}") d := fx.mirror(s) refreshDoorKeys(t, d) if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 1 || rec.App != "titogo" { t.Fatalf("record = %+v", rec) } wantDoorRecord(t, d, "bae", "classic", true) if err := s.Delete(context.Background(), doorkey.RecordKey("bae", "titogo")); err != nil { t.Fatal(err) } refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", false) wantDoorRecord(t, d, "bae", "classic", true) } // A failed list keeps the last map, but only for ten minutes after the last // successful list: then the whole map is cleared. func TestDoorKeyMirrorClearsTenMinutesAfterTheLastSuccessfulList(t *testing.T) { fx := newDoorKeyFixture(t) s := &listFailingStore{Store: meshTestStore(t)} putJSON(t, s.Store, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1))) d := fx.mirror(s) refreshDoorKeys(t, d) s.setFail(true) fx.clk.advance(9 * time.Minute) if err := d.Refresh(context.Background()); err == nil { t.Fatal("a failed list must be reported") } wantDoorRecord(t, d, "bae", "titogo", true) fx.clk.advance(time.Minute) if err := d.Refresh(context.Background()); err == nil { t.Fatal("a failed list must be reported") } wantDoorRecord(t, d, "bae", "titogo", false) // The bucket comes back: the record is admitted again. s.setFail(false) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", true) } // The bound holds at read time too: a mirror whose loop has stopped // refreshing does not keep admitting a record for longer than ten minutes. func TestDoorKeyMirrorRecordRefusesPastTheBoundWithoutARefresh(t *testing.T) { fx := newDoorKeyFixture(t) s := meshTestStore(t) putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1))) d := fx.mirror(s) refreshDoorKeys(t, d) fx.clk.advance(10 * time.Minute) wantDoorRecord(t, d, "bae", "titogo", false) } // One record's GET failing keeps that record under the same ten-minute bound, // measured from its own last successful read. Its neighbours are unaffected. func TestDoorKeyMirrorKeepsARecordThroughAGetFailureForTenMinutes(t *testing.T) { fx := newDoorKeyFixture(t) s := &oneKeyFailingStore{Store: meshTestStore(t)} putJSON(t, s.Store, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1))) putJSON(t, s.Store, doorkey.RecordKey("bae", "classic"), string(fx.signed(t, fx.priv, "bae", "classic", 1))) d := fx.mirror(s) refreshDoorKeys(t, d) s.failKey(doorkey.RecordKey("bae", "titogo")) fx.clk.advance(5 * time.Minute) if err := d.Refresh(context.Background()); err == nil { t.Fatal("a failed GET must be reported") } wantDoorRecord(t, d, "bae", "titogo", true) wantDoorRecord(t, d, "bae", "classic", true) fx.clk.advance(5 * time.Minute) if err := d.Refresh(context.Background()); err == nil { t.Fatal("a failed GET must be reported") } wantDoorRecord(t, d, "bae", "titogo", false) wantDoorRecord(t, d, "bae", "classic", true) } // A record that fails verification (signature, shape, expiry) or whose // payload names another account or app than its path is dropped at once, // not carried forward like an unreadable one. func TestDoorKeyMirrorDropsAnUnverifiableRecordAtOnce(t *testing.T) { fx := newDoorKeyFixture(t) _, otherPriv, _ := ed25519.GenerateKey(rand.Reader) now := fx.clk.now() for _, c := range []struct { name string wire func() []byte }{ {"signed by another key", func() []byte { return fx.signed(t, otherPriv, "bae", "titogo", 2) }}, {"not a signed record", func() []byte { return []byte(`{"payload":"e30=","sig":"AAAA"}`) }}, {"expired", func() []byte { return fx.signedAt(t, fx.priv, "bae", "titogo", 2, now.Add(-2*time.Hour), now.Add(-time.Hour)) }}, {"another app's record", func() []byte { return fx.signed(t, fx.priv, "bae", "other", 2) }}, {"another account's record", func() []byte { return fx.signed(t, fx.priv, "zed", "titogo", 2) }}, } { t.Run(c.name, func(t *testing.T) { captureLog(t) s := meshTestStore(t) key := doorkey.RecordKey("bae", "titogo") putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 1))) d := fx.mirror(s) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", true) putJSON(t, s, key, string(c.wire())) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", false) if c.name == "another app's record" { wantDoorRecord(t, d, "bae", "other", false) } }) } } // Record refuses a record whose expiry has passed at read time, between // refreshes. func TestDoorKeyMirrorRecordRefusesAnExpiredRecordAtReadTime(t *testing.T) { fx := newDoorKeyFixture(t) s := meshTestStore(t) now := fx.clk.now() putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signedAt(t, fx.priv, "bae", "titogo", 1, now, now.Add(time.Minute)))) d := fx.mirror(s) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", true) fx.clk.advance(time.Minute) wantDoorRecord(t, d, "bae", "titogo", false) } // A lower revision than this process last admitted is kept out: the newer // record stays while it is unexpired, the lower one is logged once, and it is // never admitted for the life of the process, even after the newer expires. func TestDoorKeyMirrorKeepsTheNewerRevisionAndLogsTheLowerOnce(t *testing.T) { buf := captureLog(t) fx := newDoorKeyFixture(t) s := meshTestStore(t) key := doorkey.RecordKey("bae", "titogo") now := fx.clk.now() putJSON(t, s, key, string(fx.signedAt(t, fx.priv, "bae", "titogo", 5, now, now.Add(time.Hour)))) d := fx.mirror(s) refreshDoorKeys(t, d) putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 4))) for i := 0; i < 3; i++ { refreshDoorKeys(t, d) fx.clk.advance(30 * time.Second) } if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 5 { t.Fatalf("revision %d admitted, want the newer 5 kept", rec.Revision) } if n := strings.Count(buf.String(), "revision 4"); n != 1 { t.Fatalf("the lower revision was logged %d times, want once:\n%s", n, buf.String()) } // The kept record expires; the lower one is still refused. fx.clk.advance(time.Hour) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", false) // An equal revision (a renewal) and a higher one are admitted. putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 5))) refreshDoorKeys(t, d) wantDoorRecord(t, d, "bae", "titogo", true) putJSON(t, s, key, string(fx.signed(t, fx.priv, "bae", "titogo", 6))) refreshDoorKeys(t, d) if rec := wantDoorRecord(t, d, "bae", "titogo", true); rec.Revision != 6 { t.Fatalf("revision %d, want 6", rec.Revision) } } func TestDoorKeyMirrorNilIsInert(t *testing.T) { var d *doorKeyMirror if _, ok := d.Record("bae", "titogo"); ok { t.Fatal("a nil mirror admitted a record") } } // run polls on its own loop until stop closes. func TestDoorKeyMirrorRunPollsUntilStopped(t *testing.T) { captureLog(t) old := doorKeyInterval doorKeyInterval = 10 * time.Millisecond t.Cleanup(func() { doorKeyInterval = old }) fx := newDoorKeyFixture(t) s := meshTestStore(t) d := fx.mirror(s) stop := make(chan struct{}) done := make(chan struct{}) go func() { d.run(stop); close(done) }() putJSON(t, s, doorkey.RecordKey("bae", "titogo"), string(fx.signed(t, fx.priv, "bae", "titogo", 1))) deadline := time.Now().Add(5 * time.Second) for { if _, ok := d.Record("bae", "titogo"); ok { break } if time.Now().After(deadline) { t.Fatal("run never admitted the record") } time.Sleep(5 * time.Millisecond) } close(stop) select { case <-done: case <-time.After(5 * time.Second): t.Fatal("run did not return after stop") } }