Files
next.orly.dev/pkg/neo4j/expiration_test.go

571 lines
14 KiB
Go

package neo4j
import (
"bytes"
"context"
"encoding/json"
"os"
"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"
)
func TestExpiration_SaveEventWithExpiration(t *testing.T) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
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.From(futureExpiration).String()))
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)
}
// Query the event to verify it was saved
evs, err := db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
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.From(pastExpiration).String()))
if err := expiredEv.Sign(signer); err != nil {
t.Fatalf("Failed to sign expired event: %v", err)
}
if _, err := db.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.From(futureExpiration).String()))
if err := validEv.Sign(signer); err != nil {
t.Fatalf("Failed to sign valid event: %v", err)
}
if _, err := db.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 := db.SaveEvent(ctx, permanentEv); err != nil {
t.Fatalf("Failed to save permanent event: %v", err)
}
// Verify all 3 events exist
evs, err := db.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
db.DeleteExpired()
// Verify only expired event was deleted
evs, err = db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
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 := db.SaveEvent(ctx, ev); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
// Run DeleteExpired - event should not be deleted
db.DeleteExpired()
// Verify event still exists
evs, err := db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
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 := db.SaveEvent(ctx, ev); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
}
// Export all events
var buf bytes.Buffer
db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
// 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 := db.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 := db.SaveEvent(ctx, ev); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
}
// Export only Alice's events
var buf bytes.Buffer
db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
// Export from empty database
var buf bytes.Buffer
db.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) {
neo4jURI := os.Getenv("ORLY_NEO4J_URI")
if neo4jURI == "" {
t.Skip("Skipping Neo4j test: ORLY_NEO4J_URI not set")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tempDir := t.TempDir()
db, err := New(ctx, cancel, tempDir, "debug")
if err != nil {
t.Fatalf("Failed to create database: %v", err)
}
defer db.Close()
<-db.Ready()
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
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 := db.SaveEvent(ctx, ev); err != nil {
t.Fatalf("Failed to save event: %v", err)
}
originalEvents[i] = ev
}
// Export events
var buf bytes.Buffer
db.Export(ctx, &buf)
// Wipe database
if err := db.Wipe(); err != nil {
t.Fatalf("Failed to wipe database: %v", err)
}
// Verify database is empty
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) != 0 {
t.Fatalf("Expected 0 events after wipe, got %d", len(evs))
}
// Import events
db.Import(bytes.NewReader(buf.Bytes()))
// Verify events were restored
evs, err = db.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))
}