A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
at test-validate 259 lines 6.4 kB view raw
1package plc_test 2 3import ( 4 "context" 5 "encoding/json" 6 "net/http" 7 "net/http/httptest" 8 "testing" 9 "time" 10 11 "tangled.org/atscan.net/plcbundle/bundle" 12 "tangled.org/atscan.net/plcbundle/plc" 13) 14 15// TestPLCOperation tests operation parsing and methods 16func TestPLCOperation(t *testing.T) { 17 t.Run("IsNullified", func(t *testing.T) { 18 tests := []struct { 19 name string 20 nullified interface{} 21 want bool 22 }{ 23 {"nil", nil, false}, 24 {"false", false, false}, 25 {"true", true, true}, 26 {"empty string", "", false}, 27 {"non-empty string", "cid123", true}, 28 } 29 30 for _, tt := range tests { 31 t.Run(tt.name, func(t *testing.T) { 32 op := plc.PLCOperation{Nullified: tt.nullified} 33 if got := op.IsNullified(); got != tt.want { 34 t.Errorf("IsNullified() = %v, want %v", got, tt.want) 35 } 36 }) 37 } 38 }) 39 40 t.Run("GetNullifyingCID", func(t *testing.T) { 41 op := plc.PLCOperation{Nullified: "bafytest123"} 42 if cid := op.GetNullifyingCID(); cid != "bafytest123" { 43 t.Errorf("expected 'bafytest123', got '%s'", cid) 44 } 45 46 op2 := plc.PLCOperation{Nullified: true} 47 if cid := op2.GetNullifyingCID(); cid != "" { 48 t.Errorf("expected empty string, got '%s'", cid) 49 } 50 }) 51 52 t.Run("JSONParsing", func(t *testing.T) { 53 jsonData := `{ 54 "did": "did:plc:test123", 55 "cid": "bafytest", 56 "createdAt": "2024-01-01T12:00:00.000Z", 57 "operation": {"type": "create"}, 58 "nullified": false 59 }` 60 61 var op plc.PLCOperation 62 if err := json.Unmarshal([]byte(jsonData), &op); err != nil { 63 t.Fatalf("failed to parse operation: %v", err) 64 } 65 66 if op.DID != "did:plc:test123" { 67 t.Errorf("unexpected DID: %s", op.DID) 68 } 69 if op.CID != "bafytest" { 70 t.Errorf("unexpected CID: %s", op.CID) 71 } 72 }) 73} 74 75// TestClient tests PLC client operations 76func TestClient(t *testing.T) { 77 t.Run("Export", func(t *testing.T) { 78 // Create mock server 79 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 80 if r.URL.Path != "/export" { 81 t.Errorf("unexpected path: %s", r.URL.Path) 82 } 83 84 // Check query parameters 85 count := r.URL.Query().Get("count") 86 if count != "100" { 87 t.Errorf("unexpected count: %s", count) 88 } 89 90 // Return mock JSONL data 91 w.Header().Set("Content-Type", "application/x-ndjson") 92 for i := 0; i < 10; i++ { 93 op := plc.PLCOperation{ 94 DID: "did:plc:test" + string(rune(i)), 95 CID: "bafytest" + string(rune(i)), 96 CreatedAt: time.Now(), 97 Operation: map[string]interface{}{"type": "create"}, 98 } 99 json.NewEncoder(w).Encode(op) 100 } 101 })) 102 defer server.Close() 103 104 // Create client 105 client := plc.NewClient(server.URL) 106 defer client.Close() 107 108 // Test export 109 ctx := context.Background() 110 ops, err := client.Export(ctx, plc.ExportOptions{ 111 Count: 100, 112 }) 113 if err != nil { 114 t.Fatalf("Export failed: %v", err) 115 } 116 117 if len(ops) != 10 { 118 t.Errorf("expected 10 operations, got %d", len(ops)) 119 } 120 121 // Check that RawJSON is preserved 122 if len(ops[0].RawJSON) == 0 { 123 t.Error("RawJSON not preserved") 124 } 125 }) 126 127 t.Run("RateLimitRetry", func(t *testing.T) { 128 attempts := 0 129 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 130 attempts++ 131 if attempts < 2 { 132 // Return 429 on first attempt 133 w.Header().Set("Retry-After", "1") 134 w.WriteHeader(http.StatusTooManyRequests) 135 return 136 } 137 // Success on second attempt 138 w.Header().Set("Content-Type", "application/x-ndjson") 139 op := plc.PLCOperation{DID: "did:plc:test", CID: "bafytest", CreatedAt: time.Now()} 140 json.NewEncoder(w).Encode(op) 141 })) 142 defer server.Close() 143 144 client := plc.NewClient(server.URL) 145 defer client.Close() 146 147 ctx := context.Background() 148 ops, err := client.Export(ctx, plc.ExportOptions{Count: 1}) 149 if err != nil { 150 t.Fatalf("Export failed after retry: %v", err) 151 } 152 153 if len(ops) != 1 { 154 t.Errorf("expected 1 operation, got %d", len(ops)) 155 } 156 157 if attempts < 2 { 158 t.Errorf("expected at least 2 attempts, got %d", attempts) 159 } 160 }) 161 162 t.Run("GetDID", func(t *testing.T) { 163 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 164 if r.URL.Path != "/did:plc:test123" { 165 t.Errorf("unexpected path: %s", r.URL.Path) 166 } 167 168 doc := plc.DIDDocument{ 169 Context: []string{"https://www.w3.org/ns/did/v1"}, 170 ID: "did:plc:test123", 171 } 172 json.NewEncoder(w).Encode(doc) 173 })) 174 defer server.Close() 175 176 client := plc.NewClient(server.URL) 177 defer client.Close() 178 179 ctx := context.Background() 180 doc, err := client.GetDID(ctx, "did:plc:test123") 181 if err != nil { 182 t.Fatalf("GetDID failed: %v", err) 183 } 184 185 if doc.ID != "did:plc:test123" { 186 t.Errorf("unexpected DID: %s", doc.ID) 187 } 188 }) 189} 190 191// TestRateLimiter tests rate limiting functionality 192func TestRateLimiter(t *testing.T) { 193 t.Run("BasicRateLimit", func(t *testing.T) { 194 // 10 requests per second 195 rl := plc.NewRateLimiter(10, time.Second) 196 defer rl.Stop() 197 198 ctx := context.Background() 199 200 // First 10 should be fast 201 start := time.Now() 202 for i := 0; i < 10; i++ { 203 if err := rl.Wait(ctx); err != nil { 204 t.Fatalf("Wait failed: %v", err) 205 } 206 } 207 elapsed := time.Since(start) 208 209 // Should be very fast (less than 100ms) 210 if elapsed > 100*time.Millisecond { 211 t.Errorf("expected fast execution, took %v", elapsed) 212 } 213 }) 214 215 t.Run("ContextCancellation", func(t *testing.T) { 216 rl := plc.NewRateLimiter(1, time.Minute) // Very slow rate 217 defer rl.Stop() 218 219 // Consume the one available token 220 ctx := context.Background() 221 rl.Wait(ctx) 222 223 // Try to wait with cancelled context 224 cancelCtx, cancel := context.WithCancel(context.Background()) 225 cancel() // Cancel immediately 226 227 err := rl.Wait(cancelCtx) 228 if err != context.Canceled { 229 t.Errorf("expected context.Canceled, got %v", err) 230 } 231 }) 232} 233 234// Benchmark tests 235func BenchmarkSerializeJSONL(b *testing.B) { 236 ops := make([]plc.PLCOperation, 10000) 237 for i := 0; i < 10000; i++ { 238 ops[i] = plc.PLCOperation{ 239 DID: "did:plc:test", 240 CID: "bafytest", 241 CreatedAt: time.Now(), 242 Operation: map[string]interface{}{"type": "create"}, 243 } 244 } 245 246 logger := &benchLogger{} 247 operations, _ := bundle.NewOperations(logger) 248 defer operations.Close() 249 250 b.ResetTimer() 251 for i := 0; i < b.N; i++ { 252 _ = operations.SerializeJSONL(ops) 253 } 254} 255 256type benchLogger struct{} 257 258func (l *benchLogger) Printf(format string, v ...interface{}) {} 259func (l *benchLogger) Println(v ...interface{}) {}