expiration_test.go raw

   1  //go:build integration
   2  // +build integration
   3  
   4  package neo4j
   5  
   6  import (
   7  	"bytes"
   8  	"context"
   9  	"encoding/json"
  10  	"testing"
  11  	"time"
  12  
  13  	"next.orly.dev/pkg/nostr/encoders/event"
  14  	"next.orly.dev/pkg/nostr/encoders/filter"
  15  	"next.orly.dev/pkg/nostr/encoders/hex"
  16  	"next.orly.dev/pkg/nostr/encoders/kind"
  17  	"next.orly.dev/pkg/nostr/encoders/tag"
  18  	"next.orly.dev/pkg/nostr/encoders/timestamp"
  19  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  20  )
  21  
  22  // All tests in this file use the shared testDB instance from testmain_test.go
  23  // to avoid Neo4j authentication rate limiting from too many connections.
  24  
  25  func TestExpiration_SaveEventWithExpiration(t *testing.T) {
  26  	if testDB == nil {
  27  		t.Skip("Neo4j not available")
  28  	}
  29  
  30  	cleanTestDatabase()
  31  
  32  	ctx := context.Background()
  33  
  34  	signer, err := p8k.New()
  35  	if err != nil {
  36  		t.Fatalf("Failed to create signer: %v", err)
  37  	}
  38  	if err := signer.Generate(); err != nil {
  39  		t.Fatalf("Failed to generate keypair: %v", err)
  40  	}
  41  
  42  	// Create event with expiration tag (expires in 1 hour)
  43  	futureExpiration := time.Now().Add(1 * time.Hour).Unix()
  44  
  45  	ev := event.New()
  46  	ev.Pubkey = signer.Pub()
  47  	ev.CreatedAt = timestamp.Now().V
  48  	ev.Kind = 1
  49  	ev.Content = []byte("Event with expiration")
  50  	ev.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
  51  
  52  	if err := ev.Sign(signer); err != nil {
  53  		t.Fatalf("Failed to sign event: %v", err)
  54  	}
  55  
  56  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
  57  		t.Fatalf("Failed to save event: %v", err)
  58  	}
  59  
  60  	// Query the event to verify it was saved
  61  	evs, err := testDB.QueryEvents(ctx, &filter.F{
  62  		Ids: tag.NewFromBytesSlice(ev.ID),
  63  	})
  64  	if err != nil {
  65  		t.Fatalf("Failed to query event: %v", err)
  66  	}
  67  
  68  	if len(evs) != 1 {
  69  		t.Fatalf("Expected 1 event, got %d", len(evs))
  70  	}
  71  
  72  	t.Logf("✓ Event with expiration tag saved successfully")
  73  }
  74  
  75  func TestExpiration_DeleteExpiredEvents(t *testing.T) {
  76  	if testDB == nil {
  77  		t.Skip("Neo4j not available")
  78  	}
  79  
  80  	cleanTestDatabase()
  81  
  82  	ctx := context.Background()
  83  
  84  	signer, err := p8k.New()
  85  	if err != nil {
  86  		t.Fatalf("Failed to create signer: %v", err)
  87  	}
  88  	if err := signer.Generate(); err != nil {
  89  		t.Fatalf("Failed to generate keypair: %v", err)
  90  	}
  91  
  92  	// Create an expired event (expired 1 hour ago)
  93  	pastExpiration := time.Now().Add(-1 * time.Hour).Unix()
  94  
  95  	expiredEv := event.New()
  96  	expiredEv.Pubkey = signer.Pub()
  97  	expiredEv.CreatedAt = timestamp.Now().V - 7200 // 2 hours ago
  98  	expiredEv.Kind = 1
  99  	expiredEv.Content = []byte("Expired event")
 100  	expiredEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(pastExpiration).String()))
 101  
 102  	if err := expiredEv.Sign(signer); err != nil {
 103  		t.Fatalf("Failed to sign expired event: %v", err)
 104  	}
 105  
 106  	if _, err := testDB.SaveEvent(ctx, expiredEv); err != nil {
 107  		t.Fatalf("Failed to save expired event: %v", err)
 108  	}
 109  
 110  	// Create a non-expired event (expires in 1 hour)
 111  	futureExpiration := time.Now().Add(1 * time.Hour).Unix()
 112  
 113  	validEv := event.New()
 114  	validEv.Pubkey = signer.Pub()
 115  	validEv.CreatedAt = timestamp.Now().V
 116  	validEv.Kind = 1
 117  	validEv.Content = []byte("Valid event")
 118  	validEv.Tags = tag.NewS(tag.NewFromAny("expiration", timestamp.FromUnix(futureExpiration).String()))
 119  
 120  	if err := validEv.Sign(signer); err != nil {
 121  		t.Fatalf("Failed to sign valid event: %v", err)
 122  	}
 123  
 124  	if _, err := testDB.SaveEvent(ctx, validEv); err != nil {
 125  		t.Fatalf("Failed to save valid event: %v", err)
 126  	}
 127  
 128  	// Create an event without expiration
 129  	permanentEv := event.New()
 130  	permanentEv.Pubkey = signer.Pub()
 131  	permanentEv.CreatedAt = timestamp.Now().V + 1
 132  	permanentEv.Kind = 1
 133  	permanentEv.Content = []byte("Permanent event (no expiration)")
 134  
 135  	if err := permanentEv.Sign(signer); err != nil {
 136  		t.Fatalf("Failed to sign permanent event: %v", err)
 137  	}
 138  
 139  	if _, err := testDB.SaveEvent(ctx, permanentEv); err != nil {
 140  		t.Fatalf("Failed to save permanent event: %v", err)
 141  	}
 142  
 143  	// Verify all 3 events exist
 144  	evs, err := testDB.QueryEvents(ctx, &filter.F{
 145  		Authors: tag.NewFromBytesSlice(signer.Pub()),
 146  	})
 147  	if err != nil {
 148  		t.Fatalf("Failed to query events: %v", err)
 149  	}
 150  	if len(evs) != 3 {
 151  		t.Fatalf("Expected 3 events before deletion, got %d", len(evs))
 152  	}
 153  
 154  	// Run DeleteExpired
 155  	testDB.DeleteExpired()
 156  
 157  	// Verify only expired event was deleted
 158  	evs, err = testDB.QueryEvents(ctx, &filter.F{
 159  		Authors: tag.NewFromBytesSlice(signer.Pub()),
 160  	})
 161  	if err != nil {
 162  		t.Fatalf("Failed to query events after deletion: %v", err)
 163  	}
 164  
 165  	if len(evs) != 2 {
 166  		t.Fatalf("Expected 2 events after deletion (expired removed), got %d", len(evs))
 167  	}
 168  
 169  	// Verify the correct events remain
 170  	foundValid := false
 171  	foundPermanent := false
 172  	for _, ev := range evs {
 173  		if hex.Enc(ev.ID[:]) == hex.Enc(validEv.ID[:]) {
 174  			foundValid = true
 175  		}
 176  		if hex.Enc(ev.ID[:]) == hex.Enc(permanentEv.ID[:]) {
 177  			foundPermanent = true
 178  		}
 179  	}
 180  
 181  	if !foundValid {
 182  		t.Fatal("Valid event (with future expiration) was incorrectly deleted")
 183  	}
 184  	if !foundPermanent {
 185  		t.Fatal("Permanent event (no expiration) was incorrectly deleted")
 186  	}
 187  
 188  	t.Logf("✓ DeleteExpired correctly removed only expired events")
 189  }
 190  
 191  func TestExpiration_NoExpirationTag(t *testing.T) {
 192  	if testDB == nil {
 193  		t.Skip("Neo4j not available")
 194  	}
 195  
 196  	cleanTestDatabase()
 197  
 198  	ctx := context.Background()
 199  
 200  	signer, err := p8k.New()
 201  	if err != nil {
 202  		t.Fatalf("Failed to create signer: %v", err)
 203  	}
 204  	if err := signer.Generate(); err != nil {
 205  		t.Fatalf("Failed to generate keypair: %v", err)
 206  	}
 207  
 208  	// Create event without expiration tag
 209  	ev := event.New()
 210  	ev.Pubkey = signer.Pub()
 211  	ev.CreatedAt = timestamp.Now().V
 212  	ev.Kind = 1
 213  	ev.Content = []byte("Event without expiration")
 214  
 215  	if err := ev.Sign(signer); err != nil {
 216  		t.Fatalf("Failed to sign event: %v", err)
 217  	}
 218  
 219  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 220  		t.Fatalf("Failed to save event: %v", err)
 221  	}
 222  
 223  	// Run DeleteExpired - event should not be deleted
 224  	testDB.DeleteExpired()
 225  
 226  	// Verify event still exists
 227  	evs, err := testDB.QueryEvents(ctx, &filter.F{
 228  		Ids: tag.NewFromBytesSlice(ev.ID),
 229  	})
 230  	if err != nil {
 231  		t.Fatalf("Failed to query event: %v", err)
 232  	}
 233  
 234  	if len(evs) != 1 {
 235  		t.Fatalf("Expected 1 event (no expiration should not be deleted), got %d", len(evs))
 236  	}
 237  
 238  	t.Logf("✓ Events without expiration tag are not deleted")
 239  }
 240  
 241  func TestExport_AllEvents(t *testing.T) {
 242  	if testDB == nil {
 243  		t.Skip("Neo4j not available")
 244  	}
 245  
 246  	cleanTestDatabase()
 247  
 248  	ctx := context.Background()
 249  
 250  	signer, err := p8k.New()
 251  	if err != nil {
 252  		t.Fatalf("Failed to create signer: %v", err)
 253  	}
 254  	if err := signer.Generate(); err != nil {
 255  		t.Fatalf("Failed to generate keypair: %v", err)
 256  	}
 257  
 258  	// Create and save some events
 259  	for i := 0; i < 5; i++ {
 260  		ev := event.New()
 261  		ev.Pubkey = signer.Pub()
 262  		ev.CreatedAt = timestamp.Now().V + int64(i)
 263  		ev.Kind = 1
 264  		ev.Content = []byte("Test event for export")
 265  		ev.Tags = tag.NewS(tag.NewFromAny("t", "test"))
 266  
 267  		if err := ev.Sign(signer); err != nil {
 268  			t.Fatalf("Failed to sign event: %v", err)
 269  		}
 270  
 271  		if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 272  			t.Fatalf("Failed to save event: %v", err)
 273  		}
 274  	}
 275  
 276  	// Export all events
 277  	var buf bytes.Buffer
 278  	testDB.Export(ctx, &buf)
 279  
 280  	// Parse the exported JSONL
 281  	lines := bytes.Split(buf.Bytes(), []byte("\n"))
 282  	validLines := 0
 283  	for _, line := range lines {
 284  		if len(line) == 0 {
 285  			continue
 286  		}
 287  		var ev event.E
 288  		if err := json.Unmarshal(line, &ev); err != nil {
 289  			t.Fatalf("Failed to parse exported event: %v", err)
 290  		}
 291  		validLines++
 292  	}
 293  
 294  	if validLines != 5 {
 295  		t.Fatalf("Expected 5 exported events, got %d", validLines)
 296  	}
 297  
 298  	t.Logf("✓ Export all events returned %d events in JSONL format", validLines)
 299  }
 300  
 301  func TestExport_FilterByPubkey(t *testing.T) {
 302  	if testDB == nil {
 303  		t.Skip("Neo4j not available")
 304  	}
 305  
 306  	cleanTestDatabase()
 307  
 308  	ctx := context.Background()
 309  
 310  	// Create two signers
 311  	alice, _ := p8k.New()
 312  	alice.Generate()
 313  
 314  	bob, _ := p8k.New()
 315  	bob.Generate()
 316  
 317  	baseTs := timestamp.Now().V
 318  
 319  	// Create events from Alice
 320  	for i := 0; i < 3; i++ {
 321  		ev := event.New()
 322  		ev.Pubkey = alice.Pub()
 323  		ev.CreatedAt = baseTs + int64(i)
 324  		ev.Kind = 1
 325  		ev.Content = []byte("Alice's event")
 326  
 327  		if err := ev.Sign(alice); err != nil {
 328  			t.Fatalf("Failed to sign event: %v", err)
 329  		}
 330  
 331  		if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 332  			t.Fatalf("Failed to save event: %v", err)
 333  		}
 334  	}
 335  
 336  	// Create events from Bob
 337  	for i := 0; i < 2; i++ {
 338  		ev := event.New()
 339  		ev.Pubkey = bob.Pub()
 340  		ev.CreatedAt = baseTs + int64(i) + 10
 341  		ev.Kind = 1
 342  		ev.Content = []byte("Bob's event")
 343  
 344  		if err := ev.Sign(bob); err != nil {
 345  			t.Fatalf("Failed to sign event: %v", err)
 346  		}
 347  
 348  		if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 349  			t.Fatalf("Failed to save event: %v", err)
 350  		}
 351  	}
 352  
 353  	// Export only Alice's events
 354  	var buf bytes.Buffer
 355  	testDB.Export(ctx, &buf, alice.Pub())
 356  
 357  	// Parse the exported JSONL
 358  	lines := bytes.Split(buf.Bytes(), []byte("\n"))
 359  	validLines := 0
 360  	alicePubkey := hex.Enc(alice.Pub())
 361  	for _, line := range lines {
 362  		if len(line) == 0 {
 363  			continue
 364  		}
 365  		var ev event.E
 366  		if err := json.Unmarshal(line, &ev); err != nil {
 367  			t.Fatalf("Failed to parse exported event: %v", err)
 368  		}
 369  		if hex.Enc(ev.Pubkey[:]) != alicePubkey {
 370  			t.Fatalf("Exported event has wrong pubkey (expected Alice)")
 371  		}
 372  		validLines++
 373  	}
 374  
 375  	if validLines != 3 {
 376  		t.Fatalf("Expected 3 events from Alice, got %d", validLines)
 377  	}
 378  
 379  	t.Logf("✓ Export with pubkey filter returned %d events from Alice only", validLines)
 380  }
 381  
 382  func TestExport_Empty(t *testing.T) {
 383  	if testDB == nil {
 384  		t.Skip("Neo4j not available")
 385  	}
 386  
 387  	cleanTestDatabase()
 388  
 389  	ctx := context.Background()
 390  
 391  	// Export from empty database
 392  	var buf bytes.Buffer
 393  	testDB.Export(ctx, &buf)
 394  
 395  	// Should be empty or just whitespace
 396  	content := bytes.TrimSpace(buf.Bytes())
 397  	if len(content) != 0 {
 398  		t.Fatalf("Expected empty export, got: %s", string(content))
 399  	}
 400  
 401  	t.Logf("✓ Export from empty database returns empty result")
 402  }
 403  
 404  func TestImportExport_RoundTrip(t *testing.T) {
 405  	if testDB == nil {
 406  		t.Skip("Neo4j not available")
 407  	}
 408  
 409  	cleanTestDatabase()
 410  
 411  	ctx := context.Background()
 412  
 413  	signer, _ := p8k.New()
 414  	signer.Generate()
 415  
 416  	// Create original events
 417  	originalEvents := make([]*event.E, 3)
 418  	for i := 0; i < 3; i++ {
 419  		ev := event.New()
 420  		ev.Pubkey = signer.Pub()
 421  		ev.CreatedAt = timestamp.Now().V + int64(i)
 422  		ev.Kind = 1
 423  		ev.Content = []byte("Round trip test event")
 424  		ev.Tags = tag.NewS(tag.NewFromAny("t", "roundtrip"))
 425  
 426  		if err := ev.Sign(signer); err != nil {
 427  			t.Fatalf("Failed to sign event: %v", err)
 428  		}
 429  
 430  		if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 431  			t.Fatalf("Failed to save event: %v", err)
 432  		}
 433  		originalEvents[i] = ev
 434  	}
 435  
 436  	// Export events
 437  	var buf bytes.Buffer
 438  	testDB.Export(ctx, &buf)
 439  
 440  	// Wipe database
 441  	if err := testDB.Wipe(); err != nil {
 442  		t.Fatalf("Failed to wipe database: %v", err)
 443  	}
 444  
 445  	// Verify database is empty
 446  	evs, err := testDB.QueryEvents(ctx, &filter.F{
 447  		Kinds: kind.NewS(kind.New(1)),
 448  	})
 449  	if err != nil {
 450  		t.Fatalf("Failed to query events: %v", err)
 451  	}
 452  	if len(evs) != 0 {
 453  		t.Fatalf("Expected 0 events after wipe, got %d", len(evs))
 454  	}
 455  
 456  	// Import events
 457  	testDB.Import(bytes.NewReader(buf.Bytes()))
 458  
 459  	// Verify events were restored
 460  	evs, err = testDB.QueryEvents(ctx, &filter.F{
 461  		Authors: tag.NewFromBytesSlice(signer.Pub()),
 462  	})
 463  	if err != nil {
 464  		t.Fatalf("Failed to query imported events: %v", err)
 465  	}
 466  
 467  	if len(evs) != 3 {
 468  		t.Fatalf("Expected 3 imported events, got %d", len(evs))
 469  	}
 470  
 471  	// Verify event IDs match
 472  	importedIDs := make(map[string]bool)
 473  	for _, ev := range evs {
 474  		importedIDs[hex.Enc(ev.ID[:])] = true
 475  	}
 476  
 477  	for _, orig := range originalEvents {
 478  		if !importedIDs[hex.Enc(orig.ID[:])] {
 479  			t.Fatalf("Original event %s not found after import", hex.Enc(orig.ID[:]))
 480  		}
 481  	}
 482  
 483  	t.Logf("✓ Export/Import round trip preserved %d events correctly", len(evs))
 484  }
 485