diff --git a/go.mod b/go.mod index d7ebf16..61a84c2 100644 --- a/go.mod +++ b/go.mod @@ -49,6 +49,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // 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/stretchr/objx v0.5.2 // indirect github.com/templexxx/cpu v0.1.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.62.0 // indirect diff --git a/go.sum b/go.sum index bd20f82..f219181 100644 --- a/go.sum +++ b/go.sum @@ -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/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.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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/socketapi/handleEvent.go b/socketapi/handleEvent.go index 2a8d578..87a7efb 100644 --- a/socketapi/handleEvent.go +++ b/socketapi/handleEvent.go @@ -232,7 +232,7 @@ func (a *A) ProcessDelete(c context.T, target *event.T, env *eventenvelope.Submi } 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) { return } diff --git a/socketapi/handleEvent_test.go b/socketapi/handleEvent_test.go new file mode 100644 index 0000000..5092bdf --- /dev/null +++ b/socketapi/handleEvent_test.go @@ -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)) +} diff --git a/socketapi/handleReq_test.go b/socketapi/handleReq_test.go new file mode 100644 index 0000000..0a5300f --- /dev/null +++ b/socketapi/handleReq_test.go @@ -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) + } +} diff --git a/socketapi/publisher.go b/socketapi/publisher.go index 4329e1d..2a00e7c 100644 --- a/socketapi/publisher.go +++ b/socketapi/publisher.go @@ -71,6 +71,7 @@ func (p *S) Receive(msg typer.T) { p.Mx.Lock() if subs, ok := p.Map[m.Listener]; !ok { subs = make(map[string]*filters.T) + subs[m.Id] = m.Filters p.Map[m.Listener] = subs } else { subs[m.Id] = m.Filters diff --git a/socketapi/socketapi_test.go b/socketapi/socketapi_test.go new file mode 100644 index 0000000..c195b78 --- /dev/null +++ b/socketapi/socketapi_test.go @@ -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) +}