Browse Source

refactor and test Chat

Mike 2 years ago
parent
commit
48889c8116
12 changed files with 500 additions and 361 deletions
  1. 2 1
      .mockery.yaml
  2. 9 10
      cmd/main.go
  3. 43 0
      oscar/snacs.go
  4. 91 0
      server/MockChatHandler.go
  5. 8 20
      server/MockOServiceHandler.go
  6. 49 108
      server/chat.go
  7. 155 64
      server/chat_test.go
  8. 2 7
      server/icbm.go
  9. 36 19
      server/icbm_test.go
  10. 4 13
      server/oservice.go
  11. 98 118
      server/oservice_test.go
  12. 3 1
      server/protocol.go

+ 2 - 1
.mockery.yaml

@@ -13,4 +13,5 @@ packages:
       LocateHandler:
       OServiceHandler:
       FeedbagHandler:
-      ChatNavHandler:
+      ChatNavHandler:
+      ChatHandler:

+ 9 - 10
cmd/main.go

@@ -125,13 +125,13 @@ func handleBOSConnection(cfg server.Config, sm *server.InMemorySessionManager, f
 		conn.Close()
 	}()
 
-	onClientReady := func(sess *server.Session, sm server.SessionManager) ([]server.XMessage, error) {
+	onClientReady := func(sess *server.Session, sm server.SessionManager) error {
 		if err := server.NotifyArrival(sess, sm, fm); err != nil {
-			return []server.XMessage{}, err
+			return err
 		}
 		buddies, err := fm.Buddies(sess.ScreenName)
 		if err != nil {
-			return []server.XMessage{}, err
+			return err
 		}
 		for _, buddy := range buddies {
 			err := server.NotifyBuddyArrived(buddy, sess.ScreenName, sm)
@@ -139,10 +139,10 @@ func handleBOSConnection(cfg server.Config, sm *server.InMemorySessionManager, f
 			case errors.Is(err, server.ErrSessNotFound):
 				continue
 			case err != nil:
-				return []server.XMessage{}, err
+				return err
 			}
 		}
-		return []server.XMessage{}, nil
+		return nil
 	}
 
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D}
@@ -188,12 +188,11 @@ func handleChatConnection(cfg server.Config, fm *server.FeedbagStore, cr *server
 
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D, 0x000E}
 
