tag_model_test.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"testing"
   7  
   8  	"next.orly.dev/pkg/nostr/encoders/event"
   9  	"next.orly.dev/pkg/nostr/encoders/filter"
  10  	"next.orly.dev/pkg/nostr/encoders/hex"
  11  	"next.orly.dev/pkg/nostr/encoders/tag"
  12  	"next.orly.dev/pkg/nostr/encoders/timestamp"
  13  	"next.orly.dev/pkg/nostr/interfaces/signer/p8k"
  14  )
  15  
  16  // =============================================================================
  17  // Tag-Based E/P Model Tests
  18  // =============================================================================
  19  
  20  // TestTagBasedModel_ETagCreatesTagNode verifies that e-tags create Tag nodes
  21  // with type='e' and TAGGED_WITH relationships from the event.
  22  func TestTagBasedModel_ETagCreatesTagNode(t *testing.T) {
  23  	if testDB == nil {
  24  		t.Skip("Neo4j not available")
  25  	}
  26  
  27  	ctx := context.Background()
  28  	cleanTestDatabase()
  29  
  30  	signer, err := p8k.New()
  31  	if err != nil {
  32  		t.Fatalf("Failed to create signer: %v", err)
  33  	}
  34  	if err := signer.Generate(); err != nil {
  35  		t.Fatalf("Failed to generate keypair: %v", err)
  36  	}
  37  
  38  	// Create a target event first
  39  	targetEvent := event.New()
  40  	targetEvent.Pubkey = signer.Pub()
  41  	targetEvent.CreatedAt = timestamp.Now().V
  42  	targetEvent.Kind = 1
  43  	targetEvent.Content = []byte("Target event")
  44  	if err := targetEvent.Sign(signer); err != nil {
  45  		t.Fatalf("Failed to sign target event: %v", err)
  46  	}
  47  
  48  	if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
  49  		t.Fatalf("Failed to save target event: %v", err)
  50  	}
  51  
  52  	targetID := hex.Enc(targetEvent.ID[:])
  53  
  54  	// Create event with e-tag referencing the target
  55  	ev := event.New()
  56  	ev.Pubkey = signer.Pub()
  57  	ev.CreatedAt = timestamp.Now().V + 1
  58  	ev.Kind = 1
  59  	ev.Content = []byte("Event with e-tag")
  60  	ev.Tags = tag.NewS(
  61  		tag.NewFromAny("e", targetID, "", "reply"),
  62  	)
  63  	if err := ev.Sign(signer); err != nil {
  64  		t.Fatalf("Failed to sign event: %v", err)
  65  	}
  66  
  67  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
  68  		t.Fatalf("Failed to save event: %v", err)
  69  	}
  70  
  71  	eventID := hex.Enc(ev.ID[:])
  72  
  73  	// Verify Tag node was created
  74  	tagCypher := `
  75  		MATCH (t:Tag {type: 'e', value: $targetId})
  76  		RETURN t.type AS type, t.value AS value
  77  	`
  78  	tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{"targetId": targetID})
  79  	if err != nil {
  80  		t.Fatalf("Failed to query Tag node: %v", err)
  81  	}
  82  
  83  	if !tagResult.Next(ctx) {
  84  		t.Fatal("Expected Tag node with type='e' to be created")
  85  	}
  86  
  87  	record := tagResult.Record()
  88  	tagType := record.Values[0].(string)
  89  	tagValue := record.Values[1].(string)
  90  
  91  	if tagType != "e" {
  92  		t.Errorf("Expected tag type 'e', got %q", tagType)
  93  	}
  94  	if tagValue != targetID {
  95  		t.Errorf("Expected tag value %q, got %q", targetID, tagValue)
  96  	}
  97  
  98  	// Verify TAGGED_WITH relationship exists
  99  	taggedWithCypher := `
 100  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $targetId})
 101  		RETURN count(t) AS count
 102  	`
 103  	twResult, err := testDB.ExecuteRead(ctx, taggedWithCypher, map[string]any{
 104  		"eventId":  eventID,
 105  		"targetId": targetID,
 106  	})
 107  	if err != nil {
 108  		t.Fatalf("Failed to query TAGGED_WITH: %v", err)
 109  	}
 110  
 111  	if twResult.Next(ctx) {
 112  		count := twResult.Record().Values[0].(int64)
 113  		if count != 1 {
 114  			t.Errorf("Expected 1 TAGGED_WITH relationship, got %d", count)
 115  		}
 116  	} else {
 117  		t.Fatal("Expected TAGGED_WITH relationship to exist")
 118  	}
 119  
 120  	// Verify REFERENCES relationship from Tag to Event
 121  	refCypher := `
 122  		MATCH (t:Tag {type: 'e', value: $targetId})-[:REFERENCES]->(target:Event {id: $targetId})
 123  		RETURN count(target) AS count
 124  	`
 125  	refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"targetId": targetID})
 126  	if err != nil {
 127  		t.Fatalf("Failed to query REFERENCES: %v", err)
 128  	}
 129  
 130  	if refResult.Next(ctx) {
 131  		count := refResult.Record().Values[0].(int64)
 132  		if count != 1 {
 133  			t.Errorf("Expected 1 REFERENCES relationship from Tag to Event, got %d", count)
 134  		}
 135  	} else {
 136  		t.Fatal("Expected REFERENCES relationship from Tag to Event")
 137  	}
 138  
 139  	t.Logf("Tag-based e-tag model verified: Event -> Tag{e} -> Event")
 140  }
 141  
 142  // TestTagBasedModel_PTagCreatesTagNode verifies that p-tags create Tag nodes
 143  // with type='p' and REFERENCES relationships to NostrUser nodes.
 144  func TestTagBasedModel_PTagCreatesTagNode(t *testing.T) {
 145  	if testDB == nil {
 146  		t.Skip("Neo4j not available")
 147  	}
 148  
 149  	ctx := context.Background()
 150  	cleanTestDatabase()
 151  
 152  	// Create two signers: author and mentioned user
 153  	author, err := p8k.New()
 154  	if err != nil {
 155  		t.Fatalf("Failed to create author signer: %v", err)
 156  	}
 157  	if err := author.Generate(); err != nil {
 158  		t.Fatalf("Failed to generate author keypair: %v", err)
 159  	}
 160  
 161  	mentioned, err := p8k.New()
 162  	if err != nil {
 163  		t.Fatalf("Failed to create mentioned signer: %v", err)
 164  	}
 165  	if err := mentioned.Generate(); err != nil {
 166  		t.Fatalf("Failed to generate mentioned keypair: %v", err)
 167  	}
 168  
 169  	mentionedPubkey := hex.Enc(mentioned.Pub())
 170  
 171  	// Create event with p-tag
 172  	ev := event.New()
 173  	ev.Pubkey = author.Pub()
 174  	ev.CreatedAt = timestamp.Now().V
 175  	ev.Kind = 1
 176  	ev.Content = []byte("Event mentioning someone")
 177  	ev.Tags = tag.NewS(
 178  		tag.NewFromAny("p", mentionedPubkey),
 179  	)
 180  	if err := ev.Sign(author); err != nil {
 181  		t.Fatalf("Failed to sign event: %v", err)
 182  	}
 183  
 184  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 185  		t.Fatalf("Failed to save event: %v", err)
 186  	}
 187  
 188  	eventID := hex.Enc(ev.ID[:])
 189  
 190  	// Verify Tag node was created
 191  	tagCypher := `
 192  		MATCH (t:Tag {type: 'p', value: $pubkey})
 193  		RETURN t.type AS type, t.value AS value
 194  	`
 195  	tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{"pubkey": mentionedPubkey})
 196  	if err != nil {
 197  		t.Fatalf("Failed to query Tag node: %v", err)
 198  	}
 199  
 200  	if !tagResult.Next(ctx) {
 201  		t.Fatal("Expected Tag node with type='p' to be created")
 202  	}
 203  
 204  	// Verify TAGGED_WITH relationship exists
 205  	taggedWithCypher := `
 206  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $pubkey})
 207  		RETURN count(t) AS count
 208  	`
 209  	twResult, err := testDB.ExecuteRead(ctx, taggedWithCypher, map[string]any{
 210  		"eventId": eventID,
 211  		"pubkey":  mentionedPubkey,
 212  	})
 213  	if err != nil {
 214  		t.Fatalf("Failed to query TAGGED_WITH: %v", err)
 215  	}
 216  
 217  	if twResult.Next(ctx) {
 218  		count := twResult.Record().Values[0].(int64)
 219  		if count != 1 {
 220  			t.Errorf("Expected 1 TAGGED_WITH relationship, got %d", count)
 221  		}
 222  	}
 223  
 224  	// Verify REFERENCES relationship from Tag to NostrUser
 225  	refCypher := `
 226  		MATCH (t:Tag {type: 'p', value: $pubkey})-[:REFERENCES]->(u:NostrUser {pubkey: $pubkey})
 227  		RETURN count(u) AS count
 228  	`
 229  	refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"pubkey": mentionedPubkey})
 230  	if err != nil {
 231  		t.Fatalf("Failed to query REFERENCES: %v", err)
 232  	}
 233  
 234  	if refResult.Next(ctx) {
 235  		count := refResult.Record().Values[0].(int64)
 236  		if count != 1 {
 237  			t.Errorf("Expected 1 REFERENCES relationship from Tag to NostrUser, got %d", count)
 238  		}
 239  	} else {
 240  		t.Fatal("Expected REFERENCES relationship from Tag to NostrUser")
 241  	}
 242  
 243  	// Verify NostrUser was created for the mentioned pubkey
 244  	userCypher := `
 245  		MATCH (u:NostrUser {pubkey: $pubkey})
 246  		RETURN u.pubkey AS pubkey
 247  	`
 248  	userResult, err := testDB.ExecuteRead(ctx, userCypher, map[string]any{"pubkey": mentionedPubkey})
 249  	if err != nil {
 250  		t.Fatalf("Failed to query NostrUser: %v", err)
 251  	}
 252  
 253  	if !userResult.Next(ctx) {
 254  		t.Fatal("Expected NostrUser to be created for mentioned pubkey")
 255  	}
 256  
 257  	t.Logf("Tag-based p-tag model verified: Event -> Tag{p} -> NostrUser")
 258  }
 259  
 260  // TestTagBasedModel_ETagWithoutTargetEvent verifies that e-tags create Tag nodes
 261  // even when the referenced event doesn't exist, but don't create REFERENCES.
 262  func TestTagBasedModel_ETagWithoutTargetEvent(t *testing.T) {
 263  	if testDB == nil {
 264  		t.Skip("Neo4j not available")
 265  	}
 266  
 267  	ctx := context.Background()
 268  	cleanTestDatabase()
 269  
 270  	signer, err := p8k.New()
 271  	if err != nil {
 272  		t.Fatalf("Failed to create signer: %v", err)
 273  	}
 274  	if err := signer.Generate(); err != nil {
 275  		t.Fatalf("Failed to generate keypair: %v", err)
 276  	}
 277  
 278  	// Non-existent event ID
 279  	nonExistentID := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
 280  
 281  	// Create event with e-tag referencing non-existent event
 282  	ev := event.New()
 283  	ev.Pubkey = signer.Pub()
 284  	ev.CreatedAt = timestamp.Now().V
 285  	ev.Kind = 1
 286  	ev.Content = []byte("Reply to ghost event")
 287  	ev.Tags = tag.NewS(
 288  		tag.NewFromAny("e", nonExistentID, "", "reply"),
 289  	)
 290  	if err := ev.Sign(signer); err != nil {
 291  		t.Fatalf("Failed to sign event: %v", err)
 292  	}
 293  
 294  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 295  		t.Fatalf("Failed to save event: %v", err)
 296  	}
 297  
 298  	eventID := hex.Enc(ev.ID[:])
 299  
 300  	// Verify Tag node WAS created (for query purposes)
 301  	tagCypher := `
 302  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $targetId})
 303  		RETURN t.value AS value
 304  	`
 305  	tagResult, err := testDB.ExecuteRead(ctx, tagCypher, map[string]any{
 306  		"eventId":  eventID,
 307  		"targetId": nonExistentID,
 308  	})
 309  	if err != nil {
 310  		t.Fatalf("Failed to query Tag node: %v", err)
 311  	}
 312  
 313  	if !tagResult.Next(ctx) {
 314  		t.Fatal("Expected Tag node to be created even for non-existent target")
 315  	}
 316  
 317  	// Verify REFERENCES was NOT created (target doesn't exist)
 318  	refCypher := `
 319  		MATCH (t:Tag {type: 'e', value: $targetId})-[:REFERENCES]->(target:Event)
 320  		RETURN count(target) AS count
 321  	`
 322  	refResult, err := testDB.ExecuteRead(ctx, refCypher, map[string]any{"targetId": nonExistentID})
 323  	if err != nil {
 324  		t.Fatalf("Failed to query REFERENCES: %v", err)
 325  	}
 326  
 327  	if refResult.Next(ctx) {
 328  		count := refResult.Record().Values[0].(int64)
 329  		if count != 0 {
 330  			t.Errorf("Expected 0 REFERENCES for non-existent event, got %d", count)
 331  		}
 332  	}
 333  
 334  	t.Logf("Correctly handled e-tag to non-existent event: Tag created, no REFERENCES")
 335  }
 336  
 337  // =============================================================================
 338  // Tag Filter Query Tests (#e and #p filters)
 339  // =============================================================================
 340  
 341  // TestTagFilter_ETagQuery tests that #e filters work with the Tag-based model.
 342  func TestTagFilter_ETagQuery(t *testing.T) {
 343  	if testDB == nil {
 344  		t.Skip("Neo4j not available")
 345  	}
 346  
 347  	ctx := context.Background()
 348  	cleanTestDatabase()
 349  
 350  	signer, err := p8k.New()
 351  	if err != nil {
 352  		t.Fatalf("Failed to create signer: %v", err)
 353  	}
 354  	if err := signer.Generate(); err != nil {
 355  		t.Fatalf("Failed to generate keypair: %v", err)
 356  	}
 357  
 358  	// Create root event
 359  	rootEvent := event.New()
 360  	rootEvent.Pubkey = signer.Pub()
 361  	rootEvent.CreatedAt = timestamp.Now().V
 362  	rootEvent.Kind = 1
 363  	rootEvent.Content = []byte("Root event")
 364  	if err := rootEvent.Sign(signer); err != nil {
 365  		t.Fatalf("Failed to sign root event: %v", err)
 366  	}
 367  
 368  	if _, err := testDB.SaveEvent(ctx, rootEvent); err != nil {
 369  		t.Fatalf("Failed to save root event: %v", err)
 370  	}
 371  
 372  	rootID := hex.Enc(rootEvent.ID[:])
 373  
 374  	// Create reply event with e-tag
 375  	replyEvent := event.New()
 376  	replyEvent.Pubkey = signer.Pub()
 377  	replyEvent.CreatedAt = timestamp.Now().V + 1
 378  	replyEvent.Kind = 1
 379  	replyEvent.Content = []byte("Reply to root")
 380  	replyEvent.Tags = tag.NewS(
 381  		tag.NewFromAny("e", rootID, "", "root"),
 382  	)
 383  	if err := replyEvent.Sign(signer); err != nil {
 384  		t.Fatalf("Failed to sign reply event: %v", err)
 385  	}
 386  
 387  	if _, err := testDB.SaveEvent(ctx, replyEvent); err != nil {
 388  		t.Fatalf("Failed to save reply event: %v", err)
 389  	}
 390  
 391  	// Create unrelated event (no e-tag)
 392  	unrelatedEvent := event.New()
 393  	unrelatedEvent.Pubkey = signer.Pub()
 394  	unrelatedEvent.CreatedAt = timestamp.Now().V + 2
 395  	unrelatedEvent.Kind = 1
 396  	unrelatedEvent.Content = []byte("Unrelated event")
 397  	if err := unrelatedEvent.Sign(signer); err != nil {
 398  		t.Fatalf("Failed to sign unrelated event: %v", err)
 399  	}
 400  
 401  	if _, err := testDB.SaveEvent(ctx, unrelatedEvent); err != nil {
 402  		t.Fatalf("Failed to save unrelated event: %v", err)
 403  	}
 404  
 405  	// Query events with #e filter
 406  	f := &filter.F{
 407  		Tags: tag.NewS(tag.NewFromAny("e", rootID)),
 408  	}
 409  
 410  	events, err := testDB.QueryEvents(ctx, f)
 411  	if err != nil {
 412  		t.Fatalf("Failed to query with #e filter: %v", err)
 413  	}
 414  
 415  	if len(events) != 1 {
 416  		t.Errorf("Expected 1 event with #e filter, got %d", len(events))
 417  	}
 418  
 419  	if len(events) > 0 {
 420  		foundID := hex.Enc(events[0].ID[:])
 421  		expectedID := hex.Enc(replyEvent.ID[:])
 422  		if foundID != expectedID {
 423  			t.Errorf("Expected to find reply event, got event %s", foundID[:8])
 424  		}
 425  	}
 426  
 427  	t.Logf("#e filter query working correctly with Tag-based model")
 428  }
 429  
 430  // TestTagFilter_PTagQuery tests that #p filters work with the Tag-based model.
 431  func TestTagFilter_PTagQuery(t *testing.T) {
 432  	if testDB == nil {
 433  		t.Skip("Neo4j not available")
 434  	}
 435  
 436  	ctx := context.Background()
 437  	cleanTestDatabase()
 438  
 439  	// Create two signers
 440  	author, err := p8k.New()
 441  	if err != nil {
 442  		t.Fatalf("Failed to create author signer: %v", err)
 443  	}
 444  	if err := author.Generate(); err != nil {
 445  		t.Fatalf("Failed to generate author keypair: %v", err)
 446  	}
 447  
 448  	mentioned, err := p8k.New()
 449  	if err != nil {
 450  		t.Fatalf("Failed to create mentioned signer: %v", err)
 451  	}
 452  	if err := mentioned.Generate(); err != nil {
 453  		t.Fatalf("Failed to generate mentioned keypair: %v", err)
 454  	}
 455  
 456  	mentionedPubkey := hex.Enc(mentioned.Pub())
 457  
 458  	// Create event that mentions someone
 459  	mentionEvent := event.New()
 460  	mentionEvent.Pubkey = author.Pub()
 461  	mentionEvent.CreatedAt = timestamp.Now().V
 462  	mentionEvent.Kind = 1
 463  	mentionEvent.Content = []byte("Hey @someone")
 464  	mentionEvent.Tags = tag.NewS(
 465  		tag.NewFromAny("p", mentionedPubkey),
 466  	)
 467  	if err := mentionEvent.Sign(author); err != nil {
 468  		t.Fatalf("Failed to sign mention event: %v", err)
 469  	}
 470  
 471  	if _, err := testDB.SaveEvent(ctx, mentionEvent); err != nil {
 472  		t.Fatalf("Failed to save mention event: %v", err)
 473  	}
 474  
 475  	// Create event without p-tag
 476  	regularEvent := event.New()
 477  	regularEvent.Pubkey = author.Pub()
 478  	regularEvent.CreatedAt = timestamp.Now().V + 1
 479  	regularEvent.Kind = 1
 480  	regularEvent.Content = []byte("Regular post")
 481  	if err := regularEvent.Sign(author); err != nil {
 482  		t.Fatalf("Failed to sign regular event: %v", err)
 483  	}
 484  
 485  	if _, err := testDB.SaveEvent(ctx, regularEvent); err != nil {
 486  		t.Fatalf("Failed to save regular event: %v", err)
 487  	}
 488  
 489  	// Query events with #p filter
 490  	f := &filter.F{
 491  		Tags: tag.NewS(tag.NewFromAny("p", mentionedPubkey)),
 492  	}
 493  
 494  	events, err := testDB.QueryEvents(ctx, f)
 495  	if err != nil {
 496  		t.Fatalf("Failed to query with #p filter: %v", err)
 497  	}
 498  
 499  	if len(events) != 1 {
 500  		t.Errorf("Expected 1 event with #p filter, got %d", len(events))
 501  	}
 502  
 503  	if len(events) > 0 {
 504  		foundID := hex.Enc(events[0].ID[:])
 505  		expectedID := hex.Enc(mentionEvent.ID[:])
 506  		if foundID != expectedID {
 507  			t.Errorf("Expected to find mention event, got event %s", foundID[:8])
 508  		}
 509  	}
 510  
 511  	t.Logf("#p filter query working correctly with Tag-based model")
 512  }
 513  
 514  // TestTagFilter_MultiplePTags tests events with multiple p-tags.
 515  func TestTagFilter_MultiplePTags(t *testing.T) {
 516  	if testDB == nil {
 517  		t.Skip("Neo4j not available")
 518  	}
 519  
 520  	ctx := context.Background()
 521  	cleanTestDatabase()
 522  
 523  	author, err := p8k.New()
 524  	if err != nil {
 525  		t.Fatalf("Failed to create author signer: %v", err)
 526  	}
 527  	if err := author.Generate(); err != nil {
 528  		t.Fatalf("Failed to generate author keypair: %v", err)
 529  	}
 530  
 531  	// Generate 5 pubkeys to mention
 532  	var mentionedPubkeys []string
 533  	for i := 0; i < 5; i++ {
 534  		mentionedPubkeys = append(mentionedPubkeys, fmt.Sprintf("%064x", i+1))
 535  	}
 536  
 537  	// Create event mentioning all 5
 538  	ev := event.New()
 539  	ev.Pubkey = author.Pub()
 540  	ev.CreatedAt = timestamp.Now().V
 541  	ev.Kind = 1
 542  	ev.Content = []byte("Group mention")
 543  	tags := tag.NewS()
 544  	for _, pk := range mentionedPubkeys {
 545  		tags.Append(tag.NewFromAny("p", pk))
 546  	}
 547  	ev.Tags = tags
 548  	if err := ev.Sign(author); err != nil {
 549  		t.Fatalf("Failed to sign event: %v", err)
 550  	}
 551  
 552  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 553  		t.Fatalf("Failed to save event: %v", err)
 554  	}
 555  
 556  	// Verify all Tag nodes were created
 557  	countCypher := `
 558  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})
 559  		RETURN count(t) AS count
 560  	`
 561  	result, err := testDB.ExecuteRead(ctx, countCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
 562  	if err != nil {
 563  		t.Fatalf("Failed to count p-tag Tags: %v", err)
 564  	}
 565  
 566  	if result.Next(ctx) {
 567  		count := result.Record().Values[0].(int64)
 568  		if count != int64(len(mentionedPubkeys)) {
 569  			t.Errorf("Expected %d p-tag Tag nodes, got %d", len(mentionedPubkeys), count)
 570  		}
 571  	}
 572  
 573  	// Query for events mentioning any of the pubkeys
 574  	f := &filter.F{
 575  		Tags: tag.NewS(tag.NewFromAny("p", mentionedPubkeys[2])), // Query for the 3rd pubkey
 576  	}
 577  
 578  	events, err := testDB.QueryEvents(ctx, f)
 579  	if err != nil {
 580  		t.Fatalf("Failed to query with #p filter: %v", err)
 581  	}
 582  
 583  	if len(events) != 1 {
 584  		t.Errorf("Expected 1 event mentioning pubkey, got %d", len(events))
 585  	}
 586  
 587  	t.Logf("Multiple p-tags correctly stored and queryable")
 588  }
 589  
 590  // =============================================================================
 591  // CheckForDeleted with Tag Traversal Tests
 592  // =============================================================================
 593  
 594  // TestCheckForDeleted_WithTagModel tests that CheckForDeleted works with
 595  // the new Tag-based model for e-tag references.
 596  func TestCheckForDeleted_WithTagModel(t *testing.T) {
 597  	if testDB == nil {
 598  		t.Skip("Neo4j not available")
 599  	}
 600  
 601  	ctx := context.Background()
 602  	cleanTestDatabase()
 603  
 604  	signer, err := p8k.New()
 605  	if err != nil {
 606  		t.Fatalf("Failed to create signer: %v", err)
 607  	}
 608  	if err := signer.Generate(); err != nil {
 609  		t.Fatalf("Failed to generate keypair: %v", err)
 610  	}
 611  
 612  	// Create target event
 613  	targetEvent := event.New()
 614  	targetEvent.Pubkey = signer.Pub()
 615  	targetEvent.CreatedAt = timestamp.Now().V
 616  	targetEvent.Kind = 1
 617  	targetEvent.Content = []byte("Event to be deleted")
 618  	if err := targetEvent.Sign(signer); err != nil {
 619  		t.Fatalf("Failed to sign target event: %v", err)
 620  	}
 621  
 622  	if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
 623  		t.Fatalf("Failed to save target event: %v", err)
 624  	}
 625  
 626  	targetID := hex.Enc(targetEvent.ID[:])
 627  
 628  	// Create kind 5 deletion event with e-tag
 629  	deleteEvent := event.New()
 630  	deleteEvent.Pubkey = signer.Pub()
 631  	deleteEvent.CreatedAt = timestamp.Now().V + 1
 632  	deleteEvent.Kind = 5
 633  	deleteEvent.Content = []byte("Deleting my event")
 634  	deleteEvent.Tags = tag.NewS(
 635  		tag.NewFromAny("e", targetID),
 636  	)
 637  	if err := deleteEvent.Sign(signer); err != nil {
 638  		t.Fatalf("Failed to sign delete event: %v", err)
 639  	}
 640  
 641  	if _, err := testDB.SaveEvent(ctx, deleteEvent); err != nil {
 642  		t.Fatalf("Failed to save delete event: %v", err)
 643  	}
 644  
 645  	// Verify the Tag-based traversal exists:
 646  	// DeleteEvent-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->TargetEvent
 647  	traversalCypher := `
 648  		MATCH (delete:Event {kind: 5})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target:Event {id: $targetId})
 649  		RETURN delete.id AS deleteId
 650  	`
 651  	result, err := testDB.ExecuteRead(ctx, traversalCypher, map[string]any{"targetId": targetID})
 652  	if err != nil {
 653  		t.Fatalf("Failed to query traversal: %v", err)
 654  	}
 655  
 656  	if !result.Next(ctx) {
 657  		t.Fatal("Expected Tag-based traversal from delete event to target")
 658  	}
 659  
 660  	// Test CheckForDeleted
 661  	admins := [][]byte{} // No admins, author can delete own events
 662  	err = testDB.CheckForDeleted(targetEvent, admins)
 663  
 664  	if err == nil {
 665  		t.Error("Expected CheckForDeleted to return error for deleted event")
 666  	} else if err.Error() != "event has been deleted" {
 667  		t.Errorf("Unexpected error message: %v", err)
 668  	}
 669  
 670  	t.Logf("CheckForDeleted correctly detects deletion via Tag-based traversal")
 671  }
 672  
 673  // TestCheckForDeleted_NotDeleted verifies CheckForDeleted returns nil for
 674  // events that haven't been deleted.
 675  func TestCheckForDeleted_NotDeleted(t *testing.T) {
 676  	if testDB == nil {
 677  		t.Skip("Neo4j not available")
 678  	}
 679  
 680  	ctx := context.Background()
 681  	cleanTestDatabase()
 682  
 683  	signer, err := p8k.New()
 684  	if err != nil {
 685  		t.Fatalf("Failed to create signer: %v", err)
 686  	}
 687  	if err := signer.Generate(); err != nil {
 688  		t.Fatalf("Failed to generate keypair: %v", err)
 689  	}
 690  
 691  	// Create event that won't be deleted
 692  	ev := event.New()
 693  	ev.Pubkey = signer.Pub()
 694  	ev.CreatedAt = timestamp.Now().V
 695  	ev.Kind = 1
 696  	ev.Content = []byte("Regular event")
 697  	if err := ev.Sign(signer); err != nil {
 698  		t.Fatalf("Failed to sign event: %v", err)
 699  	}
 700  
 701  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 702  		t.Fatalf("Failed to save event: %v", err)
 703  	}
 704  
 705  	// CheckForDeleted should return nil
 706  	admins := [][]byte{}
 707  	err = testDB.CheckForDeleted(ev, admins)
 708  
 709  	if err != nil {
 710  		t.Errorf("Expected nil for non-deleted event, got: %v", err)
 711  	}
 712  
 713  	t.Logf("CheckForDeleted correctly returns nil for non-deleted event")
 714  }
 715  
 716  // =============================================================================
 717  // Migration v3 Tests
 718  // =============================================================================
 719  
 720  // TestMigrationV3_ConvertDirectReferences tests that the v3 migration
 721  // correctly converts direct REFERENCES relationships to Tag-based model.
 722  func TestMigrationV3_ConvertDirectReferences(t *testing.T) {
 723  	if testDB == nil {
 724  		t.Skip("Neo4j not available")
 725  	}
 726  
 727  	ctx := context.Background()
 728  	cleanTestDatabase()
 729  
 730  	// Manually create old-style direct REFERENCES relationship
 731  	// (simulating pre-migration data)
 732  	setupCypher := `
 733  		// Create two events
 734  		CREATE (source:Event {
 735  			id: '1111111111111111111111111111111111111111111111111111111111111111',
 736  			pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
 737  			kind: 1,
 738  			created_at: 1700000000,
 739  			content: 'Source event',
 740  			sig: '0000000000000000000000000000000000000000000000000000000000000000' +
 741  			     '0000000000000000000000000000000000000000000000000000000000000000',
 742  			tags: '[]',
 743  			serial: 1,
 744  			expiration: 0
 745  		})
 746  		CREATE (target:Event {
 747  			id: '2222222222222222222222222222222222222222222222222222222222222222',
 748  			pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
 749  			kind: 1,
 750  			created_at: 1699999999,
 751  			content: 'Target event',
 752  			sig: '0000000000000000000000000000000000000000000000000000000000000000' +
 753  			     '0000000000000000000000000000000000000000000000000000000000000000',
 754  			tags: '[]',
 755  			serial: 2,
 756  			expiration: 0
 757  		})
 758  		// Create old-style direct REFERENCES (pre-migration)
 759  		CREATE (source)-[:REFERENCES]->(target)
 760  	`
 761  
 762  	if _, err := testDB.ExecuteWrite(ctx, setupCypher, nil); err != nil {
 763  		t.Fatalf("Failed to setup pre-migration data: %v", err)
 764  	}
 765  
 766  	// Verify old-style relationship exists
 767  	checkOldCypher := `
 768  		MATCH (s:Event)-[r:REFERENCES]->(t:Event)
 769  		WHERE NOT (s)-[:TAGGED_WITH]->(:Tag)
 770  		RETURN count(r) AS count
 771  	`
 772  	result, err := testDB.ExecuteRead(ctx, checkOldCypher, nil)
 773  	if err != nil {
 774  		t.Fatalf("Failed to check old relationship: %v", err)
 775  	}
 776  
 777  	var oldCount int64
 778  	if result.Next(ctx) {
 779  		oldCount = result.Record().Values[0].(int64)
 780  	}
 781  
 782  	if oldCount == 0 {
 783  		t.Skip("No old-style REFERENCES to migrate")
 784  	}
 785  
 786  	t.Logf("Found %d old-style REFERENCES to migrate", oldCount)
 787  
 788  	// Run migration
 789  	err = migrateToTagBasedReferences(ctx, testDB)
 790  	if err != nil {
 791  		t.Fatalf("Migration failed: %v", err)
 792  	}
 793  
 794  	// Verify old-style relationship was removed
 795  	result, err = testDB.ExecuteRead(ctx, checkOldCypher, nil)
 796  	if err != nil {
 797  		t.Fatalf("Failed to check post-migration: %v", err)
 798  	}
 799  
 800  	if result.Next(ctx) {
 801  		count := result.Record().Values[0].(int64)
 802  		if count != 0 {
 803  			t.Errorf("Expected 0 old-style REFERENCES after migration, got %d", count)
 804  		}
 805  	}
 806  
 807  	// Verify new Tag-based structure exists
 808  	checkNewCypher := `
 809  		MATCH (s:Event {id: '1111111111111111111111111111111111111111111111111111111111111111'})
 810  			-[:TAGGED_WITH]->(t:Tag {type: 'e'})
 811  			-[:REFERENCES]->(target:Event {id: '2222222222222222222222222222222222222222222222222222222222222222'})
 812  		RETURN t.value AS tagValue
 813  	`
 814  	result, err = testDB.ExecuteRead(ctx, checkNewCypher, nil)
 815  	if err != nil {
 816  		t.Fatalf("Failed to check new structure: %v", err)
 817  	}
 818  
 819  	if !result.Next(ctx) {
 820  		t.Error("Expected Tag-based structure after migration")
 821  	} else {
 822  		tagValue := result.Record().Values[0].(string)
 823  		expectedValue := "2222222222222222222222222222222222222222222222222222222222222222"
 824  		if tagValue != expectedValue {
 825  			t.Errorf("Expected tag value %s, got %s", expectedValue, tagValue)
 826  		}
 827  	}
 828  
 829  	t.Logf("Migration v3 correctly converted REFERENCES to Tag-based model")
 830  }
 831  
 832  // TestMigrationV3_ConvertDirectMentions tests that the v3 migration
 833  // correctly converts direct MENTIONS relationships to Tag-based model.
 834  func TestMigrationV3_ConvertDirectMentions(t *testing.T) {
 835  	if testDB == nil {
 836  		t.Skip("Neo4j not available")
 837  	}
 838  
 839  	ctx := context.Background()
 840  	cleanTestDatabase()
 841  
 842  	// Manually create old-style direct MENTIONS relationship
 843  	setupCypher := `
 844  		// Create event and user
 845  		CREATE (source:Event {
 846  			id: '3333333333333333333333333333333333333333333333333333333333333333',
 847  			pubkey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
 848  			kind: 1,
 849  			created_at: 1700000000,
 850  			content: 'Event mentioning user',
 851  			sig: '0000000000000000000000000000000000000000000000000000000000000000' +
 852  			     '0000000000000000000000000000000000000000000000000000000000000000',
 853  			tags: '[]',
 854  			serial: 3,
 855  			expiration: 0
 856  		})
 857  		MERGE (user:NostrUser {pubkey: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'})
 858  		// Create old-style direct MENTIONS (pre-migration)
 859  		CREATE (source)-[:MENTIONS]->(user)
 860  	`
 861  
 862  	if _, err := testDB.ExecuteWrite(ctx, setupCypher, nil); err != nil {
 863  		t.Fatalf("Failed to setup pre-migration data: %v", err)
 864  	}
 865  
 866  	// Verify old-style relationship exists
 867  	checkOldCypher := `
 868  		MATCH (e:Event)-[r:MENTIONS]->(u:NostrUser)
 869  		RETURN count(r) AS count
 870  	`
 871  	result, err := testDB.ExecuteRead(ctx, checkOldCypher, nil)
 872  	if err != nil {
 873  		t.Fatalf("Failed to check old relationship: %v", err)
 874  	}
 875  
 876  	var oldCount int64
 877  	if result.Next(ctx) {
 878  		oldCount = result.Record().Values[0].(int64)
 879  	}
 880  
 881  	if oldCount == 0 {
 882  		t.Skip("No old-style MENTIONS to migrate")
 883  	}
 884  
 885  	t.Logf("Found %d old-style MENTIONS to migrate", oldCount)
 886  
 887  	// Run migration
 888  	err = migrateToTagBasedReferences(ctx, testDB)
 889  	if err != nil {
 890  		t.Fatalf("Migration failed: %v", err)
 891  	}
 892  
 893  	// Verify old-style relationship was removed
 894  	result, err = testDB.ExecuteRead(ctx, checkOldCypher, nil)
 895  	if err != nil {
 896  		t.Fatalf("Failed to check post-migration: %v", err)
 897  	}
 898  
 899  	if result.Next(ctx) {
 900  		count := result.Record().Values[0].(int64)
 901  		if count != 0 {
 902  			t.Errorf("Expected 0 old-style MENTIONS after migration, got %d", count)
 903  		}
 904  	}
 905  
 906  	// Verify new Tag-based structure exists
 907  	checkNewCypher := `
 908  		MATCH (e:Event {id: '3333333333333333333333333333333333333333333333333333333333333333'})
 909  			-[:TAGGED_WITH]->(t:Tag {type: 'p'})
 910  			-[:REFERENCES]->(u:NostrUser {pubkey: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'})
 911  		RETURN t.value AS tagValue
 912  	`
 913  	result, err = testDB.ExecuteRead(ctx, checkNewCypher, nil)
 914  	if err != nil {
 915  		t.Fatalf("Failed to check new structure: %v", err)
 916  	}
 917  
 918  	if !result.Next(ctx) {
 919  		t.Error("Expected Tag-based structure after migration")
 920  	} else {
 921  		tagValue := result.Record().Values[0].(string)
 922  		expectedValue := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
 923  		if tagValue != expectedValue {
 924  			t.Errorf("Expected tag value %s, got %s", expectedValue, tagValue)
 925  		}
 926  	}
 927  
 928  	t.Logf("Migration v3 correctly converted MENTIONS to Tag-based model")
 929  }
 930  
 931  // TestMigrationV3_Idempotent tests that the v3 migration is idempotent
 932  // (safe to run multiple times).
 933  func TestMigrationV3_Idempotent(t *testing.T) {
 934  	if testDB == nil {
 935  		t.Skip("Neo4j not available")
 936  	}
 937  
 938  	ctx := context.Background()
 939  	cleanTestDatabase()
 940  
 941  	// Create proper Tag-based structure (as if migration already ran)
 942  	signer, err := p8k.New()
 943  	if err != nil {
 944  		t.Fatalf("Failed to create signer: %v", err)
 945  	}
 946  	if err := signer.Generate(); err != nil {
 947  		t.Fatalf("Failed to generate keypair: %v", err)
 948  	}
 949  
 950  	// Create event with e-tag using new model
 951  	targetEvent := event.New()
 952  	targetEvent.Pubkey = signer.Pub()
 953  	targetEvent.CreatedAt = timestamp.Now().V
 954  	targetEvent.Kind = 1
 955  	targetEvent.Content = []byte("Target")
 956  	if err := targetEvent.Sign(signer); err != nil {
 957  		t.Fatalf("Failed to sign target event: %v", err)
 958  	}
 959  
 960  	if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
 961  		t.Fatalf("Failed to save target event: %v", err)
 962  	}
 963  
 964  	replyEvent := event.New()
 965  	replyEvent.Pubkey = signer.Pub()
 966  	replyEvent.CreatedAt = timestamp.Now().V + 1
 967  	replyEvent.Kind = 1
 968  	replyEvent.Content = []byte("Reply")
 969  	replyEvent.Tags = tag.NewS(
 970  		tag.NewFromAny("e", hex.Enc(targetEvent.ID[:])),
 971  	)
 972  	if err := replyEvent.Sign(signer); err != nil {
 973  		t.Fatalf("Failed to sign reply event: %v", err)
 974  	}
 975  
 976  	if _, err := testDB.SaveEvent(ctx, replyEvent); err != nil {
 977  		t.Fatalf("Failed to save reply event: %v", err)
 978  	}
 979  
 980  	// Count Tag nodes before running migration
 981  	countBefore := countNodes(t, "Tag")
 982  
 983  	// Run migration (should be no-op since data is already correct)
 984  	err = migrateToTagBasedReferences(ctx, testDB)
 985  	if err != nil {
 986  		t.Fatalf("Migration failed: %v", err)
 987  	}
 988  
 989  	// Count Tag nodes after - should be unchanged
 990  	countAfter := countNodes(t, "Tag")
 991  
 992  	if countBefore != countAfter {
 993  		t.Errorf("Migration changed Tag count: before=%d, after=%d", countBefore, countAfter)
 994  	}
 995  
 996  	// Run migration again - should still be idempotent
 997  	err = migrateToTagBasedReferences(ctx, testDB)
 998  	if err != nil {
 999  		t.Fatalf("Second migration run failed: %v", err)
1000  	}
1001  
1002  	countAfterSecond := countNodes(t, "Tag")
1003  	if countAfter != countAfterSecond {
1004  		t.Errorf("Second migration run changed Tag count: %d -> %d", countAfter, countAfterSecond)
1005  	}
1006  
1007  	t.Logf("Migration v3 is idempotent (safe to run multiple times)")
1008  }
1009  
1010  // =============================================================================
1011  // Large Dataset Tests
1012  // =============================================================================
1013  
1014  // TestLargeETagBatch tests events with many e-tags are handled correctly.
1015  func TestLargeETagBatch(t *testing.T) {
1016  	if testDB == nil {
1017  		t.Skip("Neo4j not available")
1018  	}
1019  
1020  	ctx := context.Background()
1021  	cleanTestDatabase()
1022  
1023  	signer, err := p8k.New()
1024  	if err != nil {
1025  		t.Fatalf("Failed to create signer: %v", err)
1026  	}
1027  	if err := signer.Generate(); err != nil {
1028  		t.Fatalf("Failed to generate keypair: %v", err)
1029  	}
1030  
1031  	// Create 100 target events
1032  	numTargets := 100
1033  	var targetIDs []string
1034  	for i := 0; i < numTargets; i++ {
1035  		targetEvent := event.New()
1036  		targetEvent.Pubkey = signer.Pub()
1037  		targetEvent.CreatedAt = timestamp.Now().V + int64(i)
1038  		targetEvent.Kind = 1
1039  		targetEvent.Content = []byte(fmt.Sprintf("Target %d", i))
1040  		if err := targetEvent.Sign(signer); err != nil {
1041  			t.Fatalf("Failed to sign target event %d: %v", i, err)
1042  		}
1043  
1044  		if _, err := testDB.SaveEvent(ctx, targetEvent); err != nil {
1045  			t.Fatalf("Failed to save target event %d: %v", i, err)
1046  		}
1047  
1048  		targetIDs = append(targetIDs, hex.Enc(targetEvent.ID[:]))
1049  	}
1050  
1051  	// Create event referencing all 100 targets
1052  	ev := event.New()
1053  	ev.Pubkey = signer.Pub()
1054  	ev.CreatedAt = timestamp.Now().V + int64(numTargets+1)
1055  	ev.Kind = 1
1056  	ev.Content = []byte("Event with many e-tags")
1057  	tags := tag.NewS()
1058  	for _, id := range targetIDs {
1059  		tags.Append(tag.NewFromAny("e", id))
1060  	}
1061  	ev.Tags = tags
1062  	if err := ev.Sign(signer); err != nil {
1063  		t.Fatalf("Failed to sign event: %v", err)
1064  	}
1065  
1066  	if _, err := testDB.SaveEvent(ctx, ev); err != nil {
1067  		t.Fatalf("Failed to save event with %d e-tags: %v", numTargets, err)
1068  	}
1069  
1070  	// Verify all Tag nodes were created
1071  	countCypher := `
1072  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})
1073  		RETURN count(t) AS count
1074  	`
1075  	result, err := testDB.ExecuteRead(ctx, countCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
1076  	if err != nil {
1077  		t.Fatalf("Failed to count e-tag Tags: %v", err)
1078  	}
1079  
1080  	if result.Next(ctx) {
1081  		count := result.Record().Values[0].(int64)
1082  		if count != int64(numTargets) {
1083  			t.Errorf("Expected %d e-tag Tag nodes, got %d", numTargets, count)
1084  		}
1085  	}
1086  
1087  	// Verify all REFERENCES were created (since all targets exist)
1088  	refCountCypher := `
1089  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(target:Event)
1090  		RETURN count(target) AS count
1091  	`
1092  	refResult, err := testDB.ExecuteRead(ctx, refCountCypher, map[string]any{"eventId": hex.Enc(ev.ID[:])})
1093  	if err != nil {
1094  		t.Fatalf("Failed to count REFERENCES: %v", err)
1095  	}
1096  
1097  	if refResult.Next(ctx) {
1098  		count := refResult.Record().Values[0].(int64)
1099  		if count != int64(numTargets) {
1100  			t.Errorf("Expected %d REFERENCES, got %d", numTargets, count)
1101  		}
1102  	}
1103  
1104  	t.Logf("Large e-tag batch (%d tags) handled correctly", numTargets)
1105  }
1106