Sfoglia il codice sorgente

refactor and test ChatNav

Mike 2 anni fa
parent
commit
2dc6212e17
6 ha cambiato i file con 527 aggiunte e 170 eliminazioni
  1. 2 1
      .mockery.yaml
  2. 10 0
      oscar/snacs.go
  3. 184 0
      server/MockChatNavHandler.go
  4. 136 129
      server/chat_nav.go
  5. 192 39
      server/chat_nav_test.go
  6. 3 1
      server/protocol.go

+ 2 - 1
.mockery.yaml

@@ -12,4 +12,5 @@ packages:
       ICBMHandler:
       LocateHandler:
       OServiceHandler:
-      FeedbagHandler:
+      FeedbagHandler:
+      ChatNavHandler:

+ 10 - 0
oscar/snacs.go

@@ -350,6 +350,16 @@ type SNAC_0x09_0x03_PDRightsReply struct {
 //
 
 const (
+	ChatNavErr                 uint16 = 0x0001
+	ChatNavRequestChatRights   uint16 = 0x0002
+	ChatNavRequestExchangeInfo uint16 = 0x0003
+	ChatNavRequestRoomInfo     uint16 = 0x0004
+	ChatNavRequestMoreRoomInfo uint16 = 0x0005
+	ChatNavRequestOccupantList uint16 = 0x0006
+	ChatNavSearchForRoom       uint16 = 0x0007
+	ChatNavCreateRoom          uint16 = 0x0008
+	ChatNavNavInfo             uint16 = 0x0009
+
 	ChatNavTLVRedirect     uint16 = 0x01
 	ChatNavTLVMaxRooms     uint16 = 0x02
 	ChatNavTLVExchangeInfo uint16 = 0x03

+ 184 - 0
server/MockChatNavHandler.go

@@ -0,0 +1,184 @@
+// 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"
+)
+
+// MockChatNavHandler is an autogenerated mock type for the ChatNavHandler type
+type MockChatNavHandler struct {
+	mock.Mock
+}
+
+type MockChatNavHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockChatNavHandler) EXPECT() *MockChatNavHandler_Expecter {
+	return &MockChatNavHandler_Expecter{mock: &_m.Mock}
+}
+
+// CreateRoomHandler provides a mock function with given fields: sess, cr, newChatRoom, snacPayloadIn
+func (_m *MockChatNavHandler) CreateRoomHandler(sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
+	ret := _m.Called(sess, cr, newChatRoom, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)); ok {
+		return rf(sess, cr, newChatRoom, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) XMessage); ok {
+		r0 = rf(sess, cr, newChatRoom, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) error); ok {
+		r1 = rf(sess, cr, newChatRoom, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockChatNavHandler_CreateRoomHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRoomHandler'
+type MockChatNavHandler_CreateRoomHandler_Call struct {
+	*mock.Call
+}
+
+// CreateRoomHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - cr *ChatRegistry
+//   - newChatRoom ChatRoomFactory
+//   - snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate
+func (_e *MockChatNavHandler_Expecter) CreateRoomHandler(sess interface{}, cr interface{}, newChatRoom interface{}, snacPayloadIn interface{}) *MockChatNavHandler_CreateRoomHandler_Call {
+	return &MockChatNavHandler_CreateRoomHandler_Call{Call: _e.mock.On("CreateRoomHandler", sess, cr, newChatRoom, snacPayloadIn)}
+}
+
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) Run(run func(sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate)) *MockChatNavHandler_CreateRoomHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(*ChatRegistry), args[2].(ChatRoomFactory), args[3].(oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
+	})
+	return _c
+}
+
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) Return(_a0 XMessage, _a1 error) *MockChatNavHandler_CreateRoomHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) RunAndReturn(run func(*Session, *ChatRegistry, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)) *MockChatNavHandler_CreateRoomHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RequestChatRightsHandler provides a mock function with given fields:
+func (_m *MockChatNavHandler) RequestChatRightsHandler() XMessage {
+	ret := _m.Called()
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func() XMessage); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockChatNavHandler_RequestChatRightsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestChatRightsHandler'
+type MockChatNavHandler_RequestChatRightsHandler_Call struct {
+	*mock.Call
+}
+
+// RequestChatRightsHandler is a helper method to define mock.On call
+func (_e *MockChatNavHandler_Expecter) RequestChatRightsHandler() *MockChatNavHandler_RequestChatRightsHandler_Call {
+	return &MockChatNavHandler_RequestChatRightsHandler_Call{Call: _e.mock.On("RequestChatRightsHandler")}
+}
+
+func (_c *MockChatNavHandler_RequestChatRightsHandler_Call) Run(run func()) *MockChatNavHandler_RequestChatRightsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockChatNavHandler_RequestChatRightsHandler_Call) Return(_a0 XMessage) *MockChatNavHandler_RequestChatRightsHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockChatNavHandler_RequestChatRightsHandler_Call) RunAndReturn(run func() XMessage) *MockChatNavHandler_RequestChatRightsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RequestRoomInfoHandler provides a mock function with given fields: cr, snacPayloadIn
+func (_m *MockChatNavHandler) RequestRoomInfoHandler(cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
+	ret := _m.Called(cr, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)); ok {
+		return rf(cr, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) XMessage); ok {
+		r0 = rf(cr, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) error); ok {
+		r1 = rf(cr, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockChatNavHandler_RequestRoomInfoHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestRoomInfoHandler'
+type MockChatNavHandler_RequestRoomInfoHandler_Call struct {
+	*mock.Call
+}
+
+// RequestRoomInfoHandler is a helper method to define mock.On call
+//   - cr *ChatRegistry
+//   - snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo
+func (_e *MockChatNavHandler_Expecter) RequestRoomInfoHandler(cr interface{}, snacPayloadIn interface{}) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+	return &MockChatNavHandler_RequestRoomInfoHandler_Call{Call: _e.mock.On("RequestRoomInfoHandler", cr, snacPayloadIn)}
+}
+
+func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) Run(run func(cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*ChatRegistry), args[1].(oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo))
+	})
+	return _c
+}
+
+func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) Return(_a0 XMessage, _a1 error) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockChatNavHandler_RequestRoomInfoHandler_Call) RunAndReturn(run func(*ChatRegistry, oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)) *MockChatNavHandler_RequestRoomInfoHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockChatNavHandler creates a new instance of MockChatNavHandler. 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 NewMockChatNavHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockChatNavHandler {
+	mock := &MockChatNavHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 136 - 129
server/chat_nav.go

@@ -2,95 +2,117 @@ package server
 
 import (
 	"errors"
-	"fmt"
 	"github.com/google/uuid"
 	"github.com/mkaminski/goaim/oscar"
 	"io"
 	"time"
 )
 
-const (
-	ChatNavErr                 uint16 = 0x0001
-	ChatNavRequestChatRights          = 0x0002
-	ChatNavRequestExchangeInfo        = 0x0003
-	ChatNavRequestRoomInfo            = 0x0004
-	ChatNavRequestMoreRoomInfo        = 0x0005
-	ChatNavRequestOccupantList        = 0x0006
-	ChatNavSearchForRoom              = 0x0007
-	ChatNavCreateRoom                 = 0x0008
-	ChatNavNavInfo                    = 0x0009
-)
+type ChatNavHandler interface {
+	CreateRoomHandler(sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error)
+	RequestChatRightsHandler() XMessage
+	RequestRoomInfoHandler(cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error)
+}
+
+func NewChatNavRouter() ChatNavRouter {
+	return ChatNavRouter{
+		ChatNavHandler: ChatNavService{},
+	}
+}
 
-func routeChatNav(sess *Session, cr *ChatRegistry, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	switch snac.SubGroup {
-	case ChatNavRequestChatRights:
-		return SendAndReceiveNextChatRights(snac, w, sequence)
-	case ChatNavRequestRoomInfo:
-		return SendAndReceiveRequestRoomInfo(cr, snac, r, w, sequence)
-	case ChatNavCreateRoom:
-		return SendAndReceiveCreateRoom(sess, cr, NewChatRoom, snac, r, w, sequence)
+type ChatNavRouter struct {
+	ChatNavHandler
+}
+
+func (rt *ChatNavRouter) RouteChatNav(sess *Session, cr *ChatRegistry, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.ChatNavRequestChatRights:
+		outSNAC := rt.RequestChatRightsHandler()
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.ChatNavRequestRoomInfo:
+		inSNAC := oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.RequestRoomInfoHandler(cr, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.ChatNavCreateRoom:
+		snacPayloadIn := oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
+		if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.CreateRoomHandler(sess, cr, NewChatRoom, snacPayloadIn)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}
 }
 
+type ChatNavService struct {
+}
+
 type ChatCookie struct {
 	Cookie []byte `len_prefix:"uint16"`
 	SessID string `len_prefix:"uint16"`
 }
 
-func SendAndReceiveNextChatRights(snac oscar.SnacFrame, w io.Writer, sequence *uint32) error {
-	fmt.Printf("sendAndReceiveNextChatRights read SNAC frame: %+v\n", snac)
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: CHAT_NAV,
-		SubGroup:  ChatNavNavInfo,
-	}
-
-	snacPayloadOut := oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: 0x02,
-					Val:   uint8(10),
-				},
-				{
-					TType: 0x03,
-					Val: oscar.SNAC_0x0D_0x09_TLVExchangeInfo{
-						Identifier: 4,
-						TLVBlock: oscar.TLVBlock{
-							TLVList: oscar.TLVList{
-								{
-									TType: 0x0002,
-									Val:   uint16(0x0010),
-								},
-								{
-									TType: 0x00c9,
-									Val:   uint16(15),
-								},
-								{
-									TType: 0x00d3,
-									Val:   "default Exchange",
-								},
-								{
-									TType: 0x00d5,
-									Val:   uint8(2),
-								},
-								{
-									TType: 0xd6,
-									Val:   "us-ascii",
-								},
-								{
-									TType: 0xd7,
-									Val:   "en",
-								},
-								{
-									TType: 0xd8,
-									Val:   "us-ascii",
-								},
-								{
-									TType: 0xd9,
-									Val:   "en",
+func (s ChatNavService) RequestChatRightsHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT_NAV,
+			SubGroup:  oscar.ChatNavNavInfo,
+		},
+		snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: 0x02,
+						Val:   uint8(10),
+					},
+					{
+						TType: 0x03,
+						Val: oscar.SNAC_0x0D_0x09_TLVExchangeInfo{
+							Identifier: 4,
+							TLVBlock: oscar.TLVBlock{
+								TLVList: oscar.TLVList{
+									{
+										TType: 0x0002,
+										Val:   uint16(0x0010),
+									},
+									{
+										TType: 0x00c9,
+										Val:   uint16(15),
+									},
+									{
+										TType: 0x00d3,
+										Val:   "default Exchange",
+									},
+									{
+										TType: 0x00d5,
+										Val:   uint8(2),
+									},
+									{
+										TType: 0xd6,
+										Val:   "us-ascii",
+									},
+									{
+										TType: 0xd7,
+										Val:   "en",
+									},
+									{
+										TType: 0xd8,
+										Val:   "us-ascii",
+									},
+									{
+										TType: 0xd9,
+										Val:   "en",
+									},
 								},
 							},
 						},
@@ -99,8 +121,6 @@ func SendAndReceiveNextChatRights(snac oscar.SnacFrame, w io.Writer, sequence *u
 			},
 		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
 func NewChatRoom() ChatRoom {
@@ -113,15 +133,10 @@ func NewChatRoom() ChatRoom {
 
 type ChatRoomFactory func() ChatRoom
 
-func SendAndReceiveCreateRoom(sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s ChatNavService) CreateRoomHandler(sess *Session, cr *ChatRegistry, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (XMessage, error) {
 	name, hasName := snacPayloadIn.GetString(oscar.ChatTLVRoomName)
 	if !hasName {
-		return errors.New("unable to find chat name")
+		return XMessage{}, errors.New("unable to find chat name")
 	}
 
 	room := newChatRoom()
@@ -134,68 +149,60 @@ func SendAndReceiveCreateRoom(sess *Session, cr *ChatRegistry, newChatRoom ChatR
 	// add user to chat room
 	room.NewSessionWithSN(sess.ID, sess.ScreenName)
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: CHAT_NAV,
-		SubGroup:  ChatNavNavInfo,
-	}
-	snacPayloadOut := oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: oscar.ChatNavTLVRoomInfo,
-					Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-						Exchange:       snacPayloadIn.Exchange,
-						Cookie:         room.Cookie,
-						InstanceNumber: snacPayloadIn.InstanceNumber,
-						DetailLevel:    snacPayloadIn.DetailLevel,
-						TLVBlock: oscar.TLVBlock{
-							TLVList: room.TLVList(),
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT_NAV,
+			SubGroup:  oscar.ChatNavNavInfo,
+		},
+		snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: oscar.ChatNavTLVRoomInfo,
+						Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+							Exchange:       snacPayloadIn.Exchange,
+							Cookie:         room.Cookie,
+							InstanceNumber: snacPayloadIn.InstanceNumber,
+							DetailLevel:    snacPayloadIn.DetailLevel,
+							TLVBlock: oscar.TLVBlock{
+								TLVList: room.TLVList(),
+							},
 						},
 					},
 				},
 			},
 		},
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	}, nil
 }
 
-func SendAndReceiveRequestRoomInfo(cr *ChatRegistry, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveRequestRoomInfo read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s ChatNavService) RequestRoomInfoHandler(cr *ChatRegistry, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (XMessage, error) {
 	room, err := cr.Retrieve(string(snacPayloadIn.Cookie))
 	if err != nil {
-		return err
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: CHAT_NAV,
-		SubGroup:  ChatNavNavInfo,
+		return XMessage{}, err
 	}
 
-	snacPayloadOut := oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: 0x04,
-					Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-						Exchange:       4,
-						Cookie:         room.Cookie,
-						InstanceNumber: 100,
-						DetailLevel:    2,
-						TLVBlock: oscar.TLVBlock{
-							TLVList: room.TLVList(),
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT_NAV,
+			SubGroup:  oscar.ChatNavNavInfo,
+		},
+		snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: 0x04,
+						Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+							Exchange:       4,
+							Cookie:         room.Cookie,
+							InstanceNumber: 100,
+							DetailLevel:    2,
+							TLVBlock: oscar.TLVBlock{
+								TLVList: room.TLVList(),
+							},
 						},
 					},
 				},
 			},
 		},
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	}, nil
 }

