tangled
alpha
login
or
join now
angrydutchman.peedee.es
/
plcbundle
forked from
atscan.net/plcbundle
0
fork
atom
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
0
fork
atom
overview
issues
pulls
pipelines
basic tests
tree.fail
4 months ago
cb6e2c64
fb37be17
+769
-11
7 changed files
expand all
collapse all
unified
split
README.md
bundle
bundle_test.go
cmd
plcbundle
compare.go
server.go
go.mod
plc
plc_test.go
plc_bundles.json
+26
README.md
···
1
1
# PLC Bundle
2
2
3
3
+
```
4
4
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⡀⠀⠀⠀⠀⠀⠀⢀⠀⠀⡀⠀⢀⠀⢀⡀⣤⡢⣤⡤⡀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
5
5
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡄⡄⠐⡀⠈⣀⠀⡠⡠⠀⣢⣆⢌⡾⢙⠺⢽⠾⡋⣻⡷⡫⢵⣭⢦⣴⠦⠀⢠⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
6
6
+
⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣽⣥⡈⠧⣂⢧⢾⠕⠞⠡⠊⠁⣐⠉⠀⠉⢍⠀⠉⠌⡉⠀⠂⠁⠱⠉⠁⢝⠻⠎⣬⢌⡌⣬⣡⣀⣢⣄⡄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀
7
7
+
⠀⠀⠀⠀⠀⠀⠀⢀⢸⣿⣿⢿⣾⣯⣑⢄⡂⠀⠄⠂⠀⠀⢀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠄⠐⠀⠀⠀⠀⣄⠭⠂⠈⠜⣩⣿⢝⠃⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀
8
8
+
⠀⠀⠀⠀⠀⠀⠀⢀⣻⡟⠏⠀⠚⠈⠚⡉⡝⢶⣱⢤⣅⠈⠀⠄⠀⠀⠀⠀⠀⠠⠀⠀⡂⠐⣤⢕⡪⢼⣈⡹⡇⠏⠏⠋⠅⢃⣪⡏⡇⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀
9
9
+
⠀⠀⠀⠀⠀⠀⠀⠀⠺⣻⡄⠀⠀⠀⢠⠌⠃⠐⠉⢡⠱⠧⠝⡯⣮⢶⣴⣤⡆⢐⣣⢅⣮⡟⠦⠍⠉⠀⠁⠐⠀⠀⠀⠄⠐⠡⣽⡸⣎⢁⠀⠀⠀⠀⠀⠀⠀⠀⠀
10
10
+
⠀⠀⠀⠀⠀⠀⠀⢈⡻⣧⠀⠁⠐⠀⠀⠀⠀⠀⠀⠊⠀⠕⢀⡉⠈⡫⠽⡿⡟⠿⠟⠁⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠬⠥⣋⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
11
11
+
⠀⠀⠀⠀⠀⠀⠀⡀⣾⡍⠕⡀⠀⠀⠀⠄⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠥⣤⢌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠄⢀⠀⢝⢞⣫⡆⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀
12
12
+
⠀⠀⠀⠀⠀⠀⠀⠀⣽⡶⡄⠐⡀⠀⠀⠀⠀⠀⠀⢀⠀⠄⠀⠀⠀⠄⠁⠇⣷⡆⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡸⢝⣮⠍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
13
13
+
⠀⠀⠀⠀⠀⠀⢀⠀⢾⣷⠀⠠⡀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠁⡁⠀⠀⣾⡥⠖⠀⠀⠀⠂⠀⠀⠀⠀⠀⠁⠀⡀⠁⠀⠀⠻⢳⣻⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
14
14
+
⠀⠀⠀⠀⠀⠀⠀⠀⣞⡙⠨⣀⠠⠄⠀⠂⠀⠀⠀⠈⢀⠀⠀⠀⠀⠀⠤⢚⢢⣟⠀⠀⠀⠀⡐⠀⠀⡀⠀⠀⠀⠀⠁⠈⠌⠊⣯⣮⡏⠡⠂⠀⠀⠀⠀⠀⠀⠀⠀
15
15
+
⠀⠀⠀⠀⠀⠀⠀⠀⣻⡟⡄⡡⣄⠀⠠⠀⠀⡅⠀⠐⠀⡀⠀⡀⠀⠄⠈⠃⠳⠪⠤⠀⠀⠀⠀⡀⠀⠂⠀⠀⠀⠁⠈⢠⣠⠒⠻⣻⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
16
16
+
⠀⠀⠀⠀⠀⠀⠀⠀⠪⡎⠠⢌⠑⡀⠂⠀⠄⠠⠀⠠⠀⠁⡀⠠⠠⡀⣀⠜⢏⡅⠀⠀⡀⠁⠀⠀⠁⠁⠐⠄⡀⢀⠂⠀⠄⢑⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
17
17
+
⠀⠀⠀⠀⠀⠀⠀⠀⠼⣻⠧⣣⣀⠐⠨⠁⠕⢈⢀⢀⡁⠀⠈⠠⢀⠀⠐⠜⣽⡗⡤⠀⠂⠀⠠⠀⢂⠠⠀⠁⠄⠀⠔⠀⠑⣨⣿⢯⠋⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀
18
18
+
⠀⠀⠀⠀⠀⠀⠀⠀⡚⣷⣭⠎⢃⡗⠄⡄⢀⠁⠀⠅⢀⢅⡀⠠⠀⢠⡀⡩⠷⢇⠀⡀⠄⡠⠤⠆⣀⡀⠄⠉⣠⠃⠴⠀⠈⢁⣿⡛⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
19
19
+
⠀⠀⠀⠀⠀⠀⠀⠘⡬⡿⣿⡏⡻⡯⠌⢁⢛⠠⠓⠐⠐⠐⠌⠃⠋⠂⡢⢰⣈⢏⣰⠂⠈⠀⠠⠒⠡⠌⠫⠭⠩⠢⡬⠆⠿⢷⢿⡽⡧⠉⠊⠀⠀⠀⠀⠀⠀⠀⠀
20
20
+
⠀⠀⠀⠀⠀⠀⠀⠀⠺⣷⣺⣗⣿⡶⡎⡅⣣⢎⠠⡅⣢⡖⠴⠬⡈⠂⡨⢡⠾⣣⣢⠀⠀⡹⠄⡄⠄⡇⣰⡖⡊⠔⢹⣄⣿⣭⣵⣿⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
21
21
+
⠀⠀⠀⠀⠀⠀⠀⠀⠩⣿⣿⣲⣿⣷⣟⣼⠟⣬⢉⡠⣪⢜⣂⣁⠥⠓⠚⡁⢶⣷⣠⠂⡄⡢⣀⡐⠧⢆⣒⡲⡳⡫⢟⡃⢪⡧⣟⡟⣯⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀
22
22
+
⠀⠀⠀⠀⠀⠀⠀⠀⢺⠟⢿⢟⢻⡗⡮⡿⣲⢷⣆⣏⣇⡧⣄⢖⠾⡷⣿⣤⢳⢷⣣⣦⡜⠗⣭⢂⠩⣹⢿⡲⢎⡧⣕⣖⣓⣽⡿⡖⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
23
23
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠂⠂⠏⠿⢻⣥⡪⢽⣳⣳⣥⡶⣫⣍⢐⣥⣻⣾⡻⣅⢭⡴⢭⣿⠕⣧⡭⣞⣻⣣⣻⢿⠟⠛⠙⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
24
24
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠋⠫⠯⣍⢻⣿⣿⣷⣕⣵⣹⣽⣿⣷⣇⡏⣿⡿⣍⡝⠵⠯⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
25
25
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠠⠁⠋⢣⠓⡍⣫⠹⣿⣿⣷⡿⠯⠺⠁⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
26
26
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠋⢀⠋⢈⡿⠿⠁⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
27
27
+
```
28
28
+
3
29
A Go library and CLI tool for managing [DID PLC Directory](https://plc.directory/) bundles with transparent synchronization, compression, and verification.
4
30
5
31
## Features
+437
bundle/bundle_test.go
···
1
1
+
package bundle_test
2
2
+
3
3
+
import (
4
4
+
"path/filepath"
5
5
+
"testing"
6
6
+
"time"
7
7
+
8
8
+
"github.com/atscan/plcbundle/bundle"
9
9
+
"github.com/atscan/plcbundle/plc"
10
10
+
)
11
11
+
12
12
+
// TestIndex tests index operations
13
13
+
func TestIndex(t *testing.T) {
14
14
+
t.Run("CreateNewIndex", func(t *testing.T) {
15
15
+
idx := bundle.NewIndex()
16
16
+
if idx == nil {
17
17
+
t.Fatal("NewIndex returned nil")
18
18
+
}
19
19
+
if idx.Version != bundle.INDEX_VERSION {
20
20
+
t.Errorf("expected version %s, got %s", bundle.INDEX_VERSION, idx.Version)
21
21
+
}
22
22
+
if idx.Count() != 0 {
23
23
+
t.Errorf("expected empty index, got count %d", idx.Count())
24
24
+
}
25
25
+
})
26
26
+
27
27
+
t.Run("AddBundle", func(t *testing.T) {
28
28
+
idx := bundle.NewIndex()
29
29
+
meta := &bundle.BundleMetadata{
30
30
+
BundleNumber: 1,
31
31
+
StartTime: time.Now(),
32
32
+
EndTime: time.Now().Add(time.Hour),
33
33
+
OperationCount: bundle.BUNDLE_SIZE,
34
34
+
DIDCount: 1000,
35
35
+
Hash: "abc123",
36
36
+
CompressedHash: "def456",
37
37
+
}
38
38
+
39
39
+
idx.AddBundle(meta)
40
40
+
41
41
+
if idx.Count() != 1 {
42
42
+
t.Errorf("expected count 1, got %d", idx.Count())
43
43
+
}
44
44
+
45
45
+
retrieved, err := idx.GetBundle(1)
46
46
+
if err != nil {
47
47
+
t.Fatalf("GetBundle failed: %v", err)
48
48
+
}
49
49
+
if retrieved.Hash != meta.Hash {
50
50
+
t.Errorf("expected hash %s, got %s", meta.Hash, retrieved.Hash)
51
51
+
}
52
52
+
})
53
53
+
54
54
+
t.Run("SaveAndLoad", func(t *testing.T) {
55
55
+
tmpDir := t.TempDir()
56
56
+
indexPath := filepath.Join(tmpDir, "test_index.json")
57
57
+
58
58
+
// Create and save
59
59
+
idx := bundle.NewIndex()
60
60
+
idx.AddBundle(&bundle.BundleMetadata{
61
61
+
BundleNumber: 1,
62
62
+
StartTime: time.Now(),
63
63
+
EndTime: time.Now().Add(time.Hour),
64
64
+
OperationCount: bundle.BUNDLE_SIZE,
65
65
+
Hash: "test123",
66
66
+
})
67
67
+
68
68
+
if err := idx.Save(indexPath); err != nil {
69
69
+
t.Fatalf("Save failed: %v", err)
70
70
+
}
71
71
+
72
72
+
// Load
73
73
+
loaded, err := bundle.LoadIndex(indexPath)
74
74
+
if err != nil {
75
75
+
t.Fatalf("LoadIndex failed: %v", err)
76
76
+
}
77
77
+
78
78
+
if loaded.Count() != 1 {
79
79
+
t.Errorf("expected count 1, got %d", loaded.Count())
80
80
+
}
81
81
+
})
82
82
+
83
83
+
t.Run("GetBundleRange", func(t *testing.T) {
84
84
+
idx := bundle.NewIndex()
85
85
+
for i := 1; i <= 5; i++ {
86
86
+
idx.AddBundle(&bundle.BundleMetadata{
87
87
+
BundleNumber: i,
88
88
+
StartTime: time.Now(),
89
89
+
EndTime: time.Now().Add(time.Hour),
90
90
+
OperationCount: bundle.BUNDLE_SIZE,
91
91
+
})
92
92
+
}
93
93
+
94
94
+
bundles := idx.GetBundleRange(2, 4)
95
95
+
if len(bundles) != 3 {
96
96
+
t.Errorf("expected 3 bundles, got %d", len(bundles))
97
97
+
}
98
98
+
if bundles[0].BundleNumber != 2 || bundles[2].BundleNumber != 4 {
99
99
+
t.Errorf("unexpected bundle range")
100
100
+
}
101
101
+
})
102
102
+
103
103
+
t.Run("FindGaps", func(t *testing.T) {
104
104
+
idx := bundle.NewIndex()
105
105
+
// Add bundles 1, 2, 4, 5 (missing 3)
106
106
+
for _, num := range []int{1, 2, 4, 5} {
107
107
+
idx.AddBundle(&bundle.BundleMetadata{
108
108
+
BundleNumber: num,
109
109
+
StartTime: time.Now(),
110
110
+
EndTime: time.Now().Add(time.Hour),
111
111
+
OperationCount: bundle.BUNDLE_SIZE,
112
112
+
})
113
113
+
}
114
114
+
115
115
+
gaps := idx.FindGaps()
116
116
+
if len(gaps) != 1 {
117
117
+
t.Errorf("expected 1 gap, got %d", len(gaps))
118
118
+
}
119
119
+
if len(gaps) > 0 && gaps[0] != 3 {
120
120
+
t.Errorf("expected gap at 3, got %d", gaps[0])
121
121
+
}
122
122
+
})
123
123
+
}
124
124
+
125
125
+
// TestBundle tests bundle operations
126
126
+
func TestBundle(t *testing.T) {
127
127
+
t.Run("ValidateForSave", func(t *testing.T) {
128
128
+
tests := []struct {
129
129
+
name string
130
130
+
bundle *bundle.Bundle
131
131
+
wantErr bool
132
132
+
}{
133
133
+
{
134
134
+
name: "valid bundle",
135
135
+
bundle: &bundle.Bundle{
136
136
+
BundleNumber: 1,
137
137
+
StartTime: time.Now(),
138
138
+
EndTime: time.Now().Add(time.Hour),
139
139
+
Operations: makeTestOperations(bundle.BUNDLE_SIZE),
140
140
+
},
141
141
+
wantErr: false,
142
142
+
},
143
143
+
{
144
144
+
name: "invalid bundle number",
145
145
+
bundle: &bundle.Bundle{
146
146
+
BundleNumber: 0,
147
147
+
Operations: makeTestOperations(bundle.BUNDLE_SIZE),
148
148
+
},
149
149
+
wantErr: true,
150
150
+
},
151
151
+
{
152
152
+
name: "wrong operation count",
153
153
+
bundle: &bundle.Bundle{
154
154
+
BundleNumber: 1,
155
155
+
Operations: makeTestOperations(100),
156
156
+
},
157
157
+
wantErr: true,
158
158
+
},
159
159
+
{
160
160
+
name: "start after end",
161
161
+
bundle: &bundle.Bundle{
162
162
+
BundleNumber: 1,
163
163
+
StartTime: time.Now().Add(time.Hour),
164
164
+
EndTime: time.Now(),
165
165
+
Operations: makeTestOperations(bundle.BUNDLE_SIZE),
166
166
+
},
167
167
+
wantErr: true,
168
168
+
},
169
169
+
}
170
170
+
171
171
+
for _, tt := range tests {
172
172
+
t.Run(tt.name, func(t *testing.T) {
173
173
+
err := tt.bundle.ValidateForSave()
174
174
+
if (err != nil) != tt.wantErr {
175
175
+
t.Errorf("ValidateForSave() error = %v, wantErr %v", err, tt.wantErr)
176
176
+
}
177
177
+
})
178
178
+
}
179
179
+
})
180
180
+
181
181
+
t.Run("CompressionRatio", func(t *testing.T) {
182
182
+
b := &bundle.Bundle{
183
183
+
CompressedSize: 1000,
184
184
+
UncompressedSize: 5000,
185
185
+
}
186
186
+
ratio := b.CompressionRatio()
187
187
+
if ratio != 5.0 {
188
188
+
t.Errorf("expected ratio 5.0, got %f", ratio)
189
189
+
}
190
190
+
})
191
191
+
}
192
192
+
193
193
+
// TestMempool tests mempool operations
194
194
+
func TestMempool(t *testing.T) {
195
195
+
tmpDir := t.TempDir()
196
196
+
logger := &testLogger{t: t}
197
197
+
198
198
+
t.Run("CreateAndAdd", func(t *testing.T) {
199
199
+
minTime := time.Now().Add(-time.Hour)
200
200
+
m, err := bundle.NewMempool(tmpDir, 1, minTime, logger)
201
201
+
if err != nil {
202
202
+
t.Fatalf("NewMempool failed: %v", err)
203
203
+
}
204
204
+
205
205
+
ops := makeTestOperations(100)
206
206
+
added, err := m.Add(ops)
207
207
+
if err != nil {
208
208
+
t.Fatalf("Add failed: %v", err)
209
209
+
}
210
210
+
if added != 100 {
211
211
+
t.Errorf("expected 100 added, got %d", added)
212
212
+
}
213
213
+
if m.Count() != 100 {
214
214
+
t.Errorf("expected count 100, got %d", m.Count())
215
215
+
}
216
216
+
})
217
217
+
218
218
+
t.Run("ChronologicalValidation", func(t *testing.T) {
219
219
+
minTime := time.Now().Add(-time.Hour)
220
220
+
m, err := bundle.NewMempool(tmpDir, 2, minTime, logger)
221
221
+
if err != nil {
222
222
+
t.Fatalf("NewMempool failed: %v", err)
223
223
+
}
224
224
+
225
225
+
// Add operations in order
226
226
+
ops := makeTestOperations(10)
227
227
+
_, err = m.Add(ops)
228
228
+
if err != nil {
229
229
+
t.Fatalf("Add failed: %v", err)
230
230
+
}
231
231
+
232
232
+
// Try to add operation before last one (should fail)
233
233
+
oldOp := []plc.PLCOperation{
234
234
+
{
235
235
+
DID: "did:plc:old",
236
236
+
CID: "old123",
237
237
+
CreatedAt: time.Now().Add(-2 * time.Hour),
238
238
+
},
239
239
+
}
240
240
+
_, err = m.Add(oldOp)
241
241
+
if err == nil {
242
242
+
t.Error("expected chronological validation error")
243
243
+
}
244
244
+
})
245
245
+
246
246
+
t.Run("TakeOperations", func(t *testing.T) {
247
247
+
minTime := time.Now().Add(-time.Hour)
248
248
+
m, err := bundle.NewMempool(tmpDir, 3, minTime, logger)
249
249
+
if err != nil {
250
250
+
t.Fatalf("NewMempool failed: %v", err)
251
251
+
}
252
252
+
253
253
+
ops := makeTestOperations(100)
254
254
+
m.Add(ops)
255
255
+
256
256
+
taken, err := m.Take(50)
257
257
+
if err != nil {
258
258
+
t.Fatalf("Take failed: %v", err)
259
259
+
}
260
260
+
if len(taken) != 50 {
261
261
+
t.Errorf("expected 50 operations, got %d", len(taken))
262
262
+
}
263
263
+
if m.Count() != 50 {
264
264
+
t.Errorf("expected 50 remaining, got %d", m.Count())
265
265
+
}
266
266
+
})
267
267
+
268
268
+
t.Run("SaveAndLoad", func(t *testing.T) {
269
269
+
minTime := time.Now().Add(-time.Hour)
270
270
+
m, err := bundle.NewMempool(tmpDir, 4, minTime, logger)
271
271
+
if err != nil {
272
272
+
t.Fatalf("NewMempool failed: %v", err)
273
273
+
}
274
274
+
275
275
+
ops := makeTestOperations(50)
276
276
+
m.Add(ops)
277
277
+
278
278
+
if err := m.Save(); err != nil {
279
279
+
t.Fatalf("Save failed: %v", err)
280
280
+
}
281
281
+
282
282
+
// Create new mempool and load
283
283
+
m2, err := bundle.NewMempool(tmpDir, 4, minTime, logger)
284
284
+
if err != nil {
285
285
+
t.Fatalf("NewMempool failed: %v", err)
286
286
+
}
287
287
+
288
288
+
if m2.Count() != 50 {
289
289
+
t.Errorf("expected 50 operations after load, got %d", m2.Count())
290
290
+
}
291
291
+
})
292
292
+
293
293
+
t.Run("Validate", func(t *testing.T) {
294
294
+
minTime := time.Now().Add(-time.Hour)
295
295
+
m, err := bundle.NewMempool(tmpDir, 5, minTime, logger)
296
296
+
if err != nil {
297
297
+
t.Fatalf("NewMempool failed: %v", err)
298
298
+
}
299
299
+
300
300
+
ops := makeTestOperations(10)
301
301
+
m.Add(ops)
302
302
+
303
303
+
if err := m.Validate(); err != nil {
304
304
+
t.Errorf("Validate failed: %v", err)
305
305
+
}
306
306
+
})
307
307
+
}
308
308
+
309
309
+
// TestOperations tests low-level operations
310
310
+
func TestOperations(t *testing.T) {
311
311
+
tmpDir := t.TempDir()
312
312
+
logger := &testLogger{t: t}
313
313
+
314
314
+
ops, err := bundle.NewOperations(bundle.CompressionBetter, logger)
315
315
+
if err != nil {
316
316
+
t.Fatalf("NewOperations failed: %v", err)
317
317
+
}
318
318
+
defer ops.Close()
319
319
+
320
320
+
t.Run("SerializeJSONL", func(t *testing.T) {
321
321
+
operations := makeTestOperations(10)
322
322
+
data := ops.SerializeJSONL(operations)
323
323
+
if len(data) == 0 {
324
324
+
t.Error("SerializeJSONL returned empty data")
325
325
+
}
326
326
+
})
327
327
+
328
328
+
t.Run("Hash", func(t *testing.T) {
329
329
+
data := []byte("test data")
330
330
+
hash := ops.Hash(data)
331
331
+
if len(hash) != 64 { // SHA256 hex = 64 chars
332
332
+
t.Errorf("expected hash length 64, got %d", len(hash))
333
333
+
}
334
334
+
335
335
+
// Same data should produce same hash
336
336
+
hash2 := ops.Hash(data)
337
337
+
if hash != hash2 {
338
338
+
t.Error("same data produced different hashes")
339
339
+
}
340
340
+
})
341
341
+
342
342
+
t.Run("SaveAndLoadBundle", func(t *testing.T) {
343
343
+
operations := makeTestOperations(bundle.BUNDLE_SIZE)
344
344
+
path := filepath.Join(tmpDir, "test_bundle.jsonl.zst")
345
345
+
346
346
+
// Save
347
347
+
uncompHash, compHash, uncompSize, compSize, err := ops.SaveBundle(path, operations)
348
348
+
if err != nil {
349
349
+
t.Fatalf("SaveBundle failed: %v", err)
350
350
+
}
351
351
+
352
352
+
if uncompHash == "" || compHash == "" {
353
353
+
t.Error("empty hashes returned")
354
354
+
}
355
355
+
if uncompSize == 0 || compSize == 0 {
356
356
+
t.Error("zero sizes returned")
357
357
+
}
358
358
+
if compSize >= uncompSize {
359
359
+
t.Error("compressed size should be smaller than uncompressed")
360
360
+
}
361
361
+
362
362
+
// Load
363
363
+
loaded, err := ops.LoadBundle(path)
364
364
+
if err != nil {
365
365
+
t.Fatalf("LoadBundle failed: %v", err)
366
366
+
}
367
367
+
368
368
+
if len(loaded) != len(operations) {
369
369
+
t.Errorf("expected %d operations, got %d", len(operations), len(loaded))
370
370
+
}
371
371
+
})
372
372
+
373
373
+
t.Run("ExtractUniqueDIDs", func(t *testing.T) {
374
374
+
operations := []plc.PLCOperation{
375
375
+
{DID: "did:plc:1"},
376
376
+
{DID: "did:plc:2"},
377
377
+
{DID: "did:plc:1"}, // duplicate
378
378
+
{DID: "did:plc:3"},
379
379
+
}
380
380
+
381
381
+
dids := ops.ExtractUniqueDIDs(operations)
382
382
+
if len(dids) != 3 {
383
383
+
t.Errorf("expected 3 unique DIDs, got %d", len(dids))
384
384
+
}
385
385
+
})
386
386
+
387
387
+
t.Run("GetBoundaryCIDs", func(t *testing.T) {
388
388
+
baseTime := time.Now()
389
389
+
operations := []plc.PLCOperation{
390
390
+
{CID: "cid1", CreatedAt: baseTime},
391
391
+
{CID: "cid2", CreatedAt: baseTime.Add(time.Second)},
392
392
+
{CID: "cid3", CreatedAt: baseTime.Add(2 * time.Second)},
393
393
+
{CID: "cid4", CreatedAt: baseTime.Add(2 * time.Second)}, // same as cid3
394
394
+
{CID: "cid5", CreatedAt: baseTime.Add(2 * time.Second)}, // same as cid3
395
395
+
}
396
396
+
397
397
+
boundaryTime, cids := ops.GetBoundaryCIDs(operations)
398
398
+
if !boundaryTime.Equal(baseTime.Add(2 * time.Second)) {
399
399
+
t.Error("unexpected boundary time")
400
400
+
}
401
401
+
if len(cids) != 3 { // cid3, cid4, cid5
402
402
+
t.Errorf("expected 3 boundary CIDs, got %d", len(cids))
403
403
+
}
404
404
+
})
405
405
+
}
406
406
+
407
407
+
// Helper functions
408
408
+
409
409
+
func makeTestOperations(count int) []plc.PLCOperation {
410
410
+
ops := make([]plc.PLCOperation, count)
411
411
+
baseTime := time.Now().Add(-time.Hour)
412
412
+
413
413
+
for i := 0; i < count; i++ {
414
414
+
ops[i] = plc.PLCOperation{
415
415
+
DID: "did:plc:test" + string(rune(i)),
416
416
+
CID: "bafytest" + string(rune(i)),
417
417
+
CreatedAt: baseTime.Add(time.Duration(i) * time.Second),
418
418
+
Operation: map[string]interface{}{
419
419
+
"type": "create",
420
420
+
},
421
421
+
}
422
422
+
}
423
423
+
424
424
+
return ops
425
425
+
}
426
426
+
427
427
+
type testLogger struct {
428
428
+
t *testing.T
429
429
+
}
430
430
+
431
431
+
func (l *testLogger) Printf(format string, v ...interface{}) {
432
432
+
l.t.Logf(format, v...)
433
433
+
}
434
434
+
435
435
+
func (l *testLogger) Println(v ...interface{}) {
436
436
+
l.t.Log(v...)
437
437
+
}
+10
-6
cmd/plcbundle/compare.go
···
135
135
}
136
136
sort.Ints(comparison.ExtraBundles)
137
137
138
138
-
// Find hash mismatches
138
138
+
// Find hash mismatches (compare UNCOMPRESSED hash - the canonical hash)
139
139
for bundleNum, localMeta := range localMap {
140
140
if targetMeta, exists := targetMap[bundleNum]; exists {
141
141
comparison.CommonCount++
142
142
-
if localMeta.CompressedHash != targetMeta.CompressedHash {
142
142
+
if localMeta.Hash != targetMeta.Hash {
143
143
comparison.HashMismatches = append(comparison.HashMismatches, HashMismatch{
144
144
BundleNumber: bundleNum,
145
145
-
LocalHash: localMeta.CompressedHash,
146
146
-
TargetHash: targetMeta.CompressedHash,
145
145
+
LocalHash: localMeta.Hash,
146
146
+
TargetHash: targetMeta.Hash,
147
147
})
148
148
}
149
149
}
···
232
232
// Hash mismatches
233
233
if len(c.HashMismatches) > 0 {
234
234
fmt.Printf("\n")
235
235
-
fmt.Printf("Hash Mismatches\n")
236
236
-
fmt.Printf("───────────────\n")
235
235
+
fmt.Printf("Hash Mismatches (uncompressed data)\n")
236
236
+
fmt.Printf("────────────────────────────────────\n")
237
237
238
238
displayCount := len(c.HashMismatches)
239
239
if displayCount > 10 && !verbose {
···
245
245
fmt.Printf(" Bundle %06d:\n", m.BundleNumber)
246
246
fmt.Printf(" Local: %s\n", m.LocalHash[:16]+"...")
247
247
fmt.Printf(" Target: %s\n", m.TargetHash[:16]+"...")
248
248
+
if verbose {
249
249
+
fmt.Printf(" Local (full): %s\n", m.LocalHash)
250
250
+
fmt.Printf(" Target (full): %s\n", m.TargetHash)
251
251
+
}
248
252
}
249
253
250
254
if len(c.HashMismatches) > displayCount {
+29
-4
cmd/plcbundle/server.go
···
244
244
baseURL := getBaseURL(r)
245
245
wsURL := getWSURL(r)
246
246
247
247
-
fmt.Fprint(w, `
248
248
-
||||| PLC Bundle Server |||||
247
247
+
fmt.Fprintf(w, `
248
248
+
249
249
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠀⡀⠀⠀⠀⠀⠀⠀⢀⠀⠀⡀⠀⢀⠀⢀⡀⣤⡢⣤⡤⡀⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
250
250
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⡄⡄⠐⡀⠈⣀⠀⡠⡠⠀⣢⣆⢌⡾⢙⠺⢽⠾⡋⣻⡷⡫⢵⣭⢦⣴⠦⠀⢠⠀⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
251
251
+
⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣽⣥⡈⠧⣂⢧⢾⠕⠞⠡⠊⠁⣐⠉⠀⠉⢍⠀⠉⠌⡉⠀⠂⠁⠱⠉⠁⢝⠻⠎⣬⢌⡌⣬⣡⣀⣢⣄⡄⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀
252
252
+
⠀⠀⠀⠀⠀⠀⠀⢀⢸⣿⣿⢿⣾⣯⣑⢄⡂⠀⠄⠂⠀⠀⢀⠀⠀⠐⠀⠀⠀⠀⠀⠀⠀⠀⠄⠐⠀⠀⠀⠀⣄⠭⠂⠈⠜⣩⣿⢝⠃⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀
253
253
+
⠀⠀⠀⠀⠀⠀⠀⢀⣻⡟⠏⠀⠚⠈⠚⡉⡝⢶⣱⢤⣅⠈⠀⠄⠀⠀⠀⠀⠀⠠⠀⠀⡂⠐⣤⢕⡪⢼⣈⡹⡇⠏⠏⠋⠅⢃⣪⡏⡇⡍⠀⠀⠀⠀⠀⠀⠀⠀⠀
254
254
+
⠀⠀⠀⠀⠀⠀⠀⠀⠺⣻⡄⠀⠀⠀⢠⠌⠃⠐⠉⢡⠱⠧⠝⡯⣮⢶⣴⣤⡆⢐⣣⢅⣮⡟⠦⠍⠉⠀⠁⠐⠀⠀⠀⠄⠐⠡⣽⡸⣎⢁⠀⠀⠀⠀⠀⠀⠀⠀⠀
255
255
+
⠀⠀⠀⠀⠀⠀⠀⢈⡻⣧⠀⠁⠐⠀⠀⠀⠀⠀⠀⠊⠀⠕⢀⡉⠈⡫⠽⡿⡟⠿⠟⠁⠀⠀⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠬⠥⣋⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
256
256
+
⠀⠀⠀⠀⠀⠀⠀⡀⣾⡍⠕⡀⠀⠀⠀⠄⠠⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠥⣤⢌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠄⢀⠀⢝⢞⣫⡆⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀
257
257
+
⠀⠀⠀⠀⠀⠀⠀⠀⣽⡶⡄⠐⡀⠀⠀⠀⠀⠀⠀⢀⠀⠄⠀⠀⠀⠄⠁⠇⣷⡆⠀⠀⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡸⢝⣮⠍⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
258
258
+
⠀⠀⠀⠀⠀⠀⢀⠀⢾⣷⠀⠠⡀⠀⠀⠀⠀⢀⠀⠀⠀⠀⠀⠁⡁⠀⠀⣾⡥⠖⠀⠀⠀⠂⠀⠀⠀⠀⠀⠁⠀⡀⠁⠀⠀⠻⢳⣻⢄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
259
259
+
⠀⠀⠀⠀⠀⠀⠀⠀⣞⡙⠨⣀⠠⠄⠀⠂⠀⠀⠀⠈⢀⠀⠀⠀⠀⠀⠤⢚⢢⣟⠀⠀⠀⠀⡐⠀⠀⡀⠀⠀⠀⠀⠁⠈⠌⠊⣯⣮⡏⠡⠂⠀⠀⠀⠀⠀⠀⠀⠀
260
260
+
⠀⠀⠀⠀⠀⠀⠀⠀⣻⡟⡄⡡⣄⠀⠠⠀⠀⡅⠀⠐⠀⡀⠀⡀⠀⠄⠈⠃⠳⠪⠤⠀⠀⠀⠀⡀⠀⠂⠀⠀⠀⠁⠈⢠⣠⠒⠻⣻⡧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
261
261
+
⠀⠀⠀⠀⠀⠀⠀⠀⠪⡎⠠⢌⠑⡀⠂⠀⠄⠠⠀⠠⠀⠁⡀⠠⠠⡀⣀⠜⢏⡅⠀⠀⡀⠁⠀⠀⠁⠁⠐⠄⡀⢀⠂⠀⠄⢑⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
262
262
+
⠀⠀⠀⠀⠀⠀⠀⠀⠼⣻⠧⣣⣀⠐⠨⠁⠕⢈⢀⢀⡁⠀⠈⠠⢀⠀⠐⠜⣽⡗⡤⠀⠂⠀⠠⠀⢂⠠⠀⠁⠄⠀⠔⠀⠑⣨⣿⢯⠋⡅⠀⠀⠀⠀⠀⠀⠀⠀⠀
263
263
+
⠀⠀⠀⠀⠀⠀⠀⠀⡚⣷⣭⠎⢃⡗⠄⡄⢀⠁⠀⠅⢀⢅⡀⠠⠀⢠⡀⡩⠷⢇⠀⡀⠄⡠⠤⠆⣀⡀⠄⠉⣠⠃⠴⠀⠈⢁⣿⡛⡯⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
264
264
+
⠀⠀⠀⠀⠀⠀⠀⠘⡬⡿⣿⡏⡻⡯⠌⢁⢛⠠⠓⠐⠐⠐⠌⠃⠋⠂⡢⢰⣈⢏⣰⠂⠈⠀⠠⠒⠡⠌⠫⠭⠩⠢⡬⠆⠿⢷⢿⡽⡧⠉⠊⠀⠀⠀⠀⠀⠀⠀⠀
265
265
+
⠀⠀⠀⠀⠀⠀⠀⠀⠺⣷⣺⣗⣿⡶⡎⡅⣣⢎⠠⡅⣢⡖⠴⠬⡈⠂⡨⢡⠾⣣⣢⠀⠀⡹⠄⡄⠄⡇⣰⡖⡊⠔⢹⣄⣿⣭⣵⣿⢷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
266
266
+
⠀⠀⠀⠀⠀⠀⠀⠀⠩⣿⣿⣲⣿⣷⣟⣼⠟⣬⢉⡠⣪⢜⣂⣁⠥⠓⠚⡁⢶⣷⣠⠂⡄⡢⣀⡐⠧⢆⣒⡲⡳⡫⢟⡃⢪⡧⣟⡟⣯⠐⠀⠀⠀⠀⠀⠀⠀⠀⠀
267
267
+
⠀⠀⠀⠀⠀⠀⠀⠀⢺⠟⢿⢟⢻⡗⡮⡿⣲⢷⣆⣏⣇⡧⣄⢖⠾⡷⣿⣤⢳⢷⣣⣦⡜⠗⣭⢂⠩⣹⢿⡲⢎⡧⣕⣖⣓⣽⡿⡖⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
268
268
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠂⠂⠏⠿⢻⣥⡪⢽⣳⣳⣥⡶⣫⣍⢐⣥⣻⣾⡻⣅⢭⡴⢭⣿⠕⣧⡭⣞⣻⣣⣻⢿⠟⠛⠙⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
269
269
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠄⠋⠫⠯⣍⢻⣿⣿⣷⣕⣵⣹⣽⣿⣷⣇⡏⣿⡿⣍⡝⠵⠯⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
270
270
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠠⠁⠋⢣⠓⡍⣫⠹⣿⣿⣷⡿⠯⠺⠁⠁⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
271
271
+
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠋⢀⠋⢈⡿⠿⠁⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
272
272
+
273
273
+
plcbundle server (%s)
249
274
250
250
-
`)
275
275
+
`, version)
251
276
252
277
fmt.Fprintf(w, "What is PLC Bundle?\n")
253
278
fmt.Fprintf(w, "━━━━━━━━━━━━━━━━━━━━\n")
···
375
400
}
376
401
377
402
fmt.Fprintf(w, "\n────────────────────────────────────────────────────────────────\n")
378
378
-
fmt.Fprintf(w, "plcbundle v%s | https://github.com/atscan/plcbundle\n", version)
403
403
+
fmt.Fprintf(w, "plcbundle %s | https://github.com/atscan/plcbundle\n", version)
379
404
}
380
405
381
406
// handleSync returns sync status and mempool info as JSON
+1
-1
go.mod
···
4
4
5
5
require github.com/klauspost/compress v1.18.1
6
6
7
7
-
require github.com/gorilla/websocket v1.5.3 // indirect
7
7
+
require github.com/gorilla/websocket v1.5.3
+259
plc/plc_test.go
···
1
1
+
package plc_test
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/json"
6
6
+
"net/http"
7
7
+
"net/http/httptest"
8
8
+
"testing"
9
9
+
"time"
10
10
+
11
11
+
"github.com/atscan/plcbundle/bundle"
12
12
+
"github.com/atscan/plcbundle/plc"
13
13
+
)
14
14
+
15
15
+
// TestPLCOperation tests operation parsing and methods
16
16
+
func TestPLCOperation(t *testing.T) {
17
17
+
t.Run("IsNullified", func(t *testing.T) {
18
18
+
tests := []struct {
19
19
+
name string
20
20
+
nullified interface{}
21
21
+
want bool
22
22
+
}{
23
23
+
{"nil", nil, false},
24
24
+
{"false", false, false},
25
25
+
{"true", true, true},
26
26
+
{"empty string", "", false},
27
27
+
{"non-empty string", "cid123", true},
28
28
+
}
29
29
+
30
30
+
for _, tt := range tests {
31
31
+
t.Run(tt.name, func(t *testing.T) {
32
32
+
op := plc.PLCOperation{Nullified: tt.nullified}
33
33
+
if got := op.IsNullified(); got != tt.want {
34
34
+
t.Errorf("IsNullified() = %v, want %v", got, tt.want)
35
35
+
}
36
36
+
})
37
37
+
}
38
38
+
})
39
39
+
40
40
+
t.Run("GetNullifyingCID", func(t *testing.T) {
41
41
+
op := plc.PLCOperation{Nullified: "bafytest123"}
42
42
+
if cid := op.GetNullifyingCID(); cid != "bafytest123" {
43
43
+
t.Errorf("expected 'bafytest123', got '%s'", cid)
44
44
+
}
45
45
+
46
46
+
op2 := plc.PLCOperation{Nullified: true}
47
47
+
if cid := op2.GetNullifyingCID(); cid != "" {
48
48
+
t.Errorf("expected empty string, got '%s'", cid)
49
49
+
}
50
50
+
})
51
51
+
52
52
+
t.Run("JSONParsing", func(t *testing.T) {
53
53
+
jsonData := `{
54
54
+
"did": "did:plc:test123",
55
55
+
"cid": "bafytest",
56
56
+
"createdAt": "2024-01-01T12:00:00.000Z",
57
57
+
"operation": {"type": "create"},
58
58
+
"nullified": false
59
59
+
}`
60
60
+
61
61
+
var op plc.PLCOperation
62
62
+
if err := json.Unmarshal([]byte(jsonData), &op); err != nil {
63
63
+
t.Fatalf("failed to parse operation: %v", err)
64
64
+
}
65
65
+
66
66
+
if op.DID != "did:plc:test123" {
67
67
+
t.Errorf("unexpected DID: %s", op.DID)
68
68
+
}
69
69
+
if op.CID != "bafytest" {
70
70
+
t.Errorf("unexpected CID: %s", op.CID)
71
71
+
}
72
72
+
})
73
73
+
}
74
74
+
75
75
+
// TestClient tests PLC client operations
76
76
+
func TestClient(t *testing.T) {
77
77
+
t.Run("Export", func(t *testing.T) {
78
78
+
// Create mock server
79
79
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
80
80
+
if r.URL.Path != "/export" {
81
81
+
t.Errorf("unexpected path: %s", r.URL.Path)
82
82
+
}
83
83
+
84
84
+
// Check query parameters
85
85
+
count := r.URL.Query().Get("count")
86
86
+
if count != "100" {
87
87
+
t.Errorf("unexpected count: %s", count)
88
88
+
}
89
89
+
90
90
+
// Return mock JSONL data
91
91
+
w.Header().Set("Content-Type", "application/x-ndjson")
92
92
+
for i := 0; i < 10; i++ {
93
93
+
op := plc.PLCOperation{
94
94
+
DID: "did:plc:test" + string(rune(i)),
95
95
+
CID: "bafytest" + string(rune(i)),
96
96
+
CreatedAt: time.Now(),
97
97
+
Operation: map[string]interface{}{"type": "create"},
98
98
+
}
99
99
+
json.NewEncoder(w).Encode(op)
100
100
+
}
101
101
+
}))
102
102
+
defer server.Close()
103
103
+
104
104
+
// Create client
105
105
+
client := plc.NewClient(server.URL)
106
106
+
defer client.Close()
107
107
+
108
108
+
// Test export
109
109
+
ctx := context.Background()
110
110
+
ops, err := client.Export(ctx, plc.ExportOptions{
111
111
+
Count: 100,
112
112
+
})
113
113
+
if err != nil {
114
114
+
t.Fatalf("Export failed: %v", err)
115
115
+
}
116
116
+
117
117
+
if len(ops) != 10 {
118
118
+
t.Errorf("expected 10 operations, got %d", len(ops))
119
119
+
}
120
120
+
121
121
+
// Check that RawJSON is preserved
122
122
+
if len(ops[0].RawJSON) == 0 {
123
123
+
t.Error("RawJSON not preserved")
124
124
+
}
125
125
+
})
126
126
+
127
127
+
t.Run("RateLimitRetry", func(t *testing.T) {
128
128
+
attempts := 0
129
129
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
130
130
+
attempts++
131
131
+
if attempts < 2 {
132
132
+
// Return 429 on first attempt
133
133
+
w.Header().Set("Retry-After", "1")
134
134
+
w.WriteHeader(http.StatusTooManyRequests)
135
135
+
return
136
136
+
}
137
137
+
// Success on second attempt
138
138
+
w.Header().Set("Content-Type", "application/x-ndjson")
139
139
+
op := plc.PLCOperation{DID: "did:plc:test", CID: "bafytest", CreatedAt: time.Now()}
140
140
+
json.NewEncoder(w).Encode(op)
141
141
+
}))
142
142
+
defer server.Close()
143
143
+
144
144
+
client := plc.NewClient(server.URL)
145
145
+
defer client.Close()
146
146
+
147
147
+
ctx := context.Background()
148
148
+
ops, err := client.Export(ctx, plc.ExportOptions{Count: 1})
149
149
+
if err != nil {
150
150
+
t.Fatalf("Export failed after retry: %v", err)
151
151
+
}
152
152
+
153
153
+
if len(ops) != 1 {
154
154
+
t.Errorf("expected 1 operation, got %d", len(ops))
155
155
+
}
156
156
+
157
157
+
if attempts < 2 {
158
158
+
t.Errorf("expected at least 2 attempts, got %d", attempts)
159
159
+
}
160
160
+
})
161
161
+
162
162
+
t.Run("GetDID", func(t *testing.T) {
163
163
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
164
164
+
if r.URL.Path != "/did:plc:test123" {
165
165
+
t.Errorf("unexpected path: %s", r.URL.Path)
166
166
+
}
167
167
+
168
168
+
doc := plc.DIDDocument{
169
169
+
Context: []string{"https://www.w3.org/ns/did/v1"},
170
170
+
ID: "did:plc:test123",
171
171
+
}
172
172
+
json.NewEncoder(w).Encode(doc)
173
173
+
}))
174
174
+
defer server.Close()
175
175
+
176
176
+
client := plc.NewClient(server.URL)
177
177
+
defer client.Close()
178
178
+
179
179
+
ctx := context.Background()
180
180
+
doc, err := client.GetDID(ctx, "did:plc:test123")
181
181
+
if err != nil {
182
182
+
t.Fatalf("GetDID failed: %v", err)
183
183
+
}
184
184
+
185
185
+
if doc.ID != "did:plc:test123" {
186
186
+
t.Errorf("unexpected DID: %s", doc.ID)
187
187
+
}
188
188
+
})
189
189
+
}
190
190
+
191
191
+
// TestRateLimiter tests rate limiting functionality
192
192
+
func TestRateLimiter(t *testing.T) {
193
193
+
t.Run("BasicRateLimit", func(t *testing.T) {
194
194
+
// 10 requests per second
195
195
+
rl := plc.NewRateLimiter(10, time.Second)
196
196
+
defer rl.Stop()
197
197
+
198
198
+
ctx := context.Background()
199
199
+
200
200
+
// First 10 should be fast
201
201
+
start := time.Now()
202
202
+
for i := 0; i < 10; i++ {
203
203
+
if err := rl.Wait(ctx); err != nil {
204
204
+
t.Fatalf("Wait failed: %v", err)
205
205
+
}
206
206
+
}
207
207
+
elapsed := time.Since(start)
208
208
+
209
209
+
// Should be very fast (less than 100ms)
210
210
+
if elapsed > 100*time.Millisecond {
211
211
+
t.Errorf("expected fast execution, took %v", elapsed)
212
212
+
}
213
213
+
})
214
214
+
215
215
+
t.Run("ContextCancellation", func(t *testing.T) {
216
216
+
rl := plc.NewRateLimiter(1, time.Minute) // Very slow rate
217
217
+
defer rl.Stop()
218
218
+
219
219
+
// Consume the one available token
220
220
+
ctx := context.Background()
221
221
+
rl.Wait(ctx)
222
222
+
223
223
+
// Try to wait with cancelled context
224
224
+
cancelCtx, cancel := context.WithCancel(context.Background())
225
225
+
cancel() // Cancel immediately
226
226
+
227
227
+
err := rl.Wait(cancelCtx)
228
228
+
if err != context.Canceled {
229
229
+
t.Errorf("expected context.Canceled, got %v", err)
230
230
+
}
231
231
+
})
232
232
+
}
233
233
+
234
234
+
// Benchmark tests
235
235
+
func BenchmarkSerializeJSONL(b *testing.B) {
236
236
+
ops := make([]plc.PLCOperation, 10000)
237
237
+
for i := 0; i < 10000; i++ {
238
238
+
ops[i] = plc.PLCOperation{
239
239
+
DID: "did:plc:test",
240
240
+
CID: "bafytest",
241
241
+
CreatedAt: time.Now(),
242
242
+
Operation: map[string]interface{}{"type": "create"},
243
243
+
}
244
244
+
}
245
245
+
246
246
+
logger := &benchLogger{}
247
247
+
operations, _ := bundle.NewOperations(bundle.CompressionBetter, logger)
248
248
+
defer operations.Close()
249
249
+
250
250
+
b.ResetTimer()
251
251
+
for i := 0; i < b.N; i++ {
252
252
+
_ = operations.SerializeJSONL(ops)
253
253
+
}
254
254
+
}
255
255
+
256
256
+
type benchLogger struct{}
257
257
+
258
258
+
func (l *benchLogger) Printf(format string, v ...interface{}) {}
259
259
+
func (l *benchLogger) Println(v ...interface{}) {}
+7
plc_bundles.json
···
1
1
+
{
2
2
+
"version": "1.0",
3
3
+
"last_bundle": 0,
4
4
+
"updated_at": "2025-10-28T02:44:11.775384Z",
5
5
+
"total_size_bytes": 0,
6
6
+
"bundles": []
7
7
+
}