Some checks failed
Go / build-and-release (push) Has been cancelled
Merged 'Author' nodes into 'NostrUser' for unified identity tracking and social graph representation. Introduced migrations framework to handle schema changes, including retroactive updates for existing relationships and constraints. Updated tests, schema definitions, and documentation to reflect these changes.
452 lines
12 KiB
Go
452 lines
12 KiB
Go
package neo4j
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event"
|
|
"git.mleku.dev/mleku/nostr/encoders/filter"
|
|
"git.mleku.dev/mleku/nostr/encoders/hex"
|
|
"git.mleku.dev/mleku/nostr/encoders/kind"
|
|
"git.mleku.dev/mleku/nostr/encoders/tag"
|
|
"git.mleku.dev/mleku/nostr/encoders/timestamp"
|
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k"
|
|
)
|
|
|
|
// setupTestDatabase creates a fresh Neo4j database connection for testing
|
|
func setupTestDatabase(t *testing.T) (*N, context.Context, context.CancelFunc) {
|
|
t.Helper()
|
|
|
|
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
|
|
if neo4jURI == "" {
|
|
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
tempDir := t.TempDir()
|
|
db, err := New(ctx, cancel, tempDir, "debug")
|
|
if err != nil {
|
|
cancel()
|
|
t.Fatalf("Failed to create database: %v", err)
|
|
}
|
|
|
|
<-db.Ready()
|
|
|
|
if err := db.Wipe(); err != nil {
|
|
db.Close()
|
|
cancel()
|
|
t.Fatalf("Failed to wipe database: %v", err)
|
|
}
|
|
|
|
return db, ctx, cancel
|
|
}
|
|
|
|
// createTestSigner creates a new signer for test events
|
|
func createTestSigner(t *testing.T) *p8k.Signer {
|
|
t.Helper()
|
|
|
|
signer, err := p8k.New()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create signer: %v", err)
|
|
}
|
|
if err := signer.Generate(); err != nil {
|
|
t.Fatalf("Failed to generate keypair: %v", err)
|
|
}
|
|
return signer
|
|
}
|
|
|
|
// createAndSaveEvent creates a signed event and saves it to the database
|
|
func createAndSaveEvent(t *testing.T, ctx context.Context, db *N, signer *p8k.Signer, k uint16, content string, tags *tag.S, ts int64) *event.E {
|
|
t.Helper()
|
|
|
|
ev := event.New()
|
|
ev.Pubkey = signer.Pub()
|
|
ev.CreatedAt = ts
|
|
ev.Kind = k
|
|
ev.Content = []byte(content)
|
|
ev.Tags = tags
|
|
|
|
if err := ev.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := db.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
|
|
return ev
|
|
}
|
|
|
|
func TestQueryEventsByID(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
|
|
// Create and save a test event
|
|
ev := createAndSaveEvent(t, ctx, db, signer, 1, "Test event for ID query", nil, timestamp.Now().V)
|
|
|
|
// Query by ID
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Ids: tag.NewFromBytesSlice(ev.ID),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by ID: %v", err)
|
|
}
|
|
|
|
if len(evs) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(evs))
|
|
}
|
|
|
|
if hex.Enc(evs[0].ID[:]) != hex.Enc(ev.ID[:]) {
|
|
t.Fatalf("Event ID mismatch: got %s, expected %s",
|
|
hex.Enc(evs[0].ID[:]), hex.Enc(ev.ID[:]))
|
|
}
|
|
|
|
t.Logf("✓ Query by ID returned correct event")
|
|
}
|
|
|
|
func TestQueryEventsByKind(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events of different kinds
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Kind 1 event A", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Kind 1 event B", nil, baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, signer, 7, "Kind 7 reaction", nil, baseTs+2)
|
|
createAndSaveEvent(t, ctx, db, signer, 30023, "Kind 30023 article", nil, baseTs+3)
|
|
|
|
// Query for kind 1
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by kind: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 kind 1 events, got %d", len(evs))
|
|
}
|
|
|
|
for _, ev := range evs {
|
|
if ev.Kind != 1 {
|
|
t.Fatalf("Expected kind 1, got %d", ev.Kind)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ Query by kind returned %d correct events", len(evs))
|
|
}
|
|
|
|
func TestQueryEventsByAuthor(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
alice := createTestSigner(t)
|
|
bob := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events from different authors
|
|
createAndSaveEvent(t, ctx, db, alice, 1, "Alice's event 1", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, alice, 1, "Alice's event 2", nil, baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, bob, 1, "Bob's event", nil, baseTs+2)
|
|
|
|
// Query for Alice's events
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Authors: tag.NewFromBytesSlice(alice.Pub()),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by author: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 events from Alice, got %d", len(evs))
|
|
}
|
|
|
|
alicePubkey := hex.Enc(alice.Pub())
|
|
for _, ev := range evs {
|
|
if hex.Enc(ev.Pubkey[:]) != alicePubkey {
|
|
t.Fatalf("Expected author %s, got %s", alicePubkey, hex.Enc(ev.Pubkey[:]))
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ Query by author returned %d correct events", len(evs))
|
|
}
|
|
|
|
func TestQueryEventsByTimeRange(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events at different times
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Old event", nil, baseTs-7200) // 2 hours ago
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Recent event", nil, baseTs-1800) // 30 min ago
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Current event", nil, baseTs)
|
|
|
|
// Query for events in the last hour
|
|
since := ×tamp.T{V: baseTs - 3600}
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Since: since,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by time range: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 events in last hour, got %d", len(evs))
|
|
}
|
|
|
|
for _, ev := range evs {
|
|
if ev.CreatedAt < since.V {
|
|
t.Fatalf("Event created_at %d is before since %d", ev.CreatedAt, since.V)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ Query by time range returned %d correct events", len(evs))
|
|
}
|
|
|
|
func TestQueryEventsByTag(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events with tags
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Bitcoin post",
|
|
tag.NewS(tag.NewFromAny("t", "bitcoin")), baseTs)
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Nostr post",
|
|
tag.NewS(tag.NewFromAny("t", "nostr")), baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Bitcoin and Nostr post",
|
|
tag.NewS(tag.NewFromAny("t", "bitcoin"), tag.NewFromAny("t", "nostr")), baseTs+2)
|
|
|
|
// Query for bitcoin tagged events
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Tags: tag.NewS(tag.NewFromAny("t", "bitcoin")),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by tag: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 bitcoin-tagged events, got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query by tag returned %d correct events", len(evs))
|
|
}
|
|
|
|
func TestQueryEventsByKindAndAuthor(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
alice := createTestSigner(t)
|
|
bob := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events
|
|
createAndSaveEvent(t, ctx, db, alice, 1, "Alice note", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, alice, 7, "Alice reaction", nil, baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, bob, 1, "Bob note", nil, baseTs+2)
|
|
|
|
// Query for Alice's kind 1 events
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
Authors: tag.NewFromBytesSlice(alice.Pub()),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events by kind and author: %v", err)
|
|
}
|
|
|
|
if len(evs) != 1 {
|
|
t.Fatalf("Expected 1 kind 1 event from Alice, got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query by kind and author returned correct events")
|
|
}
|
|
|
|
func TestQueryEventsWithLimit(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create many events
|
|
for i := 0; i < 20; i++ {
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Event", nil, baseTs+int64(i))
|
|
}
|
|
|
|
// Query with limit
|
|
limit := uint(5)
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
Limit: &limit,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events with limit: %v", err)
|
|
}
|
|
|
|
if len(evs) != int(limit) {
|
|
t.Fatalf("Expected %d events with limit, got %d", limit, len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query with limit returned %d events", len(evs))
|
|
}
|
|
|
|
func TestQueryEventsOrderByCreatedAt(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events at different times
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "First", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Second", nil, baseTs+100)
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Third", nil, baseTs+200)
|
|
|
|
// Query and verify order (should be descending by created_at)
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
|
|
if len(evs) < 2 {
|
|
t.Fatalf("Expected at least 2 events, got %d", len(evs))
|
|
}
|
|
|
|
// Verify descending order
|
|
for i := 1; i < len(evs); i++ {
|
|
if evs[i-1].CreatedAt < evs[i].CreatedAt {
|
|
t.Fatalf("Events not in descending order: %d < %d at index %d",
|
|
evs[i-1].CreatedAt, evs[i].CreatedAt, i)
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ Query returned events in correct descending order")
|
|
}
|
|
|
|
func TestQueryEventsEmpty(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
// Query for non-existent kind
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(99999)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
|
|
if len(evs) != 0 {
|
|
t.Fatalf("Expected 0 events, got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query for non-existent kind returned empty result")
|
|
}
|
|
|
|
func TestQueryEventsMultipleKinds(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events of different kinds
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Note", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, signer, 7, "Reaction", nil, baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, signer, 30023, "Article", nil, baseTs+2)
|
|
|
|
// Query for multiple kinds
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1), kind.New(7)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 events (kind 1 and 7), got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query for multiple kinds returned correct events")
|
|
}
|
|
|
|
func TestQueryEventsMultipleAuthors(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
alice := createTestSigner(t)
|
|
bob := createTestSigner(t)
|
|
charlie := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events from different authors
|
|
createAndSaveEvent(t, ctx, db, alice, 1, "Alice", nil, baseTs)
|
|
createAndSaveEvent(t, ctx, db, bob, 1, "Bob", nil, baseTs+1)
|
|
createAndSaveEvent(t, ctx, db, charlie, 1, "Charlie", nil, baseTs+2)
|
|
|
|
// Query for Alice and Bob's events
|
|
authors := tag.NewFromBytesSlice(alice.Pub(), bob.Pub())
|
|
|
|
evs, err := db.QueryEvents(ctx, &filter.F{
|
|
Authors: authors,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 events from Alice and Bob, got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Query for multiple authors returned correct events")
|
|
}
|
|
|
|
func TestCountEvents(t *testing.T) {
|
|
db, ctx, cancel := setupTestDatabase(t)
|
|
defer db.Close()
|
|
defer cancel()
|
|
|
|
signer := createTestSigner(t)
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events
|
|
for i := 0; i < 5; i++ {
|
|
createAndSaveEvent(t, ctx, db, signer, 1, "Event", nil, baseTs+int64(i))
|
|
}
|
|
|
|
// Count events
|
|
count, _, err := db.CountEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to count events: %v", err)
|
|
}
|
|
|
|
if count != 5 {
|
|
t.Fatalf("Expected count 5, got %d", count)
|
|
}
|
|
|
|
t.Logf("✓ Count events returned correct count: %d", count)
|
|
}
|