save-event_test.go raw

   1  package neo4j
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"strings"
   7  	"testing"
   8  
   9  	"next.orly.dev/pkg/nostr/encoders/event"
  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  // TestBuildBaseEventCypher verifies the base event creation query generates correct Cypher.
  17  // The new architecture separates event creation from tag processing to avoid stack overflow.
  18  func TestBuildBaseEventCypher(t *testing.T) {
  19  	n := &N{}
  20  
  21  	signer, err := p8k.New()
  22  	if err != nil {
  23  		t.Fatalf("Failed to create signer: %v", err)
  24  	}
  25  	if err := signer.Generate(); err != nil {
  26  		t.Fatalf("Failed to generate keypair: %v", err)
  27  	}
  28  
  29  	tests := []struct {
  30  		name        string
  31  		tags        *tag.S
  32  		description string
  33  	}{
  34  		{
  35  			name:        "NoTags",
  36  			tags:        nil,
  37  			description: "Event without tags",
  38  		},
  39  		{
  40  			name: "WithPTags",
  41  			tags: tag.NewS(
  42  				tag.NewFromAny("p", "0000000000000000000000000000000000000000000000000000000000000001"),
  43  				tag.NewFromAny("p", "0000000000000000000000000000000000000000000000000000000000000002"),
  44  			),
  45  			description: "Event with p-tags (stored in tags JSON, relationships added separately)",
  46  		},
  47  		{
  48  			name: "WithETags",
  49  			tags: tag.NewS(
  50  				tag.NewFromAny("e", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
  51  				tag.NewFromAny("e", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
  52  			),
  53  			description: "Event with e-tags (stored in tags JSON, relationships added separately)",
  54  		},
  55  		{
  56  			name: "MixedTags",
  57  			tags: tag.NewS(
  58  				tag.NewFromAny("t", "nostr"),
  59  				tag.NewFromAny("e", "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"),
  60  				tag.NewFromAny("p", "0000000000000000000000000000000000000000000000000000000000000003"),
  61  			),
  62  			description: "Event with mixed tags",
  63  		},
  64  	}
  65  
  66  	for _, tt := range tests {
  67  		t.Run(tt.name, func(t *testing.T) {
  68  			ev := event.New()
  69  			ev.Pubkey = signer.Pub()
  70  			ev.CreatedAt = timestamp.Now().V
  71  			ev.Kind = 1
  72  			ev.Content = []byte(fmt.Sprintf("Test content for %s", tt.name))
  73  			ev.Tags = tt.tags
  74  
  75  			if err := ev.Sign(signer); err != nil {
  76  				t.Fatalf("Failed to sign event: %v", err)
  77  			}
  78  
  79  			cypher, params := n.buildBaseEventCypher(ev, 12345)
  80  
  81  			// Base event Cypher should NOT contain tag relationship clauses
  82  			// (tags are added separately via addTagsInBatches)
  83  			if strings.Contains(cypher, "OPTIONAL MATCH") {
  84  				t.Errorf("%s: buildBaseEventCypher should NOT contain OPTIONAL MATCH", tt.description)
  85  			}
  86  			if strings.Contains(cypher, "UNWIND") {
  87  				t.Errorf("%s: buildBaseEventCypher should NOT contain UNWIND", tt.description)
  88  			}
  89  			if strings.Contains(cypher, ":REFERENCES") {
  90  				t.Errorf("%s: buildBaseEventCypher should NOT contain :REFERENCES", tt.description)
  91  			}
  92  			if strings.Contains(cypher, ":MENTIONS") {
  93  				t.Errorf("%s: buildBaseEventCypher should NOT contain :MENTIONS", tt.description)
  94  			}
  95  			if strings.Contains(cypher, ":TAGGED_WITH") {
  96  				t.Errorf("%s: buildBaseEventCypher should NOT contain :TAGGED_WITH", tt.description)
  97  			}
  98  
  99  			// Should contain basic event creation elements
 100  			if !strings.Contains(cypher, "CREATE (e:Event") {
 101  				t.Errorf("%s: should CREATE Event node", tt.description)
 102  			}
 103  			if !strings.Contains(cypher, "MERGE (a:NostrUser") {
 104  				t.Errorf("%s: should MERGE NostrUser node", tt.description)
 105  			}
 106  			if !strings.Contains(cypher, ":AUTHORED_BY") {
 107  				t.Errorf("%s: should create AUTHORED_BY relationship", tt.description)
 108  			}
 109  
 110  			// Should have tags serialized in params
 111  			if _, ok := params["tags"]; !ok {
 112  				t.Errorf("%s: params should contain serialized tags", tt.description)
 113  			}
 114  
 115  			// Validate params have required fields
 116  			requiredParams := []string{"eventId", "serial", "kind", "createdAt", "content", "sig", "pubkey", "tags", "expiration"}
 117  			for _, p := range requiredParams {
 118  				if _, ok := params[p]; !ok {
 119  					t.Errorf("%s: missing required param: %s", tt.description, p)
 120  				}
 121  			}
 122  
 123  			t.Logf("✓ %s: base event Cypher is clean (no tag relationships)", tt.name)
 124  		})
 125  	}
 126  }
 127  
 128  // TestSafePrefix validates the safePrefix helper function
 129  func TestSafePrefix(t *testing.T) {
 130  	tests := []struct {
 131  		input    string
 132  		n        int
 133  		expected string
 134  	}{
 135  		{"hello world", 5, "hello"},
 136  		{"hi", 5, "hi"},
 137  		{"", 5, ""},
 138  		{"1234567890", 10, "1234567890"},
 139  		{"1234567890", 11, "1234567890"},
 140  		{"0123456789abcdef", 8, "01234567"},
 141  	}
 142  
 143  	for _, tt := range tests {
 144  		t.Run(fmt.Sprintf("%q[:%d]", tt.input, tt.n), func(t *testing.T) {
 145  			result := safePrefix(tt.input, tt.n)
 146  			if result != tt.expected {
 147  				t.Errorf("safePrefix(%q, %d) = %q; want %q", tt.input, tt.n, result, tt.expected)
 148  			}
 149  		})
 150  	}
 151  }
 152  
 153  // TestSaveEvent_ETagReference tests that events with e-tags are saved correctly
 154  // using the Tag-based model: Event-[:TAGGED_WITH]->Tag-[:REFERENCES]->Event.
 155  // Uses shared testDB from testmain_test.go to avoid auth rate limiting.
 156  func TestSaveEvent_ETagReference(t *testing.T) {
 157  	if testDB == nil {
 158  		t.Skip("Neo4j not available")
 159  	}
 160  
 161  	ctx := context.Background()
 162  
 163  	// Clean up before test
 164  	cleanTestDatabase()
 165  
 166  	// Generate keypairs
 167  	alice, err := p8k.New()
 168  	if err != nil {
 169  		t.Fatalf("Failed to create signer: %v", err)
 170  	}
 171  	if err := alice.Generate(); err != nil {
 172  		t.Fatalf("Failed to generate keypair: %v", err)
 173  	}
 174  
 175  	bob, err := p8k.New()
 176  	if err != nil {
 177  		t.Fatalf("Failed to create signer: %v", err)
 178  	}
 179  	if err := bob.Generate(); err != nil {
 180  		t.Fatalf("Failed to generate keypair: %v", err)
 181  	}
 182  
 183  	// Create a root event from Alice
 184  	rootEvent := event.New()
 185  	rootEvent.Pubkey = alice.Pub()
 186  	rootEvent.CreatedAt = timestamp.Now().V
 187  	rootEvent.Kind = 1
 188  	rootEvent.Content = []byte("This is the root event")
 189  
 190  	if err := rootEvent.Sign(alice); err != nil {
 191  		t.Fatalf("Failed to sign root event: %v", err)
 192  	}
 193  
 194  	// Save root event
 195  	exists, err := testDB.SaveEvent(ctx, rootEvent)
 196  	if err != nil {
 197  		t.Fatalf("Failed to save root event: %v", err)
 198  	}
 199  	if exists {
 200  		t.Fatal("Root event should not exist yet")
 201  	}
 202  
 203  	rootEventID := hex.Enc(rootEvent.ID[:])
 204  
 205  	// Create a reply from Bob that references the root event
 206  	replyEvent := event.New()
 207  	replyEvent.Pubkey = bob.Pub()
 208  	replyEvent.CreatedAt = timestamp.Now().V + 1
 209  	replyEvent.Kind = 1
 210  	replyEvent.Content = []byte("This is a reply to Alice")
 211  	replyEvent.Tags = tag.NewS(
 212  		tag.NewFromAny("e", rootEventID, "", "root"),
 213  		tag.NewFromAny("p", hex.Enc(alice.Pub())),
 214  	)
 215  
 216  	if err := replyEvent.Sign(bob); err != nil {
 217  		t.Fatalf("Failed to sign reply event: %v", err)
 218  	}
 219  
 220  	// Save reply event - this exercises the batched tag creation
 221  	exists, err = testDB.SaveEvent(ctx, replyEvent)
 222  	if err != nil {
 223  		t.Fatalf("Failed to save reply event: %v", err)
 224  	}
 225  	if exists {
 226  		t.Fatal("Reply event should not exist yet")
 227  	}
 228  
 229  	// Verify Tag-based e-tag model: Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event
 230  	cypher := `
 231  		MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $rootId})-[:REFERENCES]->(root:Event {id: $rootId})
 232  		RETURN reply.id AS replyId, t.value AS tagValue, root.id AS rootId
 233  	`
 234  	params := map[string]any{
 235  		"replyId": hex.Enc(replyEvent.ID[:]),
 236  		"rootId":  rootEventID,
 237  	}
 238  
 239  	result, err := testDB.ExecuteRead(ctx, cypher, params)
 240  	if err != nil {
 241  		t.Fatalf("Failed to query Tag-based REFERENCES: %v", err)
 242  	}
 243  
 244  	if !result.Next(ctx) {
 245  		t.Error("Expected Tag-based REFERENCES relationship between reply and root events")
 246  	} else {
 247  		record := result.Record()
 248  		returnedReplyId := record.Values[0].(string)
 249  		tagValue := record.Values[1].(string)
 250  		returnedRootId := record.Values[2].(string)
 251  		t.Logf("✓ Tag-based REFERENCES verified: Event(%s) -> Tag{e:%s} -> Event(%s)", returnedReplyId[:8], tagValue[:8], returnedRootId[:8])
 252  	}
 253  
 254  	// Verify Tag-based p-tag model: Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser
 255  	pTagCypher := `
 256  		MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $authorPubkey})-[:REFERENCES]->(author:NostrUser {pubkey: $authorPubkey})
 257  		RETURN author.pubkey AS pubkey, t.value AS tagValue
 258  	`
 259  	pTagParams := map[string]any{
 260  		"replyId":      hex.Enc(replyEvent.ID[:]),
 261  		"authorPubkey": hex.Enc(alice.Pub()),
 262  	}
 263  
 264  	pTagResult, err := testDB.ExecuteRead(ctx, pTagCypher, pTagParams)
 265  	if err != nil {
 266  		t.Fatalf("Failed to query Tag-based p-tag: %v", err)
 267  	}
 268  
 269  	if !pTagResult.Next(ctx) {
 270  		t.Error("Expected Tag-based p-tag relationship")
 271  	} else {
 272  		t.Logf("✓ Tag-based p-tag relationship verified")
 273  	}
 274  }
 275  
 276  // TestSaveEvent_ETagMissingReference tests that e-tags to non-existent events
 277  // create Tag nodes but don't create REFERENCES relationships to missing events.
 278  // Uses shared testDB from testmain_test.go to avoid auth rate limiting.
 279  func TestSaveEvent_ETagMissingReference(t *testing.T) {
 280  	if testDB == nil {
 281  		t.Skip("Neo4j not available")
 282  	}
 283  
 284  	ctx := context.Background()
 285  
 286  	// Clean up before test
 287  	cleanTestDatabase()
 288  
 289  	signer, err := p8k.New()
 290  	if err != nil {
 291  		t.Fatalf("Failed to create signer: %v", err)
 292  	}
 293  	if err := signer.Generate(); err != nil {
 294  		t.Fatalf("Failed to generate keypair: %v", err)
 295  	}
 296  
 297  	// Create an event that references a non-existent event
 298  	nonExistentEventID := "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
 299  
 300  	ev := event.New()
 301  	ev.Pubkey = signer.Pub()
 302  	ev.CreatedAt = timestamp.Now().V
 303  	ev.Kind = 1
 304  	ev.Content = []byte("Reply to ghost event")
 305  	ev.Tags = tag.NewS(
 306  		tag.NewFromAny("e", nonExistentEventID, "", "reply"),
 307  	)
 308  
 309  	if err := ev.Sign(signer); err != nil {
 310  		t.Fatalf("Failed to sign event: %v", err)
 311  	}
 312  
 313  	// Save should succeed (batched e-tag processing handles missing reference)
 314  	exists, err := testDB.SaveEvent(ctx, ev)
 315  	if err != nil {
 316  		t.Fatalf("Failed to save event with missing reference: %v", err)
 317  	}
 318  	if exists {
 319  		t.Fatal("Event should not exist yet")
 320  	}
 321  
 322  	// Verify event was saved
 323  	checkCypher := "MATCH (e:Event {id: $id}) RETURN e.id AS id"
 324  	checkParams := map[string]any{"id": hex.Enc(ev.ID[:])}
 325  
 326  	result, err := testDB.ExecuteRead(ctx, checkCypher, checkParams)
 327  	if err != nil {
 328  		t.Fatalf("Failed to check event: %v", err)
 329  	}
 330  
 331  	if !result.Next(ctx) {
 332  		t.Error("Event should have been saved despite missing reference")
 333  	}
 334  
 335  	// Verify Tag node was created with TAGGED_WITH relationship
 336  	tagCypher := `
 337  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $refId})
 338  		RETURN t.value AS tagValue
 339  	`
 340  	tagParams := map[string]any{
 341  		"eventId": hex.Enc(ev.ID[:]),
 342  		"refId":   nonExistentEventID,
 343  	}
 344  
 345  	tagResult, err := testDB.ExecuteRead(ctx, tagCypher, tagParams)
 346  	if err != nil {
 347  		t.Fatalf("Failed to check Tag node: %v", err)
 348  	}
 349  
 350  	if !tagResult.Next(ctx) {
 351  		t.Error("Expected Tag node to be created for e-tag even when target doesn't exist")
 352  	} else {
 353  		t.Logf("✓ Tag node created for missing reference")
 354  	}
 355  
 356  	// Verify no REFERENCES relationship was created from Tag (as the target Event doesn't exist)
 357  	refCypher := `
 358  		MATCH (t:Tag {type: 'e', value: $refId})-[:REFERENCES]->(ref:Event)
 359  		RETURN count(ref) AS refCount
 360  	`
 361  	refParams := map[string]any{"refId": nonExistentEventID}
 362  
 363  	refResult, err := testDB.ExecuteRead(ctx, refCypher, refParams)
 364  	if err != nil {
 365  		t.Fatalf("Failed to check REFERENCES from Tag: %v", err)
 366  	}
 367  
 368  	if refResult.Next(ctx) {
 369  		count := refResult.Record().Values[0].(int64)
 370  		if count > 0 {
 371  			t.Errorf("Expected no REFERENCES from Tag for non-existent event, got %d", count)
 372  		} else {
 373  			t.Logf("✓ Correctly handled missing reference (no REFERENCES from Tag)")
 374  		}
 375  	}
 376  }
 377  
 378  // TestSaveEvent_MultipleETags tests events with multiple e-tags using Tag-based model.
 379  // Uses shared testDB from testmain_test.go to avoid auth rate limiting.
 380  func TestSaveEvent_MultipleETags(t *testing.T) {
 381  	if testDB == nil {
 382  		t.Skip("Neo4j not available")
 383  	}
 384  
 385  	ctx := context.Background()
 386  
 387  	// Clean up before test
 388  	cleanTestDatabase()
 389  
 390  	signer, err := p8k.New()
 391  	if err != nil {
 392  		t.Fatalf("Failed to create signer: %v", err)
 393  	}
 394  	if err := signer.Generate(); err != nil {
 395  		t.Fatalf("Failed to generate keypair: %v", err)
 396  	}
 397  
 398  	// Create three events first
 399  	var eventIDs []string
 400  	for i := 0; i < 3; i++ {
 401  		ev := event.New()
 402  		ev.Pubkey = signer.Pub()
 403  		ev.CreatedAt = timestamp.Now().V + int64(i)
 404  		ev.Kind = 1
 405  		ev.Content = []byte(fmt.Sprintf("Event %d", i))
 406  
 407  		if err := ev.Sign(signer); err != nil {
 408  			t.Fatalf("Failed to sign event %d: %v", i, err)
 409  		}
 410  
 411  		if _, err := testDB.SaveEvent(ctx, ev); err != nil {
 412  			t.Fatalf("Failed to save event %d: %v", i, err)
 413  		}
 414  
 415  		eventIDs = append(eventIDs, hex.Enc(ev.ID[:]))
 416  	}
 417  
 418  	// Create an event that references all three
 419  	replyEvent := event.New()
 420  	replyEvent.Pubkey = signer.Pub()
 421  	replyEvent.CreatedAt = timestamp.Now().V + 10
 422  	replyEvent.Kind = 1
 423  	replyEvent.Content = []byte("This references multiple events")
 424  	replyEvent.Tags = tag.NewS(
 425  		tag.NewFromAny("e", eventIDs[0], "", "root"),
 426  		tag.NewFromAny("e", eventIDs[1], "", "reply"),
 427  		tag.NewFromAny("e", eventIDs[2], "", "mention"),
 428  	)
 429  
 430  	if err := replyEvent.Sign(signer); err != nil {
 431  		t.Fatalf("Failed to sign reply event: %v", err)
 432  	}
 433  
 434  	// Save reply event - tests batched e-tag creation with Tag nodes
 435  	exists, err := testDB.SaveEvent(ctx, replyEvent)
 436  	if err != nil {
 437  		t.Fatalf("Failed to save multi-reference event: %v", err)
 438  	}
 439  	if exists {
 440  		t.Fatal("Reply event should not exist yet")
 441  	}
 442  
 443  	// Verify all Tag-based REFERENCES relationships were created
 444  	// Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event
 445  	cypher := `
 446  		MATCH (reply:Event {id: $replyId})-[:TAGGED_WITH]->(t:Tag {type: 'e'})-[:REFERENCES]->(ref:Event)
 447  		RETURN ref.id AS refId
 448  	`
 449  	params := map[string]any{"replyId": hex.Enc(replyEvent.ID[:])}
 450  
 451  	result, err := testDB.ExecuteRead(ctx, cypher, params)
 452  	if err != nil {
 453  		t.Fatalf("Failed to query Tag-based REFERENCES: %v", err)
 454  	}
 455  
 456  	referencedIDs := make(map[string]bool)
 457  	for result.Next(ctx) {
 458  		refID := result.Record().Values[0].(string)
 459  		referencedIDs[refID] = true
 460  	}
 461  
 462  	if len(referencedIDs) != 3 {
 463  		t.Errorf("Expected 3 Tag-based REFERENCES, got %d", len(referencedIDs))
 464  	}
 465  
 466  	for i, id := range eventIDs {
 467  		if !referencedIDs[id] {
 468  			t.Errorf("Missing Tag-based REFERENCES to event %d (%s)", i, id[:8])
 469  		}
 470  	}
 471  
 472  	t.Logf("✓ All %d Tag-based REFERENCES created successfully", len(referencedIDs))
 473  }
 474  
 475  // TestSaveEvent_LargePTagBatch tests that events with many p-tags are saved correctly
 476  // using batched Tag-based processing to avoid Neo4j stack overflow.
 477  // Uses shared testDB from testmain_test.go to avoid auth rate limiting.
 478  func TestSaveEvent_LargePTagBatch(t *testing.T) {
 479  	if testDB == nil {
 480  		t.Skip("Neo4j not available")
 481  	}
 482  
 483  	ctx := context.Background()
 484  
 485  	// Clean up before test
 486  	cleanTestDatabase()
 487  
 488  	signer, err := p8k.New()
 489  	if err != nil {
 490  		t.Fatalf("Failed to create signer: %v", err)
 491  	}
 492  	if err := signer.Generate(); err != nil {
 493  		t.Fatalf("Failed to generate keypair: %v", err)
 494  	}
 495  
 496  	// Create event with many p-tags (enough to require multiple batches)
 497  	// With tagBatchSize = 500, this will require 2 batches
 498  	numTags := 600
 499  	manyPTags := tag.NewS()
 500  	for i := 0; i < numTags; i++ {
 501  		manyPTags.Append(tag.NewFromAny("p", fmt.Sprintf("%064x", i)))
 502  	}
 503  
 504  	ev := event.New()
 505  	ev.Pubkey = signer.Pub()
 506  	ev.CreatedAt = timestamp.Now().V
 507  	ev.Kind = 3 // Contact list
 508  	ev.Content = []byte("")
 509  	ev.Tags = manyPTags
 510  
 511  	if err := ev.Sign(signer); err != nil {
 512  		t.Fatalf("Failed to sign event: %v", err)
 513  	}
 514  
 515  	// This should succeed with batched processing
 516  	exists, err := testDB.SaveEvent(ctx, ev)
 517  	if err != nil {
 518  		t.Fatalf("Failed to save event with %d p-tags: %v", numTags, err)
 519  	}
 520  	if exists {
 521  		t.Fatal("Event should not exist yet")
 522  	}
 523  
 524  	// Verify all Tag nodes were created with TAGGED_WITH relationships
 525  	tagCountCypher := `
 526  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})
 527  		RETURN count(t) AS tagCount
 528  	`
 529  	tagCountParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
 530  
 531  	tagResult, err := testDB.ExecuteRead(ctx, tagCountCypher, tagCountParams)
 532  	if err != nil {
 533  		t.Fatalf("Failed to count p-tag Tag nodes: %v", err)
 534  	}
 535  
 536  	if tagResult.Next(ctx) {
 537  		count := tagResult.Record().Values[0].(int64)
 538  		if count != int64(numTags) {
 539  			t.Errorf("Expected %d Tag nodes, got %d", numTags, count)
 540  		} else {
 541  			t.Logf("✓ All %d p-tag Tag nodes created via batched processing", count)
 542  		}
 543  	}
 544  
 545  	// Verify all REFERENCES relationships to NostrUser were created
 546  	refCountCypher := `
 547  		MATCH (e:Event {id: $eventId})-[:TAGGED_WITH]->(t:Tag {type: 'p'})-[:REFERENCES]->(u:NostrUser)
 548  		RETURN count(u) AS refCount
 549  	`
 550  	refCountParams := map[string]any{"eventId": hex.Enc(ev.ID[:])}
 551  
 552  	refResult, err := testDB.ExecuteRead(ctx, refCountCypher, refCountParams)
 553  	if err != nil {
 554  		t.Fatalf("Failed to count Tag-based REFERENCES to NostrUser: %v", err)
 555  	}
 556  
 557  	if refResult.Next(ctx) {
 558  		count := refResult.Record().Values[0].(int64)
 559  		if count != int64(numTags) {
 560  			t.Errorf("Expected %d REFERENCES to NostrUser, got %d", numTags, count)
 561  		} else {
 562  			t.Logf("✓ All %d Tag-based REFERENCES to NostrUser created via batched processing", count)
 563  		}
 564  	}
 565  }
 566