Răsfoiți Sursa

refactor and test Locate

Mike 2 ani în urmă
părinte
comite
4f2007b2c5
6 a modificat fișierele cu 790 adăugiri și 309 ștergeri
  1. 2 1
      .mockery.yaml
  2. 34 0
      oscar/snacs.go
  3. 260 0
      server/MockLocateHandler.go
  4. 155 185
      server/locate.go
  5. 335 121
      server/locate_test.go
  6. 4 2
      server/protocol.go

+ 2 - 1
.mockery.yaml

@@ -9,4 +9,5 @@ packages:
       FeedbagManager:
       SessionManager:
       ProfileManager:
-      ICBMHandler:
+      ICBMHandler:
+      LocateHandler:

+ 34 - 0
oscar/snacs.go

@@ -92,11 +92,45 @@ type SNAC_0x01_0x1E_OServiceSetUserInfoFields struct {
 //
 
 const (
+	LocateErr                  uint16 = 0x0001
+	LocateRightsQuery          uint16 = 0x0002
+	LocateRightsReply          uint16 = 0x0003
+	LocateSetInfo              uint16 = 0x0004
+	LocateUserInfoQuery        uint16 = 0x0005
+	LocateUserInfoReply        uint16 = 0x0006
+	LocateWatcherSubRequest    uint16 = 0x0007
+	LocateWatcherNotification  uint16 = 0x0008
+	LocateSetDirInfo           uint16 = 0x0009
+	LocateSetDirReply          uint16 = 0x000A
+	LocateGetDirInfo           uint16 = 0x000B
+	LocateGetDirReply          uint16 = 0x000C
+	LocateGroupCapabilityQuery uint16 = 0x000D
+	LocateGroupCapabilityReply uint16 = 0x000E
+	LocateSetKeywordInfo       uint16 = 0x000F
+	LocateSetKeywordReply      uint16 = 0x0010
+	LocateGetKeywordInfo       uint16 = 0x0011
+	LocateGetKeywordReply      uint16 = 0x0012
+	LocateFindListByEmail      uint16 = 0x0013
+	LocateFindListReply        uint16 = 0x0014
+	LocateUserInfoQuery2       uint16 = 0x0015
+
 	LocateType2Sig          uint32 = 0x00000001
 	LocateType2Unavailable  uint32 = 0x00000002
 	LocateType2Capabilities uint32 = 0x00000004
 	LocateType2Certs        uint32 = 0x00000008
 	LocateType2HtmlInfo     uint32 = 0x00000400
+
+	LocateTLVTagsInfoSigMime         uint16 = 0x01
+	LocateTLVTagsInfoSigData         uint16 = 0x02
+	LocateTLVTagsInfoUnavailableMime uint16 = 0x03
+	LocateTLVTagsInfoUnavailableData uint16 = 0x04
+	LocateTLVTagsInfoCapabilities    uint16 = 0x05
+	LocateTLVTagsInfoCerts           uint16 = 0x06
+	LocateTLVTagsInfoSigTime         uint16 = 0x0A
+	LocateTLVTagsInfoUnavailableTime uint16 = 0x0B
+	LocateTLVTagsInfoSupportHostSig  uint16 = 0x0C
+	LocateTLVTagsInfoHtmlInfoData    uint16 = 0x0E
+	LocateTLVTagsInfoHtmlInfoType    uint16 = 0x0D
 )
 
 type SNAC_0x02_0x03_LocateRightsReply struct {

+ 260 - 0
server/MockLocateHandler.go

@@ -0,0 +1,260 @@
+// 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"
+)
+
+// MockLocateHandler is an autogenerated mock type for the LocateHandler type
+type MockLocateHandler struct {
+	mock.Mock
+}
+
+type MockLocateHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockLocateHandler) EXPECT() *MockLocateHandler_Expecter {
+	return &MockLocateHandler_Expecter{mock: &_m.Mock}
+}
+
+// RightsQueryHandler provides a mock function with given fields:
+func (_m *MockLocateHandler) RightsQueryHandler() 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
+}
+
+// MockLocateHandler_RightsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RightsQueryHandler'
+type MockLocateHandler_RightsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RightsQueryHandler is a helper method to define mock.On call
+func (_e *MockLocateHandler_Expecter) RightsQueryHandler() *MockLocateHandler_RightsQueryHandler_Call {
+	return &MockLocateHandler_RightsQueryHandler_Call{Call: _e.mock.On("RightsQueryHandler")}
+}
+
+func (_c *MockLocateHandler_RightsQueryHandler_Call) Run(run func()) *MockLocateHandler_RightsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockLocateHandler_RightsQueryHandler_Call) Return(_a0 XMessage) *MockLocateHandler_RightsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockLocateHandler_RightsQueryHandler_Call) RunAndReturn(run func() XMessage) *MockLocateHandler_RightsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetDirInfoHandler provides a mock function with given fields:
+func (_m *MockLocateHandler) SetDirInfoHandler() 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
+}
+
+// MockLocateHandler_SetDirInfoHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDirInfoHandler'
+type MockLocateHandler_SetDirInfoHandler_Call struct {
+	*mock.Call
+}
+
+// SetDirInfoHandler is a helper method to define mock.On call
+func (_e *MockLocateHandler_Expecter) SetDirInfoHandler() *MockLocateHandler_SetDirInfoHandler_Call {
+	return &MockLocateHandler_SetDirInfoHandler_Call{Call: _e.mock.On("SetDirInfoHandler")}
+}
+
+func (_c *MockLocateHandler_SetDirInfoHandler_Call) Run(run func()) *MockLocateHandler_SetDirInfoHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockLocateHandler_SetDirInfoHandler_Call) Return(_a0 XMessage) *MockLocateHandler_SetDirInfoHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockLocateHandler_SetDirInfoHandler_Call) RunAndReturn(run func() XMessage) *MockLocateHandler_SetDirInfoHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetInfoHandler provides a mock function with given fields: sess, sm, fm, pm, snacPayloadIn
+func (_m *MockLocateHandler) SetInfoHandler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
+	ret := _m.Called(sess, sm, fm, pm, snacPayloadIn)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x04_LocateSetInfo) error); ok {
+		r0 = rf(sess, sm, fm, pm, snacPayloadIn)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockLocateHandler_SetInfoHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetInfoHandler'
+type MockLocateHandler_SetInfoHandler_Call struct {
+	*mock.Call
+}
+
+// SetInfoHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - sm SessionManager
+//   - fm FeedbagManager
+//   - pm ProfileManager
+//   - snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo
+func (_e *MockLocateHandler_Expecter) SetInfoHandler(sess interface{}, sm interface{}, fm interface{}, pm interface{}, snacPayloadIn interface{}) *MockLocateHandler_SetInfoHandler_Call {
+	return &MockLocateHandler_SetInfoHandler_Call{Call: _e.mock.On("SetInfoHandler", sess, sm, fm, pm, snacPayloadIn)}
+}
+
+func (_c *MockLocateHandler_SetInfoHandler_Call) Run(run func(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo)) *MockLocateHandler_SetInfoHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(SessionManager), args[2].(FeedbagManager), args[3].(ProfileManager), args[4].(oscar.SNAC_0x02_0x04_LocateSetInfo))
+	})
+	return _c
+}
+
+func (_c *MockLocateHandler_SetInfoHandler_Call) Return(_a0 error) *MockLocateHandler_SetInfoHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockLocateHandler_SetInfoHandler_Call) RunAndReturn(run func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x04_LocateSetInfo) error) *MockLocateHandler_SetInfoHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetKeywordInfoHandler provides a mock function with given fields:
+func (_m *MockLocateHandler) SetKeywordInfoHandler() 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
+}
+
+// MockLocateHandler_SetKeywordInfoHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetKeywordInfoHandler'
+type MockLocateHandler_SetKeywordInfoHandler_Call struct {
+	*mock.Call
+}
+
+// SetKeywordInfoHandler is a helper method to define mock.On call
+func (_e *MockLocateHandler_Expecter) SetKeywordInfoHandler() *MockLocateHandler_SetKeywordInfoHandler_Call {
+	return &MockLocateHandler_SetKeywordInfoHandler_Call{Call: _e.mock.On("SetKeywordInfoHandler")}
+}
+
+func (_c *MockLocateHandler_SetKeywordInfoHandler_Call) Run(run func()) *MockLocateHandler_SetKeywordInfoHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockLocateHandler_SetKeywordInfoHandler_Call) Return(_a0 XMessage) *MockLocateHandler_SetKeywordInfoHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockLocateHandler_SetKeywordInfoHandler_Call) RunAndReturn(run func() XMessage) *MockLocateHandler_SetKeywordInfoHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UserInfoQuery2Handler provides a mock function with given fields: sess, sm, fm, pm, snacPayloadIn
+func (_m *MockLocateHandler) UserInfoQuery2Handler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
+	ret := _m.Called(sess, sm, fm, pm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)); ok {
+		return rf(sess, sm, fm, pm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) XMessage); ok {
+		r0 = rf(sess, sm, fm, pm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) error); ok {
+		r1 = rf(sess, sm, fm, pm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockLocateHandler_UserInfoQuery2Handler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserInfoQuery2Handler'
+type MockLocateHandler_UserInfoQuery2Handler_Call struct {
+	*mock.Call
+}
+
+// UserInfoQuery2Handler is a helper method to define mock.On call
+//   - sess *Session
+//   - sm SessionManager
+//   - fm FeedbagManager
+//   - pm ProfileManager
+//   - snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2
+func (_e *MockLocateHandler_Expecter) UserInfoQuery2Handler(sess interface{}, sm interface{}, fm interface{}, pm interface{}, snacPayloadIn interface{}) *MockLocateHandler_UserInfoQuery2Handler_Call {
+	return &MockLocateHandler_UserInfoQuery2Handler_Call{Call: _e.mock.On("UserInfoQuery2Handler", sess, sm, fm, pm, snacPayloadIn)}
+}
+
+func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) Run(run func(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2)) *MockLocateHandler_UserInfoQuery2Handler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(SessionManager), args[2].(FeedbagManager), args[3].(ProfileManager), args[4].(oscar.SNAC_0x02_0x15_LocateUserInfoQuery2))
+	})
+	return _c
+}
+
+func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) Return(_a0 XMessage, _a1 error) *MockLocateHandler_UserInfoQuery2Handler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockLocateHandler_UserInfoQuery2Handler_Call) RunAndReturn(run func(*Session, SessionManager, FeedbagManager, ProfileManager, oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)) *MockLocateHandler_UserInfoQuery2Handler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockLocateHandler creates a new instance of MockLocateHandler. 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 NewMockLocateHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockLocateHandler {
+	mock := &MockLocateHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 155 - 185
server/locate.go

@@ -2,257 +2,227 @@ package server
 
 import (
 	"errors"
-	"fmt"
-	"github.com/mkaminski/goaim/oscar"
 	"io"
-)
 
-const (
-	LocateErr                  uint16 = 0x0001
-	LocateRightsQuery                 = 0x0002
-	LocateRightsReply                 = 0x0003
-	LocateSetInfo                     = 0x0004
-	LocateUserInfoQuery               = 0x0005
-	LocateUserInfoReply               = 0x0006
-	LocateWatcherSubRequest           = 0x0007
-	LocateWatcherNotification         = 0x0008
-	LocateSetDirInfo                  = 0x0009
-	LocateSetDirReply                 = 0x000A
-	LocateGetDirInfo                  = 0x000B
-	LocateGetDirReply                 = 0x000C
-	LocateGroupCapabilityQuery        = 0x000D
-	LocateGroupCapabilityReply        = 0x000E
-	LocateSetKeywordInfo              = 0x000F
-	LocateSetKeywordReply             = 0x0010
-	LocateGetKeywordInfo              = 0x0011
-	LocateGetKeywordReply             = 0x0012
-	LocateFindListByEmail             = 0x0013
-	LocateFindListReply               = 0x0014
-	LocateUserInfoQuery2              = 0x0015
+	"github.com/mkaminski/goaim/oscar"
 )
 
-const (
-	LocateTLVTagsInfoSigMime         uint16 = 0x01
-	LocateTLVTagsInfoSigData         uint16 = 0x02
-	LocateTLVTagsInfoUnavailableMime uint16 = 0x03
-	LocateTLVTagsInfoUnavailableData uint16 = 0x04
-	LocateTLVTagsInfoCapabilities    uint16 = 0x05
-	LocateTLVTagsInfoCerts           uint16 = 0x06
-	LocateTLVTagsInfoSigTime         uint16 = 0x0A
-	LocateTLVTagsInfoUnavailableTime uint16 = 0x0B
-	LocateTLVTagsInfoSupportHostSig  uint16 = 0x0C
-	LocateTLVTagsInfoHtmlInfoData    uint16 = 0x0E
-	LocateTLVTagsInfoHtmlInfoType    uint16 = 0x0D
-)
+type LocateHandler interface {
+	RightsQueryHandler() XMessage
+	SetDirInfoHandler() XMessage
+	SetInfoHandler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error
+	SetKeywordInfoHandler() XMessage
+	UserInfoQuery2Handler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error)
+}
+
+func NewLocateRouter() LocateRouter {
+	return LocateRouter{
+		LocateHandler: LocateService{},
+	}
+}
+
+type LocateRouter struct {
+	LocateHandler
+}
 
