//go:build !js package ws import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/websocket" "lol.mleku.dev/chk" "next.orly.dev/pkg/crypto/p256k" "next.orly.dev/pkg/encoders/event" "next.orly.dev/pkg/encoders/filter" "next.orly.dev/pkg/encoders/hex" "next.orly.dev/pkg/encoders/kind" "next.orly.dev/pkg/encoders/tag" "next.orly.dev/pkg/utils" "next.orly.dev/pkg/utils/normalize" ) func TestPublish(t *testing.T) { // test note to be sent over websocket priv, pub := makeKeyPair(t) textNote := &event.E{ Kind: kind.TextNote.K, Content: []byte("hello"), CreatedAt: 1672068534, // random fixed timestamp Tags: tag.NewS(tag.NewFromAny("foo", "bar")), Pubkey: pub, } sign := &p256k.Signer{} var err error if err = sign.InitSec(priv); chk.E(err) { } err = textNote.Sign(sign) assert.NoError(t, 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 err := websocket.JSON.Receive(conn, &raw) assert.NoError(t, err) event := parseEventMessage(t, raw) assert.True( t, utils.FastEqual(event.Serialize(), textNote.Serialize()), ) // send back an ok nip-20 command result res := []any{"OK", hex.Enc(textNote.ID), true, ""} err = websocket.JSON.Send(conn, res) assert.NoError(t, err) }, ) defer ws.Close() // connect a client and send the text note rl := mustRelayConnect(t, ws.URL) err = rl.Publish(context.Background(), textNote) assert.NoError(t, err) assert.True(t, published, "fake relay server saw no event") } // func TestPublishBlocked(t *testing.T) { // // test note to be sent over websocket // textNote := &event.E{ // Kind: kind.TextNote, Content: []byte("hello"), // CreatedAt: timestamp.Now(), // } // textNote.ID = textNote.GetIDBytes() // // // fake relay server // ws := newWebsocketServer( // func(conn *websocket.Conn) { // // discard received message; not interested // var raw []json.RawMessage // err := websocket.JSON.Receive(conn, &raw) // assert.NoError(t, err) // // // send back a not ok nip-20 command result // res := []any{"OK", textNote.IdString(), false, "blocked"} // websocket.JSON.Send(conn, res) // }, // ) // defer ws.Close() // // // connect a client and send a text note // rl := mustRelayConnect(t, ws.URL) // err := rl.Publish(context.Background(), textNote) // assert.Error(t, err) // } func TestPublishWriteFailed(t *testing.T) { // test note to be sent over websocket textNote := &event.E{ Kind: kind.TextNote.K, Content: []byte("hello"), CreatedAt: time.Now().Unix(), } textNote.ID = textNote.GetIDBytes() // 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(t, ws.URL) // Force brief period of time so that publish always fails on closed socket. time.Sleep(1 * time.Millisecond) err := rl.Publish(context.Background(), textNote) assert.Error(t, 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) assert.NoError(t, err) defer r.Close() mu.Lock() defer mu.Unlock() assert.True(t, connected, "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) assert.ErrorIs(t, err, context.Canceled) } 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 := NewRelay( context.Background(), string(normalize.URL(ws.URL)), WithRequestHeader(http.Header{"origin": {"https://example.com"}}), ) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() err := r.Connect(ctx) assert.NoError(t, 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) (sec, pub []byte) { t.Helper() sign := &p256k.Signer{} var err error if err = sign.Generate(); chk.E(err) { return } sec = sign.Sec() pub = sign.Pub() assert.NoError(t, err) return } func mustRelayConnect(t *testing.T, url string) *Client { t.Helper() rl, err := RelayConnect(context.Background(), url) require.NoError(t, err) return rl } func parseEventMessage(t *testing.T, raw []json.RawMessage) *event.E { t.Helper() assert.Condition( t, func() (success bool) { return len(raw) >= 2 }, ) var typ string err := json.Unmarshal(raw[0], &typ) assert.NoError(t, err) assert.Equal(t, "EVENT", typ) event := &event.E{} _, err = event.Unmarshal(raw[1]) require.NoError(t, err) return event } func parseSubscriptionMessage( t *testing.T, raw []json.RawMessage, ) (subid string, ff *filter.S) { t.Helper() assert.Greater(t, len(raw), 3) var typ string err := json.Unmarshal(raw[0], &typ) assert.NoError(t, err) assert.Equal(t, "REQ", typ) var id string err = json.Unmarshal(raw[1], &id) assert.NoError(t, err) ff = &filter.S{} for _, b := range raw[2:] { var f *filter.F err = json.Unmarshal(b, &f) assert.NoError(t, err) *ff = append(*ff, f) } return id, ff }