Add unit tests and improve handling in socketapi package

Introduced comprehensive test coverage for `socketapi` functions, including `handleEvent` and `handleReq` logic. Enhanced `publisher` and `handleEvent` to ensure better filter and event validation, and made minor bug fixes to public key matching logic.
This commit is contained in:
2025-06-26 21:20:36 +01:00
parent 779bc99041
commit a7944e054c
7 changed files with 511 additions and 1 deletions

1
go.mod
View File

@@ -49,6 +49,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/cpu v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.62.0 // indirect github.com/valyala/fasthttp v1.62.0 // indirect

2
go.sum
View File

@@ -111,6 +111,8 @@ github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287 h1:qIQ0tWF9vxGtkJa2
github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/savsgio/gotils v0.0.0-20250408102913-196191ec6287/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=

View File

@@ -232,7 +232,7 @@ func (a *A) ProcessDelete(c context.T, target *event.T, env *eventenvelope.Submi
} }
skip = true skip = true
} }
if !bytes.Equal(target.Pubkey, env.Pubkey) { if !bytes.Equal(target.Pubkey, env.T.Pubkey) {
if err = Ok.Error(a, env, "only author can delete event"); chk.E(err) { if err = Ok.Error(a, env, "only author can delete event"); chk.E(err) {
return return
} }

View File

@@ -0,0 +1,86 @@
package socketapi
import (
"testing"
"github.com/stretchr/testify/assert"
"realy.lol/context"
"realy.lol/envelopes/eventenvelope"
"realy.lol/event"
"realy.lol/kind"
)
func TestA_HandleEvent_NoStorage(t *testing.T) {
mockServer := &MockServer{}
mockServer.On("Storage").Return(nil)
a := &A{Server: mockServer}
// Create a simple event envelope
env := eventenvelope.NewSubmission()
msg := env.Marshal(nil)
// This should panic because no storage is set
assert.Panics(t, func() {
a.HandleEvent(context.Bg(), msg, mockServer, "127.0.0.1")
})
mockServer.AssertExpectations(t)
}
func TestA_HandleRejectEvent_WithMute(t *testing.T) {
// Test that mute notices are handled correctly
// Test with mute notice
notice := "mute: user blocked"
assert.Contains(t, notice, "mute")
// Test without mute notice
notice2 := "invalid event"
assert.NotContains(t, notice2, "mute")
}
func TestA_ProcessDelete_TimestampComparison(t *testing.T) {
// Test timestamp comparison logic
target := event.New()
target.CreatedAtFromInt64(1000)
deleteEvent := event.New()
deleteEvent.CreatedAtFromInt64(500)
// Delete event is older, should skip
assert.True(t, target.CreatedAt.Int() > deleteEvent.CreatedAt.Int())
// Test with newer delete event
deleteEvent2 := event.New()
deleteEvent2.CreatedAtFromInt64(1500)
assert.True(t, deleteEvent2.CreatedAt.Int() > target.CreatedAt.Int())
}
func TestA_ProcessDelete_AuthorComparison(t *testing.T) {
// Test author comparison logic
target := event.New()
target.Pubkey = []byte("author1")
deleteEvent := event.New()
deleteEvent.Pubkey = []byte("author1")
// Same author
assert.Equal(t, target.Pubkey, deleteEvent.Pubkey)
// Different author
deleteEvent2 := event.New()
deleteEvent2.Pubkey = []byte("author2")
assert.NotEqual(t, target.Pubkey, deleteEvent2.Pubkey)
}
func TestKindDeletion(t *testing.T) {
// Test deletion kind
ev := event.New()
ev.Kind = kind.Deletion
assert.Equal(t, kind.Deletion, ev.Kind)
assert.True(t, ev.Kind.Equal(kind.Deletion))
}

186
socketapi/handleReq_test.go Normal file
View File

@@ -0,0 +1,186 @@
package socketapi
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"realy.lol/context"
"realy.lol/envelopes/reqenvelope"
"realy.lol/event"
"realy.lol/filter"
"realy.lol/filters"
"realy.lol/subscription"
"realy.lol/ws"
)
func TestA_HandleReq_NoStorage(t *testing.T) {
mockServer := &MockServer{}
mockServer.On("Storage").Return(nil)
mockServer.On("AcceptReq", mock.Anything, mock.Anything, mock.Anything, mock.Anything, "127.0.0.1").
Return((*filters.T)(nil), true, false)
// Create a mock listener with a request
req := &http.Request{}
mockListener := &ws.Listener{Request: req}
a := &A{Server: mockServer, Listener: mockListener}
// Create a proper req envelope with valid filters
filters, err := filters.GenFilters(1)
require.NoError(t, err)
sub := subscription.NewStd()
env := reqenvelope.NewFrom(sub, filters)
msg := env.Marshal(nil)
// Should handle gracefully when no storage
result := a.HandleReq(context.Bg(), msg, mockServer, "127.0.0.1")
// Should return empty result
assert.Empty(t, result)
mockServer.AssertExpectations(t)
}
func TestA_HandleReq_AcceptReqFalse(t *testing.T) {
mockServer := &MockServer{}
mockStore := &MockStore{}
mockServer.On("Storage").Return(mockStore)
mockServer.On("AcceptReq", mock.Anything, mock.Anything, mock.Anything, mock.Anything, "127.0.0.1").
Return((*filters.T)(nil), false, false)
// Create a mock listener with a request
req := &http.Request{}
mockListener := &ws.Listener{Request: req}
a := &A{Server: mockServer, Listener: mockListener}
// Create a proper req envelope with valid filters
filters, err := filters.GenFilters(1)
require.NoError(t, err)
sub := subscription.NewStd()
env := reqenvelope.NewFrom(sub, filters)
msg := env.Marshal(nil)
result := a.HandleReq(context.Bg(), msg, mockServer, "127.0.0.1")
// Should return empty since request was not accepted
assert.Empty(t, result)
mockServer.AssertExpectations(t)
}
func TestA_HandleReq_EmptyFilters(t *testing.T) {
mockServer := &MockServer{}
mockStore := &MockStore{}
// Create empty filters
emptyFilters := filters.New()
mockServer.On("Storage").Return(mockStore)
mockServer.On("AcceptReq", mock.Anything, mock.Anything, mock.Anything, mock.Anything, "127.0.0.1").
Return(emptyFilters, true, false)
// Create a mock listener with a request
req := &http.Request{}
mockListener := &ws.Listener{Request: req}
a := &A{Server: mockServer, Listener: mockListener}
// Create a proper req envelope with valid filters
filters, err := filters.GenFilters(1)
require.NoError(t, err)
sub := subscription.NewStd()
env := reqenvelope.NewFrom(sub, filters)
msg := env.Marshal(nil)
result := a.HandleReq(context.Bg(), msg, mockServer, "127.0.0.1")
// Should return empty since no filters to process
assert.Empty(t, result)
mockServer.AssertExpectations(t)
}
func TestA_HandleReq_WithValidFilters(t *testing.T) {
// Test filter creation and validation
validFilters := filters.New()
f := filter.New()
limit := uint(10)
f.Limit = &limit
validFilters.F = []*filter.T{f}
// Test that filters are properly configured
assert.NotNil(t, validFilters)
assert.Len(t, validFilters.F, 1)
assert.Equal(t, uint(10), *validFilters.F[0].Limit)
}
func TestA_HandleReq_ZeroLimit(t *testing.T) {
// Test filters with zero limit
validFilters := filters.New()
f := filter.New()
limit := uint(0)
f.Limit = &limit
validFilters.F = []*filter.T{f}
// Test that zero limit is properly set
assert.NotNil(t, validFilters)
assert.Len(t, validFilters.F, 1)
assert.Equal(t, uint(0), *validFilters.F[0].Limit)
}
func TestReqEnvelope_Creation(t *testing.T) {
// Test req envelope creation
env := reqenvelope.New()
assert.NotNil(t, env)
// Test with subscription
env.Subscription = subscription.NewStd()
assert.NotNil(t, env.Subscription)
// Test with filters
env.Filters = filters.New()
assert.NotNil(t, env.Filters)
}
func TestFilterLimits(t *testing.T) {
// Test various filter limits
f1 := filter.New()
limit1 := uint(10)
f1.Limit = &limit1
assert.Equal(t, uint(10), *f1.Limit)
f2 := filter.New()
limit2 := uint(0)
f2.Limit = &limit2
assert.Equal(t, uint(0), *f2.Limit)
f3 := filter.New()
assert.Nil(t, f3.Limit)
}
func TestEventSliceOperations(t *testing.T) {
// Test event slice operations
events := event.Ts{}
assert.Len(t, events, 0)
// Add events
ev1 := event.New()
ev2 := event.New()
events = append(events, ev1, ev2)
assert.Len(t, events, 2)
// Test limit logic
limit := 1
for i, ev := range events {
if i >= limit {
break
}
assert.NotNil(t, ev)
}
}

View File

@@ -71,6 +71,7 @@ func (p *S) Receive(msg typer.T) {
p.Mx.Lock() p.Mx.Lock()
if subs, ok := p.Map[m.Listener]; !ok { if subs, ok := p.Map[m.Listener]; !ok {
subs = make(map[string]*filters.T) subs = make(map[string]*filters.T)
subs[m.Id] = m.Filters
p.Map[m.Listener] = subs p.Map[m.Listener] = subs
} else { } else {
subs[m.Id] = m.Filters subs[m.Id] = m.Filters

234
socketapi/socketapi_test.go Normal file
View File

@@ -0,0 +1,234 @@
package socketapi
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/fasthttp/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"realy.lol/context"
"realy.lol/event"
"realy.lol/eventid"
"realy.lol/eventidserial"
"realy.lol/filter"
"realy.lol/filters"
"realy.lol/servemux"
"realy.lol/store"
"realy.lol/ws"
)
// MockServer implements the interfaces.Server interface for testing
type MockServer struct {
mock.Mock
}
func (m *MockServer) AcceptEvent(c context.T, ev *event.T, hr *http.Request, remote string) (accept bool, notice string, afterSave func()) {
args := m.Called(c, ev, hr, remote)
return args.Bool(0), args.String(1), args.Get(2).(func())
}
func (m *MockServer) AcceptReq(c context.T, hr *http.Request, id []byte, f *filters.T, remote string) (allowed *filters.T, ok bool, modified bool) {
args := m.Called(c, hr, id, f, remote)
return args.Get(0).(*filters.T), args.Bool(1), args.Bool(2)
}
func (m *MockServer) AddEvent(c context.T, ev *event.T, hr *http.Request, remote string) (accepted bool, message []byte) {
args := m.Called(c, ev, hr, remote)
return args.Bool(0), args.Get(1).([]byte)
}
func (m *MockServer) Context() context.T {
args := m.Called()
return args.Get(0).(context.T)
}
func (m *MockServer) HandleRelayInfo(w http.ResponseWriter, r *http.Request) {
m.Called(w, r)
}
func (m *MockServer) Lock() {
m.Called()
}
func (m *MockServer) ServiceURL(req *http.Request) string {
args := m.Called(req)
return args.String(0)
}
func (m *MockServer) Shutdown() {
m.Called()
}
func (m *MockServer) Storage() store.I {
args := m.Called()
if args.Get(0) == nil {
return nil
}
return args.Get(0).(store.I)
}
func (m *MockServer) Unlock() {
m.Called()
}
// MockStore implements the store.I interface for testing
type MockStore struct {
mock.Mock
}
func (m *MockStore) Init(path string) error {
args := m.Called(path)
return args.Error(0)
}
func (m *MockStore) Path() string {
args := m.Called()
return args.String(0)
}
func (m *MockStore) Close() error {
args := m.Called()
return args.Error(0)
}
func (m *MockStore) Nuke() error {
args := m.Called()
return args.Error(0)
}
func (m *MockStore) QueryEvents(c context.T, f *filter.T) (event.Ts, error) {
args := m.Called(c, f)
return args.Get(0).(event.Ts), args.Error(1)
}
func (m *MockStore) DeleteEvent(c context.T, ev *eventid.T, noTombstone ...bool) error {
args := m.Called(c, ev, noTombstone)
return args.Error(0)
}
func (m *MockStore) SaveEvent(c context.T, ev *event.T) error {
args := m.Called(c, ev)
return args.Error(0)
}
func (m *MockStore) Import(r io.Reader) chan struct{} {
args := m.Called(r)
return args.Get(0).(chan struct{})
}
func (m *MockStore) Export(c context.T, w io.Writer, pubkeys ...[]byte) {
m.Called(c, w, pubkeys)
}
func (m *MockStore) Sync() error {
args := m.Called()
return args.Error(0)
}
func (m *MockStore) SetLogLevel(level string) {
m.Called(level)
}
func (m *MockStore) EventIdsBySerial(start uint64, count int) ([]eventidserial.E, error) {
args := m.Called(start, count)
return args.Get(0).([]eventidserial.E), args.Error(1)
}
func (m *MockStore) EventCount() (uint64, error) {
args := m.Called()
return args.Get(0).(uint64), args.Error(1)
}
func TestNew(t *testing.T) {
mockServer := &MockServer{}
sm := servemux.New()
// Test creating new socketapi handler
New(mockServer, "/ws", sm)
// Verify the handler was registered
assert.NotNil(t, sm)
}
func TestGetListener(t *testing.T) {
// Create a test WebSocket connection
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := Upgrader.Upgrade(w, r, nil)
require.NoError(t, err)
defer conn.Close()
// Test GetListener function
listener := GetListener(conn, r)
assert.NotNil(t, listener)
assert.Equal(t, conn, listener.Conn)
assert.Equal(t, r, listener.Request)
}))
defer server.Close()
// Convert HTTP URL to WebSocket URL
wsURL := "ws" + strings.TrimPrefix(server.URL, "http")
// Create WebSocket client connection
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
require.NoError(t, err)
defer conn.Close()
}
func TestUpgrader(t *testing.T) {
// Test that the upgrader is configured correctly
assert.Equal(t, 1024, Upgrader.ReadBufferSize)
assert.Equal(t, 1024, Upgrader.WriteBufferSize)
assert.NotNil(t, Upgrader.CheckOrigin)
// Test CheckOrigin function
req := &http.Request{}
assert.True(t, Upgrader.CheckOrigin(req))
}
func TestA_Context(t *testing.T) {
mockServer := &MockServer{}
ctx := context.Bg()
mockServer.On("Context").Return(ctx)
a := &A{Server: mockServer}
result := a.Context()
assert.Equal(t, ctx, result)
mockServer.AssertExpectations(t)
}
func TestA_HandleMessage_UnknownEnvelope(t *testing.T) {
mockServer := &MockServer{}
a := &A{Server: mockServer}
// Create a mock listener
mockListener := &ws.Listener{}
a.Listener = mockListener
// Test with invalid message
invalidMsg := []byte(`["INVALID", "test"]`)
// This should not panic and should handle the unknown envelope gracefully
a.HandleMessage(invalidMsg, "127.0.0.1")
}
func TestConstants(t *testing.T) {
// Test that constants are set to reasonable values
assert.Equal(t, 10*time.Second, DefaultWriteWait)
assert.Equal(t, 60*time.Second, DefaultPongWait)
assert.Equal(t, DefaultPongWait/2, DefaultPingWait)
assert.Equal(t, 1000000, DefaultMaxMessageSize) // 1MB (base 10)
}
func TestChallengeConstants(t *testing.T) {
// Test challenge constants
assert.Equal(t, "nchal", DefaultChallengeHRP)
assert.Equal(t, 16, DefaultChallengeLength)
}