-func routeLocate(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt LocateRouter) RouteLocate(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
-	case LocateRightsQuery:
-		return SendAndReceiveLocateRights(snac, w, sequence)
-	case LocateSetInfo:
-		return ReceiveSetInfo(sess, sm, fm, snac, r)
-	case LocateSetDirInfo:
-		return SendAndReceiveSetDirInfo(snac, r, w, sequence)
-	case LocateGetDirInfo:
-		return ReceiveLocateGetDirInfo(snac, r)
-	case LocateSetKeywordInfo:
-		return SendAndReceiveSetKeywordInfo(snac, r, w, sequence)
-	case LocateUserInfoQuery2:
-		return SendAndReceiveUserInfoQuery2(sess, sm, fm, fm, snac, r, w, sequence)
+	case oscar.LocateRightsQuery:
+		outSNAC := rt.RightsQueryHandler()
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.LocateSetInfo:
+		snacPayloadIn := oscar.SNAC_0x02_0x04_LocateSetInfo{}
+		if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+			return err
+		}
+		return rt.SetInfoHandler(sess, sm, fm, fm, snacPayloadIn)
+	case oscar.LocateSetDirInfo:
+		snacPayloadIn := oscar.SNAC_0x02_0x09_LocateSetDirInfo{}
+		if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+			return err
+		}
+		outSNAC := rt.SetDirInfoHandler()
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.LocateGetDirInfo:
+		snacPayloadIn := oscar.SNAC_0x02_0x0B_LocateGetDirInfo{}
+		return oscar.Unmarshal(&snacPayloadIn, r)
+	case oscar.LocateSetKeywordInfo:
+		snacPayloadIn := oscar.SNAC_0x02_0x0F_LocateSetKeywordInfo{}
+		if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+			return err
+		}
+		outSNAC := rt.SetKeywordInfoHandler()
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.LocateUserInfoQuery2:
+		snacPayloadIn := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}
+		if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.UserInfoQuery2Handler(sess, sm, fm, fm, snacPayloadIn)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(snac, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}
 }
 
