package store import ( "bytes" "encoding/hex" "fmt" "os" "testing" "smesh.lol/pkg/nostr/event" "smesh.lol/pkg/nostr/filter" "smesh.lol/pkg/nostr/kind" "smesh.lol/pkg/nostr/signer/p8k" "smesh.lol/pkg/nostr/tag" "smesh.lol/pkg/nostr/timestamp" ) func makeSigner(t *testing.T) *p8k.Signer { t.Helper() s := p8k.MustNew() if err := s.Generate(); err != nil { t.Fatal(err) } return s } func makeEvent(t *testing.T, s *p8k.Signer, k uint16, ts int64, tags *tag.S, content string) *event.E { t.Helper() ev := &event.E{ CreatedAt: ts, Kind: k, Tags: tags, Content: []byte(content), } if err := ev.Sign(s); err != nil { t.Fatal(err) } return ev } func openTmp(t *testing.T) *Engine { t.Helper() dir := t.TempDir() eng, err := Open(dir) if err != nil { t.Fatal(err) } return eng } func TestSaveAndGetBySerial(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) ev := makeEvent(t, s, 1, 1700000000, nil, "hello") if err := eng.SaveEvent(ev); err != nil { t.Fatal(err) } // Duplicate should fail. if err := eng.SaveEvent(ev); err == nil { t.Fatal("expected duplicate error") } } func TestQueryByID(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) ev := makeEvent(t, s, 1, 1700000000, nil, "find me") eng.SaveEvent(ev) f := &filter.F{Ids: tag.NewFromBytesSlice(ev.ID)} results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } if !bytes.Equal(results[0].ID, ev.ID) { t.Fatal("ID mismatch") } } func TestQueryByKind(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) for i := 0; i < 10; i++ { k := uint16(1) if i%3 == 0 { k = 7 } eng.SaveEvent(makeEvent(t, s, k, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i))) } f := &filter.F{Kinds: kind.NewS(kind.New(uint16(7)))} results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 4 { // i=0,3,6,9 t.Fatalf("expected 4 kind-7 events, got %d", len(results)) } for _, r := range results { if r.Kind != 7 { t.Fatalf("expected kind 7, got %d", r.Kind) } } } func TestQueryByAuthor(t *testing.T) { eng := openTmp(t) defer eng.Close() s1 := makeSigner(t) s2 := makeSigner(t) for i := 0; i < 10; i++ { signer := s1 if i%2 == 0 { signer = s2 } eng.SaveEvent(makeEvent(t, signer, 1, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i))) } f := &filter.F{Authors: tag.NewFromBytesSlice(s1.Pub())} results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 5 { t.Fatalf("expected 5 events by s1, got %d", len(results)) } for _, r := range results { if !bytes.Equal(r.Pubkey, s1.Pub()) { t.Fatal("pubkey mismatch") } } } func TestQueryByTag(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) for i := 0; i < 10; i++ { var tags *tag.S if i%2 == 0 { tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("alpha"))) } else { tags = tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("beta"))) } eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), tags, fmt.Sprintf("msg %d", i))) } f := &filter.F{ Tags: tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("alpha"))), } results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 5 { t.Fatalf("expected 5 events with t=alpha, got %d", len(results)) } } func TestQueryByPTag(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) target := makeSigner(t) targetHex := hex.EncodeToString(target.Pub()) for i := 0; i < 5; i++ { tags := tag.NewS(tag.NewFromBytesSlice([]byte("p"), []byte(targetHex))) eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), tags, fmt.Sprintf("mention %d", i))) } for i := 5; i < 10; i++ { eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), nil, fmt.Sprintf("no mention %d", i))) } f := &filter.F{ Tags: tag.NewS(tag.NewFromBytesSlice([]byte("p"), []byte(targetHex))), } results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 5 { t.Fatalf("expected 5 events mentioning target, got %d", len(results)) } } func TestQueryByTimeRange(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) for i := 0; i < 20; i++ { eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i*100), nil, fmt.Sprintf("msg %d", i))) } since := int64(1700000500) until := int64(1700001500) f := &filter.F{ Since: timestamp.FromUnix(since), Until: timestamp.FromUnix(until), } results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } for _, r := range results { if r.CreatedAt < since || r.CreatedAt > until { t.Fatalf("event %d out of range [%d, %d]", r.CreatedAt, since, until) } } // Events at 500,600,...1500 → 11 events. if len(results) != 11 { t.Fatalf("expected 11 events in range, got %d", len(results)) } } func TestQueryWithLimit(t *testing.T) { eng := openTmp(t) defer eng.Close() s := makeSigner(t) for i := 0; i < 50; i++ { eng.SaveEvent(makeEvent(t, s, 1, int64(1700000000+i), nil, fmt.Sprintf("msg %d", i))) } limit := uint(10) f := &filter.F{ Kinds: kind.NewS(kind.New(uint16(1))), Limit: &limit, } results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 10 { t.Fatalf("expected 10, got %d", len(results)) } // Should be newest first. for i := 1; i < len(results); i++ { if results[i].CreatedAt > results[i-1].CreatedAt { t.Fatal("results not in reverse chronological order") } } } func TestBulk10k(t *testing.T) { if testing.Short() { t.Skip("skipping bulk test in short mode") } dir, err := os.MkdirTemp("", "store-bulk-*") if err != nil { t.Fatal(err) } defer os.RemoveAll(dir) eng, err := Open(dir) if err != nil { t.Fatal(err) } const nSigners = 10 const nEvents = 10000 signers := []*p8k.Signer{:nSigners} for i := range signers { signers[i] = makeSigner(t) } kinds := []uint16{1, 7, 30023, 10002} tagValues := []string{"nostr", "bitcoin", "lightning", "zap", "relay"} for i := 0; i < nEvents; i++ { s := signers[i%nSigners] k := kinds[i%len(kinds)] ts := int64(1700000000 + i) tags := tag.NewS( tag.NewFromBytesSlice([]byte("t"), []byte(tagValues[i%len(tagValues)])), ) if i%5 == 0 { other := signers[(i+1)%nSigners] otherHex := hex.EncodeToString(other.Pub()) tags.Append(tag.NewFromBytesSlice([]byte("p"), []byte(otherHex))) } ev := makeEvent(t, s, k, ts, tags, fmt.Sprintf("event %d", i)) if err := eng.SaveEvent(ev); err != nil { t.Fatalf("save event %d: %v", i, err) } } if err := eng.Flush(); err != nil { t.Fatal(err) } // Query by author. f := &filter.F{Authors: tag.NewFromBytesSlice(signers[0].Pub())} results, err := eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != nEvents/nSigners { t.Fatalf("author query: expected %d, got %d", nEvents/nSigners, len(results)) } // Query by kind. f = &filter.F{Kinds: kind.NewS(kind.New(uint16(7)))} results, err = eng.QueryEvents(f) if err != nil { t.Fatal(err) } expected := 0 for i := 0; i < nEvents; i++ { if kinds[i%len(kinds)] == 7 { expected++ } } if len(results) != expected { t.Fatalf("kind query: expected %d, got %d", expected, len(results)) } // Query by tag. f = &filter.F{ Tags: tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("nostr"))), } results, err = eng.QueryEvents(f) if err != nil { t.Fatal(err) } expected = 0 for i := 0; i < nEvents; i++ { if tagValues[i%len(tagValues)] == "nostr" { expected++ } } if len(results) != expected { t.Fatalf("tag query: expected %d, got %d", expected, len(results)) } // Query by time range. since := int64(1700005000) until := int64(1700005099) f = &filter.F{ Since: timestamp.FromUnix(since), Until: timestamp.FromUnix(until), } results, err = eng.QueryEvents(f) if err != nil { t.Fatal(err) } if len(results) != 100 { t.Fatalf("time range query: expected 100, got %d", len(results)) } eng.Close() }