//go:build js && wasm package wasmdb import ( "bytes" "context" "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" "next.orly.dev/pkg/database/indexes/types" ) // TestDatabaseOpen tests that we can open an IndexedDB database func TestDatabaseOpen(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Create a new database instance db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() // Wait for the database to be ready select { case <-db.Ready(): t.Log("Database ready") case <-ctx.Done(): t.Fatal("Timeout waiting for database to be ready") } t.Log("Database opened successfully") } // TestDatabaseMetaStorage tests storing and retrieving metadata func TestDatabaseMetaStorage(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Test SetMarker and GetMarker testKey := "test_key" testValue := []byte("test_value_12345") err = db.SetMarker(testKey, testValue) if err != nil { t.Fatalf("Failed to set marker: %v", err) } retrieved, err := db.GetMarker(testKey) if err != nil { t.Fatalf("Failed to get marker: %v", err) } if string(retrieved) != string(testValue) { t.Errorf("Retrieved value %q doesn't match expected %q", retrieved, testValue) } // Test HasMarker if !db.HasMarker(testKey) { t.Error("HasMarker returned false for existing key") } if db.HasMarker("nonexistent_key") { t.Error("HasMarker returned true for nonexistent key") } // Test DeleteMarker err = db.DeleteMarker(testKey) if err != nil { t.Fatalf("Failed to delete marker: %v", err) } if db.HasMarker(testKey) { t.Error("HasMarker returned true after deletion") } } // TestDatabaseSerialCounters tests the serial number generation func TestDatabaseSerialCounters(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Generate some serials and verify they're incrementing serial1, err := db.nextEventSerial() if err != nil { t.Fatalf("Failed to get first serial: %v", err) } serial2, err := db.nextEventSerial() if err != nil { t.Fatalf("Failed to get second serial: %v", err) } if serial2 != serial1+1 { t.Errorf("Serials not incrementing: got %d and %d", serial1, serial2) } t.Logf("Generated serials: %d, %d", serial1, serial2) } // TestDatabaseRelayIdentity tests relay identity key management func TestDatabaseRelayIdentity(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // First call should create a new identity skb, err := db.GetOrCreateRelayIdentitySecret() if err != nil { t.Fatalf("Failed to get/create relay identity: %v", err) } if len(skb) != 32 { t.Errorf("Expected 32-byte secret key, got %d bytes", len(skb)) } // Second call should return the same key skb2, err := db.GetOrCreateRelayIdentitySecret() if err != nil { t.Fatalf("Failed to get relay identity second time: %v", err) } if string(skb) != string(skb2) { t.Error("GetOrCreateRelayIdentitySecret returned different keys on second call") } t.Logf("Relay identity key: %x", skb[:8]) } // TestDatabaseWipe tests the wipe functionality func TestDatabaseWipe(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Store some data err = db.SetMarker("wipe_test_key", []byte("wipe_test_value")) if err != nil { t.Fatalf("Failed to set marker: %v", err) } // Wipe the database err = db.Wipe() if err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Verify data is gone if db.HasMarker("wipe_test_key") { t.Error("Marker still exists after wipe") } t.Log("Database wipe successful") } // createTestEvent creates a test event with fake ID and signature (for storage testing only) func createTestEvent(kind uint16, content string, pubkey []byte) *event.E { ev := event.New() ev.Kind = kind ev.Content = []byte(content) ev.CreatedAt = time.Now().Unix() ev.Tags = tag.NewS() // Use provided pubkey or generate a fake one if len(pubkey) == 32 { ev.Pubkey = pubkey } else { ev.Pubkey = make([]byte, 32) for i := range ev.Pubkey { ev.Pubkey[i] = byte(i) } } // Generate a fake ID (normally would be SHA256 of serialized event) ev.ID = make([]byte, 32) for i := range ev.ID { ev.ID[i] = byte(i + 100) } // Generate a fake signature (won't verify, but storage doesn't need to verify) ev.Sig = make([]byte, 64) for i := range ev.Sig { ev.Sig[i] = byte(i + 200) } return ev } // TestSaveAndFetchEvent tests saving and fetching events func TestSaveAndFetchEvent(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Wipe the database to start fresh db, err := New(ctx, cancel, "/tmp/test", "debug") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create a test event ev := createTestEvent(1, "Hello, Nostr!", nil) t.Logf("Created event with ID: %x", ev.ID[:8]) // Save the event replaced, err := db.SaveEvent(ctx, ev) if err != nil { t.Fatalf("Failed to save event: %v", err) } if replaced { t.Error("Expected replaced to be false for new event") } t.Log("Event saved successfully") // Look up the serial by ID ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial by ID: %v", err) } if ser == nil { t.Fatal("Got nil serial") } t.Logf("Event serial: %d", ser.Get()) // Fetch the event by serial fetchedEv, err := db.FetchEventBySerial(ser) if err != nil { t.Fatalf("Failed to fetch event by serial: %v", err) } if fetchedEv == nil { t.Fatal("Fetched event is nil") } // Verify the event content matches if !bytes.Equal(fetchedEv.ID, ev.ID) { t.Errorf("Event ID mismatch: got %x, want %x", fetchedEv.ID[:8], ev.ID[:8]) } if !bytes.Equal(fetchedEv.Content, ev.Content) { t.Errorf("Event content mismatch: got %q, want %q", fetchedEv.Content, ev.Content) } if fetchedEv.Kind != ev.Kind { t.Errorf("Event kind mismatch: got %d, want %d", fetchedEv.Kind, ev.Kind) } t.Log("Event fetched and verified successfully") } // TestSaveEventDuplicate tests that saving a duplicate event returns an error func TestSaveEventDuplicate(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save a test event ev := createTestEvent(1, "Duplicate test", nil) _, err = db.SaveEvent(ctx, ev) if err != nil { t.Fatalf("Failed to save first event: %v", err) } // Try to save the same event again _, err = db.SaveEvent(ctx, ev) if err == nil { t.Error("Expected error when saving duplicate event, got nil") } else { t.Logf("Got expected error for duplicate: %v", err) } } // TestSaveEventWithTags tests saving an event with tags func TestSaveEventWithTags(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create an event with tags ev := createTestEvent(1, "Event with tags", nil) // Make the ID unique ev.ID[0] = 0x42 // Add some tags ev.Tags = tag.NewS( tag.NewFromAny("t", "nostr"), tag.NewFromAny("t", "test"), ) // Save the event _, err = db.SaveEvent(ctx, ev) if err != nil { t.Fatalf("Failed to save event with tags: %v", err) } // Fetch it back ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial: %v", err) } fetchedEv, err := db.FetchEventBySerial(ser) if err != nil { t.Fatalf("Failed to fetch event: %v", err) } // Verify tags if fetchedEv.Tags == nil || fetchedEv.Tags.Len() != 2 { t.Errorf("Expected 2 tags, got %v", fetchedEv.Tags) } t.Log("Event with tags saved and fetched successfully") } // TestFetchEventsBySerials tests batch fetching of events func TestFetchEventsBySerials(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save multiple events var serials []*types.Uint40 for i := 0; i < 3; i++ { ev := createTestEvent(1, "Batch test event", nil) ev.ID[0] = byte(0x50 + i) // Make IDs unique _, err := db.SaveEvent(ctx, ev) if err != nil { t.Fatalf("Failed to save event %d: %v", i, err) } ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial for event %d: %v", i, err) } serials = append(serials, ser) } // Batch fetch events, err := db.FetchEventsBySerials(serials) if err != nil { t.Fatalf("Failed to batch fetch events: %v", err) } if len(events) != 3 { t.Errorf("Expected 3 events, got %d", len(events)) } t.Logf("Successfully batch fetched %d events", len(events)) } // TestGetFullIdPubkeyBySerial tests getting event metadata by serial func TestGetFullIdPubkeyBySerial(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "debug") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save an event ev := createTestEvent(1, "Metadata test", nil) ev.ID[0] = 0x99 // Make ID unique _, err = db.SaveEvent(ctx, ev) if err != nil { t.Fatalf("Failed to save event: %v", err) } ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial: %v", err) } // Get full ID/pubkey/timestamp metadata fidpk, err := db.GetFullIdPubkeyBySerial(ser) if err != nil { t.Fatalf("Failed to get full id pubkey: %v", err) } if fidpk == nil { t.Fatal("Got nil fidpk") } // Verify the ID matches if !bytes.Equal(fidpk.Id, ev.ID) { t.Errorf("ID mismatch: got %x, want %x", fidpk.Id[:8], ev.ID[:8]) } t.Logf("Got metadata: ID=%x, Pub=%x, Ts=%d, Ser=%d", fidpk.Id[:8], fidpk.Pub[:4], fidpk.Ts, fidpk.Ser) } // TestQueryEventsByKind tests querying events by kind func TestQueryEventsByKind(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create events of different kinds ev1 := createTestEvent(1, "Kind 1 event", nil) ev1.ID[0] = 0xA1 ev2 := createTestEvent(1, "Another kind 1", nil) ev2.ID[0] = 0xA2 ev3 := createTestEvent(7, "Kind 7 event", nil) ev3.ID[0] = 0xA3 // Save events for _, ev := range []*event.E{ev1, ev2, ev3} { if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Query for kind 1 f := &filter.F{ Kinds: kind.NewS(kind.New(1)), } evs, err := db.QueryEvents(ctx, f) if err != nil { t.Fatalf("Failed to query events: %v", err) } if len(evs) != 2 { t.Errorf("Expected 2 events of kind 1, got %d", len(evs)) } t.Logf("Query by kind 1 returned %d events", len(evs)) } // TestQueryEventsByAuthor tests querying events by author pubkey func TestQueryEventsByAuthor(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create events from different authors author1 := make([]byte, 32) for i := range author1 { author1[i] = 0x11 } author2 := make([]byte, 32) for i := range author2 { author2[i] = 0x22 } ev1 := createTestEvent(1, "From author 1", author1) ev1.ID[0] = 0xB1 ev2 := createTestEvent(1, "Also from author 1", author1) ev2.ID[0] = 0xB2 ev3 := createTestEvent(1, "From author 2", author2) ev3.ID[0] = 0xB3 // Save events for _, ev := range []*event.E{ev1, ev2, ev3} { if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Query for author1 f := &filter.F{ Authors: tag.NewFromBytesSlice(author1), } evs, err := db.QueryEvents(ctx, f) if err != nil { t.Fatalf("Failed to query events: %v", err) } if len(evs) != 2 { t.Errorf("Expected 2 events from author1, got %d", len(evs)) } t.Logf("Query by author returned %d events", len(evs)) } // TestCountEvents tests the event counting functionality func TestCountEvents(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create multiple events for i := 0; i < 5; i++ { ev := createTestEvent(1, "Count test event", nil) ev.ID[0] = byte(0xC0 + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Count all kind 1 events f := &filter.F{ Kinds: kind.NewS(kind.New(1)), } count, approx, err := db.CountEvents(ctx, f) if err != nil { t.Fatalf("Failed to count events: %v", err) } if count != 5 { t.Errorf("Expected count of 5, got %d", count) } if approx { t.Log("Count is approximate") } t.Logf("CountEvents returned %d", count) } // TestQueryEventsWithLimit tests applying a limit to query results func TestQueryEventsWithLimit(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create 10 events for i := 0; i < 10; i++ { ev := createTestEvent(1, "Limit test event", nil) ev.ID[0] = byte(0xD0 + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Query with limit of 3 limit := uint(3) f := &filter.F{ Kinds: kind.NewS(kind.New(1)), Limit: &limit, } evs, err := db.QueryEvents(ctx, f) if err != nil { t.Fatalf("Failed to query events: %v", err) } if len(evs) != 3 { t.Errorf("Expected 3 events with limit, got %d", len(evs)) } t.Logf("Query with limit returned %d events", len(evs)) } // TestQueryEventsByTag tests querying events by tag func TestQueryEventsByTag(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create events with different tags ev1 := createTestEvent(1, "Has bitcoin tag", nil) ev1.ID[0] = 0xE1 ev1.Tags = tag.NewS( tag.NewFromAny("t", "bitcoin"), ) ev2 := createTestEvent(1, "Has nostr tag", nil) ev2.ID[0] = 0xE2 ev2.Tags = tag.NewS( tag.NewFromAny("t", "nostr"), ) ev3 := createTestEvent(1, "Has bitcoin and nostr tags", nil) ev3.ID[0] = 0xE3 ev3.Tags = tag.NewS( tag.NewFromAny("t", "bitcoin"), tag.NewFromAny("t", "nostr"), ) // Save events for _, ev := range []*event.E{ev1, ev2, ev3} { if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Query for bitcoin tag f := &filter.F{ Tags: tag.NewS( tag.NewFromAny("#t", "bitcoin"), ), } evs, err := db.QueryEvents(ctx, f) if err != nil { t.Fatalf("Failed to query events: %v", err) } if len(evs) != 2 { t.Errorf("Expected 2 events with bitcoin tag, got %d", len(evs)) } t.Logf("Query by tag returned %d events", len(evs)) } // TestQueryForSerials tests the QueryForSerials method func TestQueryForSerials(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save events for i := 0; i < 3; i++ { ev := createTestEvent(1, "Serial test", nil) ev.ID[0] = byte(0xF0 + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Query for serials f := &filter.F{ Kinds: kind.NewS(kind.New(1)), } sers, err := db.QueryForSerials(ctx, f) if err != nil { t.Fatalf("Failed to query for serials: %v", err) } if len(sers) != 3 { t.Errorf("Expected 3 serials, got %d", len(sers)) } t.Logf("QueryForSerials returned %d serials", len(sers)) } // TestDeleteEvent tests deleting an event by ID func TestDeleteEvent(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save an event ev := createTestEvent(1, "Event to delete", nil) ev.ID[0] = 0xDE if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } // Verify it exists ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial: %v", err) } if ser == nil { t.Fatal("Serial should not be nil after save") } // Delete the event if err := db.DeleteEvent(ctx, ev.ID); err != nil { t.Fatalf("Failed to delete event: %v", err) } // Verify it no longer exists ser, err = db.GetSerialById(ev.ID) if err == nil && ser != nil { t.Error("Event should not exist after deletion") } t.Log("Event deleted successfully") } // TestDeleteEventBySerial tests deleting an event by serial number func TestDeleteEventBySerial(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save an event ev := createTestEvent(1, "Event to delete by serial", nil) ev.ID[0] = 0xDF if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } // Get the serial ser, err := db.GetSerialById(ev.ID) if err != nil { t.Fatalf("Failed to get serial: %v", err) } // Fetch the event fetchedEv, err := db.FetchEventBySerial(ser) if err != nil { t.Fatalf("Failed to fetch event: %v", err) } // Delete by serial if err := db.DeleteEventBySerial(ctx, ser, fetchedEv); err != nil { t.Fatalf("Failed to delete event by serial: %v", err) } // Verify event is gone fetchedEv, err = db.FetchEventBySerial(ser) if err == nil && fetchedEv != nil { t.Error("Event should not exist after deletion by serial") } t.Log("Event deleted by serial successfully") } // TestCheckForDeleted tests that deleted events are properly detected func TestCheckForDeleted(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create a regular event ev := createTestEvent(1, "Event that will be deleted", nil) ev.ID[0] = 0xDD ev.CreatedAt = time.Now().Unix() - 100 // 100 seconds ago if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } // Create a kind 5 deletion event referencing it // Use binary format for the e-tag value since GetIndexesForEvent hashes the raw value, // and NormalizeTagValueForHash converts hex to binary before hashing in filters deleteEv := createTestEvent(5, "", ev.Pubkey) deleteEv.ID[0] = 0xD5 deleteEv.CreatedAt = time.Now().Unix() // Now // Store the e-tag with binary value in the format matching JSON unmarshal // The nostr library stores e/p tag values as 33 bytes (32 bytes + null terminator) eTagValue := make([]byte, 33) copy(eTagValue[:32], ev.ID) eTagValue[32] = 0 // null terminator to match nostr library's binary format deleteEv.Tags = tag.NewS( tag.NewFromAny("e", string(eTagValue)), ) if _, err := db.SaveEvent(ctx, deleteEv); err != nil { t.Fatalf("Failed to save delete event: %v", err) } // Check if the original event is marked as deleted err = db.CheckForDeleted(ev, nil) if err == nil { t.Error("Expected error indicating event was deleted") } else { t.Logf("CheckForDeleted correctly detected deletion: %v", err) } } // ============================================================================ // NIP-43 Membership Tests // ============================================================================ // TestNIP43AddAndRemoveMember tests adding and removing NIP-43 members func TestNIP43AddAndRemoveMember(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create a test pubkey pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 1) } // Initially should not be a member isMember, err := db.IsNIP43Member(pubkey) if err != nil { t.Fatalf("Failed to check member status: %v", err) } if isMember { t.Error("Expected non-member initially") } // Add member with invite code inviteCode := "TEST-INVITE-123" if err := db.AddNIP43Member(pubkey, inviteCode); err != nil { t.Fatalf("Failed to add NIP-43 member: %v", err) } // Should now be a member isMember, err = db.IsNIP43Member(pubkey) if err != nil { t.Fatalf("Failed to check member status: %v", err) } if !isMember { t.Error("Expected to be a member after adding") } // Get membership details membership, err := db.GetNIP43Membership(pubkey) if err != nil { t.Fatalf("Failed to get membership: %v", err) } if membership.InviteCode != inviteCode { t.Errorf("Invite code mismatch: got %s, want %s", membership.InviteCode, inviteCode) } if !bytes.Equal(membership.Pubkey, pubkey) { t.Error("Pubkey mismatch in membership") } // Remove member if err := db.RemoveNIP43Member(pubkey); err != nil { t.Fatalf("Failed to remove member: %v", err) } // Should no longer be a member isMember, err = db.IsNIP43Member(pubkey) if err != nil { t.Fatalf("Failed to check member status: %v", err) } if isMember { t.Error("Expected non-member after removal") } t.Log("NIP-43 add/remove member test passed") } // TestNIP43GetAllMembers tests retrieving all members func TestNIP43GetAllMembers(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Add multiple members pubkeys := make([][]byte, 3) for i := 0; i < 3; i++ { pubkeys[i] = make([]byte, 32) for j := range pubkeys[i] { pubkeys[i][j] = byte((i+1)*10 + j) } if err := db.AddNIP43Member(pubkeys[i], "invite"+string(rune('A'+i))); err != nil { t.Fatalf("Failed to add member %d: %v", i, err) } } // Get all members members, err := db.GetAllNIP43Members() if err != nil { t.Fatalf("Failed to get all members: %v", err) } if len(members) != 3 { t.Errorf("Expected 3 members, got %d", len(members)) } t.Logf("Retrieved %d NIP-43 members", len(members)) } // TestNIP43InviteCode tests invite code functionality func TestNIP43InviteCode(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Store a valid invite code (expires in 24 hours) validCode := "VALID-CODE-ABC" expiresAt := time.Now().Add(24 * time.Hour) if err := db.StoreInviteCode(validCode, expiresAt); err != nil { t.Fatalf("Failed to store invite code: %v", err) } // Validate the code valid, err := db.ValidateInviteCode(validCode) if err != nil { t.Fatalf("Failed to validate invite code: %v", err) } if !valid { t.Error("Expected valid invite code to be valid") } // Check non-existent code valid, err = db.ValidateInviteCode("NONEXISTENT") if err != nil { t.Fatalf("Unexpected error: %v", err) } if valid { t.Error("Expected non-existent code to be invalid") } // Delete the code if err := db.DeleteInviteCode(validCode); err != nil { t.Fatalf("Failed to delete invite code: %v", err) } // Should now be invalid valid, err = db.ValidateInviteCode(validCode) if err != nil { t.Fatalf("Unexpected error: %v", err) } if valid { t.Error("Expected deleted code to be invalid") } t.Log("NIP-43 invite code test passed") } // TestNIP43ExpiredInviteCode tests that expired invite codes are invalid func TestNIP43ExpiredInviteCode(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Store an expired invite code (expired 1 hour ago) expiredCode := "EXPIRED-CODE-XYZ" expiresAt := time.Now().Add(-1 * time.Hour) if err := db.StoreInviteCode(expiredCode, expiresAt); err != nil { t.Fatalf("Failed to store invite code: %v", err) } // Validate the expired code valid, err := db.ValidateInviteCode(expiredCode) if err != nil { t.Fatalf("Unexpected error: %v", err) } if valid { t.Error("Expected expired invite code to be invalid") } t.Log("Expired invite code correctly detected as invalid") } // TestNIP43InvalidPubkeyLength tests that invalid pubkey lengths are rejected func TestNIP43InvalidPubkeyLength(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Try to add a member with invalid pubkey length shortPubkey := make([]byte, 16) // Should be 32 err = db.AddNIP43Member(shortPubkey, "test") if err == nil { t.Error("Expected error for invalid pubkey length") } t.Logf("Correctly rejected invalid pubkey length: %v", err) } // ============================================================================ // Subscription Management Tests // ============================================================================ // TestSubscriptionExtend tests extending subscriptions func TestSubscriptionExtend(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 50) } // Initially no subscription sub, err := db.GetSubscription(pubkey) if err != nil { t.Fatalf("Failed to get subscription: %v", err) } if sub != nil { t.Error("Expected nil subscription initially") } // Extend subscription by 30 days if err := db.ExtendSubscription(pubkey, 30); err != nil { t.Fatalf("Failed to extend subscription: %v", err) } // Should now have a subscription sub, err = db.GetSubscription(pubkey) if err != nil { t.Fatalf("Failed to get subscription: %v", err) } if sub == nil { t.Fatal("Expected subscription after extension") } // Verify paid until is in the future if sub.PaidUntil.Before(time.Now()) { t.Error("PaidUntil should be in the future") } // Extend again if err := db.ExtendSubscription(pubkey, 15); err != nil { t.Fatalf("Failed to extend subscription again: %v", err) } sub2, err := db.GetSubscription(pubkey) if err != nil { t.Fatalf("Failed to get subscription: %v", err) } // Second extension should add to first if !sub2.PaidUntil.After(sub.PaidUntil) { t.Error("Expected PaidUntil to increase after second extension") } t.Log("Subscription extension test passed") } // TestSubscriptionActive tests checking if subscription is active func TestSubscriptionActive(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 60) } // First check creates a trial subscription active, err := db.IsSubscriptionActive(pubkey) if err != nil { t.Fatalf("Failed to check subscription: %v", err) } if !active { t.Error("Expected new user to have active trial subscription") } // Second check should still be active active, err = db.IsSubscriptionActive(pubkey) if err != nil { t.Fatalf("Failed to check subscription: %v", err) } if !active { t.Error("Expected subscription to remain active") } t.Log("Subscription active check passed") } // TestBlossomSubscription tests blossom storage subscription func TestBlossomSubscription(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 70) } // Initially no quota quota, err := db.GetBlossomStorageQuota(pubkey) if err != nil { t.Fatalf("Failed to get quota: %v", err) } if quota != 0 { t.Error("Expected zero quota initially") } // Add blossom subscription if err := db.ExtendBlossomSubscription(pubkey, "premium", 1024, 30); err != nil { t.Fatalf("Failed to extend blossom subscription: %v", err) } // Check quota quota, err = db.GetBlossomStorageQuota(pubkey) if err != nil { t.Fatalf("Failed to get quota: %v", err) } if quota != 1024 { t.Errorf("Expected quota of 1024, got %d", quota) } // Check subscription details sub, err := db.GetSubscription(pubkey) if err != nil { t.Fatalf("Failed to get subscription: %v", err) } if sub.BlossomLevel != "premium" { t.Errorf("Expected premium level, got %s", sub.BlossomLevel) } t.Log("Blossom subscription test passed") } // TestPaymentHistory tests recording and retrieving payment history func TestPaymentHistory(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 80) } // Initially no payment history payments, err := db.GetPaymentHistory(pubkey) if err != nil { t.Fatalf("Failed to get payment history: %v", err) } if len(payments) != 0 { t.Error("Expected empty payment history initially") } // Record a payment if err := db.RecordPayment(pubkey, 10000, "lnbc100...", "preimage123"); err != nil { t.Fatalf("Failed to record payment: %v", err) } // Small delay to ensure different timestamps time.Sleep(10 * time.Millisecond) // Record another payment if err := db.RecordPayment(pubkey, 20000, "lnbc200...", "preimage456"); err != nil { t.Fatalf("Failed to record payment: %v", err) } // Check payment history payments, err = db.GetPaymentHistory(pubkey) if err != nil { t.Fatalf("Failed to get payment history: %v", err) } if len(payments) != 2 { t.Errorf("Expected 2 payments, got %d", len(payments)) } t.Logf("Retrieved %d payments from history", len(payments)) } // TestIsFirstTimeUser tests first-time user detection func TestIsFirstTimeUser(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } pubkey := make([]byte, 32) for i := range pubkey { pubkey[i] = byte(i + 90) } // First check should be true isFirst, err := db.IsFirstTimeUser(pubkey) if err != nil { t.Fatalf("Failed to check first time user: %v", err) } if !isFirst { t.Error("Expected true for first check") } // Second check should be false isFirst, err = db.IsFirstTimeUser(pubkey) if err != nil { t.Fatalf("Failed to check first time user: %v", err) } if isFirst { t.Error("Expected false for second check") } t.Log("First time user detection test passed") } // TestSubscriptionInvalidDays tests that invalid day counts are rejected func TestSubscriptionInvalidDays(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() pubkey := make([]byte, 32) // Try to extend with 0 days err = db.ExtendSubscription(pubkey, 0) if err == nil { t.Error("Expected error for 0 days") } // Try to extend with negative days err = db.ExtendSubscription(pubkey, -5) if err == nil { t.Error("Expected error for negative days") } t.Log("Invalid days correctly rejected") } // ============================================================================ // Import/Export Tests // ============================================================================ // TestImportExport tests basic import/export functionality func TestImportExport(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create and save some events for i := 0; i < 3; i++ { ev := createTestEvent(1, "Export test event", nil) ev.ID[0] = byte(0xAA + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Export to buffer var exportBuf bytes.Buffer db.Export(ctx, &exportBuf) exportData := exportBuf.String() if len(exportData) == 0 { t.Error("Expected non-empty export data") } // Count lines (should be 3 JSONL lines) lines := bytes.Count(exportBuf.Bytes(), []byte("\n")) if lines != 3 { t.Errorf("Expected 3 export lines, got %d", lines) } t.Logf("Exported %d events to %d bytes", lines, len(exportData)) } // TestImportFromReader tests importing events from JSONL reader func TestImportFromReader(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create JSONL import data // Note: These are simplified test events - real events would have valid signatures jsonl := `{"id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","pubkey":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","created_at":1700000000,"kind":1,"tags":[],"content":"Test event 1","sig":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"} {"id":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","pubkey":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","created_at":1700000001,"kind":1,"tags":[],"content":"Test event 2","sig":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"} ` reader := bytes.NewReader([]byte(jsonl)) err = db.ImportEventsFromReader(ctx, reader) if err != nil { t.Fatalf("Failed to import events: %v", err) } t.Log("Import from reader test completed") } // TestImportExportRoundTrip tests full round-trip import/export func TestImportExportRoundTrip(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Create first database db1, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database 1: %v", err) } defer db1.Close() <-db1.Ready() // Wipe to ensure clean state if err := db1.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create events with specific content for verification // Using kinds 1, 7, 10, 20, 30 to avoid kind 3 which requires p tags kinds := []uint16{1, 7, 10, 20, 30} originalEvents := make([]*event.E, 5) for i := 0; i < 5; i++ { ev := createTestEvent(kinds[i], "Round trip content "+string(rune('A'+i)), nil) ev.ID[0] = byte(0xBB + i) originalEvents[i] = ev if _, err := db1.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Export from db1 var exportBuf bytes.Buffer db1.Export(ctx, &exportBuf) // Wipe db1 (simulating a fresh database) if err := db1.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Import back db1.Import(&exportBuf) // Query all events f := &filter.F{} evs, err := db1.QueryEvents(ctx, f) if err != nil { t.Fatalf("Failed to query events: %v", err) } if len(evs) < 5 { t.Errorf("Expected at least 5 events after round trip, got %d", len(evs)) } t.Logf("Round trip test: %d events survived", len(evs)) } // TestGetSerialsByPubkey tests retrieving serials by pubkey func TestGetSerialsByPubkey(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create a specific author author := make([]byte, 32) for i := range author { author[i] = 0xCC } // Create events from this author for i := 0; i < 3; i++ { ev := createTestEvent(1, "Author test", author) ev.ID[0] = byte(0xCC + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Get serials by pubkey serials, err := db.GetSerialsByPubkey(author) if err != nil { t.Fatalf("Failed to get serials by pubkey: %v", err) } if len(serials) != 3 { t.Errorf("Expected 3 serials, got %d", len(serials)) } t.Logf("GetSerialsByPubkey returned %d serials", len(serials)) } // TestExportByPubkey tests exporting events filtered by pubkey func TestExportByPubkey(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := New(ctx, cancel, "/tmp/test", "info") if err != nil { t.Fatalf("Failed to create database: %v", err) } defer db.Close() <-db.Ready() // Wipe to ensure clean state if err := db.Wipe(); err != nil { t.Fatalf("Failed to wipe database: %v", err) } // Create two different authors author1 := make([]byte, 32) for i := range author1 { author1[i] = 0xDD } author2 := make([]byte, 32) for i := range author2 { author2[i] = 0xEE } // Create events from both authors for i := 0; i < 2; i++ { ev := createTestEvent(1, "Author 1 content", author1) ev.ID[0] = byte(0xD0 + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } for i := 0; i < 3; i++ { ev := createTestEvent(1, "Author 2 content", author2) ev.ID[0] = byte(0xE0 + i) if _, err := db.SaveEvent(ctx, ev); err != nil { t.Fatalf("Failed to save event: %v", err) } } // Export only author1's events var exportBuf bytes.Buffer db.Export(ctx, &exportBuf, author1) lines := bytes.Count(exportBuf.Bytes(), []byte("\n")) if lines != 2 { t.Errorf("Expected 2 export lines for author1, got %d", lines) } t.Logf("Exported %d events for specific pubkey", lines) }