You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
8.4 KiB
326 lines
8.4 KiB
//go:build !(js && wasm) |
|
|
|
package database |
|
|
|
import ( |
|
"context" |
|
"os" |
|
"testing" |
|
"time" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
|
"git.mleku.dev/mleku/nostr/encoders/kind" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/interfaces/signer/p8k" |
|
"lol.mleku.dev/chk" |
|
) |
|
|
|
func TestIsAddressableEventQuery(t *testing.T) { |
|
// Generate a test keypair |
|
signer := p8k.MustNew() |
|
if err := signer.Generate(); chk.E(err) { |
|
t.Fatal(err) |
|
} |
|
pub := signer.Pub() |
|
|
|
tests := []struct { |
|
name string |
|
filter *filter.F |
|
expected bool |
|
}{ |
|
{ |
|
name: "valid NIP-33 query - kind 30000", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("test-d-tag"))), |
|
}, |
|
expected: true, |
|
}, |
|
{ |
|
name: "valid NIP-33 query - kind 30382", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(30382)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("some-identifier"))), |
|
}, |
|
expected: true, |
|
}, |
|
{ |
|
name: "invalid - kind 1 (not parameterized replaceable)", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(1)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))), |
|
}, |
|
expected: false, |
|
}, |
|
{ |
|
name: "invalid - kind 10000 (replaceable, not parameterized)", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(10000)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))), |
|
}, |
|
expected: false, |
|
}, |
|
{ |
|
name: "invalid - missing d-tag", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
}, |
|
expected: false, |
|
}, |
|
{ |
|
name: "invalid - multiple kinds", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000), kind.New(30001)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))), |
|
}, |
|
expected: false, |
|
}, |
|
{ |
|
name: "invalid - no authors", |
|
filter: &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000)), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("test"))), |
|
}, |
|
expected: false, |
|
}, |
|
} |
|
|
|
for _, tt := range tests { |
|
t.Run(tt.name, func(t *testing.T) { |
|
result := IsAddressableEventQuery(tt.filter) |
|
if result != tt.expected { |
|
t.Errorf("IsAddressableEventQuery() = %v, want %v", result, tt.expected) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func TestQueryForAddressableEvent(t *testing.T) { |
|
// Create temporary database |
|
tempDir, err := os.MkdirTemp("", "test-addressable-*") |
|
if err != nil { |
|
t.Fatalf("Failed to create temp dir: %v", err) |
|
} |
|
defer os.RemoveAll(tempDir) |
|
|
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
db, err := New(ctx, cancel, tempDir, "info") |
|
if err != nil { |
|
t.Fatalf("failed to create database: %v", err) |
|
} |
|
defer db.Close() |
|
|
|
// Generate a test keypair |
|
signer := p8k.MustNew() |
|
if err := signer.Generate(); chk.E(err) { |
|
t.Fatal(err) |
|
} |
|
pub := signer.Pub() |
|
|
|
// Create and save a parameterized replaceable event (kind 30382) |
|
dTagValue := []byte("test-identifier-12345") |
|
ev := &event.E{ |
|
Kind: 30382, |
|
Pubkey: pub, |
|
CreatedAt: time.Now().Unix(), |
|
Content: []byte("Test content for addressable event"), |
|
Tags: tag.NewS(tag.NewFromAny("d", dTagValue)), |
|
} |
|
|
|
// Sign the event |
|
if err := ev.Sign(signer); err != nil { |
|
t.Fatalf("failed to sign event: %v", err) |
|
} |
|
|
|
// Save the event |
|
_, err = db.SaveEvent(ctx, ev) |
|
if err != nil { |
|
t.Fatalf("failed to save event: %v", err) |
|
} |
|
|
|
// Query using the fast path |
|
queryFilter := &filter.F{ |
|
Kinds: kind.NewS(kind.New(30382)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", dTagValue)), |
|
} |
|
|
|
// Test IsAddressableEventQuery |
|
if !IsAddressableEventQuery(queryFilter) { |
|
t.Errorf("Expected IsAddressableEventQuery to return true for valid NIP-33 filter") |
|
} |
|
|
|
// Test QueryForAddressableEvent |
|
serial, err := db.QueryForAddressableEvent(queryFilter) |
|
if err != nil { |
|
t.Fatalf("QueryForAddressableEvent failed: %v", err) |
|
} |
|
if serial == nil { |
|
t.Fatalf("QueryForAddressableEvent returned nil serial, expected to find event") |
|
} |
|
|
|
// Fetch the event and verify it matches |
|
fetchedEv, err := db.FetchEventBySerial(serial) |
|
if err != nil { |
|
t.Fatalf("FetchEventBySerial failed: %v", err) |
|
} |
|
if fetchedEv == nil { |
|
t.Fatalf("FetchEventBySerial returned nil event") |
|
} |
|
|
|
// Verify it's the same event |
|
if string(fetchedEv.ID[:]) != string(ev.ID[:]) { |
|
t.Errorf("Fetched event ID doesn't match: got %x, want %x", fetchedEv.ID, ev.ID) |
|
} |
|
|
|
// Test that QueryEvents also uses the fast path |
|
evs, err := db.QueryEvents(ctx, queryFilter) |
|
if err != nil { |
|
t.Fatalf("QueryEvents failed: %v", err) |
|
} |
|
if len(evs) != 1 { |
|
t.Fatalf("QueryEvents returned %d events, expected 1", len(evs)) |
|
} |
|
if string(evs[0].ID[:]) != string(ev.ID[:]) { |
|
t.Errorf("QueryEvents returned wrong event: got %x, want %x", evs[0].ID, ev.ID) |
|
} |
|
|
|
t.Logf("Successfully queried addressable event via fast path: kind=%d, d=%s", ev.Kind, string(dTagValue)) |
|
} |
|
|
|
func TestQueryForAddressableEventNotFound(t *testing.T) { |
|
// Create temporary database |
|
tempDir, err := os.MkdirTemp("", "test-addressable-notfound-*") |
|
if err != nil { |
|
t.Fatalf("Failed to create temp dir: %v", err) |
|
} |
|
defer os.RemoveAll(tempDir) |
|
|
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
db, err := New(ctx, cancel, tempDir, "info") |
|
if err != nil { |
|
t.Fatalf("failed to create database: %v", err) |
|
} |
|
defer db.Close() |
|
|
|
// Generate a test keypair |
|
signer := p8k.MustNew() |
|
if err := signer.Generate(); chk.E(err) { |
|
t.Fatal(err) |
|
} |
|
pub := signer.Pub() |
|
|
|
// Query for non-existent event |
|
queryFilter := &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", []byte("non-existent-d-tag"))), |
|
} |
|
|
|
serial, err := db.QueryForAddressableEvent(queryFilter) |
|
if err != nil { |
|
t.Fatalf("QueryForAddressableEvent failed: %v", err) |
|
} |
|
if serial != nil { |
|
t.Errorf("Expected nil serial for non-existent event, got %d", serial.Get()) |
|
} |
|
} |
|
|
|
func TestAddressableEventReplacement(t *testing.T) { |
|
// Create temporary database |
|
tempDir, err := os.MkdirTemp("", "test-addressable-replace-*") |
|
if err != nil { |
|
t.Fatalf("Failed to create temp dir: %v", err) |
|
} |
|
defer os.RemoveAll(tempDir) |
|
|
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
db, err := New(ctx, cancel, tempDir, "info") |
|
if err != nil { |
|
t.Fatalf("failed to create database: %v", err) |
|
} |
|
defer db.Close() |
|
|
|
// Generate a test keypair |
|
signer := p8k.MustNew() |
|
if err := signer.Generate(); chk.E(err) { |
|
t.Fatal(err) |
|
} |
|
pub := signer.Pub() |
|
|
|
dTagValue := []byte("replaceable-event") |
|
baseTime := time.Now().Unix() |
|
|
|
// Create and save the first event |
|
ev1 := &event.E{ |
|
Kind: 30000, |
|
Pubkey: pub, |
|
CreatedAt: baseTime, |
|
Content: []byte("First version"), |
|
Tags: tag.NewS(tag.NewFromAny("d", dTagValue)), |
|
} |
|
if err := ev1.Sign(signer); err != nil { |
|
t.Fatalf("failed to sign event 1: %v", err) |
|
} |
|
if _, err := db.SaveEvent(ctx, ev1); err != nil { |
|
t.Fatalf("failed to save event 1: %v", err) |
|
} |
|
|
|
// Create and save a newer replacement event |
|
ev2 := &event.E{ |
|
Kind: 30000, |
|
Pubkey: pub, |
|
CreatedAt: baseTime + 1000, // Newer |
|
Content: []byte("Second version - replacement"), |
|
Tags: tag.NewS(tag.NewFromAny("d", dTagValue)), |
|
} |
|
if err := ev2.Sign(signer); err != nil { |
|
t.Fatalf("failed to sign event 2: %v", err) |
|
} |
|
if _, err := db.SaveEvent(ctx, ev2); err != nil { |
|
t.Fatalf("failed to save event 2: %v", err) |
|
} |
|
|
|
// Query and verify we get the newer event via fast path |
|
queryFilter := &filter.F{ |
|
Kinds: kind.NewS(kind.New(30000)), |
|
Authors: tag.NewFromBytesSlice(pub), |
|
Tags: tag.NewS(tag.NewFromAny("#d", dTagValue)), |
|
} |
|
|
|
serial, err := db.QueryForAddressableEvent(queryFilter) |
|
if err != nil { |
|
t.Fatalf("QueryForAddressableEvent failed: %v", err) |
|
} |
|
if serial == nil { |
|
t.Fatalf("QueryForAddressableEvent returned nil serial") |
|
} |
|
|
|
fetchedEv, err := db.FetchEventBySerial(serial) |
|
if err != nil { |
|
t.Fatalf("FetchEventBySerial failed: %v", err) |
|
} |
|
|
|
// Should be the second (newer) event |
|
if string(fetchedEv.ID[:]) != string(ev2.ID[:]) { |
|
t.Errorf("Expected to get newer event (ev2), got different event") |
|
} |
|
if string(fetchedEv.Content) != "Second version - replacement" { |
|
t.Errorf("Expected content 'Second version - replacement', got '%s'", string(fetchedEv.Content)) |
|
} |
|
|
|
t.Logf("Replacement event correctly indexed: %x", ev2.ID) |
|
}
|
|
|