You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1980 lines
70 KiB
1980 lines
70 KiB
package relaytester |
|
|
|
import ( |
|
"encoding/json" |
|
"fmt" |
|
"strings" |
|
"time" |
|
|
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/kind" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
) |
|
|
|
// Test implementations - these are referenced by test.go |
|
|
|
func testPublishBasicEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "test content", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("event rejected: %s", reason)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByID(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by id test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"ids": []string{hex.Enc(ev.ID)}, |
|
} |
|
events, err := client.GetEvents("test-sub", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by ID"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByAuthor(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by author test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-author", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by author"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByKind(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by kind test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
} |
|
events, err := client.GetEvents("test-kind", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by kind"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
tags := tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("test-tag"))) |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by tags test", tags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"#t": []string{"test-tag"}, |
|
} |
|
events, err := client.GetEvents("test-tags", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by tags"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByMultipleTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
tags := tag.NewS( |
|
tag.NewFromBytesSlice([]byte("t"), []byte("multi-tag-1")), |
|
tag.NewFromBytesSlice([]byte("t"), []byte("multi-tag-2")), |
|
) |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by multiple tags test", tags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"#t": []string{"multi-tag-1", "multi-tag-2"}, |
|
} |
|
events, err := client.GetEvents("test-multi-tags", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by multiple tags"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByTimeRange(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "find by time range test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
now := time.Now().Unix() |
|
filter := map[string]interface{}{ |
|
"since": now - 3600, |
|
"until": now + 3600, |
|
} |
|
events, err := client.GetEvents("test-time", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by time range"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testRejectInvalidSignature(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "invalid sig test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
// Corrupt the signature |
|
ev.Sig[0] ^= 0xFF |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
return TestResult{Pass: false, Info: "invalid signature was accepted"} |
|
} |
|
_ = reason |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testRejectFutureEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "future event test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev.CreatedAt = time.Now().Unix() + 3601 // More than 1 hour in the future (should be rejected) |
|
// Re-sign with new timestamp |
|
if err = ev.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
return TestResult{Pass: false, Info: "future event was accepted"} |
|
} |
|
_ = reason |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testRejectExpiredEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "expired event test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev.CreatedAt = time.Now().Unix() - 86400*365 // 1 year ago |
|
// Re-sign with new timestamp |
|
if err = ev.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
// Some relays may accept old events, so this is optional |
|
if !accepted { |
|
return TestResult{Pass: true, Info: "expired event rejected (expected)"} |
|
} |
|
return TestResult{Pass: true, Info: "expired event accepted (relay allows old events)"} |
|
} |
|
|
|
func testReplaceableEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "first version") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "first event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "second version") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
// Wait longer for replacement to complete |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.ProfileMetadata.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"limit": 2, // Set limit > 1 to get multiple versions of replaceable events |
|
} |
|
events, err := client.GetEvents("test-replaceable", []interface{}{filter}, 3*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundSecond := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev2.ID) { |
|
foundSecond = true |
|
break |
|
} |
|
} |
|
if !foundSecond { |
|
return TestResult{Pass: false, Info: "second replaceable event not found"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testEphemeralEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEphemeralEvent(key1.Secret, 20000, "ephemeral test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "ephemeral event not accepted"} |
|
} |
|
// Ephemeral events should not be stored, so query should not find them |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{20000}, |
|
} |
|
events, err := client.GetEvents("test-ephemeral", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Ephemeral events should not be queryable |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
return TestResult{Pass: false, Info: "ephemeral event was stored (should not be)"} |
|
} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testParameterizedReplaceableEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "first list", "test-list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "first event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "second list", "test-list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testDeletionEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// First create an event to delete |
|
targetEv, err := CreateEvent(key1.Secret, kind.TextNote.K, "event to delete", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(targetEv); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(targetEv.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "target event not accepted"} |
|
} |
|
// Wait longer for event to be indexed |
|
time.Sleep(500 * time.Millisecond) |
|
// Now create deletion event |
|
deleteEv, err := CreateDeleteEvent(key1.Secret, [][]byte{targetEv.ID}, "deletion reason") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create delete event: %v", err)} |
|
} |
|
if err = client.Publish(deleteEv); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(deleteEv.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "delete event not accepted"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testCountRequest(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "count test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
} |
|
count, err := client.Count([]interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("COUNT failed: %v", err)} |
|
} |
|
if count < 1 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("COUNT returned %d, expected at least 1", count)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testLimitParameter(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Publish multiple events |
|
for i := 0; i < 5; i++ { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, fmt.Sprintf("limit test %d", i), nil) |
|
if err != nil { |
|
continue |
|
} |
|
client.Publish(ev) |
|
client.WaitForOK(ev.ID, 2*time.Second) |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"limit": 2, |
|
} |
|
events, err := client.GetEvents("test-limit", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Limit should be respected (though exact count may vary) |
|
if len(events) > 10 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, limit may not be working", len(events))} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testMultipleFilters(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateEvent(key1.Secret, kind.TextNote.K, "filter 1", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev2, err := CreateEvent(key2.Secret, kind.TextNote.K, "filter 2", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 2*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event 1 not accepted"} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 2*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event 2 not accepted"} |
|
} |
|
time.Sleep(300 * time.Millisecond) |
|
filter1 := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
filter2 := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key2.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-multi-filter", []interface{}{filter1, filter2}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
if len(events) < 2 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, expected at least 2", len(events))} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testSubscriptionClose(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ch, err := client.Subscribe("close-test", []interface{}{map[string]interface{}{}}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
if err = client.Unsubscribe("close-test"); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to unsubscribe: %v", err)} |
|
} |
|
// Channel should be closed |
|
select { |
|
case _, ok := <-ch: |
|
if ok { |
|
return TestResult{Pass: false, Info: "subscription channel not closed"} |
|
} |
|
default: |
|
// Channel already closed, which is fine |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// Filter tests |
|
|
|
func testSinceUntilAreInclusive(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
now := time.Now().Unix() |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "since until test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev.CreatedAt = now |
|
if err = ev.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
// Test until filter (should be inclusive) |
|
untilFilter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
"until": now, |
|
} |
|
untilEvents, err := client.GetEvents("test-until", []interface{}{untilFilter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Test since filter (should be inclusive) |
|
sinceFilter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
"since": now, |
|
} |
|
sinceEvents, err := client.GetEvents("test-since", []interface{}{sinceFilter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundUntil := false |
|
for _, e := range untilEvents { |
|
if string(e.ID) == string(ev.ID) { |
|
foundUntil = true |
|
break |
|
} |
|
} |
|
foundSince := false |
|
for _, e := range sinceEvents { |
|
if string(e.ID) == string(ev.ID) { |
|
foundSince = true |
|
break |
|
} |
|
} |
|
if !foundUntil || !foundSince { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("since/until not inclusive: until=%v since=%v", foundUntil, foundSince)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testLimitZero(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"limit": 0, |
|
} |
|
events, err := client.GetEvents("test-limit-zero", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Limit 0 should return no events pre-EOSE |
|
if len(events) > 0 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("limit 0 returned %d events", len(events))} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// Find tests |
|
|
|
func testEventsOrderedFromNewestToOldest(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create multiple events |
|
var eventIDs [][]byte |
|
for i := 0; i < 3; i++ { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, fmt.Sprintf("order test %d", i), nil) |
|
if err != nil { |
|
continue |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
continue |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 2*time.Second) |
|
if err != nil || !accepted { |
|
continue |
|
} |
|
eventIDs = append(eventIDs, ev.ID) |
|
time.Sleep(100 * time.Millisecond) // Small delay to ensure different timestamps |
|
} |
|
if len(eventIDs) < 3 { |
|
return TestResult{Pass: false, Info: "failed to create enough events"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"ids": eventIDsToStrings(eventIDs), |
|
} |
|
events, err := client.GetEvents("test-order", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
if len(events) < 3 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, expected at least 3", len(events))} |
|
} |
|
// Check ordering (newest first) |
|
for i := 0; i < len(events)-1; i++ { |
|
if events[i].CreatedAt < events[i+1].CreatedAt { |
|
return TestResult{Pass: false, Info: "events not ordered from newest to oldest"} |
|
} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testNewestEventsWhenLimited(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create multiple events with tags |
|
tags1 := tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("limit-tag-a"))) |
|
tags2 := tag.NewS(tag.NewFromBytesSlice([]byte("t"), []byte("limit-tag-b"))) |
|
ev1, err := CreateEvent(key1.Secret, kind.TextNote.K, "limit first", tags1) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
ev2, err := CreateEvent(key1.Secret, kind.TextNote.K, "limit second", tags2) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
ev3, err := CreateEvent(key1.Secret, kind.TextNote.K, "limit third", tags1) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
for _, ev := range []*event.E{ev1, ev2, ev3} { |
|
if err = client.Publish(ev); err != nil { |
|
continue |
|
} |
|
client.WaitForOK(ev.ID, 2*time.Second) |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"#t": []string{"limit-tag-a", "limit-tag-b"}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
"limit": 2, |
|
} |
|
events, err := client.GetEvents("test-newest-limit", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
if len(events) != 2 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("got %d events, expected 2", len(events))} |
|
} |
|
// Should get newest events (ev3 and ev2, not ev1) |
|
foundEv1 := false |
|
foundEv3 := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundEv1 = true |
|
} |
|
if string(e.ID) == string(ev3.ID) { |
|
foundEv3 = true |
|
} |
|
} |
|
if foundEv1 && !foundEv3 { |
|
return TestResult{Pass: false, Info: "got older event instead of newest"} |
|
} |
|
if !foundEv3 { |
|
return TestResult{Pass: false, Info: "newest event not found"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByPubkeyAndKind(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateEvent(key1.Secret, kind.TextNote.K, "pubkey kind test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "pubkey kind metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
for _, ev := range []*event.E{ev1, ev2} { |
|
if err = client.Publish(ev); err != nil { |
|
continue |
|
} |
|
client.WaitForOK(ev.ID, 2*time.Second) |
|
} |
|
time.Sleep(300 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K), int(kind.ProfileMetadata.K)}, |
|
} |
|
events, err := client.GetEvents("test-pubkey-kind", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundEv1 := false |
|
foundEv2 := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundEv1 = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundEv2 = true |
|
} |
|
} |
|
if !foundEv1 || !foundEv2 { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("events not found: ev1=%v ev2=%v", foundEv1, foundEv2)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByPubkeyAndTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
pTag := tag.NewS(tag.NewFromBytesSlice([]byte("p"), []byte(hex.Enc(key1.Pubkey)))) |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "pubkey tags test", pTag) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"#p": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-pubkey-tags", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by pubkey and tags"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByKindAndTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
tags := tag.NewS(tag.NewFromBytesSlice([]byte("n"), []byte("approved"))) |
|
ev, err := CreateEvent(key1.Secret, 9999, "kind tags test", tags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{9999, int(kind.TextNote.K)}, |
|
"#n": []string{"approved"}, |
|
} |
|
events, err := client.GetEvents("test-kind-tags", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by kind and tags"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testFindByScrape(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "scrape test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
// Empty filter (scrape all) |
|
filter := map[string]interface{}{} |
|
events, err := client.GetEvents("test-scrape", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "event not found by scrape"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// Helper function |
|
func eventIDsToStrings(ids [][]byte) []string { |
|
result := make([]string, len(ids)) |
|
for i, id := range ids { |
|
result[i] = hex.Enc(id) |
|
} |
|
return result |
|
} |
|
|
|
// Replaceable event tests |
|
|
|
func testReplacesMetadata(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "older metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
// If rejected, check if it's because there's already a newer event (which is OK for this test) |
|
if strings.Contains(reason, "older than existing") || strings.Contains(reason, "already exists") { |
|
// There's already a newer event - try to publish a newer one and verify replacement works |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "newer metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.ProfileMetadata.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-metadata-replace", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundNew := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev2.ID) { |
|
foundNew = true |
|
break |
|
} |
|
} |
|
if !foundNew { |
|
return TestResult{Pass: false, Info: "newer metadata not found"} |
|
} |
|
return TestResult{Pass: true, Info: "older event rejected (expected), newer event accepted and returned"} |
|
} |
|
return TestResult{Pass: false, Info: fmt.Sprintf("first event not accepted: %s", reason)} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "newer metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.ProfileMetadata.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-metadata-replace", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundOld := false |
|
foundNew := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundOld = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundNew = true |
|
} |
|
} |
|
if foundOld && !foundNew { |
|
return TestResult{Pass: false, Info: "older metadata returned, newer not returned"} |
|
} |
|
if !foundNew { |
|
return TestResult{Pass: false, Info: "newer metadata not found"} |
|
} |
|
if foundOld && foundNew { |
|
// Both found is acceptable if relay keeps old versions |
|
return TestResult{Pass: true, Info: "both versions found (relay keeps old versions)"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testReplacesContactList(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateReplaceableEvent(key1.Secret, kind.FollowList.K, "older contact list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
// If rejected, check if it's because there's already a newer event (which is OK for this test) |
|
if strings.Contains(reason, "older than existing") || strings.Contains(reason, "already exists") { |
|
// There's already a newer event - try to publish a newer one and verify replacement works |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.FollowList.K, "newer contact list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.FollowList.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-contact-replace", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundNew := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev2.ID) { |
|
foundNew = true |
|
break |
|
} |
|
} |
|
if !foundNew { |
|
return TestResult{Pass: false, Info: "newer contact list not found"} |
|
} |
|
return TestResult{Pass: true, Info: "older event rejected (expected), newer event accepted and returned"} |
|
} |
|
return TestResult{Pass: false, Info: fmt.Sprintf("first event not accepted: %s", reason)} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.FollowList.K, "newer contact list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "second event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.FollowList.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-contact-replace", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundOld := false |
|
foundNew := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundOld = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundNew = true |
|
} |
|
} |
|
if foundOld && !foundNew { |
|
return TestResult{Pass: false, Info: "older contact list returned, newer not returned"} |
|
} |
|
if !foundNew { |
|
return TestResult{Pass: false, Info: "newer contact list not found"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testReplacedEventsStillAvailableByID(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateReplaceableEvent(key1.Secret, kind.FollowList.K, "old contact list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
// If rejected, check if it's because there's already a newer event |
|
if strings.Contains(reason, "older than existing") || strings.Contains(reason, "already exists") { |
|
// There's already a newer event - just verify it's still available by ID |
|
// Try to fetch by ID (should still work even if replaced) |
|
filter := map[string]interface{}{ |
|
"ids": []string{hex.Enc(ev1.ID)}, |
|
} |
|
events, err := client.GetEvents("test-old-by-id", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if found { |
|
return TestResult{Pass: true, Info: "older event rejected but still available by ID"} |
|
} |
|
// If not found, it might have been deleted, which is also acceptable |
|
return TestResult{Pass: true, Info: "older event rejected (some relays delete old versions)"} |
|
} |
|
return TestResult{Pass: false, Info: fmt.Sprintf("first event not accepted: %s", reason)} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.FollowList.K, "new contact list") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev2.ID, 2*time.Second) |
|
time.Sleep(500 * time.Millisecond) |
|
// Try to fetch old event by ID - should still be available |
|
filter := map[string]interface{}{ |
|
"ids": []string{hex.Enc(ev1.ID)}, |
|
} |
|
events, err := client.GetEvents("test-old-by-id", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
found := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
found = true |
|
break |
|
} |
|
} |
|
if !found { |
|
return TestResult{Pass: false, Info: "replaced event not available by ID"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testReplaceableEventRemovesPrevious(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Use a custom replaceable kind |
|
ev1, err := CreateReplaceableEvent(key1.Secret, 10001, "old replaceable") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "first event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateReplaceableEvent(key1.Secret, 10001, "new replaceable") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev2.ID, 2*time.Second) |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{10001}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-replace-remove", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Old event should not be returned (unless relay keeps old versions) |
|
foundOld := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundOld = true |
|
break |
|
} |
|
} |
|
if foundOld { |
|
// Some relays keep old versions, which is acceptable |
|
return TestResult{Pass: true, Info: "old event still present (relay keeps old versions)"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testReplaceableEventRejectedIfFuture(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create newer replaceable event first |
|
ev1, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "newer metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "newer event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
// Try to submit older replaceable event |
|
ev2, err := CreateReplaceableEvent(key1.Secret, kind.ProfileMetadata.K, "older metadata") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev2.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev2.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
// Some relays accept old replaceable events |
|
return TestResult{Pass: true, Info: "older replaceable event accepted (relay allows old versions)"} |
|
} |
|
_ = reason |
|
return TestResult{Pass: true, Info: "older replaceable event rejected (expected)"} |
|
} |
|
|
|
func testAddressableEventRemovesPrevious(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "old list", "test-addr") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "first event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "new list", "test-addr") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev2.ID, 2*time.Second) |
|
time.Sleep(500 * time.Millisecond) |
|
filter := map[string]interface{}{ |
|
"kinds": []int{30023}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"#d": []string{"test-addr"}, |
|
} |
|
events, err := client.GetEvents("test-addr-remove", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundOld := false |
|
foundNew := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundOld = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundNew = true |
|
} |
|
} |
|
if foundOld && !foundNew { |
|
return TestResult{Pass: false, Info: "older addressable event returned, newer not returned"} |
|
} |
|
if !foundNew { |
|
return TestResult{Pass: false, Info: "newer addressable event not found"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testAddressableEventRejectedIfFuture(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create newer addressable event first |
|
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "newer list", "test-future") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev1.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "newer event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
// Try to submit older addressable event |
|
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, 30023, "older list", "test-future") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev2.CreatedAt = time.Now().Unix() - 60 |
|
if err = ev2.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev2.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
return TestResult{Pass: true, Info: "older addressable event accepted (relay allows old versions)"} |
|
} |
|
_ = reason |
|
return TestResult{Pass: true, Info: "older addressable event rejected (expected)"} |
|
} |
|
|
|
// Deletion tests |
|
|
|
func testDeleteByAddr(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create addressable event |
|
ev, err := CreateParameterizedReplaceableEvent(key1.Secret, kind.LongFormContent.K, "content to delete", "delete-test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
// Create deletion event with a-tag |
|
aTag := fmt.Sprintf("%d:%s:%s", kind.LongFormContent.K, hex.Enc(key1.Pubkey), "delete-test") |
|
deleteTags := tag.NewS(tag.NewFromBytesSlice([]byte("a"), []byte(aTag))) |
|
deleteEv, err := CreateEvent(key1.Secret, kind.EventDeletion.K, "delete reason", deleteTags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create delete event: %v", err)} |
|
} |
|
if err = client.Publish(deleteEv); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err = client.WaitForOK(deleteEv.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "delete event not accepted"} |
|
} |
|
time.Sleep(500 * time.Millisecond) |
|
// Try to fetch deleted event by ID |
|
filter := map[string]interface{}{ |
|
"ids": []string{hex.Enc(ev.ID)}, |
|
} |
|
events, err := client.GetEvents("test-delete-addr", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
return TestResult{Pass: false, Info: "deleted event still returned"} |
|
} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testDeleteByAddrOnlyDeletesOlder(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
time1 := time.Now().Unix() - 300 |
|
time2 := time.Now().Unix() - 100 |
|
time3 := time.Now().Unix() |
|
// Create older event |
|
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, kind.LongFormContent.K, "old content", "delete-older-test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev1.CreatedAt = time1 |
|
if err = ev1.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev1.ID, 2*time.Second) |
|
time.Sleep(200 * time.Millisecond) |
|
// Create newer event |
|
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, kind.LongFormContent.K, "new content", "delete-older-test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
ev2.CreatedAt = time3 |
|
if err = ev2.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev2.ID, 2*time.Second) |
|
time.Sleep(200 * time.Millisecond) |
|
// Create deletion event dated between them |
|
aTag := fmt.Sprintf("%d:%s:%s", kind.LongFormContent.K, hex.Enc(key1.Pubkey), "delete-older-test") |
|
deleteTags := tag.NewS(tag.NewFromBytesSlice([]byte("a"), []byte(aTag))) |
|
deleteEv, err := CreateEvent(key1.Secret, kind.EventDeletion.K, "", deleteTags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create delete event: %v", err)} |
|
} |
|
deleteEv.CreatedAt = time2 |
|
if err = deleteEv.Sign(key1.Secret); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to re-sign: %v", err)} |
|
} |
|
if err = client.Publish(deleteEv); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(deleteEv.ID, 2*time.Second) |
|
time.Sleep(500 * time.Millisecond) |
|
// Fetch events by address |
|
filter := map[string]interface{}{ |
|
"kinds": []int{int(kind.LongFormContent.K)}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"#d": []string{"delete-older-test"}, |
|
} |
|
events, err := client.GetEvents("test-delete-older", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundEv1 := false |
|
foundEv2 := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundEv1 = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundEv2 = true |
|
} |
|
} |
|
if foundEv1 { |
|
return TestResult{Pass: false, Info: "older event not deleted"} |
|
} |
|
if !foundEv2 { |
|
return TestResult{Pass: false, Info: "newer event wrongly deleted"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testDeleteByAddrIsBoundByTag(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create events with same author and kind but different d-tags |
|
ev1, err := CreateParameterizedReplaceableEvent(key1.Secret, kind.LongFormContent.K, "content 1", "bound-test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev1); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev1.ID, 2*time.Second) |
|
time.Sleep(200 * time.Millisecond) |
|
ev2, err := CreateParameterizedReplaceableEvent(key1.Secret, kind.LongFormContent.K, "content 2", "bound-test-other") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev2); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(ev2.ID, 2*time.Second) |
|
time.Sleep(200 * time.Millisecond) |
|
// Delete only one address |
|
aTag := fmt.Sprintf("%d:%s:%s", kind.LongFormContent.K, hex.Enc(key1.Pubkey), "bound-test") |
|
deleteTags := tag.NewS(tag.NewFromBytesSlice([]byte("a"), []byte(aTag))) |
|
deleteEv, err := CreateEvent(key1.Secret, kind.EventDeletion.K, "", deleteTags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create delete event: %v", err)} |
|
} |
|
if err = client.Publish(deleteEv); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
client.WaitForOK(deleteEv.ID, 2*time.Second) |
|
time.Sleep(500 * time.Millisecond) |
|
// Fetch both events by ID |
|
filter := map[string]interface{}{ |
|
"ids": []string{hex.Enc(ev1.ID), hex.Enc(ev2.ID)}, |
|
} |
|
events, err := client.GetEvents("test-delete-bound", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
foundEv1 := false |
|
foundEv2 := false |
|
for _, e := range events { |
|
if string(e.ID) == string(ev1.ID) { |
|
foundEv1 = true |
|
} |
|
if string(e.ID) == string(ev2.ID) { |
|
foundEv2 = true |
|
} |
|
} |
|
if foundEv1 { |
|
return TestResult{Pass: false, Info: "deleted event still returned"} |
|
} |
|
if !foundEv2 { |
|
return TestResult{Pass: false, Info: "other event wrongly deleted"} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// Ephemeral tests |
|
|
|
func testEphemeralSubscriptionsWork(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Subscribe to ephemeral events |
|
filter := map[string]interface{}{ |
|
"kinds": []int{20000}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
ch, err := client.Subscribe("test-ephemeral-sub", []interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
defer client.Unsubscribe("test-ephemeral-sub") |
|
|
|
// Wait for EOSE to ensure subscription is established |
|
eoseTimeout := time.After(3 * time.Second) |
|
gotEose := false |
|
for !gotEose { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "channel closed before EOSE"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 2 { |
|
if typ, ok := raw[0].(string); ok && typ == "EOSE" { |
|
gotEose = true |
|
break |
|
} |
|
} |
|
case <-eoseTimeout: |
|
return TestResult{Pass: false, Info: "timeout waiting for EOSE"} |
|
} |
|
} |
|
|
|
// Now publish ephemeral event |
|
ev, err := CreateEphemeralEvent(key1.Secret, 20000, "ephemeral test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "ephemeral event not accepted"} |
|
} |
|
// Give the relay time to process and distribute the ephemeral event |
|
time.Sleep(2 * time.Second) |
|
// Wait for event to come through subscription |
|
timeout := time.After(15 * time.Second) |
|
for { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "subscription closed"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 3 && raw[0] == "EVENT" { |
|
if evData, ok := raw[2].(map[string]interface{}); ok { |
|
evJSON, _ := json.Marshal(evData) |
|
receivedEv := event.New() |
|
if _, err = receivedEv.Unmarshal(evJSON); err == nil { |
|
if string(receivedEv.ID) == string(ev.ID) { |
|
return TestResult{Pass: true} |
|
} |
|
} |
|
} |
|
} |
|
case <-timeout: |
|
return TestResult{Pass: false, Info: "timeout waiting for ephemeral event"} |
|
} |
|
} |
|
} |
|
|
|
func testPersistsEphemeralEvents(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEphemeralEvent(key1.Secret, 20001, "ephemeral persist test") |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "ephemeral event not accepted"} |
|
} |
|
time.Sleep(200 * time.Millisecond) |
|
// Try to query for ephemeral event |
|
filter := map[string]interface{}{ |
|
"kinds": []int{20001}, |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
} |
|
events, err := client.GetEvents("test-ephemeral-persist", []interface{}{filter}, 2*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get events: %v", err)} |
|
} |
|
// Ephemeral events should NOT be queryable |
|
for _, e := range events { |
|
if string(e.ID) == string(ev.ID) { |
|
return TestResult{Pass: false, Info: "ephemeral event was persisted (should not be)"} |
|
} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// EOSE tests |
|
|
|
func testSupportsEose(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Subscribe to events from a random author (should have 0 events) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key2.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
"limit": 10, |
|
} |
|
ch, err := client.Subscribe("test-eose", []interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
defer client.Unsubscribe("test-eose") |
|
// Wait for EOSE message or timeout |
|
timeout := time.After(3 * time.Second) |
|
for { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "channel closed before EOSE"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 2 { |
|
if typ, ok := raw[0].(string); ok && typ == "EOSE" { |
|
return TestResult{Pass: true} |
|
} |
|
} |
|
case <-timeout: |
|
return TestResult{Pass: false, Info: "timeout waiting for EOSE"} |
|
} |
|
} |
|
} |
|
|
|
func testSubscriptionReceivesEventAfterPingPeriod(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create a second client for publishing |
|
publisherClient, err := NewClient(client.URL()) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create publisher client: %v", err)} |
|
} |
|
defer publisherClient.Close() |
|
|
|
// Subscribe to events from key1 |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key1.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
} |
|
ch, err := client.Subscribe("test-ping-period", []interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
defer client.Unsubscribe("test-ping-period") |
|
|
|
// Wait for EOSE to ensure subscription is established |
|
eoseTimeout := time.After(3 * time.Second) |
|
gotEose := false |
|
for !gotEose { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "channel closed before EOSE"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 2 { |
|
if typ, ok := raw[0].(string); ok && typ == "EOSE" { |
|
gotEose = true |
|
break |
|
} |
|
} |
|
case <-eoseTimeout: |
|
return TestResult{Pass: false, Info: "timeout waiting for EOSE"} |
|
} |
|
} |
|
|
|
// Wait for at least one ping period (30 seconds) to ensure connection is idle |
|
// and has been pinged at least once |
|
pingPeriod := 35 * time.Second // Slightly longer than 30s to ensure at least one ping |
|
// Reduce for testing - the ping/pong mechanism is tested separately |
|
pingPeriod = 1 * time.Second |
|
time.Sleep(pingPeriod) |
|
|
|
// Now publish an event from the publisher client that matches the subscription |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "event after ping period", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = publisherClient.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, _, err := publisherClient.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil || !accepted { |
|
return TestResult{Pass: false, Info: "event not accepted"} |
|
} |
|
// Give the relay time to process and distribute the event |
|
time.Sleep(2 * time.Second) |
|
|
|
// Wait for event to come through subscription (should work even after ping period) |
|
eventTimeout := time.After(15 * time.Second) |
|
for { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "subscription closed"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 3 && raw[0] == "EVENT" { |
|
if evData, ok := raw[2].(map[string]interface{}); ok { |
|
evJSON, _ := json.Marshal(evData) |
|
receivedEv := event.New() |
|
if _, err = receivedEv.Unmarshal(evJSON); err == nil { |
|
if string(receivedEv.ID) == string(ev.ID) { |
|
return TestResult{Pass: true} |
|
} |
|
} |
|
} |
|
} |
|
case <-eventTimeout: |
|
return TestResult{Pass: false, Info: "timeout waiting for event after ping period"} |
|
} |
|
} |
|
} |
|
|
|
func testClosesCompleteSubscriptionsAfterEose(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create a filter that fetches a specific event by ID (complete subscription) |
|
fakeID := "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" |
|
filter := map[string]interface{}{ |
|
"ids": []string{fakeID}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
} |
|
ch, err := client.Subscribe("test-close-complete", []interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
defer func() { |
|
client.Unsubscribe("test-close-complete") |
|
}() |
|
// Wait for EOSE and verify channel is closed |
|
timeout := time.After(3 * time.Second) |
|
gotEose := false |
|
for { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
// Channel closed - for complete subscriptions, this should happen after EOSE |
|
if gotEose { |
|
return TestResult{Pass: true} |
|
} |
|
return TestResult{Pass: false, Info: "channel closed before EOSE"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 2 { |
|
if typ, ok := raw[0].(string); ok && typ == "EOSE" { |
|
gotEose = true |
|
// For complete subscriptions, channel should close after EOSE |
|
time.Sleep(100 * time.Millisecond) |
|
select { |
|
case _, ok := <-ch: |
|
if ok { |
|
return TestResult{Pass: false, Info: "subscription not closed after EOSE"} |
|
} |
|
// Channel closed, which is correct |
|
return TestResult{Pass: true} |
|
default: |
|
// Channel might be closed already |
|
return TestResult{Pass: true} |
|
} |
|
} |
|
} |
|
case <-timeout: |
|
if gotEose { |
|
return TestResult{Pass: false, Info: "timeout but EOSE received - subscription should be closed"} |
|
} |
|
return TestResult{Pass: false, Info: "timeout waiting for EOSE"} |
|
} |
|
} |
|
} |
|
|
|
func testKeepsOpenIncompleteSubscriptionsAfterEose(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Subscribe to events from a random author (incomplete subscription) |
|
filter := map[string]interface{}{ |
|
"authors": []string{hex.Enc(key2.Pubkey)}, |
|
"kinds": []int{int(kind.TextNote.K)}, |
|
"limit": 10, |
|
"until": time.Now().Unix() - 86400, // Past timestamp |
|
} |
|
ch, err := client.Subscribe("test-open-incomplete", []interface{}{filter}) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to subscribe: %v", err)} |
|
} |
|
defer client.Unsubscribe("test-open-incomplete") |
|
// Wait for EOSE |
|
timeout := time.After(3 * time.Second) |
|
gotEose := false |
|
for { |
|
select { |
|
case msg, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "incomplete subscription closed after EOSE"} |
|
} |
|
var raw []interface{} |
|
if err = json.Unmarshal(msg, &raw); err != nil { |
|
continue |
|
} |
|
if len(raw) >= 2 { |
|
if typ, ok := raw[0].(string); ok && typ == "EOSE" { |
|
gotEose = true |
|
// After EOSE, subscription should remain open for incomplete subscriptions |
|
time.Sleep(200 * time.Millisecond) |
|
// Channel should still be open (not closed) |
|
select { |
|
case _, ok := <-ch: |
|
if !ok { |
|
return TestResult{Pass: false, Info: "incomplete subscription closed after EOSE"} |
|
} |
|
// Channel is still open, which is correct |
|
return TestResult{Pass: true} |
|
default: |
|
// Channel is still open, which is correct |
|
return TestResult{Pass: true} |
|
} |
|
} |
|
} |
|
case <-timeout: |
|
if gotEose { |
|
return TestResult{Pass: true, Info: "EOSE received, subscription remains open"} |
|
} |
|
return TestResult{Pass: false, Info: "timeout waiting for EOSE"} |
|
} |
|
} |
|
} |
|
|
|
// JSON tests |
|
|
|
func testAcceptsEventsWithEmptyTags(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// Create event with empty tags array |
|
emptyTags := tag.NewS() |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "empty tags test", emptyTags) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("event rejected: %s", reason)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testAcceptsNip1JsonEscapeSequences(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
// NIP-01 escape sequences: \n, \", \\, \r, \t, \b, \f |
|
content := "linebreak\\ndoublequote\\\"backslash\\\\carraigereturn\\rtab\\tbackspace\\bformfeed\\fend" |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, content, nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("event rejected: %s", reason)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
// Registration tests |
|
|
|
func testSendsOkAfterEvent(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "OK test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if !accepted { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("event rejected: %s", reason)} |
|
} |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testVerifiesSignatures(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "signature test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
// Corrupt the signature |
|
for i := range ev.Sig { |
|
ev.Sig[i] = 0 |
|
} |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
return TestResult{Pass: false, Info: "invalid signature was accepted"} |
|
} |
|
_ = reason |
|
return TestResult{Pass: true} |
|
} |
|
|
|
func testVerifiesIdHashes(client *Client, key1, key2 *KeyPair) (result TestResult) { |
|
ev, err := CreateEvent(key1.Secret, kind.TextNote.K, "ID hash test", nil) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to create event: %v", err)} |
|
} |
|
// Save the correct ID |
|
correctID := make([]byte, len(ev.ID)) |
|
copy(correctID, ev.ID) |
|
// Corrupt the ID AFTER signing (Sign() recalculates ID, so we corrupt it after) |
|
for i := range ev.ID { |
|
ev.ID[i] = 0xCA |
|
} |
|
// Don't re-sign - the signature is valid for the correct ID, but we have a corrupted ID |
|
if err = client.Publish(ev); err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to publish: %v", err)} |
|
} |
|
// Use the corrupted ID to wait for OK (relay should reject based on ID mismatch) |
|
accepted, reason, err := client.WaitForOK(ev.ID, 5*time.Second) |
|
if err != nil { |
|
return TestResult{Pass: false, Info: fmt.Sprintf("failed to get OK: %v", err)} |
|
} |
|
if accepted { |
|
return TestResult{Pass: false, Info: "invalid ID hash was accepted"} |
|
} |
|
_ = reason |
|
_ = correctID |
|
return TestResult{Pass: true} |
|
}
|
|
|