A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
1package bundle_test
2
3import (
4 "path/filepath"
5 "testing"
6 "time"
7
8 "tangled.org/atscan.net/plcbundle/bundle"
9 "tangled.org/atscan.net/plcbundle/plc"
10)
11
12// TestIndex tests index operations
13func TestIndex(t *testing.T) {
14 t.Run("CreateNewIndex", func(t *testing.T) {
15 idx := bundle.NewIndex("test-origin")
16 if idx == nil {
17 t.Fatal("NewIndex returned nil")
18 }
19 if idx.Version != bundle.INDEX_VERSION {
20 t.Errorf("expected version %s, got %s", bundle.INDEX_VERSION, idx.Version)
21 }
22 if idx.Count() != 0 {
23 t.Errorf("expected empty index, got count %d", idx.Count())
24 }
25 })
26
27 t.Run("AddBundle", func(t *testing.T) {
28 idx := bundle.NewIndex("test-origin")
29 meta := &bundle.BundleMetadata{
30 BundleNumber: 1,
31 StartTime: time.Now(),
32 EndTime: time.Now().Add(time.Hour),
33 OperationCount: bundle.BUNDLE_SIZE,
34 DIDCount: 1000,
35 Hash: "abc123",
36 CompressedHash: "def456",
37 }
38
39 idx.AddBundle(meta)
40
41 if idx.Count() != 1 {
42 t.Errorf("expected count 1, got %d", idx.Count())
43 }
44
45 retrieved, err := idx.GetBundle(1)
46 if err != nil {
47 t.Fatalf("GetBundle failed: %v", err)
48 }
49 if retrieved.Hash != meta.Hash {
50 t.Errorf("expected hash %s, got %s", meta.Hash, retrieved.Hash)
51 }
52 })
53
54 t.Run("SaveAndLoad", func(t *testing.T) {
55 tmpDir := t.TempDir()
56 indexPath := filepath.Join(tmpDir, "test_index.json")
57
58 // Create and save
59 idx := bundle.NewIndex("test-origin")
60 idx.AddBundle(&bundle.BundleMetadata{
61 BundleNumber: 1,
62 StartTime: time.Now(),
63 EndTime: time.Now().Add(time.Hour),
64 OperationCount: bundle.BUNDLE_SIZE,
65 Hash: "test123",
66 })
67
68 if err := idx.Save(indexPath); err != nil {
69 t.Fatalf("Save failed: %v", err)
70 }
71
72 // Load
73 loaded, err := bundle.LoadIndex(indexPath)
74 if err != nil {
75 t.Fatalf("LoadIndex failed: %v", err)
76 }
77
78 if loaded.Count() != 1 {
79 t.Errorf("expected count 1, got %d", loaded.Count())
80 }
81 })
82
83 t.Run("GetBundleRange", func(t *testing.T) {
84 idx := bundle.NewIndex("test-origin")
85 for i := 1; i <= 5; i++ {
86 idx.AddBundle(&bundle.BundleMetadata{
87 BundleNumber: i,
88 StartTime: time.Now(),
89 EndTime: time.Now().Add(time.Hour),
90 OperationCount: bundle.BUNDLE_SIZE,
91 })
92 }
93
94 bundles := idx.GetBundleRange(2, 4)
95 if len(bundles) != 3 {
96 t.Errorf("expected 3 bundles, got %d", len(bundles))
97 }
98 if bundles[0].BundleNumber != 2 || bundles[2].BundleNumber != 4 {
99 t.Errorf("unexpected bundle range")
100 }
101 })
102
103 t.Run("FindGaps", func(t *testing.T) {
104 idx := bundle.NewIndex("test-origin")
105 // Add bundles 1, 2, 4, 5 (missing 3)
106 for _, num := range []int{1, 2, 4, 5} {
107 idx.AddBundle(&bundle.BundleMetadata{
108 BundleNumber: num,
109 StartTime: time.Now(),
110 EndTime: time.Now().Add(time.Hour),
111 OperationCount: bundle.BUNDLE_SIZE,
112 })
113 }
114
115 gaps := idx.FindGaps()
116 if len(gaps) != 1 {
117 t.Errorf("expected 1 gap, got %d", len(gaps))
118 }
119 if len(gaps) > 0 && gaps[0] != 3 {
120 t.Errorf("expected gap at 3, got %d", gaps[0])
121 }
122 })
123}
124
125// TestBundle tests bundle operations
126func TestBundle(t *testing.T) {
127 t.Run("ValidateForSave", func(t *testing.T) {
128 tests := []struct {
129 name string
130 bundle *bundle.Bundle
131 wantErr bool
132 }{
133 {
134 name: "valid bundle",
135 bundle: &bundle.Bundle{
136 BundleNumber: 1,
137 StartTime: time.Now(),
138 EndTime: time.Now().Add(time.Hour),
139 Operations: makeTestOperations(bundle.BUNDLE_SIZE),
140 },
141 wantErr: false,
142 },
143 {
144 name: "invalid bundle number",
145 bundle: &bundle.Bundle{
146 BundleNumber: 0,
147 Operations: makeTestOperations(bundle.BUNDLE_SIZE),
148 },
149 wantErr: true,
150 },
151 {
152 name: "wrong operation count",
153 bundle: &bundle.Bundle{
154 BundleNumber: 1,
155 Operations: makeTestOperations(100),
156 },
157 wantErr: true,
158 },
159 {
160 name: "start after end",
161 bundle: &bundle.Bundle{
162 BundleNumber: 1,
163 StartTime: time.Now().Add(time.Hour),
164 EndTime: time.Now(),
165 Operations: makeTestOperations(bundle.BUNDLE_SIZE),
166 },
167 wantErr: true,
168 },
169 }
170
171 for _, tt := range tests {
172 t.Run(tt.name, func(t *testing.T) {
173 err := tt.bundle.ValidateForSave()
174 if (err != nil) != tt.wantErr {
175 t.Errorf("ValidateForSave() error = %v, wantErr %v", err, tt.wantErr)
176 }
177 })
178 }
179 })
180
181 t.Run("CompressionRatio", func(t *testing.T) {
182 b := &bundle.Bundle{
183 CompressedSize: 1000,
184 UncompressedSize: 5000,
185 }
186 ratio := b.CompressionRatio()
187 if ratio != 5.0 {
188 t.Errorf("expected ratio 5.0, got %f", ratio)
189 }
190 })
191}
192
193// TestMempool tests mempool operations
194func TestMempool(t *testing.T) {
195 tmpDir := t.TempDir()
196 logger := &testLogger{t: t}
197
198 t.Run("CreateAndAdd", func(t *testing.T) {
199 minTime := time.Now().Add(-time.Hour)
200 m, err := bundle.NewMempool(tmpDir, 1, minTime, logger)
201 if err != nil {
202 t.Fatalf("NewMempool failed: %v", err)
203 }
204
205 ops := makeTestOperations(100)
206 added, err := m.Add(ops)
207 if err != nil {
208 t.Fatalf("Add failed: %v", err)
209 }
210 if added != 100 {
211 t.Errorf("expected 100 added, got %d", added)
212 }
213 if m.Count() != 100 {
214 t.Errorf("expected count 100, got %d", m.Count())
215 }
216 })
217
218 t.Run("ChronologicalValidation", func(t *testing.T) {
219 minTime := time.Now().Add(-time.Hour)
220 m, err := bundle.NewMempool(tmpDir, 2, minTime, logger)
221 if err != nil {
222 t.Fatalf("NewMempool failed: %v", err)
223 }
224
225 // Add operations in order
226 ops := makeTestOperations(10)
227 _, err = m.Add(ops)
228 if err != nil {
229 t.Fatalf("Add failed: %v", err)
230 }
231
232 // Try to add operation before last one (should fail)
233 oldOp := []plc.PLCOperation{
234 {
235 DID: "did:plc:old",
236 CID: "old123",
237 CreatedAt: time.Now().Add(-2 * time.Hour),
238 },
239 }
240 _, err = m.Add(oldOp)
241 if err == nil {
242 t.Error("expected chronological validation error")
243 }
244 })
245
246 t.Run("TakeOperations", func(t *testing.T) {
247 minTime := time.Now().Add(-time.Hour)
248 m, err := bundle.NewMempool(tmpDir, 3, minTime, logger)
249 if err != nil {
250 t.Fatalf("NewMempool failed: %v", err)
251 }
252
253 ops := makeTestOperations(100)
254 m.Add(ops)
255
256 taken, err := m.Take(50)
257 if err != nil {
258 t.Fatalf("Take failed: %v", err)
259 }
260 if len(taken) != 50 {
261 t.Errorf("expected 50 operations, got %d", len(taken))
262 }
263 if m.Count() != 50 {
264 t.Errorf("expected 50 remaining, got %d", m.Count())
265 }
266 })
267
268 t.Run("SaveAndLoad", func(t *testing.T) {
269 minTime := time.Now().Add(-time.Hour)
270 m, err := bundle.NewMempool(tmpDir, 4, minTime, logger)
271 if err != nil {
272 t.Fatalf("NewMempool failed: %v", err)
273 }
274
275 ops := makeTestOperations(50)
276 m.Add(ops)
277
278 if err := m.Save(); err != nil {
279 t.Fatalf("Save failed: %v", err)
280 }
281
282 // Create new mempool and load
283 m2, err := bundle.NewMempool(tmpDir, 4, minTime, logger)
284 if err != nil {
285 t.Fatalf("NewMempool failed: %v", err)
286 }
287
288 if m2.Count() != 50 {
289 t.Errorf("expected 50 operations after load, got %d", m2.Count())
290 }
291 })
292
293 t.Run("Validate", func(t *testing.T) {
294 minTime := time.Now().Add(-time.Hour)
295 m, err := bundle.NewMempool(tmpDir, 5, minTime, logger)
296 if err != nil {
297 t.Fatalf("NewMempool failed: %v", err)
298 }
299
300 ops := makeTestOperations(10)
301 m.Add(ops)
302
303 if err := m.Validate(); err != nil {
304 t.Errorf("Validate failed: %v", err)
305 }
306 })
307}
308
309// TestOperations tests low-level operations
310func TestOperations(t *testing.T) {
311 tmpDir := t.TempDir()
312 logger := &testLogger{t: t}
313
314 ops, err := bundle.NewOperations(logger)
315 if err != nil {
316 t.Fatalf("NewOperations failed: %v", err)
317 }
318 defer ops.Close()
319
320 t.Run("SerializeJSONL", func(t *testing.T) {
321 operations := makeTestOperations(10)
322 data := ops.SerializeJSONL(operations)
323 if len(data) == 0 {
324 t.Error("SerializeJSONL returned empty data")
325 }
326 })
327
328 t.Run("Hash", func(t *testing.T) {
329 data := []byte("test data")
330 hash := ops.Hash(data)
331 if len(hash) != 64 { // SHA256 hex = 64 chars
332 t.Errorf("expected hash length 64, got %d", len(hash))
333 }
334
335 // Same data should produce same hash
336 hash2 := ops.Hash(data)
337 if hash != hash2 {
338 t.Error("same data produced different hashes")
339 }
340 })
341
342 t.Run("SaveAndLoadBundle", func(t *testing.T) {
343 operations := makeTestOperations(bundle.BUNDLE_SIZE)
344 path := filepath.Join(tmpDir, "test_bundle.jsonl.zst")
345
346 // Save
347 uncompHash, compHash, uncompSize, compSize, err := ops.SaveBundle(path, operations)
348 if err != nil {
349 t.Fatalf("SaveBundle failed: %v", err)
350 }
351
352 if uncompHash == "" || compHash == "" {
353 t.Error("empty hashes returned")
354 }
355 if uncompSize == 0 || compSize == 0 {
356 t.Error("zero sizes returned")
357 }
358 if compSize >= uncompSize {
359 t.Error("compressed size should be smaller than uncompressed")
360 }
361
362 // Load
363 loaded, err := ops.LoadBundle(path)
364 if err != nil {
365 t.Fatalf("LoadBundle failed: %v", err)
366 }
367
368 if len(loaded) != len(operations) {
369 t.Errorf("expected %d operations, got %d", len(operations), len(loaded))
370 }
371 })
372
373 t.Run("ExtractUniqueDIDs", func(t *testing.T) {
374 operations := []plc.PLCOperation{
375 {DID: "did:plc:1"},
376 {DID: "did:plc:2"},
377 {DID: "did:plc:1"}, // duplicate
378 {DID: "did:plc:3"},
379 }
380
381 dids := ops.ExtractUniqueDIDs(operations)
382 if len(dids) != 3 {
383 t.Errorf("expected 3 unique DIDs, got %d", len(dids))
384 }
385 })
386
387 t.Run("GetBoundaryCIDs", func(t *testing.T) {
388 baseTime := time.Now()
389 operations := []plc.PLCOperation{
390 {CID: "cid1", CreatedAt: baseTime},
391 {CID: "cid2", CreatedAt: baseTime.Add(time.Second)},
392 {CID: "cid3", CreatedAt: baseTime.Add(2 * time.Second)},
393 {CID: "cid4", CreatedAt: baseTime.Add(2 * time.Second)}, // same as cid3
394 {CID: "cid5", CreatedAt: baseTime.Add(2 * time.Second)}, // same as cid3
395 }
396
397 boundaryTime, cids := ops.GetBoundaryCIDs(operations)
398 if !boundaryTime.Equal(baseTime.Add(2 * time.Second)) {
399 t.Error("unexpected boundary time")
400 }
401 if len(cids) != 3 { // cid3, cid4, cid5
402 t.Errorf("expected 3 boundary CIDs, got %d", len(cids))
403 }
404 })
405}
406
407// Helper functions
408
409func makeTestOperations(count int) []plc.PLCOperation {
410 ops := make([]plc.PLCOperation, count)
411 baseTime := time.Now().Add(-time.Hour)
412
413 for i := 0; i < count; i++ {
414 ops[i] = plc.PLCOperation{
415 DID: "did:plc:test" + string(rune(i)),
416 CID: "bafytest" + string(rune(i)),
417 CreatedAt: baseTime.Add(time.Duration(i) * time.Second),
418 /*Operation: map[string]interface{}{
419 "type": "create",
420 },*/
421 }
422 }
423
424 return ops
425}
426
427type testLogger struct {
428 t *testing.T
429}
430
431func (l *testLogger) Printf(format string, v ...interface{}) {
432 l.t.Logf(format, v...)
433}
434
435func (l *testLogger) Println(v ...interface{}) {
436 l.t.Log(v...)
437}