engine_test.mx raw
1 package store
2
3 import (
4 "bytes"
5 "encoding/hex"
6 "fmt"
7 "os"
8 "testing"
9
10 "smesh.lol/pkg/nostr/event"
11 "smesh.lol/pkg/nostr/filter"
12 "smesh.lol/pkg/nostr/kind"
13 "smesh.lol/pkg/nostr/signer/p8k"
14 "smesh.lol/pkg/nostr/tag"
15 "smesh.lol/pkg/nostr/timestamp"
16 )
17
18 func makeSigner(t *testing.T) *p8k.Signer {
19 t.Helper()
20 s := p8k.MustNew()
21 if err := s.Generate(); err != nil {
22 t.Fatal(err)
23 }
24 return s
25 }
26
27 func makeEvent(t *testing.T, s *p8k.Signer, k uint16, ts int64, tags *tag.S, content string) *event.E {
28 t.Helper()
29 ev := &event.E{
30 CreatedAt: ts,
31 Kind: k,
32 Tags: tags,
33 Content: []byte(content),
34 }
35 if err := ev.Sign(s); err != nil {
36 t.Fatal(err)
37 }
38 return ev
39 }
40
41 func openTmp(t *testing.T) *Engine {
42 t.Helper()
43 dir := t.TempDir()
44 eng, err := Open(dir)
45 if err != nil {
46 t.Fatal(err)
47 }
48 return eng
49 }
50
51 func TestSaveAndGetBySerial(t *testing.T) {
52 eng := openTmp(t)
53 defer eng.Close()
54
55 s := makeSigner(t)
56 ev := makeEvent(t, s, 1, 1700000000, nil, "hello")
57
58 if err := eng.SaveEvent(ev); err != nil {
59 t.Fatal(err)
60 }
61 // Duplicate should fail.
62 if err := eng.SaveEvent(ev); err == nil {
63 t.Fatal("expected duplicate error")
64 }
65 }
66
67 func TestQueryByID(t *testing.T) {
68 eng := openTmp(t)
69 defer eng.Close()
70
71 s := makeSigner(t)
72 ev := makeEvent(t, s, 1, 1700000000, nil, "find me")
73 eng.SaveEvent(ev)
74
75 f := &filter.F{Ids: tag.NewFromBytesSlice(ev.ID)}
76 results, err := eng.QueryEvents(f)
77 if err != nil {
78 t.Fatal(err)
79 }
80 if len(results) != 1 {
81 t.Fatalf("expected 1 result, got %d", len(results))
82 }
83 if !bytes.Equal(results[0].ID, ev.ID) {
84 t.Fatal("ID mismatch")
85 }
86 }
87
88 func TestQueryByKind(t *testing.T) {
89 eng := openTmp(t)
90 defer eng.Close()
91
92 s := makeSigner(t)
93 for i := 0; i < 10; i++ {
94 k := uint16(1)
95 if i%3 == 0 {
96 k = 7
97 }
98 eng.SaveEvent(makeEvent(t, s, k, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i)))
99 }
100
101 f := &filter.F{Kinds: kind.NewS(kind.New(uint16(7)))}
102 results, err := eng.QueryEvents(f)
103 if err != nil {
104 t.Fatal(err)
105 }
106 if len(results) != 4 { // i=0,3,6,9
107 t.Fatalf("expected 4 kind-7 events, got %d", len(results))
108 }
109 for _, r := range results {
110 if r.Kind != 7 {
111 t.Fatalf("expected kind 7, got %d", r.Kind)
112 }
113 }
114 }
115
116 func TestQueryByAuthor(t *testing.T) {
117 eng := openTmp(t)
118 defer eng.Close()
119
120 s1 := makeSigner(t)
121 s2 := makeSigner(t)
122 for i := 0; i < 10; i++ {
123 signer := s1
124 if i%2 == 0 {
125 signer = s2
126 }
127 eng.SaveEvent(makeEvent(t, signer, 1, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i)))
128 }
129
130 f := &filter.F{Authors: tag.NewFromBytesSlice(s1.Pub())}
131 results, err := eng.QueryEvents(f)
132 if err != nil {
133 t.Fatal(err)
134 }
135 if len(results) != 5 {
136 t.Fatalf("expected 5 events by s1, got %d", len(results))
137 }
138 for _, r := range results {
139 if !bytes.Equal(r.Pubkey, s1.Pub()) {
140 t.Fatal("pubkey mismatch")
141 }
142 }
143 }
144
145 func TestQueryByTag(t *testing.T) {
146 eng := openTmp(t)
147 defer eng.Close()
148
149 s := makeSigner(t)
150 for i := 0; i < 10; i++ {
151 var tags *tag.S
152 if i%2 == 0 {
153 tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("alpha")))
154 } else {
155 tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("beta")))
156 }
157 eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), tags, fmt.Sprintf("msg %d", i)))
158 }
159
160 f := &filter.F{
161 Tags: tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("alpha"))),
162 }
163 results, err := eng.QueryEvents(f)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if len(results) != 5 {
168 t.Fatalf("expected 5 events with t=alpha, got %d", len(results))
169 }
170 }
171
172 func TestQueryByPTag(t *testing.T) {
173 eng := openTmp(t)
174 defer eng.Close()
175
176 s := makeSigner(t)
177 target := makeSigner(t)
178 targetHex := hex.EncodeToString(target.Pub())
179
180 for i := 0; i < 5; i++ {
181 tags := tag.NewS(tag.NewFromBytesSlice([]byte("p"), []byte(targetHex)))
182 eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), tags, fmt.Sprintf("mention %d", i)))
183 }
184 for i := 5; i < 10; i++ {
185 eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), nil, fmt.Sprintf("no mention %d", i)))
186 }
187
188 f := &filter.F{
189 Tags: tag.NewS(tag.NewFromBytesSlice([]byte("p"), []byte(targetHex))),
190 }
191 results, err := eng.QueryEvents(f)
192 if err != nil {
193 t.Fatal(err)
194 }
195 if len(results) != 5 {
196 t.Fatalf("expected 5 events mentioning target, got %d", len(results))
197 }
198 }
199
200 func TestQueryByTimeRange(t *testing.T) {
201 eng := openTmp(t)
202 defer eng.Close()
203
204 s := makeSigner(t)
205 for i := 0; i < 20; i++ {
206 eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i*100), nil, fmt.Sprintf("msg %d", i)))
207 }
208
209 since := int64(1700000500)
210 until := int64(1700001500)
211 f := &filter.F{
212 Since: timestamp.FromUnix(since),
213 Until: timestamp.FromUnix(until),
214 }
215 results, err := eng.QueryEvents(f)
216 if err != nil {
217 t.Fatal(err)
218 }
219 for _, r := range results {
220 if r.CreatedAt < since || r.CreatedAt > until {
221 t.Fatalf("event %d out of range [%d, %d]", r.CreatedAt, since, until)
222 }
223 }
224 // Events at 500,600,...1500 → 11 events.
225 if len(results) != 11 {
226 t.Fatalf("expected 11 events in range, got %d", len(results))
227 }
228 }
229
230 func TestQueryWithLimit(t *testing.T) {
231 eng := openTmp(t)
232 defer eng.Close()
233
234 s := makeSigner(t)
235 for i := 0; i < 50; i++ {
236 eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i)))
237 }
238
239 limit := uint(10)
240 f := &filter.F{
241 Kinds: kind.NewS(kind.New(uint16(1))),
242 Limit: &limit,
243 }
244 results, err := eng.QueryEvents(f)
245 if err != nil {
246 t.Fatal(err)
247 }
248 if len(results) != 10 {
249 t.Fatalf("expected 10, got %d", len(results))
250 }
251 // Should be newest first.
252 for i := 1; i < len(results); i++ {
253 if results[i].CreatedAt > results[i-1].CreatedAt {
254 t.Fatal("results not in reverse chronological order")
255 }
256 }
257 }
258
259 func TestBulk10k(t *testing.T) {
260 if testing.Short() {
261 t.Skip("skipping bulk test in short mode")
262 }
263 dir, err := os.MkdirTemp("", "store-bulk-*")
264 if err != nil {
265 t.Fatal(err)
266 }
267 defer os.RemoveAll(dir)
268
269 eng, err := Open(dir)
270 if err != nil {
271 t.Fatal(err)
272 }
273
274 const nSigners = 10
275 const nEvents = 10000
276 signers := []*p8k.Signer{:nSigners}
277 for i := range signers {
278 signers[i] = makeSigner(t)
279 }
280
281 kinds := []uint16{1, 7, 30023, 10002}
282 tagValues := []string{"nostr", "bitcoin", "lightning", "zap", "relay"}
283
284 for i := 0; i < nEvents; i++ {
285 s := signers[i%nSigners]
286 k := kinds[i%len(kinds)]
287 ts := int64(1700000000 + i)
288 tags := tag.NewS(
289 tag.NewFromBytesSlice([]byte("t"), []byte(tagValues[i%len(tagValues)])),
290 )
291 if i%5 == 0 {
292 other := signers[(i+1)%nSigners]
293 otherHex := hex.EncodeToString(other.Pub())
294 tags.Append(tag.NewFromBytesSlice([]byte("p"), []byte(otherHex)))
295 }
296 ev := makeEvent(t, s, k, ts, tags, fmt.Sprintf("event %d", i))
297 if err := eng.SaveEvent(ev); err != nil {
298 t.Fatalf("save event %d: %v", i, err)
299 }
300 }
301
302 if err := eng.Flush(); err != nil {
303 t.Fatal(err)
304 }
305
306 // Query by author.
307 f := &filter.F{Authors: tag.NewFromBytesSlice(signers[0].Pub())}
308 results, err := eng.QueryEvents(f)
309 if err != nil {
310 t.Fatal(err)
311 }
312 if len(results) != nEvents/nSigners {
313 t.Fatalf("author query: expected %d, got %d", nEvents/nSigners, len(results))
314 }
315
316 // Query by kind.
317 f = &filter.F{Kinds: kind.NewS(kind.New(uint16(7)))}
318 results, err = eng.QueryEvents(f)
319 if err != nil {
320 t.Fatal(err)
321 }
322 expected := 0
323 for i := 0; i < nEvents; i++ {
324 if kinds[i%len(kinds)] == 7 {
325 expected++
326 }
327 }
328 if len(results) != expected {
329 t.Fatalf("kind query: expected %d, got %d", expected, len(results))
330 }
331
332 // Query by tag.
333 f = &filter.F{
334 Tags: tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("nostr"))),
335 }
336 results, err = eng.QueryEvents(f)
337 if err != nil {
338 t.Fatal(err)
339 }
340 expected = 0
341 for i := 0; i < nEvents; i++ {
342 if tagValues[i%len(tagValues)] == "nostr" {
343 expected++
344 }
345 }
346 if len(results) != expected {
347 t.Fatalf("tag query: expected %d, got %d", expected, len(results))
348 }
349
350 // Query by time range.
351 since := int64(1700005000)
352 until := int64(1700005099)
353 f = &filter.F{
354 Since: timestamp.FromUnix(since),
355 Until: timestamp.FromUnix(until),
356 }
357 results, err = eng.QueryEvents(f)
358 if err != nil {
359 t.Fatal(err)
360 }
361 if len(results) != 100 {
362 t.Fatalf("time range query: expected 100, got %d", len(results))
363 }
364
365 eng.Close()
366 }
367