engine_test.go raw
1 package store
2
3 import (
4 "bytes"
5 "encoding/binary"
6 "fmt"
7 "hash/crc32"
8 "os"
9 "path/filepath"
10 "testing"
11 "time"
12
13 "git.smesh.lol/torly/pkg/nostr"
14 )
15
16 func testKey(seed byte) [32]byte {
17 var sk [32]byte
18 sk[31] = seed
19 return sk
20 }
21
22 func signedEvent(t *testing.T, seed byte, kind int, created int64, tags nostr.Tags, content string) *nostr.Event {
23 t.Helper()
24 ev := &nostr.Event{Kind: kind, CreatedAt: created, Tags: tags, Content: content}
25 if err := ev.Sign(testKey(seed)); err != nil {
26 t.Fatalf("sign: %v", err)
27 }
28 return ev
29 }
30
31 func openTest(t *testing.T) (*Engine, string) {
32 t.Helper()
33 dir := t.TempDir()
34 e, err := Open(dir, Options{})
35 if err != nil {
36 t.Fatalf("open: %v", err)
37 }
38 t.Cleanup(func() { e.Close() })
39 return e, dir
40 }
41
42 func save(t *testing.T, e *Engine, ev *nostr.Event) {
43 t.Helper()
44 ok, err := e.SaveEvent(ev)
45 if err != nil {
46 t.Fatalf("save kind=%d: %v", ev.Kind, err)
47 }
48 if !ok {
49 t.Fatal("save reported not stored")
50 }
51 }
52
53 func ids(evs []*nostr.Event) []string {
54 out := make([]string, len(evs))
55 for i, ev := range evs {
56 out[i] = nostr.Hex(ev.ID[:])[:12]
57 }
58 return out
59 }
60
61 func TestSaveAndQuery(t *testing.T) {
62 e, _ := openTest(t)
63 now := int64(1700000000)
64 ev1 := signedEvent(t, 1, 1, now, nostr.Tags{{"t", "nostr"}, {"p", "abc"}}, "hello world")
65 ev2 := signedEvent(t, 2, 1, now+10, nostr.Tags{{"t", "go"}}, "second event")
66 ev3 := signedEvent(t, 1, 0, now+20, nil, `{"name":"alice"}`)
67 ev4 := signedEvent(t, 3, 7, now+30, nil, "reaction")
68 for _, ev := range []*nostr.Event{ev1, ev2, ev3, ev4} {
69 save(t, e, ev)
70 }
71
72 // Duplicate is rejected.
73 if ok, err := e.SaveEvent(ev1); ok || err != ErrDuplicate {
74 t.Fatalf("duplicate: ok=%v err=%v", ok, err)
75 }
76
77 got, err := e.Query(&nostr.Filter{Kinds: []int{1}})
78 if err != nil {
79 t.Fatal(err)
80 }
81 if len(got) != 2 {
82 t.Fatalf("kinds=1: got %d events", len(got))
83 }
84 if got[0].ID != ev2.ID {
85 t.Fatalf("newest first: got %x want %x", got[0].ID, ev2.ID)
86 }
87
88 got, err = e.Query(&nostr.Filter{Authors: []string{nostr.Hex(ev1.PubKey[:])}})
89 if err != nil {
90 t.Fatal(err)
91 }
92 if len(got) != 2 {
93 t.Fatalf("author: got %d, want 2", len(got))
94 }
95
96 got, err = e.Query(&nostr.Filter{Tags: map[string][]string{"t": {"nostr"}}})
97 if err != nil {
98 t.Fatal(err)
99 }
100 if len(got) != 1 || got[0].ID != ev1.ID {
101 t.Fatalf("tag query: got %v", ids(got))
102 }
103
104 got, err = e.Query(&nostr.Filter{IDs: []string{nostr.Hex(ev3.ID[:])}})
105 if err != nil {
106 t.Fatal(err)
107 }
108 if len(got) != 1 || got[0].ID != ev3.ID {
109 t.Fatalf("id query: got %v", ids(got))
110 }
111
112 got, err = e.Query(&nostr.Filter{})
113 if err != nil {
114 t.Fatal(err)
115 }
116 if len(got) != 4 {
117 t.Fatalf("all: got %d, want 4", len(got))
118 }
119
120 got, err = e.Query(&nostr.Filter{Limit: 2})
121 if err != nil {
122 t.Fatal(err)
123 }
124 if len(got) != 2 || got[0].ID != ev4.ID {
125 t.Fatalf("limit: got %v", ids(got))
126 }
127
128 if n, approx, err := e.Count(&nostr.Filter{Kinds: []int{1}}); err != nil || n != 2 || approx {
129 t.Fatalf("count: n=%d approx=%v err=%v", n, approx, err)
130 }
131 }
132
133 func TestSearch(t *testing.T) {
134 e, _ := openTest(t)
135 ev1 := signedEvent(t, 1, 1, 100, nil, "The quick brown fox jumps")
136 ev2 := signedEvent(t, 1, 1, 200, nil, "A slow blue fox sleeps")
137 for _, ev := range []*nostr.Event{ev1, ev2} {
138 save(t, e, ev)
139 }
140 got, err := e.Query(&nostr.Filter{Search: "fox"})
141 if err != nil || len(got) != 2 {
142 t.Fatalf("fox: got %d err=%v", len(got), err)
143 }
144 got, err = e.Query(&nostr.Filter{Search: "quick brown"})
145 if err != nil || len(got) != 1 || got[0].ID != ev1.ID {
146 t.Fatalf("quick brown: got %v err=%v", ids(got), err)
147 }
148 got, err = e.Query(&nostr.Filter{Search: "quick slow"})
149 if err != nil || len(got) != 0 {
150 t.Fatalf("quick slow: got %d err=%v", len(got), err)
151 }
152 }
153
154 func TestReplaceable(t *testing.T) {
155 e, _ := openTest(t)
156 older := signedEvent(t, 1, 0, 100, nil, `{"name":"old"}`)
157 newer := signedEvent(t, 1, 0, 200, nil, `{"name":"new"}`)
158 save(t, e, older)
159 save(t, e, newer)
160
161 got, err := e.Query(&nostr.Filter{Kinds: []int{0}})
162 if err != nil {
163 t.Fatal(err)
164 }
165 if len(got) != 1 || got[0].ID != newer.ID {
166 t.Fatalf("replaceable: got %v", ids(got))
167 }
168 // Re-publishing the older event is refused.
169 if ok, err := e.SaveEvent(older); ok || err != ErrReplaced {
170 t.Fatalf("older republish: ok=%v err=%v", ok, err)
171 }
172 if _, err := e.GetByID(older.ID[:]); err != ErrNotFound {
173 t.Fatalf("older should be gone, got %v", err)
174 }
175 }
176
177 func TestAddressable(t *testing.T) {
178 e, _ := openTest(t)
179 a := signedEvent(t, 1, 30023, 100, nostr.Tags{{"d", "post-1"}}, "first")
180 b := signedEvent(t, 1, 30023, 200, nostr.Tags{{"d", "post-1"}}, "second")
181 c := signedEvent(t, 1, 30023, 150, nostr.Tags{{"d", "post-2"}}, "other")
182 for _, ev := range []*nostr.Event{a, b, c} {
183 save(t, e, ev)
184 }
185 got, err := e.Query(&nostr.Filter{Kinds: []int{30023}})
186 if err != nil {
187 t.Fatal(err)
188 }
189 if len(got) != 2 {
190 t.Fatalf("addressable: got %d events, want 2 (one per d tag)", len(got))
191 }
192 for _, ev := range got {
193 if ev.DTag() == "post-1" && ev.ID != b.ID {
194 t.Fatalf("post-1 should be the newer event")
195 }
196 }
197 }
198
199 func TestDeletion(t *testing.T) {
200 e, _ := openTest(t)
201 target := signedEvent(t, 1, 1, 100, nil, "delete me")
202 save(t, e, target)
203
204 // Another author cannot delete it.
205 otherDel := signedEvent(t, 2, 5, 110, nostr.Tags{{"e", nostr.Hex(target.ID[:])}}, "")
206 save(t, e, otherDel)
207 if _, err := e.GetByID(target.ID[:]); err != nil {
208 t.Fatalf("target should survive a foreign deletion: %v", err)
209 }
210
211 del := signedEvent(t, 1, 5, 120, nostr.Tags{{"e", nostr.Hex(target.ID[:])}}, "")
212 save(t, e, del)
213 if _, err := e.GetByID(target.ID[:]); err != ErrNotFound {
214 t.Fatalf("target should be deleted, got %v", err)
215 }
216 // The deletion event itself remains.
217 got, err := e.Query(&nostr.Filter{Kinds: []int{5}})
218 if err != nil {
219 t.Fatal(err)
220 }
221 if len(got) != 2 {
222 t.Fatalf("deletion events: got %d, want 2", len(got))
223 }
224 }
225
226 func TestDeletionByAddress(t *testing.T) {
227 e, _ := openTest(t)
228 ev := signedEvent(t, 1, 30023, 100, nostr.Tags{{"d", "slug"}}, "article")
229 save(t, e, ev)
230 addr := fmt.Sprintf("30023:%s:slug", nostr.Hex(ev.PubKey[:]))
231 del := signedEvent(t, 1, 5, 120, nostr.Tags{{"a", addr}}, "")
232 save(t, e, del)
233 if _, err := e.GetByID(ev.ID[:]); err != ErrNotFound {
234 t.Fatalf("address deletion failed: %v", err)
235 }
236 }
237
238 func TestExpiration(t *testing.T) {
239 e, _ := openTest(t)
240 now := time.Now().Unix()
241 expired := signedEvent(t, 1, 1, 100, nostr.Tags{{"expiration", fmt.Sprint(now - 10)}}, "old")
242 live := signedEvent(t, 1, 1, 100, nostr.Tags{{"expiration", fmt.Sprint(now + 100000)}}, "live")
243 save(t, e, expired)
244 save(t, e, live)
245 n, err := e.PurgeExpired(now)
246 if err != nil {
247 t.Fatal(err)
248 }
249 if n != 1 {
250 t.Fatalf("purged %d, want 1", n)
251 }
252 got, err := e.Query(&nostr.Filter{Kinds: []int{1}})
253 if err != nil {
254 t.Fatal(err)
255 }
256 if len(got) != 1 || got[0].ID != live.ID {
257 t.Fatalf("after purge: %v", ids(got))
258 }
259 }
260
261 func TestReopenPersists(t *testing.T) {
262 dir := t.TempDir()
263 e, err := Open(dir, Options{})
264 if err != nil {
265 t.Fatal(err)
266 }
267 evs := make([]*nostr.Event, 0, 20)
268 for i := 0; i < 20; i++ {
269 ev := signedEvent(t, byte(i%3+1), 1, int64(1000+i), nostr.Tags{{"t", "persist"}}, fmt.Sprintf("event %d", i))
270 save(t, e, ev)
271 evs = append(evs, ev)
272 }
273 if err := e.Close(); err != nil {
274 t.Fatal(err)
275 }
276
277 e2, err := Open(dir, Options{})
278 if err != nil {
279 t.Fatal(err)
280 }
281 defer e2.Close()
282 got, err := e2.Query(&nostr.Filter{Tags: map[string][]string{"t": {"persist"}}})
283 if err != nil {
284 t.Fatal(err)
285 }
286 if len(got) != len(evs) {
287 t.Fatalf("after reopen: got %d events, want %d", len(got), len(evs))
288 }
289 for i := range evs {
290 if got[len(got)-1-i].ID != evs[i].ID {
291 t.Fatalf("order mismatch at %d", i)
292 }
293 }
294 }
295
296 func TestRecoveryFromWALWithoutCheckpoint(t *testing.T) {
297 dir := t.TempDir()
298 e, err := Open(dir, Options{})
299 if err != nil {
300 t.Fatal(err)
301 }
302 var want []*nostr.Event
303 for i := 0; i < 10; i++ {
304 ev := signedEvent(t, 1, 1, int64(2000+i), nostr.Tags{{"t", "wal"}}, "wal replay")
305 save(t, e, ev)
306 want = append(want, ev)
307 }
308 // Copy the directory while the engine is open: this is the state a crash
309 // leaves behind after the WAL writes but before a checkpoint.
310 copyDir := t.TempDir()
311 copyTree(t, dir, copyDir)
312 e.Close()
313
314 e2, err := Open(copyDir, Options{})
315 if err != nil {
316 t.Fatal(err)
317 }
318 defer e2.Close()
319 got, err := e2.Query(&nostr.Filter{Tags: map[string][]string{"t": {"wal"}}})
320 if err != nil {
321 t.Fatal(err)
322 }
323 if len(got) != len(want) {
324 t.Fatalf("recovered %d events, want %d", len(got), len(want))
325 }
326 }
327
328 func copyTree(t *testing.T, src, dst string) {
329 t.Helper()
330 err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
331 if err != nil {
332 return err
333 }
334 rel, err := filepath.Rel(src, path)
335 if err != nil {
336 return err
337 }
338 target := filepath.Join(dst, rel)
339 if info.IsDir() {
340 return os.MkdirAll(target, 0o755)
341 }
342 data, err := os.ReadFile(path)
343 if err != nil {
344 return err
345 }
346 return os.WriteFile(target, data, 0o644)
347 })
348 if err != nil {
349 t.Fatal(err)
350 }
351 }
352
353 func TestTombstoneSurvivesReopen(t *testing.T) {
354 dir := t.TempDir()
355 e, err := Open(dir, Options{})
356 if err != nil {
357 t.Fatal(err)
358 }
359 target := signedEvent(t, 1, 1, 100, nil, "gone")
360 save(t, e, target)
361 del := signedEvent(t, 1, 5, 110, nostr.Tags{{"e", nostr.Hex(target.ID[:])}}, "")
362 save(t, e, del)
363 e.Close()
364
365 e2, err := Open(dir, Options{})
366 if err != nil {
367 t.Fatal(err)
368 }
369 defer e2.Close()
370 if _, err := e2.GetByID(target.ID[:]); err != ErrNotFound {
371 t.Fatalf("deleted event returned after reopen: %v", err)
372 }
373 }
374
375 func TestEventEncodingRoundTrip(t *testing.T) {
376 ev := signedEvent(t, 1, 30023, 12345, nostr.Tags{{"d", "x"}, {"t", "tag"}, {"expiration", "99999"}}, "content \"quoted\"\n")
377 data := encodeEvent(ev)
378 got, err := decodeEvent(data)
379 if err != nil {
380 t.Fatal(err)
381 }
382 if !bytes.Equal(got.ID[:], ev.ID[:]) || got.Content != ev.Content || len(got.Tags) != len(ev.Tags) {
383 t.Fatalf("round trip mismatch: %+v vs %+v", got, ev)
384 }
385 if _, err := decodeEvent(data[:len(data)-1]); err == nil {
386 t.Fatal("truncated record should not decode")
387 }
388 }
389
390 // TestMultipleRunsSurviveReopen guards the index run model: each QuickFlush
391 // writes a separate sorted run, and a merge must not assume that concatenated
392 // runs are globally sorted.
393 func TestMultipleRunsSurviveReopen(t *testing.T) {
394 dir := t.TempDir()
395 e, err := Open(dir, Options{})
396 if err != nil {
397 t.Fatal(err)
398 }
399 ev1 := signedEvent(t, 1, 1, 100, nostr.Tags{{"t", "zeta"}}, "first run")
400 save(t, e, ev1)
401 // A query calls flushPending, turning the buffered records into a run.
402 if _, err := e.Query(&nostr.Filter{Kinds: []int{1}}); err != nil {
403 t.Fatal(err)
404 }
405 ev2 := signedEvent(t, 2, 1, 200, nostr.Tags{{"t", "alpha"}}, "second run")
406 save(t, e, ev2)
407 if _, err := e.Query(&nostr.Filter{Kinds: []int{1}}); err != nil {
408 t.Fatal(err)
409 }
410
411 // Both events must be visible before the merge as well.
412 got, err := e.Query(&nostr.Filter{Tags: map[string][]string{"t": {"zeta"}}})
413 if err != nil || len(got) != 1 || got[0].ID != ev1.ID {
414 t.Fatalf("pre-merge tag query: %v err=%v", ids(got), err)
415 }
416 if err := e.Close(); err != nil {
417 t.Fatal(err)
418 }
419
420 e2, err := Open(dir, Options{})
421 if err != nil {
422 t.Fatal(err)
423 }
424 defer e2.Close()
425 got, err = e2.Query(&nostr.Filter{Kinds: []int{1}})
426 if err != nil {
427 t.Fatal(err)
428 }
429 if len(got) != 2 {
430 t.Fatalf("after reopen: got %d events, want 2", len(got))
431 }
432 got, err = e2.Query(&nostr.Filter{Tags: map[string][]string{"t": {"alpha"}}})
433 if err != nil || len(got) != 1 || got[0].ID != ev2.ID {
434 t.Fatalf("alpha: %v err=%v", ids(got), err)
435 }
436 }
437
438 func TestTombstonedRepublishRejected(t *testing.T) {
439 e, _ := openTest(t)
440 target := signedEvent(t, 1, 1, 100, nil, "will be deleted")
441 save(t, e, target)
442 del := signedEvent(t, 1, 5, 110, nostr.Tags{{"e", nostr.Hex(target.ID[:])}}, "")
443 save(t, e, del)
444 if ok, err := e.SaveEvent(target); ok || err != ErrDeleted {
445 t.Fatalf("republish of deleted event: ok=%v err=%v", ok, err)
446 }
447 }
448
449 func TestDeleteAuthorization(t *testing.T) {
450 e, _ := openTest(t)
451 target := signedEvent(t, 1, 1, 100, nil, "delete authorization")
452 save(t, e, target)
453 other := signedEvent(t, 2, 1, 100, nil, "other").PubKey
454 if ok, err := e.Delete(target.ID[:], other[:]); ok || err != nil {
455 t.Fatalf("foreign delete: ok=%v err=%v", ok, err)
456 }
457 if ok, err := e.Delete(make([]byte, 32), target.PubKey[:]); ok || err != nil {
458 t.Fatalf("missing delete: ok=%v err=%v", ok, err)
459 }
460 if ok, err := e.Delete(target.ID[:], target.PubKey[:]); !ok || err != nil {
461 t.Fatalf("owner delete: ok=%v err=%v", ok, err)
462 }
463 }
464
465 func TestEmptyStore(t *testing.T) {
466 e, _ := openTest(t)
467 got, err := e.Query(&nostr.Filter{})
468 if err != nil || len(got) != 0 {
469 t.Fatalf("query: %d %v", len(got), err)
470 }
471 n, approx, err := e.Count(&nostr.Filter{})
472 if err != nil || n != 0 || approx {
473 t.Fatalf("count: %d %v %v", n, approx, err)
474 }
475 if _, err := e.GetByID(make([]byte, 32)); err != ErrNotFound {
476 t.Fatalf("get missing: %v", err)
477 }
478 }
479
480 func TestResultCapAndApproximateCount(t *testing.T) {
481 dir := t.TempDir()
482 e, err := Open(dir, Options{MaxResults: 2})
483 if err != nil {
484 t.Fatal(err)
485 }
486 defer e.Close()
487 for i := 0; i < 4; i++ {
488 save(t, e, signedEvent(t, 1, 1, int64(100+i), nil, "capped"))
489 }
490 got, err := e.Query(&nostr.Filter{})
491 if err != nil {
492 t.Fatal(err)
493 }
494 if len(got) != 2 {
495 t.Fatalf("query cap: got %d, want 2", len(got))
496 }
497 if got[0].CreatedAt != 103 || got[1].CreatedAt != 102 {
498 t.Fatalf("cap keeps newest: %d, %d", got[0].CreatedAt, got[1].CreatedAt)
499 }
500 n, approx, err := e.Count(&nostr.Filter{})
501 if err != nil || n != 2 || !approx {
502 t.Fatalf("count cap: n=%d approx=%v err=%v", n, approx, err)
503 }
504 }
505
506 func TestCombinedFilterPaths(t *testing.T) {
507 e, _ := openTest(t)
508 tagX := nostr.Tags{{"t", "x"}, {"p", "abc"}}
509 ev1 := signedEvent(t, 1, 1, 100, tagX, "one")
510 ev2 := signedEvent(t, 2, 1, 110, nostr.Tags{{"t", "x"}}, "two")
511 ev3 := signedEvent(t, 1, 2, 120, tagX, "three")
512 ev4 := signedEvent(t, 1, 1, 130, nostr.Tags{{"t", "y"}}, "four")
513 for _, ev := range []*nostr.Event{ev1, ev2, ev3, ev4} {
514 save(t, e, ev)
515 }
516 author1 := nostr.Hex(ev1.PubKey[:])
517
518 cases := []struct {
519 name string
520 f *nostr.Filter
521 want []*nostr.Event
522 }{
523 {"tags+kinds", &nostr.Filter{Kinds: []int{1}, Tags: map[string][]string{"t": {"x"}}}, []*nostr.Event{ev1, ev2}},
524 {"tags+authors", &nostr.Filter{Authors: []string{author1}, Tags: map[string][]string{"t": {"x"}}}, []*nostr.Event{ev1, ev3}},
525 {"tags+kinds+authors", &nostr.Filter{Kinds: []int{1}, Authors: []string{author1}, Tags: map[string][]string{"t": {"x"}}}, []*nostr.Event{ev1}},
526 {"kinds+authors", &nostr.Filter{Kinds: []int{1}, Authors: []string{author1}}, []*nostr.Event{ev1, ev4}},
527 {"multi tag values", &nostr.Filter{Tags: map[string][]string{"t": {"x", "y"}}}, []*nostr.Event{ev1, ev2, ev3, ev4}},
528 }
529 for _, tc := range cases {
530 t.Run(tc.name, func(t *testing.T) {
531 got, err := e.Query(tc.f)
532 if err != nil {
533 t.Fatal(err)
534 }
535 if len(got) != len(tc.want) {
536 t.Fatalf("got %d events (%v), want %d", len(got), ids(got), len(tc.want))
537 }
538 for _, want := range tc.want {
539 found := false
540 for _, ev := range got {
541 if ev.ID == want.ID {
542 found = true
543 }
544 }
545 if !found {
546 t.Fatalf("missing event %x in %v", want.ID[:4], ids(got))
547 }
548 }
549 })
550 }
551 }
552
553 func TestSearchIgnoresShortWords(t *testing.T) {
554 e, _ := openTest(t)
555 save(t, e, signedEvent(t, 1, 1, 100, nil, "ab cd efgh"))
556 got, err := e.Query(&nostr.Filter{Search: "ab"})
557 if err != nil {
558 t.Fatal(err)
559 }
560 if len(got) != 0 {
561 t.Fatalf("short word matched: %d", len(got))
562 }
563 got, err = e.Query(&nostr.Filter{Search: "efgh"})
564 if err != nil || len(got) != 1 {
565 t.Fatalf("long word: %d %v", len(got), err)
566 }
567 }
568
569 func TestTornWALTailIsRepaired(t *testing.T) {
570 dir := t.TempDir()
571 e, err := Open(dir, Options{})
572 if err != nil {
573 t.Fatal(err)
574 }
575 for i := 0; i < 3; i++ {
576 save(t, e, signedEvent(t, byte(i+1), 1, int64(100+i), nostr.Tags{{"t", "torn"}}, "torn tail"))
577 }
578 broken := t.TempDir()
579 copyTree(t, dir, broken)
580 e.Close()
581
582 // Chop the last record in half, as a crash mid-write would.
583 walPath := filepath.Join(broken, "wal", "seg-000.dat")
584 info, err := os.Stat(walPath)
585 if err != nil {
586 t.Fatal(err)
587 }
588 if err := os.Truncate(walPath, info.Size()-5); err != nil {
589 t.Fatal(err)
590 }
591
592 e2, err := Open(broken, Options{})
593 if err != nil {
594 t.Fatalf("open after torn write: %v", err)
595 }
596 defer e2.Close()
597 got, err := e2.Query(&nostr.Filter{Tags: map[string][]string{"t": {"torn"}}})
598 if err != nil {
599 t.Fatal(err)
600 }
601 if len(got) != 2 {
602 t.Fatalf("recovered %d events, want the 2 complete ones", len(got))
603 }
604 }
605
606 func TestStaleCheckpointTriggersRebuild(t *testing.T) {
607 dir := t.TempDir()
608 e, err := Open(dir, Options{})
609 if err != nil {
610 t.Fatal(err)
611 }
612 ev := signedEvent(t, 1, 1, 100, nostr.Tags{{"t", "stale"}}, "stale checkpoint")
613 save(t, e, ev)
614 if err := e.Close(); err != nil {
615 t.Fatal(err)
616 }
617
618 // Write a valid checkpoint with a position past the end of the WAL.
619 buf := make([]byte, 24)
620 copy(buf, "TCK1")
621 binary.BigEndian.PutUint64(buf[4:12], 1<<40)
622 binary.BigEndian.PutUint64(buf[12:20], 1)
623 binary.BigEndian.PutUint32(buf[20:24], crc32.ChecksumIEEE(buf[:20]))
624 if err := os.WriteFile(filepath.Join(dir, "checkpoint.dat"), buf, 0o644); err != nil {
625 t.Fatal(err)
626 }
627
628 e2, err := Open(dir, Options{})
629 if err != nil {
630 t.Fatalf("open with stale checkpoint: %v", err)
631 }
632 defer e2.Close()
633 if _, err := e2.GetByID(ev.ID[:]); err != nil {
634 t.Fatalf("rebuild lost event: %v", err)
635 }
636 }
637
638 func TestQueryLimitReturnsNewest(t *testing.T) {
639 e, _ := openTest(t)
640 const base = int64(1700000000)
641 var kind1 []*nostr.Event
642 for i := 0; i < 40; i++ {
643 tags := nostr.Tags{{"t", "limit"}}
644 ev := signedEvent(t, byte(1+i%3), 1, base+int64(i), tags, fmt.Sprintf("event %d", i))
645 save(t, e, ev)
646 kind1 = append(kind1, ev)
647 }
648 other := signedEvent(t, 9, 7, base+1000, nil, "+")
649 save(t, e, other)
650
651 check := func(name string, f *nostr.Filter, want []*nostr.Event) {
652 t.Helper()
653 got, err := e.Query(f)
654 if err != nil {
655 t.Fatalf("%s: %v", name, err)
656 }
657 if len(got) != len(want) {
658 t.Fatalf("%s: got %d events, want %d (%v)", name, len(got), len(want), ids(got))
659 }
660 for i := range want {
661 if got[i].ID != want[i].ID {
662 t.Fatalf("%s: position %d got %s want %s", name, i, ids(got)[i], ids(want)[i])
663 }
664 }
665 }
666
667 check("kind limit", &nostr.Filter{Kinds: []int{1}, Limit: 3}, []*nostr.Event{kind1[39], kind1[38], kind1[37]})
668 check("all limit", &nostr.Filter{Limit: 2}, []*nostr.Event{other, kind1[39]})
669 check("author limit", &nostr.Filter{Authors: []string{nostr.Hex(kind1[39].PubKey[:])}, Limit: 2},
670 []*nostr.Event{kind1[39], kind1[36]})
671 check("tag limit", &nostr.Filter{Tags: map[string][]string{"t": {"limit"}}, Limit: 2},
672 []*nostr.Event{kind1[39], kind1[38]})
673 check("since limit", &nostr.Filter{Kinds: []int{1}, Since: base + 20, Limit: 2},
674 []*nostr.Event{kind1[39], kind1[38]})
675 }
676