-	onClientReady := func(sess *server.Session, sm server.SessionManager) ([]server.XMessage, error) {
+	onClientReady := func(sess *server.Session, sm server.SessionManager) error {
+		server.SendChatRoomInfoUpdate(sess, sm, room)
 		server.AlertUserJoined(sess, sm)
-		return []server.XMessage{
-			server.SendChatRoomInfoUpdateTmp(room),
-			server.SetOnlineChatUsersTmp(sm),
-		}, nil
+		server.SetOnlineChatUsers(sess, sm)
+		return nil
 	}
 
 	if err := server.ReadBos(cfg, onClientReady, chatSess, seq, room.SessionManager, fm, cr, conn, foodGroups); err != nil {

+ 43 - 0
oscar/snacs.go

@@ -387,6 +387,49 @@ type SNAC_0x0D_0x09_TLVExchangeInfo struct {
 //
 
 const (
+	ChatErr                uint16 = 0x0001
+	ChatRoomInfoUpdate     uint16 = 0x0002
+	ChatUsersJoined        uint16 = 0x0003
+	ChatUsersLeft          uint16 = 0x0004
+	ChatChannelMsgToHost   uint16 = 0x0005
+	ChatChannelMsgToClient uint16 = 0x0006
+	ChatEvilRequest        uint16 = 0x0007
+	ChatEvilReply          uint16 = 0x0008
+	ChatClientErr          uint16 = 0x0009
+	ChatPauseRoomReq       uint16 = 0x000A
+	ChatPauseRoomAck       uint16 = 0x000B
+	ChatResumeRoom         uint16 = 0x000C
+	ChatShowMyRow          uint16 = 0x000D
+	ChatShowRowByUsername  uint16 = 0x000E
+	ChatShowRowByNumber    uint16 = 0x000F
+	ChatShowRowByName      uint16 = 0x0010
+	ChatRowInfo            uint16 = 0x0011
+	ChatListRows           uint16 = 0x0012
+	ChatRowListInfo        uint16 = 0x0013
+	ChatMoreRows           uint16 = 0x0014
+	ChatMoveToRow          uint16 = 0x0015
+	ChatToggleChat         uint16 = 0x0016
+	ChatSendQuestion       uint16 = 0x0017
+	ChatSendComment        uint16 = 0x0018
+	ChatTallyVote          uint16 = 0x0019
+	ChatAcceptBid          uint16 = 0x001A
+	ChatSendInvite         uint16 = 0x001B
+	ChatDeclineInvite      uint16 = 0x001C
+	ChatAcceptInvite       uint16 = 0x001D
+	ChatNotifyMessage      uint16 = 0x001E
+	ChatGotoRow            uint16 = 0x001F
+	ChatStageUserJoin      uint16 = 0x0020
+	ChatStageUserLeft      uint16 = 0x0021
+	ChatUnnamedSnac22      uint16 = 0x0022
+	ChatClose              uint16 = 0x0023
+	ChatUserBan            uint16 = 0x0024
+	ChatUserUnban          uint16 = 0x0025
+	ChatJoined             uint16 = 0x0026
+	ChatUnnamedSnac27      uint16 = 0x0027
+	ChatUnnamedSnac28      uint16 = 0x0028
+	ChatUnnamedSnac29      uint16 = 0x0029
+	ChatRoomInfoOwner      uint16 = 0x0030
+
 	ChatTLVPublicWhisperFlag    uint16 = 0x01
 	ChatTLVSenderInformation    uint16 = 0x03
 	ChatTLVEnableReflectionFlag uint16 = 0x06

+ 91 - 0
server/MockChatHandler.go

@@ -0,0 +1,91 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MockChatHandler is an autogenerated mock type for the ChatHandler type
+type MockChatHandler struct {
+	mock.Mock
+}
+
+type MockChatHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockChatHandler) EXPECT() *MockChatHandler_Expecter {
+	return &MockChatHandler_Expecter{mock: &_m.Mock}
+}
+
+// ChannelMsgToHostHandler provides a mock function with given fields: sess, sm, snacPayloadIn
+func (_m *MockChatHandler) ChannelMsgToHostHandler(sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
+	ret := _m.Called(sess, sm, snacPayloadIn)
+
+	var r0 *XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)); ok {
+		return rf(sess, sm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) *XMessage); ok {
+		r0 = rf(sess, sm, snacPayloadIn)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*XMessage)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) error); ok {
+		r1 = rf(sess, sm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockChatHandler_ChannelMsgToHostHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChannelMsgToHostHandler'
+type MockChatHandler_ChannelMsgToHostHandler_Call struct {
+	*mock.Call
+}
+
+// ChannelMsgToHostHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - sm SessionManager
+//   - snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost
+func (_e *MockChatHandler_Expecter) ChannelMsgToHostHandler(sess interface{}, sm interface{}, snacPayloadIn interface{}) *MockChatHandler_ChannelMsgToHostHandler_Call {
+	return &MockChatHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", sess, sm, snacPayloadIn)}
+}
+
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Run(run func(sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(SessionManager), args[2].(oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost))
+	})
+	return _c
+}
+
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Return(_a0 *XMessage, _a1 error) *MockChatHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(*Session, SessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockChatHandler creates a new instance of MockChatHandler. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewMockChatHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockChatHandler {
+	mock := &MockChatHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 8 - 20
server/MockOServiceHandler.go

@@ -21,29 +21,17 @@ func (_m *MockOServiceHandler) EXPECT() *MockOServiceHandler_Expecter {
 }
 
 // ClientOnlineHandler provides a mock function with given fields: snacPayloadIn, onReadyCB, sess, sm
-func (_m *MockOServiceHandler) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error) {
+func (_m *MockOServiceHandler) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) error {
 	ret := _m.Called(snacPayloadIn, onReadyCB, sess, sm)
 
-	var r0 []XMessage
-	var r1 error
-	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) ([]XMessage, error)); ok {
-		return rf(snacPayloadIn, onReadyCB, sess, sm)
-	}
-	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) []XMessage); ok {
+	var r0 error
+	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) error); ok {
 		r0 = rf(snacPayloadIn, onReadyCB, sess, sm)
 	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]XMessage)
-		}
-	}
-
-	if rf, ok := ret.Get(1).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) error); ok {
-		r1 = rf(snacPayloadIn, onReadyCB, sess, sm)
-	} else {
-		r1 = ret.Error(1)
+		r0 = ret.Error(0)
 	}
 
-	return r0, r1
+	return r0
 }
 
 // MockOServiceHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
@@ -67,12 +55,12 @@ func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Run(run func(snacPayload
 	return _c
 }
 
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Return(_a0 []XMessage, _a1 error) *MockOServiceHandler_ClientOnlineHandler_Call {
-	_c.Call.Return(_a0, _a1)
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Return(_a0 error) *MockOServiceHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(_a0)
 	return _c
 }
 
-func (_c *MockOServiceHandler_ClientOnlineHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) ([]XMessage, error)) *MockOServiceHandler_ClientOnlineHandler_Call {
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager) error) *MockOServiceHandler_ClientOnlineHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 49 - 108
server/chat.go

