A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at did-resolver 437 lines 11 kB view raw
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}