mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-19 06:12:44 +01:00
* Fix race condition on status in Relay.Publish method and failure to send A race-condition exists between setting of the (unprotected) status and the callback which sets the status upon receiving an OK. The message is sent which can receive an OK in separate goroutine (setting status) prior to the status being set to 'sent.' The OK can be received prior to the status being set. This fix also sets the status to PublishStatusFailed if the WriteJSON call fails. * Add some NIP-11 extension structures to the RelayInformationDocument struct. Added additional NIP-11 fields for relays that want to provide additional details based on NIP-11 extensions. The retention structure has been left out as it doesn't have a clean schema for kinds (array of kinds, or pairs of kinds?) Specified the fields w/ omitempty so marshaled will be same as original NIP-11 if nothing else is specified. Nested structs defined as pointers so they are omitted if not specified. * Fix TestPublishWriteFailed so that the socket is given a brief amount of time to close prior to publish being called. The test relies on Publish always failing.
251 lines
6.5 KiB
Go
251 lines
6.5 KiB
Go
package nostr
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
func TestPublish(t *testing.T) {
|
|
// test note to be sent over websocket
|
|
priv, pub := makeKeyPair(t)
|
|
textNote := Event{
|
|
Kind: 1,
|
|
Content: "hello",
|
|
CreatedAt: Timestamp(1672068534), // random fixed timestamp
|
|
Tags: Tags{[]string{"foo", "bar"}},
|
|
PubKey: pub,
|
|
}
|
|
if err := textNote.Sign(priv); err != nil {
|
|
t.Fatalf("textNote.Sign: %v", err)
|
|
}
|
|
|
|
// fake relay server
|
|
var mu sync.Mutex // guards published to satisfy go test -race
|
|
var published bool
|
|
ws := newWebsocketServer(func(conn *websocket.Conn) {
|
|
mu.Lock()
|
|
published = true
|
|
mu.Unlock()
|
|
// verify the client sent exactly the textNote
|
|
var raw []json.RawMessage
|
|
if err := websocket.JSON.Receive(conn, &raw); err != nil {
|
|
t.Errorf("websocket.JSON.Receive: %v", err)
|
|
}
|
|
event := parseEventMessage(t, raw)
|
|
if !bytes.Equal(event.Serialize(), textNote.Serialize()) {
|
|
t.Errorf("received event:\n%+v\nwant:\n%+v", event, textNote)
|
|
}
|
|
// send back an ok nip-20 command result
|
|
res := []any{"OK", textNote.ID, true, ""}
|
|
if err := websocket.JSON.Send(conn, res); err != nil {
|
|
t.Errorf("websocket.JSON.Send: %v", err)
|
|
}
|
|
})
|
|
defer ws.Close()
|
|
|
|
// connect a client and send the text note
|
|
rl := mustRelayConnect(ws.URL)
|
|
status, _ := rl.Publish(context.Background(), textNote)
|
|
if status != PublishStatusSucceeded {
|
|
t.Errorf("published status is %d, not %d", status, PublishStatusSucceeded)
|
|
}
|
|
|
|
if !published {
|
|
t.Errorf("fake relay server saw no event")
|
|
}
|
|
}
|
|
|
|
func TestPublishBlocked(t *testing.T) {
|
|
// test note to be sent over websocket
|
|
textNote := Event{Kind: 1, Content: "hello"}
|
|
textNote.ID = textNote.GetID()
|
|
|
|
// fake relay server
|
|
ws := newWebsocketServer(func(conn *websocket.Conn) {
|
|
// discard received message; not interested
|
|
var raw []json.RawMessage
|
|
if err := websocket.JSON.Receive(conn, &raw); err != nil {
|
|
t.Errorf("websocket.JSON.Receive: %v", err)
|
|
}
|
|
// send back a not ok nip-20 command result
|
|
res := []any{"OK", textNote.ID, false, "blocked"}
|
|
websocket.JSON.Send(conn, res)
|
|
})
|
|
defer ws.Close()
|
|
|
|
// connect a client and send a text note
|
|
rl := mustRelayConnect(ws.URL)
|
|
status, _ := rl.Publish(context.Background(), textNote)
|
|
if status != PublishStatusFailed {
|
|
t.Errorf("published status is %d, not %d", status, PublishStatusFailed)
|
|
}
|
|
}
|
|
|
|
func TestPublishWriteFailed(t *testing.T) {
|
|
// test note to be sent over websocket
|
|
textNote := Event{Kind: 1, Content: "hello"}
|
|
textNote.ID = textNote.GetID()
|
|
|
|
// fake relay server
|
|
ws := newWebsocketServer(func(conn *websocket.Conn) {
|
|
// reject receive - force send error
|
|
conn.Close()
|
|
})
|
|
defer ws.Close()
|
|
|
|
// connect a client and send a text note
|
|
rl := mustRelayConnect(ws.URL)
|
|
// Force brief period of time so that publish always fails on closed socket.
|
|
time.Sleep(1 * time.Millisecond)
|
|
status, err := rl.Publish(context.Background(), textNote)
|
|
if status != PublishStatusFailed {
|
|
t.Errorf("published status is %d, not %d, err: %v", status, PublishStatusFailed, err)
|
|
}
|
|
}
|
|
|
|
func TestConnectContext(t *testing.T) {
|
|
// fake relay server
|
|
var mu sync.Mutex // guards connected to satisfy go test -race
|
|
var connected bool
|
|
ws := newWebsocketServer(func(conn *websocket.Conn) {
|
|
mu.Lock()
|
|
connected = true
|
|
mu.Unlock()
|
|
io.ReadAll(conn) // discard all input
|
|
})
|
|
defer ws.Close()
|
|
|
|
// relay client
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
r, err := RelayConnect(ctx, ws.URL)
|
|
if err != nil {
|
|
t.Fatalf("RelayConnectContext: %v", err)
|
|
}
|
|
defer r.Close()
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if !connected {
|
|
t.Error("fake relay server saw no client connect")
|
|
}
|
|
}
|
|
|
|
func TestConnectContextCanceled(t *testing.T) {
|
|
// fake relay server
|
|
ws := newWebsocketServer(discardingHandler)
|
|
defer ws.Close()
|
|
|
|
// relay client
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // make ctx expired
|
|
_, err := RelayConnect(ctx, ws.URL)
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("RelayConnectContext returned %v error; want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestConnectWithOrigin(t *testing.T) {
|
|
// fake relay server
|
|
// default handler requires origin golang.org/x/net/websocket
|
|
ws := httptest.NewServer(websocket.Handler(discardingHandler))
|
|
defer ws.Close()
|
|
|
|
// relay client
|
|
r := &Relay{URL: NormalizeURL(ws.URL), RequestHeader: http.Header{"origin": {"https://example.com"}}}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
err := r.Connect(ctx)
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func discardingHandler(conn *websocket.Conn) {
|
|
io.ReadAll(conn) // discard all input
|
|
}
|
|
|
|
func newWebsocketServer(handler func(*websocket.Conn)) *httptest.Server {
|
|
return httptest.NewServer(&websocket.Server{
|
|
Handshake: anyOriginHandshake,
|
|
Handler: handler,
|
|
})
|
|
}
|
|
|
|
// anyOriginHandshake is an alternative to default in golang.org/x/net/websocket
|
|
// which checks for origin. nostr client sends no origin and it makes no difference
|
|
// for the tests here anyway.
|
|
var anyOriginHandshake = func(conf *websocket.Config, r *http.Request) error {
|
|
return nil
|
|
}
|
|
|
|
func makeKeyPair(t *testing.T) (priv, pub string) {
|
|
t.Helper()
|
|
privkey := GeneratePrivateKey()
|
|
pubkey, err := GetPublicKey(privkey)
|
|
if err != nil {
|
|
t.Fatalf("GetPublicKey(%q): %v", privkey, err)
|
|
}
|
|
return privkey, pubkey
|
|
}
|
|
|
|
func mustRelayConnect(url string) *Relay {
|
|
rl, err := RelayConnect(context.Background(), url)
|
|
if err != nil {
|
|
panic(err.Error())
|
|
}
|
|
return rl
|
|
}
|
|
|
|
func parseEventMessage(t *testing.T, raw []json.RawMessage) Event {
|
|
t.Helper()
|
|
if len(raw) < 2 {
|
|
t.Fatalf("len(raw) = %d; want at least 2", len(raw))
|
|
}
|
|
var typ string
|
|
json.Unmarshal(raw[0], &typ)
|
|
if typ != "EVENT" {
|
|
t.Errorf("typ = %q; want EVENT", typ)
|
|
}
|
|
var event Event
|
|
if err := json.Unmarshal(raw[1], &event); err != nil {
|
|
t.Errorf("json.Unmarshal(`%s`): %v", string(raw[1]), err)
|
|
}
|
|
return event
|
|
}
|
|
|
|
func parseSubscriptionMessage(t *testing.T, raw []json.RawMessage) (subid string, filters []Filter) {
|
|
t.Helper()
|
|
if len(raw) < 3 {
|
|
t.Fatalf("len(raw) = %d; want at least 3", len(raw))
|
|
}
|
|
var typ string
|
|
json.Unmarshal(raw[0], &typ)
|
|
if typ != "REQ" {
|
|
t.Errorf("typ = %q; want REQ", typ)
|
|
}
|
|
var id string
|
|
if err := json.Unmarshal(raw[1], &id); err != nil {
|
|
t.Errorf("json.Unmarshal sub id: %v", err)
|
|
}
|
|
var ff []Filter
|
|
for i, b := range raw[2:] {
|
|
var f Filter
|
|
if err := json.Unmarshal(b, &f); err != nil {
|
|
t.Errorf("json.Unmarshal filter %d: %v", i, err)
|
|
}
|
|
ff = append(ff, f)
|
|
}
|
|
return id, ff
|
|
}
|