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