-func SendAndReceiveLocateRights(snac oscar.SnacFrame, w io.Writer, sequence *uint32) error {
-	fmt.Printf("sendAndReceiveLocateRights read SNAC frame: %+v\n", snac)
+type LocateService struct {
+}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: LOCATE,
-		SubGroup:  LocateRightsReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x02_0x03_LocateRightsReply{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: 0x01,
-					Val:   uint16(1000),
-				},
-				{
-					TType: 0x02,
-					Val:   uint16(1000),
-				},
-				{
-					TType: 0x03,
-					Val:   uint16(1000),
-				},
-				{
-					TType: 0x04,
-					Val:   uint16(1000),
-				},
-				{
-					TType: 0x05,
-					Val:   uint16(1000),
+func (s LocateService) RightsQueryHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: LOCATE,
+			SubGroup:  oscar.LocateRightsReply,
+		},
+		snacOut: oscar.SNAC_0x02_0x03_LocateRightsReply{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: 0x01,
+						Val:   uint16(1000),
+					},
+					{
+						TType: 0x02,
+						Val:   uint16(1000),
+					},
+					{
+						TType: 0x03,
+						Val:   uint16(1000),
+					},
+					{
+						TType: 0x04,
+						Val:   uint16(1000),
+					},
+					{
+						TType: 0x05,
+						Val:   uint16(1000),
+					},
 				},
 			},
 		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveSetInfo(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("ReceiveSetInfo read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x02_0x04_LocateSetInfo{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s LocateService) SetInfoHandler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x04_LocateSetInfo) error {
 	// update profile
-	if profile, hasProfile := snacPayloadIn.GetString(LocateTLVTagsInfoSigData); hasProfile {
-		if err := fm.UpsertProfile(sess.ScreenName, profile); err != nil {
+	if profile, hasProfile := snacPayloadIn.GetString(oscar.LocateTLVTagsInfoSigData); hasProfile {
+		if err := pm.UpsertProfile(sess.ScreenName, profile); err != nil {
 			return err
 		}
 	}
 
 	// broadcast away message change to buddies
-	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(LocateTLVTagsInfoUnavailableData); hasAwayMsg {
+	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(oscar.LocateTLVTagsInfoUnavailableData); hasAwayMsg {
 		sess.SetAwayMessage(awayMsg)
 		if err := NotifyArrival(sess, sm, fm); err != nil {
 			return err
 		}
 	}
-
-	fmt.Printf("ReceiveSetInfo read SNAC: %+v\n", snacPayloadIn)
-
-	return nil
-}
-
-func ReceiveLocateGetDirInfo(snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("ReceiveLocateGetDirInfo read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x02_0x0B_LocateGetDirInfo{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	fmt.Printf("ReceiveLocateGetDirInfo read SNAC: %+v\n", snacPayloadIn)
-
 	return nil
 }
 
-func SendAndReceiveUserInfoQuery2(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s LocateService) UserInfoQuery2Handler(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snacPayloadIn oscar.SNAC_0x02_0x15_LocateUserInfoQuery2) (XMessage, error) {
 	blocked, err := fm.Blocked(sess.ScreenName, snacPayloadIn.ScreenName)
-	if err != nil {
-		return err
-	}
-	if blocked != BlockedNo {
-		snacFrameOut := oscar.SnacFrame{
-			FoodGroup: LOCATE,
-			SubGroup:  LocateErr,
-		}
-		snacPayloadOut := oscar.SnacError{
-			Code: ErrorCodeNotLoggedOn,
-		}
-		return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	switch {
+	case err != nil:
+		return XMessage{}, nil
+	case blocked != BlockedNo:
+		return XMessage{
+			snacFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  oscar.LocateErr,
+			},
+			snacOut: oscar.SnacError{
+				Code: ErrorCodeNotLoggedOn,
+			},
+		}, nil
 	}
 
 	buddySess, err := sm.RetrieveByScreenName(snacPayloadIn.ScreenName)
-	if err != nil {
-		if errors.Is(err, errSessNotFound) {
-			snacFrameOut := oscar.SnacFrame{
+	switch {
+	case errors.Is(err, errSessNotFound):
+		return XMessage{
+			snacFrame: oscar.SnacFrame{
 				FoodGroup: LOCATE,
-				SubGroup:  LocateErr,
-			}
-			snacPayloadOut := oscar.SnacError{
+				SubGroup:  oscar.LocateErr,
+			},
+			snacOut: oscar.SnacError{
 				Code: ErrorCodeNotLoggedOn,
-			}
-			return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-		}
-		return err
+			},
+		}, nil
+	case err != nil:
+		return XMessage{}, err
 	}
 
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: LOCATE,
-		SubGroup:  LocateUserInfoReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-		TLVUserInfo: buddySess.GetTLVUserInfo(),
-	}
+	var list oscar.TLVList
 
 	if snacPayloadIn.RequestProfile() {
 		profile, err := pm.RetrieveProfile(snacPayloadIn.ScreenName)
 		if err != nil {
-			return err
+			return XMessage{}, err
 		}
-		snacPayloadOut.LocateInfo.TLVList = append(snacPayloadOut.LocateInfo.TLVList, []oscar.TLV{
+		list.AddTLVList([]oscar.TLV{
 			{
-				TType: LocateTLVTagsInfoSigMime,
+				TType: oscar.LocateTLVTagsInfoSigMime,
 				Val:   `text/aolrtf; charset="us-ascii"`,
 			},
 			{
-				TType: LocateTLVTagsInfoSigData,
+				TType: oscar.LocateTLVTagsInfoSigData,
 				Val:   profile,
 			},
-		}...)
+		})
 	}
 
 	if snacPayloadIn.RequestAwayMessage() {
-		snacPayloadOut.LocateInfo.TLVList = append(snacPayloadOut.LocateInfo.TLVList, []oscar.TLV{
+		list.AddTLVList([]oscar.TLV{
 			{
-				TType: LocateTLVTagsInfoUnavailableMime,
+				TType: oscar.LocateTLVTagsInfoUnavailableMime,
 				Val:   `text/aolrtf; charset="us-ascii"`,
 			},
 			{
-				TType: LocateTLVTagsInfoUnavailableData,
+				TType: oscar.LocateTLVTagsInfoUnavailableData,
 				Val:   buddySess.GetAwayMessage(),
 			},
-		}...)
+		})
 	}
 
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: LOCATE,
+			SubGroup:  oscar.LocateUserInfoReply,
+		},
+		snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+			TLVUserInfo: buddySess.GetTLVUserInfo(),
+			LocateInfo: oscar.TLVRestBlock{
+				TLVList: list,
+			},
+		},
+	}, nil
 }
 
-func SendAndReceiveSetDirInfo(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveSetDirInfo read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x02_0x09_LocateSetDirInfo{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: LOCATE,
-		SubGroup:  LocateSetDirReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x02_0x0A_LocateSetDirReply{
-		Result: 1,
+func (s LocateService) SetDirInfoHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: LOCATE,
+			SubGroup:  oscar.LocateSetDirReply,
+		},
+		snacOut: oscar.SNAC_0x02_0x0A_LocateSetDirReply{
+			Result: 1,
+		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func SendAndReceiveSetKeywordInfo(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveSetKeywordInfo read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x02_0x0F_LocateSetKeywordInfo{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: LOCATE,
-		SubGroup:  LocateSetKeywordReply,
-	}
-	snacPayloadOut := oscar.SNAC_0x02_0x10_LocateSetKeywordReply{
-		Unknown: 1,
+func (s LocateService) SetKeywordInfoHandler() XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: LOCATE,
+			SubGroup:  oscar.LocateSetKeywordReply,
+		},
+		snacOut: oscar.SNAC_0x02_0x10_LocateSetKeywordReply{
+			Unknown: 1,
+		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }

+ 335 - 121
server/locate_test.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"github.com/mkaminski/goaim/oscar"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"testing"
 	"time"
 )
@@ -34,14 +35,9 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 		}
 		// userSession is the session of the user requesting the user info
 		userSession *Session
-		// expectSNACFrame is the SNAC frame sent from the server to the recipient
-		// client
-		expectSNACFrame oscar.SnacFrame
 		// inputSNAC is the SNAC sent by the sender client
-		inputSNAC oscar.SNAC_0x02_0x15_LocateUserInfoQuery2
-		// expectSNACBody is the SNAC payload sent from the server to the
-		// recipient client
-		expectSNACBody any
+		inputSNAC    oscar.SNAC_0x02_0x15_LocateUserInfoQuery2
+		expectOutput XMessage
 	}{
 		{
 			name:         "request user info, expect user info response",
@@ -60,20 +56,22 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateUserInfoReply,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				Type2:      0,
 				ScreenName: "requested-user",
 			},
-			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-				TLVUserInfo: newTestSession(Session{
-					ScreenName:  "requested-user",
-					AwayMessage: "this is my away message!",
-				}).GetTLVUserInfo(),
-				LocateInfo: oscar.TLVRestBlock{},
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+					TLVUserInfo: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}).GetTLVUserInfo(),
+					LocateInfo: oscar.TLVRestBlock{},
+				},
 			},
 		},
 		{
@@ -101,29 +99,31 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateUserInfoReply,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				// 2048 is a dummy to make sure bitmask check works
 				Type2:      oscar.LocateType2Sig | 2048,
 				ScreenName: "requested-user",
 			},