@@ -1,73 +1,49 @@
 package server
 
 import (
-	"github.com/mkaminski/goaim/oscar"
 	"io"
-)
 
-const (
-	ChatErr                uint16 = 0x0001
-	ChatRoomInfoUpdate            = 0x0002
-	ChatUsersJoined               = 0x0003
-	ChatUsersLeft                 = 0x0004
-	ChatChannelMsgTohost          = 0x0005
-	ChatChannelMsgToclient        = 0x0006
-	ChatEvilRequest               = 0x0007
-	ChatEvilReply                 = 0x0008
-	ChatClientErr                 = 0x0009
-	ChatPauseRoomReq              = 0x000A
-	ChatPauseRoomAck              = 0x000B
-	ChatResumeRoom                = 0x000C
-	ChatShowMyRow                 = 0x000D
-	ChatShowRowByUsername         = 0x000E
-	ChatShowRowByNumber           = 0x000F
-	ChatShowRowByName             = 0x0010
-	ChatRowInfo                   = 0x0011
-	ChatListRows                  = 0x0012
-	ChatRowListInfo               = 0x0013
-	ChatMoreRows                  = 0x0014
-	ChatMoveToRow                 = 0x0015
-	ChatToggleChat                = 0x0016
-	ChatSendQuestion              = 0x0017
-	ChatSendComment               = 0x0018
-	ChatTallyVote                 = 0x0019
-	ChatAcceptBid                 = 0x001A
-	ChatSendInvite                = 0x001B
-	ChatDeclineInvite             = 0x001C
-	ChatAcceptInvite              = 0x001D
-	ChatNotifyMessage             = 0x001E
-	ChatGotoRow                   = 0x001F
-	ChatStageUserJoin             = 0x0020
-	ChatStageUserLeft             = 0x0021
-	ChatUnnamedSnac22             = 0x0022
-	ChatClose                     = 0x0023
-	ChatUserBan                   = 0x0024
-	ChatUserUnban                 = 0x0025
-	ChatJoined                    = 0x0026
-	ChatUnnamedSnac27             = 0x0027
-	ChatUnnamedSnac28             = 0x0028
-	ChatUnnamedSnac29             = 0x0029
-	ChatRoomInfoOwner             = 0x0030
+	"github.com/mkaminski/goaim/oscar"
 )
 
