package moves import ( "errors" "strings" "testing" "time" "github.com/superfly/ltx" ) func sealForTest() Seal { at := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC) return Seal{V: 1, N: 7, Replica: []SealedObject{ {Key: "moves/h/mv/replica/0001/0000000000000001-0000000000000007.ltx", VersionID: "v2", Size: ltx.HeaderSize + 10}, {Key: "moves/h/mv/replica/0000/0000000000000007-0000000000000007.ltx", VersionID: "v1", Size: ltx.HeaderSize}, }, Timers: []SealedObject{{Key: "moves/h/mv/timers/timers.json", VersionID: "v3", Size: 40}}, Files: []SealedObject{{Key: "moves/h/mv/files/files-ab.sealed", VersionID: "v4", Size: 100}, {Key: "moves/h/mv/files/files-ab.json", VersionID: "v5", Size: 50}}, FromDB: true, SealedAt: at, LeaseUntil: at.Add(16 * time.Minute)} } // The seal round-trips through the claim journal's string, in one canonical // form: objects sorted by key, so two encodings of one seal are equal. func TestSealEncodeDecodeRoundTrip(t *testing.T) { s := sealForTest() enc := s.Encode() got, err := DecodeSeal(enc) if err != nil { t.Fatal(err) } if got.N != 7 || len(got.Replica) != 2 || len(got.Files) != 2 || len(got.Timers) != 1 || !got.FromDB || got.SrcCovered { t.Fatalf("decoded %+v", got) } if got.Replica[0].Key > got.Replica[1].Key { t.Fatalf("replica not sorted: %+v", got.Replica) } if again := got.Encode(); again != enc { t.Fatalf("not canonical:\n%s\n%s", enc, again) } if !got.Bound().Equal(s.LeaseUntil) { t.Fatalf("Bound %v, want LeaseUntil %v", got.Bound(), s.LeaseUntil) } if !got.HasFiles() { t.Fatal("HasFiles false on a seal that names the archive") } got.Files = nil if got.HasFiles() { t.Fatal("HasFiles true with no files") } } // A seal that pins nothing is refused, on encode-side validation and on // decode: a missing or "null" version, an empty key, a key twice, a replica // object below an LTX header, no position, no bound, another version. func TestSealRefusesWhatPinsNothing(t *testing.T) { for name, edit := range map[string]func(*Seal){ "version 2": func(s *Seal) { s.V = 2 }, "no position": func(s *Seal) { s.N = 0 }, "no replica": func(s *Seal) { s.Replica = nil }, "null version": func(s *Seal) { s.Replica[0].VersionID = "null" }, "empty version": func(s *Seal) { s.Timers[0].VersionID = "" }, "empty key": func(s *Seal) { s.Files[0].Key = "" }, "duplicate key": func(s *Seal) { s.Files[1].Key = s.Files[0].Key }, "short replica": func(s *Seal) { s.Replica[1].Size = ltx.HeaderSize - 1 }, "negative size": func(s *Seal) { s.Files[0].Size = -1 }, "no bound": func(s *Seal) { s.LeaseUntil = time.Time{} }, "not sealed": func(s *Seal) { s.SealedAt = time.Time{} }, "replica not an ltx": func(s *Seal) { s.Replica[0].Key = "moves/h/mv/replica/lock.json" }, "bound before sealed": func(s *Seal) { s.LeaseUntil = s.SealedAt.Add(-time.Second) }, } { t.Run(name, func(t *testing.T) { s := sealForTest() edit(&s) if err := s.Validate(); !errors.Is(err, ErrBadSeal) { t.Fatalf("Validate = %v, want ErrBadSeal", err) } if _, err := DecodeSeal(s.Encode()); !errors.Is(err, ErrBadSeal) { t.Fatalf("DecodeSeal = %v, want ErrBadSeal", err) } }) } for name, raw := range map[string]string{ "empty": "", "not json": "{", "unknown field": strings.Replace(sealForTest().Encode(), `"n":7`, `"n":7,"extra":1`, 1), "trailing": sealForTest().Encode() + "{}", } { t.Run(name, func(t *testing.T) { if _, err := DecodeSeal(raw); !errors.Is(err, ErrBadSeal) { t.Fatalf("DecodeSeal(%q) = %v, want ErrBadSeal", raw, err) } }) } } // The replica reaches N: some sealed level file ends at N, and none ends // after it. func TestSealReachesN(t *testing.T) { s := sealForTest() if !s.ReachesN() { t.Fatal("a seal whose newest file ends at N does not reach N") } s.N = 8 if s.ReachesN() { t.Fatal("a seal ending at 7 reaches N = 8") } s.N = 6 if s.ReachesN() { t.Fatal("a seal holding a file past N reaches N = 6") } }