-			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-				TLVUserInfo: newTestSession(Session{
-					ScreenName:  "requested-user",
-					AwayMessage: "this is my away message!",
-				}).GetTLVUserInfo(),
-				LocateInfo: oscar.TLVRestBlock{
-					TLVList: oscar.TLVList{
-						{
-							TType: LocateTLVTagsInfoSigMime,
-							Val:   `text/aolrtf; charset="us-ascii"`,
-						},
-						{
-							TType: LocateTLVTagsInfoSigData,
-							Val:   "this is my profile!",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+					TLVUserInfo: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}).GetTLVUserInfo(),
+					LocateInfo: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: oscar.LocateTLVTagsInfoSigMime,
+								Val:   `text/aolrtf; charset="us-ascii"`,
+							},
+							{
+								TType: oscar.LocateTLVTagsInfoSigData,
+								Val:   "this is my profile!",
+							},
 						},
 					},
 				},
@@ -154,29 +154,31 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateUserInfoReply,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				// 2048 is a dummy to make sure bitmask check works
 				Type2:      oscar.LocateType2Sig | 2048,
 				ScreenName: "requested-user",
 			},
-			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-				TLVUserInfo: newTestSession(Session{
-					ScreenName:  "requested-user",
-					AwayMessage: "this is my away message!",
-				}).GetTLVUserInfo(),
-				LocateInfo: oscar.TLVRestBlock{
-					TLVList: oscar.TLVList{
-						{
-							TType: LocateTLVTagsInfoSigMime,
-							Val:   `text/aolrtf; charset="us-ascii"`,
-						},
-						{
-							TType: LocateTLVTagsInfoSigData,
-							Val:   "this is my profile!",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+					TLVUserInfo: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}).GetTLVUserInfo(),
+					LocateInfo: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: oscar.LocateTLVTagsInfoSigMime,
+								Val:   `text/aolrtf; charset="us-ascii"`,
+							},
+							{
+								TType: oscar.LocateTLVTagsInfoSigData,
+								Val:   "this is my profile!",
+							},
 						},
 					},
 				},