-func routeChat(sess *Session, sm SessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	switch snac.SubGroup {
-	case ChatChannelMsgTohost:
-		return SendAndReceiveChatChannelMsgTohost(sess, sm, snac, r, w, sequence)
+type ChatHandler interface {
+	ChannelMsgToHostHandler(sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error)
+}
+
+func NewChatRouter() ChatRouter {
+	return ChatRouter{
+		ChatHandler: ChatService{},
+	}
+}
+
+type ChatRouter struct {
+	ChatHandler
+}
+
+func (rt *ChatRouter) RouteChat(sess *Session, sm SessionManager, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.ChatChannelMsgToHost:
+		inSNAC := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.ChannelMsgToHostHandler(sess, sm, inSNAC)
+		if err != nil || outSNAC == nil {
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}
 }
 
-func SendAndReceiveChatChannelMsgTohost(sess *Session, sm SessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
+type ChatService struct {
+}
 
+func (s ChatService) ChannelMsgToHostHandler(sess *Session, sm SessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*XMessage, error) {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: CHAT,
-		SubGroup:  ChatChannelMsgToclient,
+		SubGroup:  oscar.ChatChannelMsgToClient,
 	}
 	snacPayloadOut := oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
 		Cookie:  snacPayloadIn.Cookie,
@@ -76,7 +52,6 @@ func SendAndReceiveChatChannelMsgTohost(sess *Session, sm SessionManager, snac o
 			TLVList: snacPayloadIn.TLVList,
 		},
 	}
-
 	snacPayloadOut.AddTLV(oscar.TLV{
 		TType: oscar.ChatTLVSenderInformation,
 		Val: oscar.TLVUserInfo{
@@ -94,21 +69,20 @@ func SendAndReceiveChatChannelMsgTohost(sess *Session, sm SessionManager, snac o
 		snacOut:   snacPayloadOut,
 	})
 
-	if _, ackMsg := snacPayloadIn.GetTLV(oscar.ChatTLVEnableReflectionFlag); !ackMsg {
-		return nil
+	var ret *XMessage
+	if _, ackMsg := snacPayloadIn.GetTLV(oscar.ChatTLVEnableReflectionFlag); ackMsg {
+		// reflect the message back to the sender
+		ret = &XMessage{
+			snacFrame: snacFrameOut,
+			snacOut:   snacPayloadOut,
+		}
 	}
 
-	// reflect the message back to the sender
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	return ret, nil
 }
 
-func SetOnlineChatUsers(sm SessionManager, w io.Writer, sequence *uint32) error {
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: CHAT,
-		SubGroup:  ChatUsersJoined,
-	}
+func SetOnlineChatUsers(sess *Session, sm SessionManager) {
 	snacPayloadOut := oscar.SNAC_0x0E_0x03_ChatUsersJoined{}
-
 	sessions := sm.Participants()
 
 	for _, uSess := range sessions {
@@ -121,36 +95,20 @@ func SetOnlineChatUsers(sm SessionManager, w io.Writer, sequence *uint32) error
 		})
 	}
 
-	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
-}
-
-func SetOnlineChatUsersTmp(sm SessionManager) XMessage {
-	snacPayloadOut := oscar.SNAC_0x0E_0x03_ChatUsersJoined{}
-	sessions := sm.Participants()
-
-	for _, uSess := range sessions {
-		snacPayloadOut.Users = append(snacPayloadOut.Users, oscar.TLVUserInfo{
-			ScreenName:   uSess.ScreenName,
-			WarningLevel: uSess.GetWarning(),
-			TLVBlock: oscar.TLVBlock{
-				TLVList: uSess.GetUserInfo(),
-			},
-		})
-	}
-	return XMessage{
+	sm.SendToScreenName(sess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
-			SubGroup:  ChatUsersJoined,
+			SubGroup:  oscar.ChatUsersJoined,
 		},
 		snacOut: snacPayloadOut,
-	}
+	})
 }
 
 func AlertUserJoined(sess *Session, sm SessionManager) {
 	sm.BroadcastExcept(sess, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
-			SubGroup:  ChatUsersJoined,
+			SubGroup:  oscar.ChatUsersJoined,
 		},
 		snacOut: oscar.SNAC_0x0E_0x03_ChatUsersJoined{
 			Users: []oscar.TLVUserInfo{
@@ -170,7 +128,7 @@ func AlertUserLeft(sess *Session, sm SessionManager) {
 	sm.BroadcastExcept(sess, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
-			SubGroup:  ChatUsersLeft,
+			SubGroup:  oscar.ChatUsersLeft,
 		},
 		snacOut: oscar.SNAC_0x0E_0x04_ChatUsersLeft{
 			Users: []oscar.TLVUserInfo{
@@ -186,28 +144,11 @@ func AlertUserLeft(sess *Session, sm SessionManager) {
 	})
 }
 
-func SendChatRoomInfoUpdate(room ChatRoom, w io.Writer, sequence *uint32) error {
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: CHAT,
-		SubGroup:  ChatRoomInfoUpdate,
-	}
-	snacPayloadOut := oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-		Exchange:       4,
-		Cookie:         room.Cookie,
-		InstanceNumber: 100,
-		DetailLevel:    2,
-		TLVBlock: oscar.TLVBlock{
-			TLVList: room.TLVList(),
-		},
-	}
-	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
-}
-
-func SendChatRoomInfoUpdateTmp(room ChatRoom) XMessage {
-	return XMessage{
+func SendChatRoomInfoUpdate(sess *Session, sm SessionManager, room ChatRoom) {
+	sm.SendToScreenName(sess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
-			SubGroup:  ChatRoomInfoUpdate,
+			SubGroup:  oscar.ChatRoomInfoUpdate,
 		},
 		snacOut: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
 			Exchange:       4,
@@ -218,5 +159,5 @@ func SendChatRoomInfoUpdateTmp(room ChatRoom) XMessage {
 				TLVList: room.TLVList(),
 			},
 		},
-	}
+	})
 }

+ 155 - 64
server/chat_test.go

@@ -2,12 +2,14 @@ package server
 
 import (
 	"bytes"
+	"github.com/stretchr/testify/mock"
+	"testing"
+
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
-	"testing"
 )
 