+ 192 - 39
server/chat_nav_test.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 )
 
 func TestSendAndReceiveCreateRoom(t *testing.T) {
@@ -35,10 +36,6 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 	//
 	// send input SNAC
 	//
-	inputSNACFrame := oscar.SnacFrame{
-		FoodGroup: CHAT_NAV,
-		SubGroup:  ChatNavCreateRoom,
-	}
 	inputSNAC := oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
 		Exchange:       1,
 		Cookie:         "create", // actual canned value sent by AIM client
@@ -53,12 +50,10 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 			},
 		},
 	}
-	input := &bytes.Buffer{}
-	assert.NoError(t, oscar.Marshal(inputSNAC, input))
-
-	var seq uint32
-	output := &bytes.Buffer{}
-	assert.NoError(t, SendAndReceiveCreateRoom(userSess, cr, crf, inputSNACFrame, input, output, &seq))
+	svc := ChatNavService{}
+	assert.NoError(t, inputSNAC.SerializeInPlace())
+	outputSNAC, err := svc.CreateRoomHandler(userSess, cr, crf, inputSNAC)
+	assert.NoError(t, err)
 
 	//
 	// verify chat room created by handler
@@ -77,43 +72,201 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 	assert.Equal(t, expectChatRoom, chatRoom)
 
 	//