@@ -199,29 +201,31 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateUserInfoReply,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				// 2048 is a dummy to make sure bitmask check works
 				Type2:      oscar.LocateType2Unavailable | 2048,
 				ScreenName: "requested-user",
 			},
-			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-				TLVUserInfo: newTestSession(Session{
-					ScreenName:  "requested-user",
-					AwayMessage: "this is my away message!",
-				}).GetTLVUserInfo(),
-				LocateInfo: oscar.TLVRestBlock{
-					TLVList: oscar.TLVList{
-						{
-							TType: LocateTLVTagsInfoUnavailableMime,
-							Val:   `text/aolrtf; charset="us-ascii"`,
-						},
-						{
-							TType: LocateTLVTagsInfoUnavailableData,
-							Val:   "this is my away message!",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+					TLVUserInfo: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}).GetTLVUserInfo(),
+					LocateInfo: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: oscar.LocateTLVTagsInfoUnavailableMime,
+								Val:   `text/aolrtf; charset="us-ascii"`,
+							},
+							{
+								TType: oscar.LocateTLVTagsInfoUnavailableData,
+								Val:   "this is my away message!",
+							},
 						},
 					},
 				},
@@ -233,15 +237,17 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateErr,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				ScreenName: "requested-user",
 			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 		{
@@ -258,24 +264,23 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 			userSession: &Session{
 				ScreenName: "user_screen_name",
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateErr,
-			},
 			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
 				ScreenName: "non_existent_requested_user",
 			},