-func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
+func TestSendAndReceiveChatChannelMsgToHost(t *testing.T) {
 	cases := []struct {
 		// name is the unit test name
 		name string
@@ -15,15 +17,10 @@ func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
 		userSession *Session
 		// inputSNAC is the SNAC sent by the sender client
 		inputSNAC oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost
-		// expectSNACFrame is the SNAC frame sent from the server to the
-		// recipient client
-		expectSNACFrame oscar.SnacFrame
-		// expectSNACBody is the SNAC payload sent from the server to the
-		// recipient client
-		expectSNACBody any
 		// expectSNACToParticipants is the message the server broadcast to chat
 		// room participants (except the sender)
 		expectSNACToParticipants XMessage
+		expectOutput             *XMessage
 	}{
 		{
 			name: "send chat room message, expect acknowledgement to sender client",
@@ -46,36 +43,38 @@ func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: CHAT,
-				SubGroup:  ChatChannelMsgToclient,
-			},
-			expectSNACBody: oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
-				Cookie:  1234,
-				Channel: 14,
-				TLVRestBlock: oscar.TLVRestBlock{
-					TLVList: oscar.TLVList{
-						{
-							TType: oscar.ChatTLVPublicWhisperFlag,
-							Val:   []byte{},
-						},
-						{
-							TType: oscar.ChatTLVEnableReflectionFlag,
-							Val:   []byte{},
-						},
-						{
-							TType: oscar.ChatTLVSenderInformation,
-							Val: newTestSession(Session{
-								ScreenName: "user_sending_chat_msg",
-							}).GetTLVUserInfo(),
+			expectSNACToParticipants: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT,
+					SubGroup:  oscar.ChatChannelMsgToClient,
+				},
+				snacOut: oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
+					Cookie:  1234,
+					Channel: 14,
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: oscar.ChatTLVPublicWhisperFlag,
+								Val:   []byte{},
+							},
+							{
+								TType: oscar.ChatTLVEnableReflectionFlag,
+								Val:   []byte{},
+							},
+							{
+								TType: oscar.ChatTLVSenderInformation,
+								Val: newTestSession(Session{
+									ScreenName: "user_sending_chat_msg",
+								}).GetTLVUserInfo(),
+							},
 						},
 					},
 				},
 			},
-			expectSNACToParticipants: XMessage{
+			expectOutput: &XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: CHAT,
-					SubGroup:  ChatChannelMsgToclient,
+					SubGroup:  oscar.ChatChannelMsgToClient,
 				},
 				snacOut: oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
 					Cookie:  1234,
@@ -118,11 +117,10 @@ func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
 					},
 				},
 			},
-			expectSNACBody: nil,
 			expectSNACToParticipants: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: CHAT,
-					SubGroup:  ChatChannelMsgToclient,
+					SubGroup:  oscar.ChatChannelMsgToClient,
 				},
 				snacOut: oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
 					Cookie:  1234,
@@ -143,6 +141,7 @@ func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
 					},
 				},
 			},
+			expectOutput: &XMessage{},
 		},
 	}
 
@@ -157,38 +156,130 @@ func TestSendAndReceiveChatChannelMsgTohost(t *testing.T) {
 			//
 			// send input SNAC
 			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: CHAT,
-				SubGroup:  ChatChannelMsgTohost,
+			svc := ChatService{}
+			outputSNAC, err := svc.ChannelMsgToHostHandler(tc.userSession, crm, tc.inputSNAC)
+			assert.NoError(t, err)
+
+			if tc.expectOutput.snacFrame == (oscar.SnacFrame{}) {
+				return // handler doesn't return response
 			}
-			assert.NoError(t, SendAndReceiveChatChannelMsgTohost(tc.userSession, crm, snac, input, output, &seq))
-			if tc.expectSNACBody != nil {
-				//
-				// verify output
-				//
-				flap := oscar.FlapFrame{}
-				assert.NoError(t, oscar.Unmarshal(&flap, output))
-				snacFrame := oscar.SnacFrame{}
-				assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
-				assert.Equal(t, tc.expectSNACFrame, snacFrame)
-				//
-				// verify output SNAC body
-				//
-				switch v := tc.expectSNACBody.(type) {
-				case oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient:
-					assert.NoError(t, v.SerializeInPlace())
-					outputSNAC := oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{}
-					assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-					assert.Equal(t, v, outputSNAC)
-				default:
-					t.Fatalf("unexpected output SNAC type")
-				}
+
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
+
+func TestChatRouter_RouteChat(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// input is the request payload
+		input XMessage
+		// output is the response payload
+		output *XMessage
+		// handlerErr is the mocked handler error response
+		handlerErr error
+		// expectErr is the expected error returned by the router
+		expectErr error
+	}{
+		{
+			name: "receive ChatChannelMsgToHost, return ChatChannelMsgToClient",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT,
+					SubGroup:  oscar.ChatChannelMsgToHost,
+				},
+				snacOut: oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+					Channel: 4,
+				},
+			},
+			output: &XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT,
+					SubGroup:  oscar.ChatChannelMsgToClient,
+				},
+				snacOut: oscar.SNAC_0x0E_0x06_ChatChannelMsgToClient{
+					Channel: 4,
+				},
+			},
+		},
+		{
+			name: "receive ChatChannelMsgToHost, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT,
+					SubGroup:  oscar.ChatChannelMsgToHost,
+				},
+				snacOut: oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{
+					Channel: 4,
+				},
+			},
+			output: nil,
+		},
+		{
+			name: "receive ChatRowListInfo, return ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT,
+					SubGroup:  oscar.ChatRowListInfo,
+				},
+				snacOut: struct{}{},
+			},
+			output:    nil,
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockChatHandler(t)
+			svc.EXPECT().
+				ChannelMsgToHostHandler(mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+
+			router := ChatRouter{
+				ChatHandler: svc,
 			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(0)
+
+			err := router.RouteChat(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output == nil {
+				// make sure no response was sent
+				assert.Empty(t, bufOut.Bytes())
+				return
+			}
+
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+
+			// make sure the sequence increments
+			assert.Equal(t, seq, uint32(1))
+			assert.Equal(t, flap.Sequence, uint16(0))
+
+			flapBuf, err := flap.SNACBuffer(bufOut)
+			assert.NoError(t, err)
+
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
+			assert.Equal(t, tc.output.snacFrame, snacFrame)
+
+			// verify the SNAC message
+			snacBuf := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
+			assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
 		})
 	}
 }

