forked from
atscan.net/plcbundle
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
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{}) {}