query-addressable_test.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"context"
   7  	"os"
   8  	"testing"
   9  	"time"
  10  
  11  	"next.orly.dev/pkg/nostr/encoders/event"
  12  	"next.orly.dev/pkg/nostr/encoders/filter"
  13  	"next.orly.dev/pkg/nostr/encoders/kind"
  14  	"next.orly.dev/pkg/nostr/encoders/tag"
  15  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  16  	"next.orly.dev/pkg/lol/chk"
  17  )
  18  
  19  func TestIsAddressableEventQuery(t *testing.T) {
  20  	// Generate a test keypair
  21  	signer := p8k.MustNew()
  22  	if err := signer.Generate(); chk.E(err) {
  23  		t.Fatal(err)
  24  	}
  25  	pub := signer.Pub()
  26  
  27  	tests := []struct {
  28  		name     string
  29  		filter   *filter.F
  30  		expected bool
  31  	}{
  32  		{
  33  			name: "valid NIP-33 query - kind 30000",
  34  			filter: &filter.F{
  35  				Kinds:   kind.NewS(kind.New(30000)),
  36  				Authors: tag.NewFromBytesSlice(pub),
  37  				Tags:    tag.NewS(tag.NewFromAny("#d", []byte("test-d-tag"))),
  38  			},
  39  			expected: true,
  40  		},
  41  		{
  42  			name: "valid NIP-33 query - kind 30382",
  43  			filter: &filter.F{
  44  				Kinds:   kind.NewS(kind.New(30382)),
  45  				Authors: tag.NewFromBytesSlice(pub),
  46  				Tags:    tag.NewS(tag.NewFromAny("#d", []byte("some-identifier"))),
  47  			},
  48  			expected: true,
  49  		},
  50  		{
  51  			name: "invalid - kind 1 (not parameterized replaceable)",
  52  			filter: &filter.F{
  53  				Kinds:   kind.NewS(kind.New(1)),
  54  				Authors: tag.NewFromBytesSlice(pub),
  55  				Tags:    tag.NewS(tag.NewFromAny("#d", []byte("test"))),
  56  			},
  57  			expected: false,
  58  		},
  59  		{
  60  			name: "invalid - kind 10000 (replaceable, not parameterized)",
  61  			filter: &filter.F{
  62  				Kinds:   kind.NewS(kind.New(10000)),
  63  				Authors: tag.NewFromBytesSlice(pub),
  64  				Tags:    tag.NewS(tag.NewFromAny("#d", []byte("test"))),
  65  			},
  66  			expected: false,
  67  		},
  68  		{
  69  			name: "invalid - missing d-tag",
  70  			filter: &filter.F{
  71  				Kinds:   kind.NewS(kind.New(30000)),
  72  				Authors: tag.NewFromBytesSlice(pub),
  73  			},
  74  			expected: false,
  75  		},
  76  		{
  77  			name: "invalid - multiple kinds",
  78  			filter: &filter.F{
  79  				Kinds:   kind.NewS(kind.New(30000), kind.New(30001)),
  80  				Authors: tag.NewFromBytesSlice(pub),
  81  				Tags:    tag.NewS(tag.NewFromAny("#d", []byte("test"))),
  82  			},
  83  			expected: false,
  84  		},
  85  		{
  86  			name: "invalid - no authors",
  87  			filter: &filter.F{
  88  				Kinds: kind.NewS(kind.New(30000)),
  89  				Tags:  tag.NewS(tag.NewFromAny("#d", []byte("test"))),
  90  			},
  91  			expected: false,
  92  		},
  93  	}
  94  
  95  	for _, tt := range tests {
  96  		t.Run(tt.name, func(t *testing.T) {
  97  			result := IsAddressableEventQuery(tt.filter)
  98  			if result != tt.expected {
  99  				t.Errorf("IsAddressableEventQuery() = %v, want %v", result, tt.expected)
 100  			}
 101  		})
 102  	}
 103  }
 104  
 105  func TestQueryForAddressableEvent(t *testing.T) {
 106  	// Create temporary database
 107  	tempDir, err := os.MkdirTemp("", "test-addressable-*")
 108  	if err != nil {
 109  		t.Fatalf("Failed to create temp dir: %v", err)
 110  	}
 111  	defer os.RemoveAll(tempDir)
 112  
 113  	ctx, cancel := context.WithCancel(context.Background())
 114  	defer cancel()
 115  
 116  	db, err := New(ctx, cancel, tempDir, "info")
 117  	if err != nil {
 118  		t.Fatalf("failed to create database: %v", err)
 119  	}
 120  	defer db.Close()
 121  
 122  	// Generate a test keypair
 123  	signer := p8k.MustNew()
 124  	if err := signer.Generate(); chk.E(err) {
 125  		t.Fatal(err)
 126  	}
 127  	pub := signer.Pub()
 128  
 129  	// Create and save a parameterized replaceable event (kind 30382)
 130  	dTagValue := []byte("test-identifier-12345")
 131  	ev := &event.E{
 132  		Kind:      30382,
 133  		Pubkey:    pub,
 134  		CreatedAt: time.Now().Unix(),
 135  		Content:   []byte("Test content for addressable event"),
 136  		Tags:      tag.NewS(tag.NewFromAny("d", dTagValue)),
 137  	}
 138  
 139  	// Sign the event
 140  	if err := ev.Sign(signer); err != nil {
 141  		t.Fatalf("failed to sign event: %v", err)
 142  	}
 143  
 144  	// Save the event
 145  	_, err = db.SaveEvent(ctx, ev)
 146  	if err != nil {
 147  		t.Fatalf("failed to save event: %v", err)
 148  	}
 149  
 150  	// Query using the fast path
 151  	queryFilter := &filter.F{
 152  		Kinds:   kind.NewS(kind.New(30382)),
 153  		Authors: tag.NewFromBytesSlice(pub),
 154  		Tags:    tag.NewS(tag.NewFromAny("#d", dTagValue)),
 155  	}
 156  
 157  	// Test IsAddressableEventQuery
 158  	if !IsAddressableEventQuery(queryFilter) {
 159  		t.Errorf("Expected IsAddressableEventQuery to return true for valid NIP-33 filter")
 160  	}
 161  
 162  	// Test QueryForAddressableEvent
 163  	serial, err := db.QueryForAddressableEvent(queryFilter)
 164  	if err != nil {
 165  		t.Fatalf("QueryForAddressableEvent failed: %v", err)
 166  	}
 167  	if serial == nil {
 168  		t.Fatalf("QueryForAddressableEvent returned nil serial, expected to find event")
 169  	}
 170  
 171  	// Fetch the event and verify it matches
 172  	fetchedEv, err := db.FetchEventBySerial(serial)
 173  	if err != nil {
 174  		t.Fatalf("FetchEventBySerial failed: %v", err)
 175  	}
 176  	if fetchedEv == nil {
 177  		t.Fatalf("FetchEventBySerial returned nil event")
 178  	}
 179  
 180  	// Verify it's the same event
 181  	if string(fetchedEv.ID[:]) != string(ev.ID[:]) {
 182  		t.Errorf("Fetched event ID doesn't match: got %x, want %x", fetchedEv.ID, ev.ID)
 183  	}
 184  
 185  	// Test that QueryEvents also uses the fast path
 186  	evs, err := db.QueryEvents(ctx, queryFilter)
 187  	if err != nil {
 188  		t.Fatalf("QueryEvents failed: %v", err)
 189  	}
 190  	if len(evs) != 1 {
 191  		t.Fatalf("QueryEvents returned %d events, expected 1", len(evs))
 192  	}
 193  	if string(evs[0].ID[:]) != string(ev.ID[:]) {
 194  		t.Errorf("QueryEvents returned wrong event: got %x, want %x", evs[0].ID, ev.ID)
 195  	}
 196  
 197  	t.Logf("Successfully queried addressable event via fast path: kind=%d, d=%s", ev.Kind, string(dTagValue))
 198  }
 199  
 200  func TestQueryForAddressableEventNotFound(t *testing.T) {
 201  	// Create temporary database
 202  	tempDir, err := os.MkdirTemp("", "test-addressable-notfound-*")
 203  	if err != nil {
 204  		t.Fatalf("Failed to create temp dir: %v", err)
 205  	}
 206  	defer os.RemoveAll(tempDir)
 207  
 208  	ctx, cancel := context.WithCancel(context.Background())
 209  	defer cancel()
 210  
 211  	db, err := New(ctx, cancel, tempDir, "info")
 212  	if err != nil {
 213  		t.Fatalf("failed to create database: %v", err)
 214  	}
 215  	defer db.Close()
 216  
 217  	// Generate a test keypair
 218  	signer := p8k.MustNew()
 219  	if err := signer.Generate(); chk.E(err) {
 220  		t.Fatal(err)
 221  	}
 222  	pub := signer.Pub()
 223  
 224  	// Query for non-existent event
 225  	queryFilter := &filter.F{
 226  		Kinds:   kind.NewS(kind.New(30000)),
 227  		Authors: tag.NewFromBytesSlice(pub),
 228  		Tags:    tag.NewS(tag.NewFromAny("#d", []byte("non-existent-d-tag"))),
 229  	}
 230  
 231  	serial, err := db.QueryForAddressableEvent(queryFilter)
 232  	if err != nil {
 233  		t.Fatalf("QueryForAddressableEvent failed: %v", err)
 234  	}
 235  	if serial != nil {
 236  		t.Errorf("Expected nil serial for non-existent event, got %d", serial.Get())
 237  	}
 238  }
 239  
 240  func TestAddressableEventReplacement(t *testing.T) {
 241  	// Create temporary database
 242  	tempDir, err := os.MkdirTemp("", "test-addressable-replace-*")
 243  	if err != nil {
 244  		t.Fatalf("Failed to create temp dir: %v", err)
 245  	}
 246  	defer os.RemoveAll(tempDir)
 247  
 248  	ctx, cancel := context.WithCancel(context.Background())
 249  	defer cancel()
 250  
 251  	db, err := New(ctx, cancel, tempDir, "info")
 252  	if err != nil {
 253  		t.Fatalf("failed to create database: %v", err)
 254  	}
 255  	defer db.Close()
 256  
 257  	// Generate a test keypair
 258  	signer := p8k.MustNew()
 259  	if err := signer.Generate(); chk.E(err) {
 260  		t.Fatal(err)
 261  	}
 262  	pub := signer.Pub()
 263  
 264  	dTagValue := []byte("replaceable-event")
 265  	baseTime := time.Now().Unix()
 266  
 267  	// Create and save the first event
 268  	ev1 := &event.E{
 269  		Kind:      30000,
 270  		Pubkey:    pub,
 271  		CreatedAt: baseTime,
 272  		Content:   []byte("First version"),
 273  		Tags:      tag.NewS(tag.NewFromAny("d", dTagValue)),
 274  	}
 275  	if err := ev1.Sign(signer); err != nil {
 276  		t.Fatalf("failed to sign event 1: %v", err)
 277  	}
 278  	if _, err := db.SaveEvent(ctx, ev1); err != nil {
 279  		t.Fatalf("failed to save event 1: %v", err)
 280  	}
 281  
 282  	// Create and save a newer replacement event
 283  	ev2 := &event.E{
 284  		Kind:      30000,
 285  		Pubkey:    pub,
 286  		CreatedAt: baseTime + 1000, // Newer
 287  		Content:   []byte("Second version - replacement"),
 288  		Tags:      tag.NewS(tag.NewFromAny("d", dTagValue)),
 289  	}
 290  	if err := ev2.Sign(signer); err != nil {
 291  		t.Fatalf("failed to sign event 2: %v", err)
 292  	}
 293  	if _, err := db.SaveEvent(ctx, ev2); err != nil {
 294  		t.Fatalf("failed to save event 2: %v", err)
 295  	}
 296  
 297  	// Query and verify we get the newer event via fast path
 298  	queryFilter := &filter.F{
 299  		Kinds:   kind.NewS(kind.New(30000)),
 300  		Authors: tag.NewFromBytesSlice(pub),
 301  		Tags:    tag.NewS(tag.NewFromAny("#d", dTagValue)),
 302  	}
 303  
 304  	serial, err := db.QueryForAddressableEvent(queryFilter)
 305  	if err != nil {
 306  		t.Fatalf("QueryForAddressableEvent failed: %v", err)
 307  	}
 308  	if serial == nil {
 309  		t.Fatalf("QueryForAddressableEvent returned nil serial")
 310  	}
 311  
 312  	fetchedEv, err := db.FetchEventBySerial(serial)
 313  	if err != nil {
 314  		t.Fatalf("FetchEventBySerial failed: %v", err)
 315  	}
 316  
 317  	// Should be the second (newer) event
 318  	if string(fetchedEv.ID[:]) != string(ev2.ID[:]) {
 319  		t.Errorf("Expected to get newer event (ev2), got different event")
 320  	}
 321  	if string(fetchedEv.Content) != "Second version - replacement" {
 322  		t.Errorf("Expected content 'Second version - replacement', got '%s'", string(fetchedEv.Content))
 323  	}
 324  
 325  	t.Logf("Replacement event correctly indexed: %x", ev2.ID)
 326  }
 327