carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package moves
2
3import (
4 "errors"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/superfly/ltx"
10)
11
12func sealForTest() Seal {
13 at := time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)
14 return Seal{V: 1, N: 7,
15 Replica: []SealedObject{
16 {Key: "moves/h/mv/replica/0001/0000000000000001-0000000000000007.ltx", VersionID: "v2", Size: ltx.HeaderSize + 10},
17 {Key: "moves/h/mv/replica/0000/0000000000000007-0000000000000007.ltx", VersionID: "v1", Size: ltx.HeaderSize},
18 },
19 Timers: []SealedObject{{Key: "moves/h/mv/timers/timers.json", VersionID: "v3", Size: 40}},
20 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}},
21 FromDB: true,
22 SealedAt: at, LeaseUntil: at.Add(16 * time.Minute)}
23}
24
25// The seal round-trips through the claim journal's string, in one canonical
26// form: objects sorted by key, so two encodings of one seal are equal.
27func TestSealEncodeDecodeRoundTrip(t *testing.T) {
28 s := sealForTest()
29 enc := s.Encode()
30 got, err := DecodeSeal(enc)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if got.N != 7 || len(got.Replica) != 2 || len(got.Files) != 2 || len(got.Timers) != 1 || !got.FromDB || got.SrcCovered {
35 t.Fatalf("decoded %+v", got)
36 }
37 if got.Replica[0].Key > got.Replica[1].Key {
38 t.Fatalf("replica not sorted: %+v", got.Replica)
39 }
40 if again := got.Encode(); again != enc {
41 t.Fatalf("not canonical:\n%s\n%s", enc, again)
42 }
43 if !got.Bound().Equal(s.LeaseUntil) {
44 t.Fatalf("Bound %v, want LeaseUntil %v", got.Bound(), s.LeaseUntil)
45 }
46 if !got.HasFiles() {
47 t.Fatal("HasFiles false on a seal that names the archive")
48 }
49 got.Files = nil
50 if got.HasFiles() {
51 t.Fatal("HasFiles true with no files")
52 }
53}
54
55// A seal that pins nothing is refused, on encode-side validation and on
56// decode: a missing or "null" version, an empty key, a key twice, a replica
57// object below an LTX header, no position, no bound, another version.
58func TestSealRefusesWhatPinsNothing(t *testing.T) {
59 for name, edit := range map[string]func(*Seal){
60 "version 2": func(s *Seal) { s.V = 2 },
61 "no position": func(s *Seal) { s.N = 0 },
62 "no replica": func(s *Seal) { s.Replica = nil },
63 "null version": func(s *Seal) { s.Replica[0].VersionID = "null" },
64 "empty version": func(s *Seal) { s.Timers[0].VersionID = "" },
65 "empty key": func(s *Seal) { s.Files[0].Key = "" },
66 "duplicate key": func(s *Seal) { s.Files[1].Key = s.Files[0].Key },
67 "short replica": func(s *Seal) { s.Replica[1].Size = ltx.HeaderSize - 1 },
68 "negative size": func(s *Seal) { s.Files[0].Size = -1 },
69 "no bound": func(s *Seal) { s.LeaseUntil = time.Time{} },
70 "not sealed": func(s *Seal) { s.SealedAt = time.Time{} },
71 "replica not an ltx": func(s *Seal) { s.Replica[0].Key = "moves/h/mv/replica/lock.json" },
72 "bound before sealed": func(s *Seal) { s.LeaseUntil = s.SealedAt.Add(-time.Second) },
73 } {
74 t.Run(name, func(t *testing.T) {
75 s := sealForTest()
76 edit(&s)
77 if err := s.Validate(); !errors.Is(err, ErrBadSeal) {
78 t.Fatalf("Validate = %v, want ErrBadSeal", err)
79 }
80 if _, err := DecodeSeal(s.Encode()); !errors.Is(err, ErrBadSeal) {
81 t.Fatalf("DecodeSeal = %v, want ErrBadSeal", err)
82 }
83 })
84 }
85 for name, raw := range map[string]string{
86 "empty": "",
87 "not json": "{",
88 "unknown field": strings.Replace(sealForTest().Encode(), `"n":7`, `"n":7,"extra":1`, 1),
89 "trailing": sealForTest().Encode() + "{}",
90 } {
91 t.Run(name, func(t *testing.T) {
92 if _, err := DecodeSeal(raw); !errors.Is(err, ErrBadSeal) {
93 t.Fatalf("DecodeSeal(%q) = %v, want ErrBadSeal", raw, err)
94 }
95 })
96 }
97}
98
99// The replica reaches N: some sealed level file ends at N, and none ends
100// after it.
101func TestSealReachesN(t *testing.T) {
102 s := sealForTest()
103 if !s.ReachesN() {
104 t.Fatal("a seal whose newest file ends at N does not reach N")
105 }
106 s.N = 8
107 if s.ReachesN() {
108 t.Fatal("a seal ending at 7 reaches N = 8")
109 }
110 s.N = 6
111 if s.ReachesN() {
112 t.Fatal("a seal holding a file past N reaches N = 6")
113 }
114}
115