+ 2 - 7
server/icbm.go

@@ -43,15 +43,10 @@ func (rt *ICBMRouter) RouteICBM(sm SessionManager, fm FeedbagManager, sess *Sess
 			return err
 		}
 		outSNAC, err := rt.ChannelMsgToHostHandler(sm, fm, sess, inSNAC)
-		if err != nil {
+		if err != nil || outSNAC == nil {
 			return err
 		}
-		if outSNAC != nil {
-			if err := writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w); err != nil {
-				return err
-			}
-		}
-		return nil
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	case oscar.ICBMEvilRequest:
 		inSNAC := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {

+ 36 - 19
server/icbm_test.go

@@ -569,7 +569,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 		// input is the request payload
 		input XMessage
 		// output is the response payload
-		output XMessage
+		output *XMessage
 		// handlerErr is the mocked handler error response
 		handlerErr error
 		// expectErr is the expected error returned by the router
@@ -586,7 +586,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 					Channel: 1,
 				},
 			},
-			output: XMessage{}, // empty SNAC
+			output: nil,
 		},
 		{
 			name: "receive ICBMParameterQuery, return ICBMParameterReply",
@@ -597,7 +597,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 				},
 				snacOut: struct{}{}, // empty SNAC
 			},
-			output: XMessage{
+			output: &XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
 					SubGroup:  oscar.ICBMParameterReply,
@@ -618,7 +618,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 					ScreenName: "recipient-screen-name",
 				},
 			},
-			output: XMessage{
+			output: &XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
 					SubGroup:  oscar.ICBMHostAck,
@@ -628,6 +628,19 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "receive ICBMChannelMsgToHost, return no reply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  oscar.ICBMChannelMsgToHost,
+				},
+				snacOut: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+					ScreenName: "recipient-screen-name",
+				},
+			},
+			output: nil,
+		},
 		{
 			name: "receive ICBMEvilRequest, return ICBMEvilReply",
 			input: XMessage{
@@ -639,7 +652,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 					ScreenName: "recipient-screen-name",
 				},
 			},
-			output: XMessage{
+			output: &XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: ICBM,
 					SubGroup:  oscar.ICBMEvilReply,
@@ -660,7 +673,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 					Code: 4,
 				},
 			},
-			output: XMessage{}, // empty SNAC
+			output: nil,
 		},
 		{
 			name: "receive ICBMClientEvent, return no response",
@@ -673,7 +686,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 					ScreenName: "recipient-screen-name",
 				},
 			},
-			output: XMessage{}, // empty SNAC
+			output: nil,
 		},
 		{
 			name: "receive ICBMMissedCalls, expect ErrUnsupportedSubGroup",
@@ -684,7 +697,7 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 				},
 				snacOut: struct{}{}, // empty SNAC
 			},
-			output:    XMessage{}, // empty SNAC
+			output:    nil,
 			expectErr: ErrUnsupportedSubGroup,
 		},
 	}
@@ -694,20 +707,22 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 			svc := NewMockICBMHandler(t)
 			svc.EXPECT().
 				ChannelMsgToHostHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-				Return(&tc.output, tc.handlerErr).
+				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
 				ClientEventHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.handlerErr).
 				Maybe()
