roundtrip_test.go raw
1 package nostr
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8 )
9
10 // TestRoundTrip publishes events to a public relay and verifies they
11 // come back unchanged. This is the Stage 5b coherence test.
12 //
13 // Uses relay.damus.io which accepts events without auth.
14 func TestRoundTrip(t *testing.T) {
15 if testing.Short() {
16 t.Skip("skipping live relay test in short mode")
17 }
18
19 relayURL := "wss://relay.damus.io"
20
21 // Generate identity.
22 id, err := NewIdentity()
23 if err != nil {
24 t.Fatal(err)
25 }
26
27 // Compose events.
28 meta, err := id.ComposeMetadata(99, 7, "deadbeef12345678")
29 if err != nil {
30 t.Fatal(err)
31 }
32 status, err := id.ComposeStatus(99, 7, 2922, 980, 0.335, 0.284, 3)
33 if err != nil {
34 t.Fatal(err)
35 }
36
37 // Self-validate.
38 if !meta.Valid() {
39 t.Fatal("metadata event invalid before publish")
40 }
41 if !status.Valid() {
42 t.Fatal("status event invalid before publish")
43 }
44
45 published := []*Event{meta, status}
46
47 // Publish.
48 pubCtx, pubCancel := context.WithTimeout(context.Background(), 10*time.Second)
49 defer pubCancel()
50
51 client, err := Connect(pubCtx, relayURL)
52 if err != nil {
53 t.Fatalf("connect: %v", err)
54 }
55 defer client.Disconnect()
56
57 go client.Listen(pubCtx)
58
59 for _, ev := range published {
60 if err := client.Publish(pubCtx, ev); err != nil {
61 t.Fatalf("publish: %v", err)
62 }
63 }
64
65 // Wait for OK responses.
66 accepted := 0
67 for i := 0; i < len(published); i++ {
68 select {
69 case ok := <-client.OKs:
70 if !ok.Accepted {
71 t.Fatalf("relay rejected event %s: %s", ok.EventID[:16], ok.Message)
72 }
73 t.Logf("relay accepted: %s...", ok.EventID[:16])
74 accepted++
75 case <-time.After(5 * time.Second):
76 t.Log("no OK response (timeout) — relay may not send OK")
77 goto subscribe
78 }
79 }
80
81 subscribe:
82 if accepted == 0 {
83 t.Skip("relay did not confirm acceptance — cannot verify round-trip")
84 }
85
86 // Wait a moment for relay to process.
87 time.Sleep(500 * time.Millisecond)
88
89 // Subscribe back for our own events.
90 rtCtx, rtCancel := context.WithTimeout(context.Background(), 10*time.Second)
91 defer rtCancel()
92
93 rtClient, err := Connect(rtCtx, relayURL)
94 if err != nil {
95 t.Fatalf("roundtrip connect: %v", err)
96 }
97 defer rtClient.Disconnect()
98
99 go rtClient.Listen(rtCtx)
100
101 limit := 10
102 if err := rtClient.Subscribe(rtCtx, "roundtrip", Filter{
103 Authors: []string{id.PubKey},
104 Limit: &limit,
105 }); err != nil {
106 t.Fatalf("subscribe: %v", err)
107 }
108
109 // Collect received events.
110 want := make(map[string]*Event, len(published))
111 for _, ev := range published {
112 want[ev.ID] = ev
113 }
114
115 matched := 0
116 deadline := time.After(5 * time.Second)
117 for len(want) > 0 {
118 select {
119 case ev := <-rtClient.Events:
120 if orig, ok := want[ev.ID]; ok {
121 // Byte-level comparison via JSON.
122 origJSON, _ := json.Marshal(orig)
123 recvJSON, _ := json.Marshal(ev)
124 if string(origJSON) != string(recvJSON) {
125 t.Errorf("DIVERGENCE on %s...\norig: %s\nrecv: %s",
126 ev.ID[:16], origJSON, recvJSON)
127 } else {
128 t.Logf("matched: %s...", ev.ID[:16])
129 matched++
130 }
131 delete(want, ev.ID)
132 }
133 case <-deadline:
134 goto done
135 }
136 }
137
138 done:
139 t.Logf("round-trip: %d/%d matched, %d not returned", matched, len(published), len(want))
140 if matched == len(published) {
141 t.Log("ZERO DIVERGENCE — round-trip coherent")
142 } else if matched > 0 {
143 t.Log("partial round-trip — some events matched")
144 } else {
145 t.Error("no events returned — round-trip failed")
146 }
147 }
148