Some checks failed
Go / build-and-release (push) Has been cancelled
Introduce comprehensive integration tests for Neo4j bug fixes covering batching, event relationships, and processing logic. Add rate-limiting to Neo4j queries using semaphores and retry policies to prevent authentication rate limiting and connection exhaustion, ensuring system stability under load.
485 lines
11 KiB
Go
485 lines
11 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package neo4j
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// All tests in this file use the shared testDB instance from testmain_test.go
|
|
// to avoid Neo4j authentication rate limiting from too many connections.
|
|
|
|
func TestExpiration_SaveEventWithExpiration(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
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)
|
|
}
|
|
|
|
// Create event with expiration tag (expires in 1 hour)
|
|
futureExpiration := time.Now().Add(1 * time.Hour).Unix()
|
|
|
|
ev := event.New()
|
|
ev.Pubkey = signer.Pub()
|
|
ev.CreatedAt = timestamp.Now().V
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Event with expiration")
|
|
ev.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
|
|
|
|
if err := ev.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
|
|
// Query the event to verify it was saved
|
|
evs, err := testDB.QueryEvents(ctx, &filter.F{
|
|
Ids: tag.NewFromBytesSlice(ev.ID),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query event: %v", err)
|
|
}
|
|
|
|
if len(evs) != 1 {
|
|
t.Fatalf("Expected 1 event, got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Event with expiration tag saved successfully")
|
|
}
|
|
|
|
func TestExpiration_DeleteExpiredEvents(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
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)
|
|
}
|
|
|
|
// Create an expired event (expired 1 hour ago)
|
|
pastExpiration := time.Now().Add(-1 * time.Hour).Unix()
|
|
|
|
expiredEv := event.New()
|
|
expiredEv.Pubkey = signer.Pub()
|
|
expiredEv.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
|
|
expiredEv.Kind = 1
|
|
expiredEv.Content = []byte("Expired event")
|
|
expiredEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(pastExpiration).String()))
|
|
|
|
if err := expiredEv.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign expired event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, expiredEv); err != nil {
|
|
t.Fatalf("Failed to save expired event: %v", err)
|
|
}
|
|
|
|
// Create a non-expired event (expires in 1 hour)
|
|
futureExpiration := time.Now().Add(1 * time.Hour).Unix()
|
|
|
|
validEv := event.New()
|
|
validEv.Pubkey = signer.Pub()
|
|
validEv.CreatedAt = timestamp.Now().V
|
|
validEv.Kind = 1
|
|
validEv.Content = []byte("Valid event")
|
|
validEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
|
|
|
|
if err := validEv.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign valid event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, validEv); err != nil {
|
|
t.Fatalf("Failed to save valid event: %v", err)
|
|
}
|
|
|
|
// Create an event without expiration
|
|
permanentEv := event.New()
|
|
permanentEv.Pubkey = signer.Pub()
|
|
permanentEv.CreatedAt = timestamp.Now().V + 1
|
|
permanentEv.Kind = 1
|
|
permanentEv.Content = []byte("Permanent event (no expiration)")
|
|
|
|
if err := permanentEv.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign permanent event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, permanentEv); err != nil {
|
|
t.Fatalf("Failed to save permanent event: %v", err)
|
|
}
|
|
|
|
// Verify all 3 events exist
|
|
evs, err := testDB.QueryEvents(ctx, &filter.F{
|
|
Authors: tag.NewFromBytesSlice(signer.Pub()),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
if len(evs) != 3 {
|
|
t.Fatalf("Expected 3 events before deletion, got %d", len(evs))
|
|
}
|
|
|
|
// Run DeleteExpired
|
|
testDB.DeleteExpired()
|
|
|
|
// Verify only expired event was deleted
|
|
evs, err = testDB.QueryEvents(ctx, &filter.F{
|
|
Authors: tag.NewFromBytesSlice(signer.Pub()),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events after deletion: %v", err)
|
|
}
|
|
|
|
if len(evs) != 2 {
|
|
t.Fatalf("Expected 2 events after deletion (expired removed), got %d", len(evs))
|
|
}
|
|
|
|
// Verify the correct events remain
|
|
foundValid := false
|
|
foundPermanent := false
|
|
for _, ev := range evs {
|
|
if hex.Enc(ev.ID[:]) == hex.Enc(validEv.ID[:]) {
|
|
foundValid = true
|
|
}
|
|
if hex.Enc(ev.ID[:]) == hex.Enc(permanentEv.ID[:]) {
|
|
foundPermanent = true
|
|
}
|
|
}
|
|
|
|
if !foundValid {
|
|
t.Fatal("Valid event (with future expiration) was incorrectly deleted")
|
|
}
|
|
if !foundPermanent {
|
|
t.Fatal("Permanent event (no expiration) was incorrectly deleted")
|
|
}
|
|
|
|
t.Logf("✓ DeleteExpired correctly removed only expired events")
|
|
}
|
|
|
|
func TestExpiration_NoExpirationTag(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
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)
|
|
}
|
|
|
|
// Create event without expiration tag
|
|
ev := event.New()
|
|
ev.Pubkey = signer.Pub()
|
|
ev.CreatedAt = timestamp.Now().V
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Event without expiration")
|
|
|
|
if err := ev.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
|
|
// Run DeleteExpired - event should not be deleted
|
|
testDB.DeleteExpired()
|
|
|
|
// Verify event still exists
|
|
evs, err := testDB.QueryEvents(ctx, &filter.F{
|
|
Ids: tag.NewFromBytesSlice(ev.ID),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query event: %v", err)
|
|
}
|
|
|
|
if len(evs) != 1 {
|
|
t.Fatalf("Expected 1 event (no expiration should not be deleted), got %d", len(evs))
|
|
}
|
|
|
|
t.Logf("✓ Events without expiration tag are not deleted")
|
|
}
|
|
|
|
func TestExport_AllEvents(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
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)
|
|
}
|
|
|
|
// Create and save some events
|
|
for i := 0; i < 5; i++ {
|
|
ev := event.New()
|
|
ev.Pubkey = signer.Pub()
|
|
ev.CreatedAt = timestamp.Now().V + int64(i)
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Test event for export")
|
|
ev.Tags = tag.NewS(tag.NewFromAny("t", "test"))
|
|
|
|
if err := ev.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
}
|
|
|
|
// Export all events
|
|
var buf bytes.Buffer
|
|
testDB.Export(ctx, &buf)
|
|
|
|
// Parse the exported JSONL
|
|
lines := bytes.Split(buf.Bytes(), []byte("\n"))
|
|
validLines := 0
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
var ev event.E
|
|
if err := json.Unmarshal(line, &ev); err != nil {
|
|
t.Fatalf("Failed to parse exported event: %v", err)
|
|
}
|
|
validLines++
|
|
}
|
|
|
|
if validLines != 5 {
|
|
t.Fatalf("Expected 5 exported events, got %d", validLines)
|
|
}
|
|
|
|
t.Logf("✓ Export all events returned %d events in JSONL format", validLines)
|
|
}
|
|
|
|
func TestExport_FilterByPubkey(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create two signers
|
|
alice, _ := p8k.New()
|
|
alice.Generate()
|
|
|
|
bob, _ := p8k.New()
|
|
bob.Generate()
|
|
|
|
baseTs := timestamp.Now().V
|
|
|
|
// Create events from Alice
|
|
for i := 0; i < 3; i++ {
|
|
ev := event.New()
|
|
ev.Pubkey = alice.Pub()
|
|
ev.CreatedAt = baseTs + int64(i)
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Alice's event")
|
|
|
|
if err := ev.Sign(alice); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
}
|
|
|
|
// Create events from Bob
|
|
for i := 0; i < 2; i++ {
|
|
ev := event.New()
|
|
ev.Pubkey = bob.Pub()
|
|
ev.CreatedAt = baseTs + int64(i) + 10
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Bob's event")
|
|
|
|
if err := ev.Sign(bob); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
}
|
|
|
|
// Export only Alice's events
|
|
var buf bytes.Buffer
|
|
testDB.Export(ctx, &buf, alice.Pub())
|
|
|
|
// Parse the exported JSONL
|
|
lines := bytes.Split(buf.Bytes(), []byte("\n"))
|
|
validLines := 0
|
|
alicePubkey := hex.Enc(alice.Pub())
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
var ev event.E
|
|
if err := json.Unmarshal(line, &ev); err != nil {
|
|
t.Fatalf("Failed to parse exported event: %v", err)
|
|
}
|
|
if hex.Enc(ev.Pubkey[:]) != alicePubkey {
|
|
t.Fatalf("Exported event has wrong pubkey (expected Alice)")
|
|
}
|
|
validLines++
|
|
}
|
|
|
|
if validLines != 3 {
|
|
t.Fatalf("Expected 3 events from Alice, got %d", validLines)
|
|
}
|
|
|
|
t.Logf("✓ Export with pubkey filter returned %d events from Alice only", validLines)
|
|
}
|
|
|
|
func TestExport_Empty(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Export from empty database
|
|
var buf bytes.Buffer
|
|
testDB.Export(ctx, &buf)
|
|
|
|
// Should be empty or just whitespace
|
|
content := bytes.TrimSpace(buf.Bytes())
|
|
if len(content) != 0 {
|
|
t.Fatalf("Expected empty export, got: %s", string(content))
|
|
}
|
|
|
|
t.Logf("✓ Export from empty database returns empty result")
|
|
}
|
|
|
|
func TestImportExport_RoundTrip(t *testing.T) {
|
|
if testDB == nil {
|
|
t.Skip("Neo4j not available")
|
|
}
|
|
|
|
cleanTestDatabase()
|
|
|
|
ctx := context.Background()
|
|
|
|
signer, _ := p8k.New()
|
|
signer.Generate()
|
|
|
|
// Create original events
|
|
originalEvents := make([]*event.E, 3)
|
|
for i := 0; i < 3; i++ {
|
|
ev := event.New()
|
|
ev.Pubkey = signer.Pub()
|
|
ev.CreatedAt = timestamp.Now().V + int64(i)
|
|
ev.Kind = 1
|
|
ev.Content = []byte("Round trip test event")
|
|
ev.Tags = tag.NewS(tag.NewFromAny("t", "roundtrip"))
|
|
|
|
if err := ev.Sign(signer); err != nil {
|
|
t.Fatalf("Failed to sign event: %v", err)
|
|
}
|
|
|
|
if _, err := testDB.SaveEvent(ctx, ev); err != nil {
|
|
t.Fatalf("Failed to save event: %v", err)
|
|
}
|
|
originalEvents[i] = ev
|
|
}
|
|
|
|
// Export events
|
|
var buf bytes.Buffer
|
|
testDB.Export(ctx, &buf)
|
|
|
|
// Wipe database
|
|
if err := testDB.Wipe(); err != nil {
|
|
t.Fatalf("Failed to wipe database: %v", err)
|
|
}
|
|
|
|
// Verify database is empty
|
|
evs, err := testDB.QueryEvents(ctx, &filter.F{
|
|
Kinds: kind.NewS(kind.New(1)),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query events: %v", err)
|
|
}
|
|
if len(evs) != 0 {
|
|
t.Fatalf("Expected 0 events after wipe, got %d", len(evs))
|
|
}
|
|
|
|
// Import events
|
|
testDB.Import(bytes.NewReader(buf.Bytes()))
|
|
|
|
// Verify events were restored
|
|
evs, err = testDB.QueryEvents(ctx, &filter.F{
|
|
Authors: tag.NewFromBytesSlice(signer.Pub()),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to query imported events: %v", err)
|
|
}
|
|
|
|
if len(evs) != 3 {
|
|
t.Fatalf("Expected 3 imported events, got %d", len(evs))
|
|
}
|
|
|
|
// Verify event IDs match
|
|
importedIDs := make(map[string]bool)
|
|
for _, ev := range evs {
|
|
importedIDs[hex.Enc(ev.ID[:])] = true
|
|
}
|
|
|
|
for _, orig := range originalEvents {
|
|
if !importedIDs[hex.Enc(orig.ID[:])] {
|
|
t.Fatalf("Original event %s not found after import", hex.Enc(orig.ID[:]))
|
|
}
|
|
}
|
|
|
|
t.Logf("✓ Export/Import round trip preserved %d events correctly", len(evs))
|
|
}
|