-			svc.EXPECT().
-				EvilRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-				Return(tc.output, tc.handlerErr).
-				Maybe()
-			svc.EXPECT().
-				ParameterQueryHandler().
-				Return(tc.output).
-				Maybe()
+			if tc.output != nil {
+				svc.EXPECT().
+					EvilRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+					Return(*tc.output, tc.handlerErr).
+					Maybe()
+				svc.EXPECT().
+					ParameterQueryHandler().
+					Return(*tc.output).
+					Maybe()
+			}
 
 			router := ICBMRouter{
 				ICBMHandler: svc,
@@ -725,8 +740,10 @@ func TestICBMRouter_RouteICBM(t *testing.T) {
 				return
 			}
 
-			if tc.output.snacFrame == (oscar.SnacFrame{}) {
-				return // handler doesn't return response
+			if tc.output == nil {
+				// make sure no response was sent
+				assert.Empty(t, bufOut.Bytes())
+				return
 			}
 
 			// make sure the sequence number was incremented

+ 4 - 13
server/oservice.go

@@ -10,7 +10,7 @@ import (
 )
 
 type OServiceHandler interface {
-	ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error)
+	ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) error
 	ClientVersionsHandler(snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage
 	IdleNotificationHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error
 	RateParamsQueryHandler() XMessage
@@ -37,16 +37,7 @@ func (rt OServiceRouter) RouteOService(cfg Config, ready OnReadyCB, cr *ChatRegi
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		batch, err := rt.ClientOnlineHandler(inSNAC, ready, sess, sm)
-		for _, msg := range batch {
-			switch {
-			case err != nil:
-				return err
-			case batch != nil:
-				return writeOutSNAC(SNACFrame, msg.snacFrame, msg.snacOut, sequence, w)
-			}
-		}
-		return nil
+		return rt.ClientOnlineHandler(inSNAC, ready, sess, sm)
 	case oscar.OServiceServiceRequest:
 		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
@@ -207,9 +198,9 @@ func (s OServiceService) UserInfoQueryHandler(sess *Session) XMessage {
 	}
 }
 
-type OnReadyCB func(sess *Session, sm SessionManager) ([]XMessage, error)
+type OnReadyCB func(sess *Session, sm SessionManager) error
 
-func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) ([]XMessage, error) {
+func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager) error {
 	for _, version := range snacPayloadIn.GroupVersions {
 		fmt.Printf("ClientOnlineHandler read SNAC client messageType: %+v\n", version)
 	}

+ 98 - 118
server/oservice_test.go

@@ -178,7 +178,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 		// input is the request payload
 		input XMessage
 		// output is the response payload
-		output []XMessage
+		output XMessage
 		// handlerErr is the mocked handler error response
 		handlerErr error
 		// expectErr is the expected error returned by the router
@@ -204,19 +204,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: BUDDY,
-						SubGroup:  BuddyArrived,
-					},
-					snacOut: oscar.SNAC_0x03_0x0A_BuddyArrived{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "my-screen-name-1",
-						},
-					},
-				},
-			},
+			output: XMessage{},
 		},
 		{
 			name: "receive OServiceServiceRequest, return OServiceServiceResponse",
@@ -229,19 +217,17 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					FoodGroup: 10,
 				},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: OSERVICE,
-						SubGroup:  oscar.OServiceServiceResponse,
-					},
-					snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
-						TLVRestBlock: oscar.TLVRestBlock{
-							TLVList: oscar.TLVList{
-								{
-									TType: 0x01,
-									Val:   uint16(1000),
-								},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceServiceResponse,
+				},
+				snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   uint16(1000),
 							},
 						},
 					},
@@ -257,23 +243,21 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: OSERVICE,
-						SubGroup:  oscar.OServiceRateParamsReply,
-					},
-					snacOut: oscar.SNAC_0x01_0x07_OServiceRateParamsReply{
-						RateGroups: []struct {
-							ID    uint16
-							Pairs []struct {
-								FoodGroup uint16
-								SubGroup  uint16
-							} `count_prefix:"uint16"`
-						}{
-							{
-								ID: 1,
-							},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsReply,
+				},
+				snacOut: oscar.SNAC_0x01_0x07_OServiceRateParamsReply{
+					RateGroups: []struct {
+						ID    uint16
+						Pairs []struct {
+							FoodGroup uint16
+							SubGroup  uint16
+						} `count_prefix:"uint16"`
+					}{
+						{
+							ID: 1,
 						},
 					},
 				},
@@ -297,7 +281,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: []XMessage{},
+			output: XMessage{},
 		},
 		{
 			name: "receive OServiceUserInfoQuery, return OServiceUserInfoUpdate",
@@ -308,16 +292,14 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: OSERVICE,
-						SubGroup:  oscar.OServiceUserInfoUpdate,
-					},
-					snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "screen-name",
-						},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
 					},
 				},
 			},
@@ -333,7 +315,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					IdleTime: 10,
 				},
 			},
-			output: []XMessage{},
+			output: XMessage{},
 		},
 		{
 			name: "receive OServiceClientVersions, return OServiceHostVersions",
@@ -348,16 +330,14 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: OSERVICE,
-						SubGroup:  oscar.OServiceHostVersions,
-					},
-					snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
-						Versions: []uint16{
-							10,
-						},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceHostVersions,
+				},
+				snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
+					Versions: []uint16{
+						10,
 					},
 				},
 			},