-			expectSNACBody: oscar.SnacError{
-				Code: ErrorCodeNotLoggedOn,
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateErr,
+				},
+				snacOut: oscar.SnacError{
+					Code: ErrorCodeNotLoggedOn,
+				},
 			},
 		},
 	}
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			//
-			// initialize dependencies
-			//
 			fm := NewMockFeedbagManager(t)
 			fm.EXPECT().
 				Blocked(tc.userSession.ScreenName, tc.inputSNAC.ScreenName).
@@ -295,44 +300,253 @@ func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
 					Return(val.payload, val.err).
 					Maybe()
 			}
-			//
-			// send input SNAC
-			//
-			input := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			snac := oscar.SnacFrame{
-				FoodGroup: LOCATE,
-				SubGroup:  LocateUserInfoQuery2,
+
+			svc := LocateService{}
+			outputSNAC, err := svc.UserInfoQuery2Handler(tc.userSession, sm, fm, pm, tc.inputSNAC)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
+
+func TestLocateRouter_RouteLocate(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 LocateRightsQuery, return LocateRightsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateRightsQuery,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateRightsReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x03_LocateRightsReply{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   uint16(1000),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive LocateSetInfo, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateSetInfo,
+				},
+				snacOut: oscar.SNAC_0x02_0x04_LocateSetInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{},
+			},
+		},
+		{
+			name: "receive LocateSetDirInfo, return LocateSetDirReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateSetDirInfo,
+				},
+				snacOut: oscar.SNAC_0x02_0x09_LocateSetDirInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateSetDirReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x0A_LocateSetDirReply{
+					Result: 1,
+				},
+			},
+		},
+		{
+			name: "receive LocateGetDirInfo, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateGetDirInfo,
+				},
+				snacOut: oscar.SNAC_0x02_0x0B_LocateGetDirInfo{
+					WatcherScreenNames: "screen-name",
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{},
+			},
+		},
+		{
+			name: "receive LocateSetKeywordInfo, return LocateSetKeywordReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateSetKeywordInfo,
+				},
+				snacOut: oscar.SNAC_0x02_0x0F_LocateSetKeywordInfo{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateSetKeywordReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x10_LocateSetKeywordReply{
+					Unknown: 1,
+				},
+			},
+		},
+		{
+			name: "receive LocateUserInfoQuery2, return LocateUserInfoReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoQuery2,
+				},
+				snacOut: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+					Type2: 1,
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateUserInfoReply,
+				},
+				snacOut: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
+					},
+					LocateInfo: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive LocateGetKeywordInfo, expect ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: LOCATE,
+					SubGroup:  oscar.LocateGetKeywordInfo,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output:    XMessage{}, // empty SNAC
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockLocateHandler(t)
+			svc.EXPECT().
+				RightsQueryHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				SetDirInfoHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				SetInfoHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				SetKeywordInfoHandler().
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				UserInfoQuery2Handler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.output, tc.handlerErr).
+				Maybe()
+
+			router := LocateRouter{
+				LocateHandler: svc,
+			}
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(1)
+
+			err := router.RouteLocate(nil, 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 // handler doesn't return response
 			}
