Wrote roots-ws golang implementation.

This commit is contained in:
Jay
2025-11-02 14:31:46 -05:00
parent 53c42911b4
commit 82e58a193d
13 changed files with 1972 additions and 0 deletions

121
envelope/enclose.go Normal file
View File

@@ -0,0 +1,121 @@
package envelope
import (
"bytes"
"strconv"
)
// EncloseEvent creates an EVENT envelope for publishing events.
// It wraps the provided event JSON in the format ["EVENT", event].
func EncloseEvent(event []byte) Envelope {
var buf bytes.Buffer
buf.WriteString(`["EVENT",`)
buf.Write(event)
buf.WriteByte(']')
return buf.Bytes()
}
// EncloseOK creates an OK envelope acknowledging receipt of an event.
// Format: ["OK", eventID, status, message]
func EncloseOK(eventID string, status bool, message string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["OK","`)
buf.WriteString(eventID)
buf.WriteString(`",`)
buf.WriteString(strconv.FormatBool(status))
buf.WriteString(`,"`)
buf.WriteString(message)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseReq creates a REQ envelope for subscription requests.
// Format: ["REQ", subID, filter1, filter2, ...]
func EncloseReq(subID string, filters [][]byte) Envelope {
var buf bytes.Buffer
buf.WriteString(`["REQ","`)
buf.WriteString(subID)
buf.WriteString(`"`)
for _, filter := range filters {
buf.WriteString(`,`)
buf.Write(filter)
}
buf.WriteByte(']')
return buf.Bytes()
}
// EncloseSubscriptionEvent creates an EVENT envelope for delivering subscription events.
// Format: ["EVENT", subID, event]
func EncloseSubscriptionEvent(subID string, event []byte) Envelope {
var buf bytes.Buffer
buf.WriteString(`["EVENT","`)
buf.WriteString(subID)
buf.WriteString(`",`)
buf.Write(event)
buf.WriteByte(']')
return buf.Bytes()
}
// EncloseEOSE creates an EOSE (End of Stored Events) envelope.
// Format: ["EOSE", subID]
func EncloseEOSE(subID string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["EOSE","`)
buf.WriteString(subID)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseClose creates a CLOSE envelope for ending a subscription.
// Format: ["CLOSE", subID]
func EncloseClose(subID string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["CLOSE","`)
buf.WriteString(subID)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseClosed creates a CLOSED envelope for indicating a terminated subscription.
// Format: ["CLOSED", subID, message]
func EncloseClosed(subID string, message string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["CLOSED","`)
buf.WriteString(subID)
buf.WriteString(`","`)
buf.WriteString(message)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseNotice creates a NOTICE envelope for responder messages.
// Format: ["NOTICE", message]
func EncloseNotice(message string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["NOTICE","`)
buf.WriteString(message)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseAuthChallenge creates an AUTH challenge envelope.
// Format: ["AUTH", challenge]
func EncloseAuthChallenge(challenge string) Envelope {
var buf bytes.Buffer
buf.WriteString(`["AUTH","`)
buf.WriteString(challenge)
buf.WriteString(`"]`)
return buf.Bytes()
}
// EncloseAuthResponse creates an AUTH response envelope.
// Format: ["AUTH", event]
func EncloseAuthResponse(event []byte) Envelope {
var buf bytes.Buffer
buf.WriteString(`["AUTH",`)
buf.Write(event)
buf.WriteByte(']')
return buf.Bytes()
}

289
envelope/enclose_test.go Normal file
View File

@@ -0,0 +1,289 @@
package envelope
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestEncloseEvent(t *testing.T) {
cases := []struct {
name string
event []byte
want Envelope
}{
{
name: "empty event",
event: []byte("{}"),
want: []byte(`["EVENT",{}]`),
},
{
name: "invalid json",
event: []byte("in[valid,]"),
want: []byte(`["EVENT",in[valid,]]`),
},
{
name: "populated event",
event: []byte(`{"id":"abc123","kind":1,"sig":"abc123"}`),
want: []byte(`["EVENT",{"id":"abc123","kind":1,"sig":"abc123"}]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseEvent(tc.event)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseOK(t *testing.T) {
cases := []struct {
name string
eventID string
status bool
message string
want Envelope
}{
{
name: "successful event",
eventID: "abc123",
status: true,
message: "Event accepted",
want: []byte(`["OK","abc123",true,"Event accepted"]`),
},
{
name: "rejected event",
eventID: "xyz789",
status: false,
message: "Invalid signature",
want: []byte(`["OK","xyz789",false,"Invalid signature"]`),
},
{
name: "empty message",
eventID: "def456",
status: true,
message: "",
want: []byte(`["OK","def456",true,""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseOK(tc.eventID, tc.status, tc.message)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseReq(t *testing.T) {
cases := []struct {
name string
subID string
filters [][]byte
want Envelope
}{
{
name: "single filter",
subID: "sub1",
filters: [][]byte{[]byte(`{"kinds":[1],"limit":10}`)},
want: []byte(`["REQ","sub1",{"kinds":[1],"limit":10}]`),
},
{
name: "multiple filters",
subID: "sub2",
filters: [][]byte{[]byte(`{"kinds":[1]}`), []byte(`{"authors":["abc"]}`)},
want: []byte(`["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]`),
},
{
name: "no filters",
subID: "sub3",
filters: [][]byte{},
want: []byte(`["REQ","sub3"]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseReq(tc.subID, tc.filters)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseSubscriptionEvent(t *testing.T) {
cases := []struct {
name string
subID string
event []byte
want Envelope
}{
{
name: "basic event",
subID: "sub1",
event: []byte(`{"id":"abc123","kind":1}`),
want: []byte(`["EVENT","sub1",{"id":"abc123","kind":1}]`),
},
{
name: "empty event",
subID: "sub2",
event: []byte(`{}`),
want: []byte(`["EVENT","sub2",{}]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseSubscriptionEvent(tc.subID, tc.event)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseEOSE(t *testing.T) {
cases := []struct {
name string
subID string
want Envelope
}{
{
name: "valid subscription ID",
subID: "sub1",
want: []byte(`["EOSE","sub1"]`),
},
{
name: "empty subscription ID",
subID: "",
want: []byte(`["EOSE",""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseEOSE(tc.subID)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseClose(t *testing.T) {
cases := []struct {
name string
subID string
want Envelope
}{
{
name: "valid subscription ID",
subID: "sub1",
want: []byte(`["CLOSE","sub1"]`),
},
{
name: "empty subscription ID",
subID: "",
want: []byte(`["CLOSE",""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseClose(tc.subID)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseClosed(t *testing.T) {
cases := []struct {
name string
subID string
message string
want Envelope
}{
{
name: "with message",
subID: "sub1",
message: "Subscription complete",
want: []byte(`["CLOSED","sub1","Subscription complete"]`),
},
{
name: "empty message",
subID: "sub2",
message: "",
want: []byte(`["CLOSED","sub2",""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseClosed(tc.subID, tc.message)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseNotice(t *testing.T) {
cases := []struct {
name string
message string
want Envelope
}{
{
name: "valid message",
message: "This is a notice",
want: []byte(`["NOTICE","This is a notice"]`),
},
{
name: "empty message",
message: "",
want: []byte(`["NOTICE",""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseNotice(tc.message)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseAuthChallenge(t *testing.T) {
cases := []struct {
name string
challenge string
want Envelope
}{
{
name: "valid challenge",
challenge: "random-challenge-string",
want: []byte(`["AUTH","random-challenge-string"]`),
},
{
name: "empty challenge",
challenge: "",
want: []byte(`["AUTH",""]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseAuthChallenge(tc.challenge)
assert.Equal(t, tc.want, got)
})
}
}
func TestEncloseAuthResponse(t *testing.T) {
cases := []struct {
name string
event []byte
want Envelope
}{
{
name: "valid event",
event: []byte(`{"id":"abc123","kind":22242}`),
want: []byte(`["AUTH",{"id":"abc123","kind":22242}]`),
},
{
name: "empty event",
event: []byte(`{}`),
want: []byte(`["AUTH",{}]`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := EncloseAuthResponse(tc.event)
assert.Equal(t, tc.want, got)
})
}
}

55
envelope/envelope.go Normal file
View File

@@ -0,0 +1,55 @@
// Package envelope provides types and functions for working with Nostr protocol
// websocket messages. It defines the Envelope type representing a Nostr message
// and offers utilities for creating, parsing, and validating standardized message
// formats.
package envelope
import (
"encoding/json"
"fmt"
"git.wisehodl.dev/jay/go-roots-ws/errors"
)
// Envelope represents a Nostr websocket message.
type Envelope []byte
// GetLabel extracts the message label from an envelope.
// Returns the label as a string or an error if the envelope is malformed.
func GetLabel(env Envelope) (string, error) {
var arr []json.RawMessage
if err := json.Unmarshal(env, &arr); err != nil {
return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if len(arr) < 1 {
return "", fmt.Errorf("%w: empty envelope", errors.InvalidEnvelope)
}
var label string
if err := json.Unmarshal(arr[0], &label); err != nil {
return "", fmt.Errorf("%w: label is not a string", errors.WrongFieldType)
}
return label, nil
}
// GetStandardLabels returns a set of standard Nostr websocket message labels
func GetStandardLabels() map[string]struct{} {
return map[string]struct{}{
"EVENT": {},
"REQ": {},
"CLOSE": {},
"CLOSED": {},
"EOSE": {},
"NOTICE": {},
"OK": {},
"AUTH": {},
}
}
// IsStandardLabel checks if the given label is a standard Nostr websocket message label
func IsStandardLabel(label string) bool {
labels := GetStandardLabels()
_, ok := labels[label]
return ok
}

110
envelope/envelope_test.go Normal file
View File

@@ -0,0 +1,110 @@
package envelope
import (
"git.wisehodl.dev/jay/go-roots-ws/errors"
"github.com/stretchr/testify/assert"
"testing"
)
func TestGetLabel(t *testing.T) {
cases := []struct {
name string
env Envelope
wantLabel string
wantErr error
wantErrText string
}{
{
name: "valid envelope with EVENT label",
env: []byte(`["EVENT",{"id":"abc123"}]`),
wantLabel: "EVENT",
},
{
name: "valid envelope with custom label",
env: []byte(`["TEST",{"data":"value"}]`),
wantLabel: "TEST",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "empty array",
env: []byte(`[]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "empty envelope",
},
{
name: "label not a string",
env: []byte(`[123,{"id":"abc123"}]`),
wantErr: errors.WrongFieldType,
wantErrText: "label is not a string",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := GetLabel(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantLabel, got)
})
}
}
func TestGetStandardLabels(t *testing.T) {
expected := map[string]struct{}{
"EVENT": {},
"REQ": {},
"CLOSE": {},
"CLOSED": {},
"EOSE": {},
"NOTICE": {},
"OK": {},
"AUTH": {},
}
labels := GetStandardLabels()
// Check that we have the exact same number of labels
assert.Equal(t, len(expected), len(labels))
// Check that all expected labels are present
for label := range expected {
_, exists := labels[label]
assert.True(t, exists, "Expected standard label %s not found", label)
}
}
func TestIsStandardLabel(t *testing.T) {
standardCases := []string{
"EVENT", "REQ", "CLOSE", "CLOSED", "EOSE", "NOTICE", "OK", "AUTH",
}
nonStandardCases := []string{
"TEST", "CUSTOM", "event", "REQ1", "",
}
for _, label := range standardCases {
t.Run(label, func(t *testing.T) {
assert.True(t, IsStandardLabel(label), "Label %s should be standard", label)
})
}
for _, label := range nonStandardCases {
t.Run(label, func(t *testing.T) {
assert.False(t, IsStandardLabel(label), "Label %s should not be standard", label)
})
}
}

324
envelope/find.go Normal file
View File

@@ -0,0 +1,324 @@
package envelope
import (
"encoding/json"
"fmt"
"git.wisehodl.dev/jay/go-roots-ws/errors"
)
// CheckArrayLength is a helper function that ensures the JSON array has at
// least the minimum length required
func CheckArrayLength(arr []json.RawMessage, minLen int) error {
if len(arr) < minLen {
return fmt.Errorf("%w: expected %d elements, got %d", errors.InvalidEnvelope, minLen, len(arr))
}
return nil
}
// CheckLabel is a helper function that verifies that the envelope label
// matches the expected one
func CheckLabel(got, want string) error {
if got != want {
return fmt.Errorf("%w: expected %s, got %s", errors.WrongEnvelopeLabel, want, got)
}
return nil
}
// ParseElement is a helper function that unmarshals an array element into the
// provided value
func ParseElement(element json.RawMessage, value interface{}, position string) error {
if err := json.Unmarshal(element, value); err != nil {
return fmt.Errorf("%w: %s is not the expected type", errors.WrongFieldType, position)
}
return nil
}
// FindEvent extracts an event from an EVENT envelope with no subscription ID.
// Expected Format: ["EVENT", event]
func FindEvent(env Envelope) ([]byte, error) {
var arr []json.RawMessage
if err := json.Unmarshal(env, &arr); err != nil {
return nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err := CheckArrayLength(arr, 2); err != nil {
return nil, err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return nil, err
}
if err := CheckLabel(label, "EVENT"); err != nil {
return nil, err
}
return arr[1], nil
}
// FindSubscriptionEvent extracts an event and subscription ID from an EVENT envelope.
// Expected Format: ["EVENT", subID, event]
func FindSubscriptionEvent(env Envelope) (subID string, event []byte, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 3); err != nil {
return "", nil, err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", nil, err
}
if err = CheckLabel(label, "EVENT"); err != nil {
return "", nil, err
}
if err = ParseElement(arr[1], &subID, "subscription ID"); err != nil {
return "", nil, err
}
return subID, arr[2], nil
}
// FindOK extracts eventID, status, and message from an OK envelope.
// Expected Format: ["OK", eventID, status, message]
func FindOK(env Envelope) (eventID string, status bool, message string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", false, "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 4); err != nil {
return "", false, "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", false, "", err
}
if err = CheckLabel(label, "OK"); err != nil {
return "", false, "", err
}
if err = ParseElement(arr[1], &eventID, "event ID"); err != nil {
return "", false, "", err
}
if err = ParseElement(arr[2], &status, "status"); err != nil {
return "", false, "", err
}
if err = ParseElement(arr[3], &message, "message"); err != nil {
return "", false, "", err
}
return eventID, status, message, nil
}
// FindReq extracts subscription ID and filters from a REQ envelope.
// Expected Format: ["REQ", subID, filter1, filter2, ...]
func FindReq(env Envelope) (subID string, filters [][]byte, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return "", nil, err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", nil, err
}
if err = CheckLabel(label, "REQ"); err != nil {
return "", nil, err
}
if err = ParseElement(arr[1], &subID, "subscription ID"); err != nil {
return "", nil, err
}
filters = make([][]byte, 0, len(arr)-2)
for i := 2; i < len(arr); i++ {
filters = append(filters, arr[i])
}
return subID, filters, nil
}
// FindEOSE extracts subscription ID from an EOSE envelope.
// Expected Format: ["EOSE", subID]
func FindEOSE(env Envelope) (subID string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", err
}
if err = CheckLabel(label, "EOSE"); err != nil {
return "", err
}
if err = ParseElement(arr[1], &subID, "subscription ID"); err != nil {
return "", err
}
return subID, nil
}
// FindClose extracts subscription ID from a CLOSE envelope.
// Expected Format: ["CLOSE", subID]
func FindClose(env Envelope) (subID string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", err
}
if err = CheckLabel(label, "CLOSE"); err != nil {
return "", err
}
if err = ParseElement(arr[1], &subID, "subscription ID"); err != nil {
return "", err
}
return subID, nil
}
// FindClosed extracts subscription ID and message from a CLOSED envelope.
// Expected Format: ["CLOSED", subID, message]
func FindClosed(env Envelope) (subID string, message string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 3); err != nil {
return "", "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", "", err
}
if err = CheckLabel(label, "CLOSED"); err != nil {
return "", "", err
}
if err = ParseElement(arr[1], &subID, "subscription ID"); err != nil {
return "", "", err
}
if err = ParseElement(arr[2], &message, "message"); err != nil {
return "", "", err
}
return subID, message, nil
}
// FindNotice extracts message from a NOTICE envelope.
// Expected Format: ["NOTICE", message]
func FindNotice(env Envelope) (message string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", err
}
if err = CheckLabel(label, "NOTICE"); err != nil {
return "", err
}
if err = ParseElement(arr[1], &message, "message"); err != nil {
return "", err
}
return message, nil
}
// FindAuthChallenge extracts challenge from an AUTH challenge envelope.
// Expected Format: ["AUTH", challenge]
func FindAuthChallenge(env Envelope) (challenge string, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return "", err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return "", err
}
if err = CheckLabel(label, "AUTH"); err != nil {
return "", err
}
// Check if the second element is a string (AUTH challenge)
if err = ParseElement(arr[1], &challenge, "challenge"); err != nil {
return "", err
}
return challenge, nil
}
// FindAuthResponse extracts event from an AUTH response envelope.
// Expected Format: ["AUTH", event]
func FindAuthResponse(env Envelope) (event []byte, err error) {
var arr []json.RawMessage
if err = json.Unmarshal(env, &arr); err != nil {
return nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err)
}
if err = CheckArrayLength(arr, 2); err != nil {
return nil, err
}
var label string
if err := ParseElement(arr[0], &label, "envelope label"); err != nil {
return nil, err
}
if err = CheckLabel(label, "AUTH"); err != nil {
return nil, err
}
return arr[1], nil
}

630
envelope/find_test.go Normal file
View File

@@ -0,0 +1,630 @@
package envelope
import (
"testing"
"git.wisehodl.dev/jay/go-roots-ws/errors"
"github.com/stretchr/testify/assert"
)
func TestFindEvent(t *testing.T) {
cases := []struct {
name string
env Envelope
wantEvent []byte
wantErr error
wantErrText string
}{
{
name: "valid event",
env: []byte(`["EVENT",{"id":"abc123","kind":1}]`),
wantEvent: []byte(`{"id":"abc123","kind":1}`),
},
{
name: "wrong label",
env: []byte(`["REQ",{"id":"abc123","kind":1}]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected EVENT, got REQ",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["EVENT"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["EVENT",{"id":"abc123"},"extra"]`),
wantEvent: []byte(`{"id":"abc123"}`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := FindEvent(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantEvent, got)
})
}
}
func TestFindSubscriptionEvent(t *testing.T) {
cases := []struct {
name string
env Envelope
wantSubID string
wantEvent []byte
wantErr error
wantErrText string
}{
{
name: "valid event",
env: []byte(`["EVENT","sub1",{"id":"abc123","kind":1}]`),
wantSubID: "sub1",
wantEvent: []byte(`{"id":"abc123","kind":1}`),
},
{
name: "wrong label",
env: []byte(`["REQ","sub1",{"id":"abc123","kind":1}]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected EVENT, got REQ",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["EVENT","sub1"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 3 elements, got 2",
},
{
name: "extraneous elements",
env: []byte(`["EVENT","sub1",{"id":"abc123"},"extra"]`),
wantSubID: "sub1",
wantEvent: []byte(`{"id":"abc123"}`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSubID, gotEvent, err := FindSubscriptionEvent(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantSubID, gotSubID)
assert.Equal(t, tc.wantEvent, gotEvent)
})
}
}
func TestFindOK(t *testing.T) {
cases := []struct {
name string
env Envelope
wantEventID string
wantStatus bool
wantMessage string
wantErr error
wantErrText string
}{
{
name: "accepted event",
env: []byte(`["OK","abc123",true,"Event accepted"]`),
wantEventID: "abc123",
wantStatus: true,
wantMessage: "Event accepted",
},
{
name: "rejected event",
env: []byte(`["OK","xyz789",false,"Invalid signature"]`),
wantEventID: "xyz789",
wantStatus: false,
wantMessage: "Invalid signature",
},
{
name: "wrong status type",
env: []byte(`["OK","abc123","ok","Event accepted"]`),
wantErr: errors.WrongFieldType,
wantErrText: "status is not the expected type",
},
{
name: "wrong label",
env: []byte(`["EVENT","abc123",true,"Event accepted"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected OK, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["OK","abc123",true]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 4 elements, got 3",
},
{
name: "extraneous elements",
env: []byte(`["OK","abc123",true,"Event accepted","extra"]`),
wantEventID: "abc123",
wantStatus: true,
wantMessage: "Event accepted",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotEventID, gotStatus, gotMessage, err := FindOK(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantEventID, gotEventID)
assert.Equal(t, tc.wantStatus, gotStatus)
assert.Equal(t, tc.wantMessage, gotMessage)
})
}
}
func TestFindReq(t *testing.T) {
cases := []struct {
name string
env Envelope
wantSubID string
wantFilters [][]byte
wantErr error
wantErrText string
}{
{
name: "single filter",
env: []byte(`["REQ","sub1",{"kinds":[1],"limit":10}]`),
wantSubID: "sub1",
wantFilters: [][]byte{[]byte(`{"kinds":[1],"limit":10}`)},
},
{
name: "multiple filters",
env: []byte(`["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]`),
wantSubID: "sub2",
wantFilters: [][]byte{
[]byte(`{"kinds":[1]}`),
[]byte(`{"authors":["abc"]}`),
},
},
{
name: "no filters",
env: []byte(`["REQ","sub3"]`),
wantSubID: "sub3",
wantFilters: [][]byte{},
},
{
name: "wrong label",
env: []byte(`["EVENT","sub1",{"kinds":[1],"limit":10}]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected REQ, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["REQ"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSubID, gotFilters, err := FindReq(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantSubID, gotSubID)
assert.Equal(t, tc.wantFilters, gotFilters)
})
}
}
func TestFindEOSE(t *testing.T) {
cases := []struct {
name string
env Envelope
wantSubID string
wantErr error
wantErrText string
}{
{
name: "valid EOSE",
env: []byte(`["EOSE","sub1"]`),
wantSubID: "sub1",
},
{
name: "wrong label",
env: []byte(`["EVENT","sub1"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected EOSE, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["EOSE"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["EOSE","sub1","extra"]`),
wantSubID: "sub1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSubID, err := FindEOSE(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantSubID, gotSubID)
})
}
}
func TestFindClose(t *testing.T) {
cases := []struct {
name string
env Envelope
wantSubID string
wantErr error
wantErrText string
}{
{
name: "valid CLOSE",
env: []byte(`["CLOSE","sub1"]`),
wantSubID: "sub1",
},
{
name: "wrong label",
env: []byte(`["EVENT","sub1"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected CLOSE, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["CLOSE"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["CLOSE","sub1","extra"]`),
wantSubID: "sub1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSubID, err := FindClose(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantSubID, gotSubID)
})
}
}
func TestFindClosed(t *testing.T) {
cases := []struct {
name string
env Envelope
wantSubID string
wantMessage string
wantErr error
wantErrText string
}{
{
name: "valid CLOSED",
env: []byte(`["CLOSED","sub1","Subscription complete"]`),
wantSubID: "sub1",
wantMessage: "Subscription complete",
},
{
name: "wrong label",
env: []byte(`["EVENT","sub1","Subscription complete"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected CLOSED, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["CLOSED","sub1"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 3 elements, got 2",
},
{
name: "extraneous elements",
env: []byte(`["CLOSED","sub1","Subscription complete","extra"]`),
wantSubID: "sub1",
wantMessage: "Subscription complete",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotSubID, gotMessage, err := FindClosed(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantSubID, gotSubID)
assert.Equal(t, tc.wantMessage, gotMessage)
})
}
}
func TestFindNotice(t *testing.T) {
cases := []struct {
name string
env Envelope
wantMessage string
wantErr error
wantErrText string
}{
{
name: "valid NOTICE",
env: []byte(`["NOTICE","This is a notice"]`),
wantMessage: "This is a notice",
},
{
name: "wrong label",
env: []byte(`["EVENT","This is a notice"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected NOTICE, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["NOTICE"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["NOTICE","This is a notice","extra"]`),
wantMessage: "This is a notice",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotMessage, err := FindNotice(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantMessage, gotMessage)
})
}
}
func TestFindAuthChallenge(t *testing.T) {
cases := []struct {
name string
env Envelope
wantChallenge string
wantErr error
wantErrText string
}{
{
name: "valid AUTH challenge",
env: []byte(`["AUTH","random-challenge-string"]`),
wantChallenge: "random-challenge-string",
},
{
name: "wrong label",
env: []byte(`["EVENT","random-challenge-string"]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected AUTH, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["AUTH"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["AUTH","random-challenge-string","extra"]`),
wantChallenge: "random-challenge-string",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotChallenge, err := FindAuthChallenge(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantChallenge, gotChallenge)
})
}
}
func TestFindAuthResponse(t *testing.T) {
cases := []struct {
name string
env Envelope
wantEvent []byte
wantErr error
wantErrText string
}{
{
name: "valid AUTH response",
env: []byte(`["AUTH",{"id":"abc123","kind":22242}]`),
wantEvent: []byte(`{"id":"abc123","kind":22242}`),
},
{
name: "wrong label",
env: []byte(`["EVENT",{"id":"abc123","kind":22242}]`),
wantErr: errors.WrongEnvelopeLabel,
wantErrText: "expected AUTH, got EVENT",
},
{
name: "invalid json",
env: []byte(`invalid`),
wantErr: errors.InvalidJSON,
},
{
name: "missing elements",
env: []byte(`["AUTH"]`),
wantErr: errors.InvalidEnvelope,
wantErrText: "expected 2 elements, got 1",
},
{
name: "extraneous elements",
env: []byte(`["AUTH",{"id":"abc123","kind":22242},"extra"]`),
wantEvent: []byte(`{"id":"abc123","kind":22242}`),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
gotEvent, err := FindAuthResponse(tc.env)
if tc.wantErr != nil || tc.wantErrText != "" {
if tc.wantErr != nil {
assert.ErrorIs(t, err, tc.wantErr)
}
if tc.wantErrText != "" {
assert.ErrorContains(t, err, tc.wantErrText)
}
return
}
assert.NoError(t, err)
assert.Equal(t, tc.wantEvent, gotEvent)
})
}
}