@@ -380,16 +360,14 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 					},
 				},
 			},
-			output: []XMessage{
-				{
-					snacFrame: oscar.SnacFrame{
-						FoodGroup: OSERVICE,
-						SubGroup:  oscar.OServiceUserInfoUpdate,
-					},
-					snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-						TLVUserInfo: oscar.TLVUserInfo{
-							ScreenName: "screen-name",
-						},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
 					},
 				},
 			},
@@ -403,7 +381,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				},
 				snacOut: struct{}{}, // empty SNAC
 			},
-			output:    []XMessage{}, // empty SNAC
+			output:    XMessage{}, // empty SNAC
 			expectErr: ErrUnsupportedSubGroup,
 		},
 	}
@@ -411,32 +389,30 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
 			svc := NewMockOServiceHandler(t)
-			if len(tc.output) > 0 {
-				svc.EXPECT().
-					ServiceRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-					Return(tc.output[0], tc.handlerErr).
-					Maybe()
-				svc.EXPECT().
-					RateParamsQueryHandler().
-					Return(tc.output[0]).
-					Maybe()
-				svc.EXPECT().
-					UserInfoQueryHandler(mock.Anything).
-					Return(tc.output[0]).
-					Maybe()
-				svc.EXPECT().
-					ClientVersionsHandler(tc.input.snacOut).
-					Return(tc.output[0]).
-					Maybe()
-				svc.EXPECT().
-					SetUserInfoFieldsHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
-					Return(tc.output[0], tc.handlerErr).
-					Maybe()
-			}
 			svc.EXPECT().
-				ClientOnlineHandler(tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything).
+				ServiceRequestHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				RateParamsQueryHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				UserInfoQueryHandler(mock.Anything).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				ClientVersionsHandler(tc.input.snacOut).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				SetUserInfoFieldsHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
+			svc.EXPECT().
+				ClientOnlineHandler(tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything).
+				Return(tc.handlerErr).
+				Maybe()
 			svc.EXPECT().
 				RateParamsSubAddHandler(tc.input.snacOut).
 				Maybe()
@@ -453,7 +429,7 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
 
 			bufOut := &bytes.Buffer{}
-			seq := uint32(0)
+			seq := uint32(1)
 
 			err := router.RouteOService(Config{}, nil, nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
@@ -461,27 +437,31 @@ func TestOServiceRouter_RouteOService(t *testing.T) {
 				return
 			}
 
-			for i, msg := range tc.output {
-				// verify the FLAP frame
-				flap := oscar.FlapFrame{}
-				assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+			if tc.output == (XMessage{}) {
+				// make sure no response was sent
+				assert.Empty(t, bufOut.Bytes())
+				return
+			}
 
-				// make sure the sequence increments
-				assert.Equal(t, flap.Sequence, uint16(i))
+			// verify the FLAP frame
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
 
-				flapBuf, err := flap.SNACBuffer(bufOut)
-				assert.NoError(t, err)
+			// make sure the sequence number was incremented
+			assert.Equal(t, uint32(2), seq)
 
-				// verify the SNAC frame
-				snacFrame := oscar.SnacFrame{}
-				assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
-				assert.Equal(t, msg.snacFrame, snacFrame)
+			flapBuf, err := flap.SNACBuffer(bufOut)
+			assert.NoError(t, err)
 
-				// verify the SNAC message
-				snacBuf := &bytes.Buffer{}
-				assert.NoError(t, oscar.Marshal(msg.snacOut, snacBuf))
-				assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
-			}
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, flapBuf))
+			assert.Equal(t, tc.output.snacFrame, snacFrame)
+
+			// verify the SNAC message
+			snacBuf := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.output.snacOut, snacBuf))
+			assert.Equal(t, snacBuf.Bytes(), flapBuf.Bytes())
 		})
 	}
 }

+ 3 - 1
server/protocol.go

@@ -319,6 +319,7 @@ func NewRouter() Router {
 		OServiceRouter: NewOServiceRouter(),
 		FeedbagRouter:  NewFeedbagRouter(),
 		ChatNavRouter:  NewChatNavRouter(),
+		ChatRouter:     NewChatRouter(),
 	}
 }
 
@@ -328,6 +329,7 @@ type Router struct {
 	OServiceRouter
 	FeedbagRouter
 	ChatNavRouter
+	ChatRouter
 }
 
 func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
@@ -349,7 +351,7 @@ func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionM
 	case BUCP:
 		return routeBUCP()
 	case CHAT:
-		return routeChat(sess, sm, snac, buf, rw, sequence)
+		return rt.RouteChat(sess, sm, snac, buf, rw, sequence)
 	default:
 		return ErrUnsupportedFoodGroup
 	}