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