-			assert.NoError(t, SendAndReceiveUserInfoQuery2(tc.userSession, sm, fm, pm, snac, input, output, &seq))
-			//
-			// verify output
-			//
+
+			// make sure the sequence number was incremented
+			assert.Equal(t, uint32(2), seq)
+
+			// verify the FLAP frame
 			flap := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flap, output))
+			assert.NoError(t, oscar.Unmarshal(&flap, bufOut))
+			assert.Equal(t, uint16(1), flap.Sequence)
+
+			// verify the SNAC frame
 			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_0x02_0x06_LocateUserInfoReply:
-				assert.NoError(t, v.SerializeInPlace())
-				assert.NoError(t, v.LocateInfo.SerializeInPlace())
-				outputSNAC := oscar.SNAC_0x02_0x06_LocateUserInfoReply{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			case oscar.SnacError:
-				outputSNAC := oscar.SnacError{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, v, outputSNAC)
-			default:
-				t.Fatalf("unexpected output SNAC type")
-			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, bufOut))
+			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(), bufOut.Bytes())
 		})
 	}
 }

+ 4 - 2
server/protocol.go

@@ -311,12 +311,14 @@ func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm SessionM
 
 func NewRouter() Router {
 	return Router{
-		ICBMRouter: NewICBMRouter(),
+		ICBMRouter:   NewICBMRouter(),
+		LocateRouter: NewLocateRouter(),
 	}
 }
 
 type Router struct {
 	ICBMRouter
+	LocateRouter
 }
 
 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 {
@@ -326,7 +328,7 @@ func (rt *Router) routeIncomingRequests(cfg Config, ready OnReadyCB, sm SessionM
 			return err
 		}
 	case LOCATE:
-		if err := routeLocate(sess, sm, fm, snac, buf, rw, sequence); err != nil {
+		if err := rt.RouteLocate(sess, sm, fm, snac, buf, rw, sequence); err != nil {
 			return err
 		}
 	case BUDDY: