package release import ( "context" "errors" "testing" "time" ) func TestSerialBumpFromMissing(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() doc, etag, err := ReadSerial(ctx, s) if err != nil || doc.Serial != 0 || etag != "" { t.Fatalf("missing serial should read as zero: %+v %q %v", doc, etag, err) } if err := BumpSerial(ctx, s, time.Unix(1754640000, 0).UTC()); err != nil { t.Fatal(err) } doc, etag, err = ReadSerial(ctx, s) if err != nil || doc.Serial != 1 || doc.Schema != 1 || etag == "" { t.Fatalf("first bump: %+v %q %v", doc, etag, err) } if doc.BumpedAt == "" { t.Fatal("bumped_at not stamped") } } func TestSerialBumpIncrements(t *testing.T) { s := NewDirStore(t.TempDir()) ctx := context.Background() for i := 0; i < 3; i++ { if err := BumpSerial(ctx, s, time.Unix(1754640000, 0).UTC()); err != nil { t.Fatal(err) } } doc, _, _ := ReadSerial(ctx, s) if doc.Serial != 3 { t.Fatalf("serial = %d, want 3", doc.Serial) } } func TestSerialGarbageObjectStillBumps(t *testing.T) { // A bucket-writer can corrupt the object; a bump must recover, not wedge. s := NewDirStore(t.TempDir()) ctx := context.Background() if _, err := s.Put(ctx, SerialKey, []byte("not json"), ""); err != nil { t.Fatal(err) } if err := BumpSerial(ctx, s, time.Unix(1754640000, 0).UTC()); err != nil { t.Fatal(err) } doc, _, err := ReadSerial(ctx, s) if err != nil || doc.Serial != 1 { t.Fatalf("recovery bump: %+v %v", doc, err) } } // serialCancelStore cancels a context at a chosen point and is otherwise the // DirStore beneath it, whose Put ignores its context. type serialCancelStore struct { Store onGet, onPut func() puts int } func (s *serialCancelStore) Get(ctx context.Context, key string) ([]byte, string, error) { if s.onGet != nil { s.onGet() } return s.Store.Get(ctx, key) } func (s *serialCancelStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { s.puts++ if s.onPut != nil { s.onPut() return "", ErrCASConflict // a race lost, so BumpSerial retries } return s.Store.Put(ctx, key, body, ifMatch) } // A caller whose context ended while BumpSerial read the serial gets no // write, even from a store that ignores its context (db region forward, // Astra round 1, P2). func TestSerialBumpDoesNotWriteAfterItsContextEnds(t *testing.T) { dir := NewDirStore(t.TempDir()) ctx, cancel := context.WithCancel(context.Background()) s := &serialCancelStore{Store: dir, onGet: cancel} if err := BumpSerial(ctx, s, time.Now()); !errors.Is(err, context.Canceled) { t.Fatalf("err %v, want the context's", err) } if doc, etag, _ := ReadSerial(context.Background(), dir); doc.Serial != 0 || etag != "" || s.puts != 0 { t.Fatalf("the serial was written after its context ended: %+v, %d puts", doc, s.puts) } } // The same check holds before a retry's write. func TestSerialBumpRetryDoesNotWriteAfterItsContextEnds(t *testing.T) { dir := NewDirStore(t.TempDir()) ctx, cancel := context.WithCancel(context.Background()) s := &serialCancelStore{Store: dir} s.onPut = func() { cancel(); s.onPut = nil } if err := BumpSerial(ctx, s, time.Now()); !errors.Is(err, context.Canceled) { t.Fatalf("err %v, want the context's", err) } if doc, _, _ := ReadSerial(context.Background(), dir); doc.Serial != 0 || s.puts != 1 { t.Fatalf("a retry wrote the serial after its context ended: %+v, %d puts", doc, s.puts) } }