-	// verify SNAC frame
+	// send input SNAC
 	//
-	expectSNACFrame := oscar.SnacFrame{
-		FoodGroup: CHAT_NAV,
-		SubGroup:  ChatNavNavInfo,
+	expectSNAC := XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: CHAT_NAV,
+			SubGroup:  oscar.ChatNavNavInfo,
+		},
+		snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: oscar.ChatNavTLVRoomInfo,
+						Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+							Exchange:       chatRoom.Exchange,
+							Cookie:         chatRoom.Cookie,
+							InstanceNumber: chatRoom.InstanceNumber,
+							DetailLevel:    chatRoom.DetailLevel,
+							TLVBlock: oscar.TLVBlock{
+								TLVList: chatRoom.TLVList(),
+							},
+						},
+					},
+				},
+			},
+		},
 	}
-	flap := oscar.FlapFrame{}
-	assert.NoError(t, oscar.Unmarshal(&flap, output))
-	snacFrame := oscar.SnacFrame{}
-	assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
-	assert.Equal(t, expectSNACFrame, snacFrame)
 
-	//
-	// verify SNAC body
-	//
-	expectSNAC := oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: oscar.ChatNavTLVRoomInfo,
-					Val: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
-						Exchange:       chatRoom.Exchange,
-						Cookie:         chatRoom.Cookie,
-						InstanceNumber: chatRoom.InstanceNumber,
-						DetailLevel:    chatRoom.DetailLevel,
-						TLVBlock: oscar.TLVBlock{
-							TLVList: chatRoom.TLVList(),
+	assert.Equal(t, expectSNAC, outputSNAC)
+}
+
+func TestChatNavRouter_RouteChatNavRouter(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 ChatNavRequestChatRights, return ChatNavNavInfo",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavRequestChatRights,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavNavInfo,
+				},
+				snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x02,
+								Val:   uint8(10),
+							},
 						},
 					},
 				},
 			},
 		},
