Files
next.orly.dev/pkg/wasmdb/wasmdb_test.go

1740 lines
43 KiB
Go

//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)
}