+		{
+			name: "receive ChatNavRequestRoomInfo, return ChatNavNavInfo",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavRequestRoomInfo,
+				},
+				snacOut: oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
+					Exchange: 1,
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavNavInfo,
+				},
+				snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x02,
+								Val:   uint8(10),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive ChatNavCreateRoom, return ChatNavNavInfo",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavCreateRoom,
+				},
+				snacOut: oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
+					Exchange: 1,
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavNavInfo,
+				},
+				snacOut: oscar.SNAC_0x0D_0x09_ChatNavNavInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x02,
+								Val:   uint8(10),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive ChatNavRequestOccupantList, return ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: CHAT_NAV,
+					SubGroup:  oscar.ChatNavRequestOccupantList,
+				},
+				snacOut: struct{}{},
+			},
+			output:    XMessage{},
+			expectErr: ErrUnsupportedSubGroup,
+		},
 	}
-	assert.NoError(t, expectSNAC.SerializeInPlace())
-	outputSNAC := oscar.SNAC_0x0D_0x09_ChatNavNavInfo{}
-	assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-	assert.Equal(t, expectSNAC, outputSNAC)
 
-	assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockChatNavHandler(t)
+			svc.EXPECT().
+				RequestChatRightsHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				RequestRoomInfoHandler(mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				CreateRoomHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+
+			router := ChatNavRouter{
+				ChatNavHandler: svc,
+			}
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(0)
+
+			err := router.RouteChatNav(nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+
+			if tc.output.snacFrame == (oscar.SnacFrame{}) {
+				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())
+		})
+	}
 }

+ 3 - 1
server/protocol.go

@@ -318,6 +318,7 @@ func NewRouter() Router {
 		LocateRouter:   NewLocateRouter(),
 		OServiceRouter: NewOServiceRouter(),
 		FeedbagRouter:  NewFeedbagRouter(),
+		ChatNavRouter:  NewChatNavRouter(),
 	}
 }
 
@@ -326,6 +327,7 @@ type Router struct {
 	LocateRouter
 	OServiceRouter
 	FeedbagRouter
+	ChatNavRouter
 }
 
 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 {
@@ -341,7 +343,7 @@ func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionM
 	case PD:
 		return routePD(snac, buf, rw, sequence)
 	case CHAT_NAV:
-		return routeChatNav(sess, cr, snac, buf, rw, sequence)
+		return rt.RouteChatNav(sess, cr, snac, buf, rw, sequence)
 	case FEEDBAG:
 		return rt.RouteFeedbag(sm, sess, fm, snac, buf, rw, sequence)
 	case BUCP: