Mike 2 лет назад
Родитель
Сommit
c027982396
8 измененных файлов с 956 добавлено и 299 удалено
  1. 2 1
      .mockery.yaml
  2. 44 0
      oscar/snacs.go
  3. 398 0
      server/MockOServiceHandler.go
  4. 1 1
      server/icbm.go
  5. 2 2
      server/icbm_test.go
  6. 170 215
      server/oservice.go
  7. 332 75
      server/oservice_test.go
  8. 7 5
      server/protocol.go

+ 2 - 1
.mockery.yaml

@@ -10,4 +10,5 @@ packages:
       SessionManager:
       ProfileManager:
       ICBMHandler:
-      LocateHandler:
+      LocateHandler:
+      OServiceHandler:

+ 44 - 0
oscar/snacs.go

@@ -4,6 +4,50 @@ package oscar
 // 0x01: OService
 //
 
+const (
+	OServiceErr               uint16 = 0x0001
+	OServiceClientOnline      uint16 = 0x0002
+	OServiceHostOnline        uint16 = 0x0003
+	OServiceServiceRequest    uint16 = 0x0004
+	OServiceServiceResponse   uint16 = 0x0005
+	OServiceRateParamsQuery   uint16 = 0x0006
+	OServiceRateParamsReply   uint16 = 0x0007
+	OServiceRateParamsSubAdd  uint16 = 0x0008
+	OServiceRateDelParamSub   uint16 = 0x0009
+	OServiceRateParamChange   uint16 = 0x000A
+	OServicePauseReq          uint16 = 0x000B
+	OServicePauseAck          uint16 = 0x000C
+	OServiceResume            uint16 = 0x000D
+	OServiceUserInfoQuery     uint16 = 0x000E
+	OServiceUserInfoUpdate    uint16 = 0x000F
+	OServiceEvilNotification  uint16 = 0x0010
+	OServiceIdleNotification  uint16 = 0x0011
+	OServiceMigrateGroups     uint16 = 0x0012
+	OServiceMotd              uint16 = 0x0013
+	OServiceSetPrivacyFlags   uint16 = 0x0014
+	OServiceWellKnownUrls     uint16 = 0x0015
+	OServiceNoop              uint16 = 0x0016
+	OServiceClientVersions    uint16 = 0x0017
+	OServiceHostVersions      uint16 = 0x0018
+	OServiceMaxConfigQuery    uint16 = 0x0019
+	OServiceMaxConfigReply    uint16 = 0x001A
+	OServiceStoreConfig       uint16 = 0x001B
+	OServiceConfigQuery       uint16 = 0x001C
+	OServiceConfigReply       uint16 = 0x001D
+	OServiceSetUserInfoFields uint16 = 0x001E
+	OServiceProbeReq          uint16 = 0x001F
+	OServiceProbeAck          uint16 = 0x0020
+	OServiceBartReply         uint16 = 0x0021
+	OServiceBartQuery2        uint16 = 0x0022
+	OServiceBartReply2        uint16 = 0x0023
+
+	OServiceTLVTagsReconnectHere uint16 = 0x05
+	OServiceTLVTagsLoginCookie   uint16 = 0x06
+	OServiceTLVTagsGroupID       uint16 = 0x0D
+	OServiceTLVTagsSSLCertName   uint16 = 0x8D
+	OServiceTLVTagsSSLState      uint16 = 0x8E
+)
+
 type SNAC_0x01_0x02_OServiceClientOnline struct {
 	GroupVersions []struct {
 		FoodGroup   uint16

+ 398 - 0
server/MockOServiceHandler.go

@@ -0,0 +1,398 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	io "io"
+
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// MockOServiceHandler is an autogenerated mock type for the OServiceHandler type
+type MockOServiceHandler struct {
+	mock.Mock
+}
+
+type MockOServiceHandler_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockOServiceHandler) EXPECT() *MockOServiceHandler_Expecter {
+	return &MockOServiceHandler_Expecter{mock: &_m.Mock}
+}
+
+// ClientOnlineHandler provides a mock function with given fields: snacPayloadIn, onReadyCB, sess, sm, r, w, sequence
+func (_m *MockOServiceHandler) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+	ret := _m.Called(snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x02_OServiceClientOnline, OnReadyCB, *Session, SessionManager, io.Reader, io.Writer, *uint32) error); ok {
+		r0 = rf(snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceHandler_ClientOnlineHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientOnlineHandler'
+type MockOServiceHandler_ClientOnlineHandler_Call struct {
+	*mock.Call
+}
+
+// ClientOnlineHandler is a helper method to define mock.On call
+//   - snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline
+//   - onReadyCB OnReadyCB
+//   - sess *Session
+//   - sm SessionManager
+//   - r io.Reader
+//   - w io.Writer
+//   - sequence *uint32
+func (_e *MockOServiceHandler_Expecter) ClientOnlineHandler(snacPayloadIn interface{}, onReadyCB interface{}, sess interface{}, sm interface{}, r interface{}, w interface{}, sequence interface{}) *MockOServiceHandler_ClientOnlineHandler_Call {
+	return &MockOServiceHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", snacPayloadIn, onReadyCB, sess, sm, r, w, sequence)}
+}
+
+func (_c *MockOServiceHandler_ClientOnlineHandler_Call) Run(run func(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32)) *MockOServiceHandler_ClientOnlineHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[1].(OnReadyCB), args[2].(*Session), args[3].(SessionManager), args[4].(io.Reader), args[5].(io.Writer), args[6].(*uint32))
+	})
+	return _c
+}
+
+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, io.Reader, io.Writer, *uint32) error) *MockOServiceHandler_ClientOnlineHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ClientVersionsHandler provides a mock function with given fields: snacPayloadIn
+func (_m *MockOServiceHandler) ClientVersionsHandler(snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
+	ret := _m.Called(snacPayloadIn)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage); ok {
+		r0 = rf(snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceHandler_ClientVersionsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClientVersionsHandler'
+type MockOServiceHandler_ClientVersionsHandler_Call struct {
+	*mock.Call
+}
+
+// ClientVersionsHandler is a helper method to define mock.On call
+//   - snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions
+func (_e *MockOServiceHandler_Expecter) ClientVersionsHandler(snacPayloadIn interface{}) *MockOServiceHandler_ClientVersionsHandler_Call {
+	return &MockOServiceHandler_ClientVersionsHandler_Call{Call: _e.mock.On("ClientVersionsHandler", snacPayloadIn)}
+}
+
+func (_c *MockOServiceHandler_ClientVersionsHandler_Call) Run(run func(snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions)) *MockOServiceHandler_ClientVersionsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(oscar.SNAC_0x01_0x17_OServiceClientVersions))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_ClientVersionsHandler_Call) Return(_a0 XMessage) *MockOServiceHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceHandler_ClientVersionsHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage) *MockOServiceHandler_ClientVersionsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// IdleNotificationHandler provides a mock function with given fields: sess, sm, fm, snacPayloadIn
+func (_m *MockOServiceHandler) IdleNotificationHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
+	ret := _m.Called(sess, sm, fm, snacPayloadIn)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error); ok {
+		r0 = rf(sess, sm, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockOServiceHandler_IdleNotificationHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IdleNotificationHandler'
+type MockOServiceHandler_IdleNotificationHandler_Call struct {
+	*mock.Call
+}
+
+// IdleNotificationHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - sm SessionManager
+//   - fm *FeedbagStore
+//   - snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification
+func (_e *MockOServiceHandler_Expecter) IdleNotificationHandler(sess interface{}, sm interface{}, fm interface{}, snacPayloadIn interface{}) *MockOServiceHandler_IdleNotificationHandler_Call {
+	return &MockOServiceHandler_IdleNotificationHandler_Call{Call: _e.mock.On("IdleNotificationHandler", sess, sm, fm, snacPayloadIn)}
+}
+
+func (_c *MockOServiceHandler_IdleNotificationHandler_Call) Run(run func(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification)) *MockOServiceHandler_IdleNotificationHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(SessionManager), args[2].(*FeedbagStore), args[3].(oscar.SNAC_0x01_0x11_OServiceIdleNotification))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_IdleNotificationHandler_Call) Return(_a0 error) *MockOServiceHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceHandler_IdleNotificationHandler_Call) RunAndReturn(run func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x11_OServiceIdleNotification) error) *MockOServiceHandler_IdleNotificationHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsQueryHandler provides a mock function with given fields:
+func (_m *MockOServiceHandler) RateParamsQueryHandler() 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
+}
+
+// MockOServiceHandler_RateParamsQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsQueryHandler'
+type MockOServiceHandler_RateParamsQueryHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsQueryHandler is a helper method to define mock.On call
+func (_e *MockOServiceHandler_Expecter) RateParamsQueryHandler() *MockOServiceHandler_RateParamsQueryHandler_Call {
+	return &MockOServiceHandler_RateParamsQueryHandler_Call{Call: _e.mock.On("RateParamsQueryHandler")}
+}
+
+func (_c *MockOServiceHandler_RateParamsQueryHandler_Call) Run(run func()) *MockOServiceHandler_RateParamsQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_RateParamsQueryHandler_Call) Return(_a0 XMessage) *MockOServiceHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceHandler_RateParamsQueryHandler_Call) RunAndReturn(run func() XMessage) *MockOServiceHandler_RateParamsQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RateParamsSubAddHandler provides a mock function with given fields: _a0
+func (_m *MockOServiceHandler) RateParamsSubAddHandler(_a0 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
+	_m.Called(_a0)
+}
+
+// MockOServiceHandler_RateParamsSubAddHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateParamsSubAddHandler'
+type MockOServiceHandler_RateParamsSubAddHandler_Call struct {
+	*mock.Call
+}
+
+// RateParamsSubAddHandler is a helper method to define mock.On call
+//   - _a0 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd
+func (_e *MockOServiceHandler_Expecter) RateParamsSubAddHandler(_a0 interface{}) *MockOServiceHandler_RateParamsSubAddHandler_Call {
+	return &MockOServiceHandler_RateParamsSubAddHandler_Call{Call: _e.mock.On("RateParamsSubAddHandler", _a0)}
+}
+
+func (_c *MockOServiceHandler_RateParamsSubAddHandler_Call) Run(run func(_a0 oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_RateParamsSubAddHandler_Call) Return() *MockOServiceHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockOServiceHandler_RateParamsSubAddHandler_Call) RunAndReturn(run func(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)) *MockOServiceHandler_RateParamsSubAddHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// ServiceRequestHandler provides a mock function with given fields: cfg, cr, sess, snacPayloadIn
+func (_m *MockOServiceHandler) ServiceRequestHandler(cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+	ret := _m.Called(cfg, cr, sess, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)); ok {
+		return rf(cfg, cr, sess, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) XMessage); ok {
+		r0 = rf(cfg, cr, sess, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) error); ok {
+		r1 = rf(cfg, cr, sess, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceHandler_ServiceRequestHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ServiceRequestHandler'
+type MockOServiceHandler_ServiceRequestHandler_Call struct {
+	*mock.Call
+}
+
+// ServiceRequestHandler is a helper method to define mock.On call
+//   - cfg Config
+//   - cr *ChatRegistry
+//   - sess *Session
+//   - snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest
+func (_e *MockOServiceHandler_Expecter) ServiceRequestHandler(cfg interface{}, cr interface{}, sess interface{}, snacPayloadIn interface{}) *MockOServiceHandler_ServiceRequestHandler_Call {
+	return &MockOServiceHandler_ServiceRequestHandler_Call{Call: _e.mock.On("ServiceRequestHandler", cfg, cr, sess, snacPayloadIn)}
+}
+
+func (_c *MockOServiceHandler_ServiceRequestHandler_Call) Run(run func(cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest)) *MockOServiceHandler_ServiceRequestHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(Config), args[1].(*ChatRegistry), args[2].(*Session), args[3].(oscar.SNAC_0x01_0x04_OServiceServiceRequest))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_ServiceRequestHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceHandler_ServiceRequestHandler_Call) RunAndReturn(run func(Config, *ChatRegistry, *Session, oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)) *MockOServiceHandler_ServiceRequestHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SetUserInfoFieldsHandler provides a mock function with given fields: sess, sm, fm, snacPayloadIn
+func (_m *MockOServiceHandler) SetUserInfoFieldsHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
+	ret := _m.Called(sess, sm, fm, snacPayloadIn)
+
+	var r0 XMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)); ok {
+		return rf(sess, sm, fm, snacPayloadIn)
+	}
+	if rf, ok := ret.Get(0).(func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) XMessage); ok {
+		r0 = rf(sess, sm, fm, snacPayloadIn)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) error); ok {
+		r1 = rf(sess, sm, fm, snacPayloadIn)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockOServiceHandler_SetUserInfoFieldsHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUserInfoFieldsHandler'
+type MockOServiceHandler_SetUserInfoFieldsHandler_Call struct {
+	*mock.Call
+}
+
+// SetUserInfoFieldsHandler is a helper method to define mock.On call
+//   - sess *Session
+//   - sm SessionManager
+//   - fm *FeedbagStore
+//   - snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields
+func (_e *MockOServiceHandler_Expecter) SetUserInfoFieldsHandler(sess interface{}, sm interface{}, fm interface{}, snacPayloadIn interface{}) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+	return &MockOServiceHandler_SetUserInfoFieldsHandler_Call{Call: _e.mock.On("SetUserInfoFieldsHandler", sess, sm, fm, snacPayloadIn)}
+}
+
+func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) Run(run func(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(SessionManager), args[2].(*FeedbagStore), args[3].(oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) Return(_a0 XMessage, _a1 error) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockOServiceHandler_SetUserInfoFieldsHandler_Call) RunAndReturn(run func(*Session, SessionManager, *FeedbagStore, oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)) *MockOServiceHandler_SetUserInfoFieldsHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UserInfoQueryHandler provides a mock function with given fields: sess
+func (_m *MockOServiceHandler) UserInfoQueryHandler(sess *Session) XMessage {
+	ret := _m.Called(sess)
+
+	var r0 XMessage
+	if rf, ok := ret.Get(0).(func(*Session) XMessage); ok {
+		r0 = rf(sess)
+	} else {
+		r0 = ret.Get(0).(XMessage)
+	}
+
+	return r0
+}
+
+// MockOServiceHandler_UserInfoQueryHandler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UserInfoQueryHandler'
+type MockOServiceHandler_UserInfoQueryHandler_Call struct {
+	*mock.Call
+}
+
+// UserInfoQueryHandler is a helper method to define mock.On call
+//   - sess *Session
+func (_e *MockOServiceHandler_Expecter) UserInfoQueryHandler(sess interface{}) *MockOServiceHandler_UserInfoQueryHandler_Call {
+	return &MockOServiceHandler_UserInfoQueryHandler_Call{Call: _e.mock.On("UserInfoQueryHandler", sess)}
+}
+
+func (_c *MockOServiceHandler_UserInfoQueryHandler_Call) Run(run func(sess *Session)) *MockOServiceHandler_UserInfoQueryHandler_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session))
+	})
+	return _c
+}
+
+func (_c *MockOServiceHandler_UserInfoQueryHandler_Call) Return(_a0 XMessage) *MockOServiceHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockOServiceHandler_UserInfoQueryHandler_Call) RunAndReturn(run func(*Session) XMessage) *MockOServiceHandler_UserInfoQueryHandler_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockOServiceHandler creates a new instance of MockOServiceHandler. 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 NewMockOServiceHandler(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockOServiceHandler {
+	mock := &MockOServiceHandler{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 1 - 1
server/icbm.go

@@ -267,7 +267,7 @@ func (s ICBMService) EvilRequestHandler(sm SessionManager, fm FeedbagManager, se
 	sm.SendToScreenName(recipSess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: OSERVICE,
-			SubGroup:  OServiceEvilNotification,
+			SubGroup:  oscar.OServiceEvilNotification,
 		},
 		snacOut: notif,
 	})

+ 2 - 2
server/icbm_test.go

@@ -385,7 +385,7 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			expectSNACToClient: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: OSERVICE,
-					SubGroup:  OServiceEvilNotification,
+					SubGroup:  oscar.OServiceEvilNotification,
 				},
 				snacOut: oscar.SNAC_0x01_0x10_OServiceEvilNotificationAnon{
 					NewEvil: evilDeltaAnon,
@@ -432,7 +432,7 @@ func TestSendAndReceiveEvilRequest(t *testing.T) {
 			expectSNACToClient: XMessage{
 				snacFrame: oscar.SnacFrame{
 					FoodGroup: OSERVICE,
-					SubGroup:  OServiceEvilNotification,
+					SubGroup:  oscar.OServiceEvilNotification,
 				},
 				snacOut: oscar.SNAC_0x01_0x10_OServiceEvilNotification{
 					NewEvil: evilDelta,

+ 170 - 215
server/oservice.go

@@ -9,72 +9,97 @@ import (
 	"time"
 )
 
-const (
-	OServiceErr               uint16 = 0x0001
-	OServiceClientOnline             = 0x0002
-	OServiceHostOnline               = 0x0003
-	OServiceServiceRequest           = 0x0004
-	OServiceServiceResponse          = 0x0005
-	OServiceRateParamsQuery          = 0x0006
-	OServiceRateParamsReply          = 0x0007
-	OServiceRateParamsSubAdd         = 0x0008
-	OServiceRateDelParamSub          = 0x0009
-	OServiceRateParamChange          = 0x000A
-	OServicePauseReq                 = 0x000B
-	OServicePauseAck                 = 0x000C
-	OServiceResume                   = 0x000D
-	OServiceUserInfoQuery            = 0x000E
-	OServiceUserInfoUpdate           = 0x000F
-	OServiceEvilNotification         = 0x0010
-	OServiceIdleNotification         = 0x0011
-	OServiceMigrateGroups            = 0x0012
-	OServiceMotd                     = 0x0013
-	OServiceSetPrivacyFlags          = 0x0014
-	OServiceWellKnownUrls            = 0x0015
-	OServiceNoop                     = 0x0016
-	OServiceClientVersions           = 0x0017
-	OServiceHostVersions             = 0x0018
-	OServiceMaxConfigQuery           = 0x0019
-	OServiceMaxConfigReply           = 0x001A
-	OServiceStoreConfig              = 0x001B
-	OServiceConfigQuery              = 0x001C
-	OServiceConfigReply              = 0x001D
-	OServiceSetUserinfoFields        = 0x001E
-	OServiceProbeReq                 = 0x001F
-	OServiceProbeAck                 = 0x0020
-	OServiceBartReply                = 0x0021
-	OServiceBartQuery2               = 0x0022
-	OServiceBartReply2               = 0x0023
-)
+type OServiceHandler interface {
+	ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) 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
+	RateParamsSubAddHandler(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd)
+	ServiceRequestHandler(cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error)
+	SetUserInfoFieldsHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error)
+	UserInfoQueryHandler(sess *Session) XMessage
+}
+
+func NewOServiceRouter() OServiceRouter {
+	return OServiceRouter{
+		OServiceHandler: OServiceService{},
+	}
+}
+
+type OServiceRouter struct {
+	OServiceHandler
+}
 
-func routeOService(cfg Config, ready OnReadyCB, cr *ChatRegistry, sm SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	switch snac.SubGroup {
-	case OServiceClientOnline:
-		return ReceiveClientOnline(ready, sess, sm, snac, r, w, sequence)
-	case OServiceServiceRequest:
-		return ReceiveAndSendServiceRequest(cfg, cr, sess, snac, r, w, sequence)
-	case OServiceRateParamsQuery:
-		return ReceiveAndSendServiceRateParams(snac, r, w, sequence)
-	case OServiceRateParamsSubAdd:
-		return ReceiveRateParamsSubAdd(snac, r)
-	case OServiceUserInfoQuery:
-		return ReceiveAndSendServiceRequestSelfInfo(sess, snac, r, w, sequence)
-	case OServiceIdleNotification:
-		return ReceiveIdleNotification(sess, sm, fm, snac, r)
-	case OServiceClientVersions:
-		return ReceiveAndSendHostVersions(snac, r, w, sequence)
-	case OServiceSetUserinfoFields:
-		return ReceiveSetUserInfoFields(sess, sm, fm, snac, r, w, sequence)
+func (rt OServiceRouter) RouteOService(cfg Config, ready OnReadyCB, cr *ChatRegistry, sm SessionManager, fm *FeedbagStore, sess *Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+	switch SNACFrame.SubGroup {
+	case oscar.OServiceClientOnline:
+		inSNAC := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		return rt.ClientOnlineHandler(inSNAC, ready, sess, sm, r, w, sequence)
+	case oscar.OServiceServiceRequest:
+		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.ServiceRequestHandler(cfg, cr, sess, inSNAC)
+		switch {
+		case errors.Is(err, ErrUnsupportedSubGroup):
+			return sendInvalidSNACErr(SNACFrame, w, sequence)
+		case err != nil:
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceRateParamsQuery:
+		outSNAC := rt.RateParamsQueryHandler()
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceRateParamsSubAdd:
+		inSNAC := oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		rt.RateParamsSubAddHandler(inSNAC)
+		return oscar.Unmarshal(&inSNAC, r)
+	case oscar.OServiceUserInfoQuery:
+		outSNAC := rt.UserInfoQueryHandler(sess)
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceIdleNotification:
+		inSNAC := oscar.SNAC_0x01_0x11_OServiceIdleNotification{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		return rt.IdleNotificationHandler(sess, sm, fm, inSNAC)
+	case oscar.OServiceClientVersions:
+		inSNAC := oscar.SNAC_0x01_0x17_OServiceClientVersions{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC := rt.ClientVersionsHandler(inSNAC)
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
+	case oscar.OServiceSetUserInfoFields:
+		inSNAC := oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{}
+		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
+			return err
+		}
+		outSNAC, err := rt.SetUserInfoFieldsHandler(sess, sm, fm, inSNAC)
+		if err != nil {
+			return err
+		}
+		return writeOutSNAC(SNACFrame, outSNAC.snacFrame, outSNAC.snacOut, sequence, w)
 	default:
 		return ErrUnsupportedSubGroup
 	}
 }
 
+type OServiceService struct {
+}
+
 func WriteOServiceHostOnline(foodGroups []uint16, w io.Writer, sequence *uint32) error {
 	fmt.Println("writeOServiceHostOnline...")
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: OSERVICE,
-		SubGroup:  OServiceHostOnline,
+		SubGroup:  oscar.OServiceHostOnline,
 	}
 	snacPayloadOut := oscar.SNAC_0x01_0x03_OServiceHostOnline{
 		FoodGroups: foodGroups,
@@ -82,35 +107,23 @@ func WriteOServiceHostOnline(foodGroups []uint16, w io.Writer, sequence *uint32)
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveAndSendHostVersions(snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveAndSendHostVersions read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x01_0x17_OServiceClientVersions{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
-	fmt.Printf("receiveAndSendHostVersions read SNAC: %+v\n", snacPayloadIn)
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: OSERVICE,
-		SubGroup:  OServiceHostVersions,
-	}
-	snacPayloadOut := oscar.SNAC_0x01_0x18_OServiceHostVersions{
-		Versions: snacPayloadIn.Versions,
+func (s OServiceService) ClientVersionsHandler(snacPayloadIn oscar.SNAC_0x01_0x17_OServiceClientVersions) XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: OSERVICE,
+			SubGroup:  oscar.OServiceHostVersions,
+		},
+		snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
+			Versions: snacPayloadIn.Versions,
+		},
 	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveAndSendServiceRateParams(snac oscar.SnacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveAndSendServiceRateParams read SNAC frame: %+v\n", snac)
-
+func (s OServiceService) RateParamsQueryHandler() XMessage {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: OSERVICE,
-		SubGroup:  OServiceRateParamsReply,
+		SubGroup:  oscar.OServiceRateParamsReply,
 	}
-
 	snacPayloadOut := oscar.SNAC_0x01_0x07_OServiceRateParamsReply{
 		RateClasses: []struct {
 			ID              uint16
@@ -166,56 +179,31 @@ func ReceiveAndSendServiceRateParams(snac oscar.SnacFrame, _ io.Reader, w io.Wri
 				})
 		}
 	}
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-}
 
-func ReceiveAndSendServiceRequestSelfInfo(sess *Session, snac oscar.SnacFrame, _ io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveAndSendServiceRequestSelfInfo read SNAC frame: %+v\n", snac)
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: OSERVICE,
-		SubGroup:  OServiceUserInfoUpdate,
+	return XMessage{
+		snacFrame: snacFrameOut,
+		snacOut:   snacPayloadOut,
 	}
-	snacPayloadOut := oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-		TLVUserInfo: oscar.TLVUserInfo{
-			ScreenName:   sess.ScreenName,
-			WarningLevel: sess.GetWarning(),
-			TLVBlock: oscar.TLVBlock{
-				TLVList: sess.GetUserInfo(),
-			},
-		},
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveRateParamsSubAdd(snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("receiveRateParamsSubAdd read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
+func (s OServiceService) UserInfoQueryHandler(sess *Session) XMessage {
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: OSERVICE,
+			SubGroup:  oscar.OServiceUserInfoUpdate,
+		},
+		snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+			TLVUserInfo: sess.GetTLVUserInfo(),
+		},
 	}
-
-	fmt.Printf("receiveAndSendHostVersions read SNAC: %+v\n", snacPayloadIn)
-
-	return nil
 }
 
 type OnReadyCB func(sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error
 
-func ReceiveClientOnline(onReadyCB OnReadyCB, sess *Session, sm SessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveClientOnline read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s OServiceService) ClientOnlineHandler(snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, onReadyCB OnReadyCB, sess *Session, sm SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
 	for _, version := range snacPayloadIn.GroupVersions {
-		fmt.Printf("ReceiveClientOnline read SNAC client messageType: %+v\n", version)
+		fmt.Printf("ClientOnlineHandler read SNAC client messageType: %+v\n", version)
 	}
-
 	return onReadyCB(sess, sm, r, w, sequence)
 }
 
@@ -263,135 +251,102 @@ func NotifyBuddyOnline(w io.Writer, screenName string, sm SessionManager, sequen
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveSetUserInfoFields(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("receiveSetUserInfoFields read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s OServiceService) SetUserInfoFieldsHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields) (XMessage, error) {
 	if status, hasStatus := snacPayloadIn.GetUint32(0x06); hasStatus {
 		switch status {
 		case 0x000:
 			sess.SetInvisible(false)
 			if err := NotifyArrival(sess, sm, fm); err != nil {
-				return err
+				return XMessage{}, err
 			}
 		case 0x100:
 			sess.SetInvisible(true)
 			if err := NotifyDeparture(sess, sm, fm); err != nil {
-				return err
+				return XMessage{}, err
 			}
 		default:
-			return fmt.Errorf("don't know what to do with status %d", status)
+			return XMessage{}, fmt.Errorf("don't know what to do with status %d", status)
 		}
 	}
-
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: OSERVICE,
-		SubGroup:  OServiceUserInfoUpdate,
-	}
-	snacPayloadOut := oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
-		TLVUserInfo: oscar.TLVUserInfo{
-			ScreenName:   sess.ScreenName,
-			WarningLevel: sess.GetWarning(),
-			TLVBlock: oscar.TLVBlock{
-				TLVList: sess.GetUserInfo(),
-			},
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: OSERVICE,
+			SubGroup:  oscar.OServiceUserInfoUpdate,
 		},
-	}
-
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+		snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+			TLVUserInfo: sess.GetTLVUserInfo(),
+		},
+	}, nil
 }
 
-func ReceiveIdleNotification(sess *Session, sm SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
-	fmt.Printf("receiveIdleNotification read SNAC frame: %+v\n", snac)
-
-	snacPayloadIn := oscar.SNAC_0x01_0x11_OServiceIdleNotification{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
-	}
-
+func (s OServiceService) IdleNotificationHandler(sess *Session, sm SessionManager, fm *FeedbagStore, snacPayloadIn oscar.SNAC_0x01_0x11_OServiceIdleNotification) error {
 	if snacPayloadIn.IdleTime == 0 {
 		sess.SetActive()
 	} else {
 		sess.SetIdle(time.Duration(snacPayloadIn.IdleTime) * time.Second)
 	}
-
 	return NotifyArrival(sess, sm, fm)
 }
 
-const (
-	OserviceTlvTagsReconnectHere uint16 = 0x05
-	OserviceTlvTagsLoginCookie          = 0x06
-	OserviceTlvTagsGroupId              = 0x0D
-	OserviceTlvTagsSslCertname          = 0x8D
-	OserviceTlvTagsSslState             = 0x8E
-)
-
-func ReceiveAndSendServiceRequest(cfg Config, cr *ChatRegistry, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	snacPayloadIn := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
-	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
-		return err
+func (s OServiceService) ServiceRequestHandler(cfg Config, cr *ChatRegistry, sess *Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (XMessage, error) {
+	if snacPayloadIn.FoodGroup != CHAT {
+		return XMessage{}, ErrUnsupportedSubGroup
 	}
 
-	if snacPayloadIn.FoodGroup == CHAT {
-		roomMeta, ok := snacPayloadIn.GetSlice(0x01)
-		if !ok {
-			return errors.New("missing room info")
-		}
-
-		roomSnac := oscar.SNAC_0x01_0x04_TLVRoomInfo{}
-		if err := oscar.Unmarshal(&roomSnac, bytes.NewBuffer(roomMeta)); err != nil {
-			return err
-		}
-
-		room, err := cr.Retrieve(string(roomSnac.Cookie))
-		if err != nil {
-			return sendInvalidSNACErr(snac, w, sequence)
-		}
-		room.NewSessionWithSN(sess.ID, sess.ScreenName)
-
-		return sendChatRoomServiceInfo(cfg, room, sess, snac, sequence, w)
+	roomMeta, ok := snacPayloadIn.GetSlice(0x01)
+	if !ok {
+		return XMessage{}, errors.New("missing room info")
 	}
 
-	return sendInvalidSNACErr(snac, w, sequence)
-}
+	roomSnac := oscar.SNAC_0x01_0x04_TLVRoomInfo{}
+	if err := oscar.Unmarshal(&roomSnac, bytes.NewBuffer(roomMeta)); err != nil {
+		return XMessage{}, err
+	}
 
-func sendChatRoomServiceInfo(cfg Config, room ChatRoom, sess *Session, snac oscar.SnacFrame, sequence *uint32, w io.Writer) error {
-	snacFrameOut := oscar.SnacFrame{
-		FoodGroup: OSERVICE,
-		SubGroup:  OServiceServiceResponse,
+	room, err := cr.Retrieve(string(roomSnac.Cookie))
+	if err != nil {
+		return XMessage{}, ErrUnsupportedSubGroup
 	}
-	snacPayloadOut := oscar.SNAC_0x01_0x05_OServiceServiceResponse{
-		TLVRestBlock: oscar.TLVRestBlock{
-			TLVList: oscar.TLVList{
-				{
-					TType: OserviceTlvTagsReconnectHere,
-					Val:   Address(cfg.OSCARHost, cfg.ChatPort),
-				},
-				{
-					TType: OserviceTlvTagsLoginCookie,
-					Val: ChatCookie{
-						Cookie: []byte(room.Cookie),
-						SessID: sess.ID,
+	room.NewSessionWithSN(sess.ID, sess.ScreenName)
+
+	return XMessage{
+		snacFrame: oscar.SnacFrame{
+			FoodGroup: OSERVICE,
+			SubGroup:  oscar.OServiceServiceResponse,
+		},
+		snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
+			TLVRestBlock: oscar.TLVRestBlock{
+				TLVList: oscar.TLVList{
+					{
+						TType: oscar.OServiceTLVTagsReconnectHere,
+						Val:   Address(cfg.OSCARHost, cfg.ChatPort),
+					},
+					{
+						TType: oscar.OServiceTLVTagsLoginCookie,
+						Val: ChatCookie{
+							Cookie: []byte(room.Cookie),
+							SessID: sess.ID,
+						},
+					},
+					{
+						TType: oscar.OServiceTLVTagsGroupID,
+						Val:   CHAT,
+					},
+					{
+						TType: oscar.OServiceTLVTagsSSLCertName,
+						Val:   "",
+					},
+					{
+						TType: oscar.OServiceTLVTagsSSLState,
+						Val:   uint8(0x00),
 					},
-				},
-				{
-					TType: OserviceTlvTagsGroupId,
-					Val:   CHAT,
-				},
-				{
-					TType: OserviceTlvTagsSslCertname,
-					Val:   "",
-				},
-				{
-					TType: OserviceTlvTagsSslState,
-					Val:   uint8(0x00),
 				},
 			},
 		},
-	}
-	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
+	}, nil
+}
+
+// RateParamsSubAddHandler exists to capture the SNAC input in unit tests to
+// verify it's correctly unmarshalled.
+func (s OServiceService) RateParamsSubAddHandler(oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd) {
 }

+ 332 - 75
server/oservice_test.go

@@ -2,6 +2,7 @@ package server
 
 import (
 	"bytes"
+	"github.com/stretchr/testify/mock"
 	"testing"
 	"time"
 
@@ -24,10 +25,9 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 		inputSNAC oscar.SNAC_0x01_0x04_OServiceServiceRequest
 		// 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
+		expectOutput XMessage
+		// expectErr is the expected error returned by the router
+		expectErr error
 	}{
 		{
 			name: "request info for ICBM service, return invalid SNAC err",
@@ -37,13 +37,7 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 			inputSNAC: oscar.SNAC_0x01_0x04_OServiceServiceRequest{
 				FoodGroup: ICBM,
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: OSERVICE,
-				SubGroup:  OServiceErr,
-			},
-			expectSNACBody: oscar.SnacOServiceErr{
-				Code: ErrorCodeInvalidSnac,
-			},
+			expectErr: ErrUnsupportedSubGroup,
 		},
 		{
 			name: "request info for connecting to chat room, return chat service and chat room metadata",
@@ -78,35 +72,37 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: OSERVICE,
-				SubGroup:  OServiceServiceResponse,
-			},
-			expectSNACBody: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
-				TLVRestBlock: oscar.TLVRestBlock{
-					TLVList: oscar.TLVList{
-						{
-							TType: OserviceTlvTagsReconnectHere,
-							Val:   "127.0.0.1:1234",
-						},
-						{
-							TType: OserviceTlvTagsLoginCookie,
-							Val: ChatCookie{
-								Cookie: []byte("the-chat-cookie"),
-								SessID: "user-sess-id",
+			expectOutput: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceServiceResponse,
+				},
+				snacOut: oscar.SNAC_0x01_0x05_OServiceServiceResponse{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: oscar.OServiceTLVTagsReconnectHere,
+								Val:   "127.0.0.1:1234",
+							},
+							{
+								TType: oscar.OServiceTLVTagsLoginCookie,
+								Val: ChatCookie{
+									Cookie: []byte("the-chat-cookie"),
+									SessID: "user-sess-id",
+								},
+							},
+							{
+								TType: oscar.OServiceTLVTagsGroupID,
+								Val:   CHAT,
+							},
+							{
+								TType: oscar.OServiceTLVTagsSSLCertName,
+								Val:   "",
+							},
+							{
+								TType: oscar.OServiceTLVTagsSSLState,
+								Val:   uint8(0x00),
 							},
-						},
-						{
-							TType: OserviceTlvTagsGroupId,
-							Val:   CHAT,
-						},
-						{
-							TType: OserviceTlvTagsSslCertname,
-							Val:   "",
-						},
-						{
-							TType: OserviceTlvTagsSslState,
-							Val:   uint8(0x00),
 						},
 					},
 				},
@@ -138,13 +134,7 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 					},
 				},
 			},
-			expectSNACFrame: oscar.SnacFrame{
-				FoodGroup: OSERVICE,
-				SubGroup:  OServiceErr,
-			},
-			expectSNACBody: oscar.SnacOServiceErr{
-				Code: ErrorCodeInvalidSnac,
-			},
+			expectErr: ErrUnsupportedSubGroup,
 		},
 	}
 
@@ -163,44 +153,311 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 				tc.chatRoom.SessionManager = sm
 				cr.Register(*tc.chatRoom)
 			}
-
 			//
 			// send input SNAC
 			//
-			snac := oscar.SnacFrame{
-				FoodGroup: OSERVICE,
-				SubGroup:  OServiceServiceRequest,
+			assert.NoError(t, tc.inputSNAC.SerializeInPlace())
+			svc := OServiceService{}
+			outputSNAC, err := svc.ServiceRequestHandler(tc.cfg, cr, tc.userSession, tc.inputSNAC)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
 			}
-			input := &bytes.Buffer{}
-			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
-			output := &bytes.Buffer{}
-			var seq uint32
-			assert.NoError(t, ReceiveAndSendServiceRequest(tc.cfg, cr, tc.userSession, snac, input, output, &seq))
-
 			//
-			// verify server response
+			// verify output
 			//
-			flapFrame := oscar.FlapFrame{}
-			assert.NoError(t, oscar.Unmarshal(&flapFrame, output))
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
 
-			snacFrame := oscar.SnacFrame{}
-			assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
-			assert.Equal(t, tc.expectSNACFrame, snacFrame)
+func TestOServiceRouter_RouteOService(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 OServiceClientOnline, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceClientOnline,
+				},
+				snacOut: oscar.SNAC_0x01_0x02_OServiceClientOnline{
+					GroupVersions: []struct {
+						FoodGroup   uint16
+						Version     uint16
+						ToolID      uint16
+						ToolVersion uint16
+					}{
+						{
+							FoodGroup: 10,
+						},
+					},
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceServiceRequest, return OServiceServiceResponse",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceServiceRequest,
+				},
+				snacOut: oscar.SNAC_0x01_0x04_OServiceServiceRequest{
+					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),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceRateParamsQuery, return OServiceRateParamsReply",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsQuery,
+				},
+				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,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceRateParamsSubAdd, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceRateParamsSubAdd,
+				},
+				snacOut: oscar.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceUserInfoQuery, return OServiceUserInfoUpdate",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoQuery,
+				},
+				snacOut: struct{}{},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceIdleNotification, return no response",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceIdleNotification,
+				},
+				snacOut: oscar.SNAC_0x01_0x11_OServiceIdleNotification{
+					IdleTime: 10,
+				},
+			},
+			output: XMessage{},
+		},
+		{
+			name: "receive OServiceClientVersions, return OServiceHostVersions",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceClientVersions,
+				},
+				snacOut: oscar.SNAC_0x01_0x17_OServiceClientVersions{
+					Versions: []uint16{
+						10,
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceHostVersions,
+				},
+				snacOut: oscar.SNAC_0x01_0x18_OServiceHostVersions{
+					Versions: []uint16{
+						10,
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServiceSetUserInfoFields, return OServiceUserInfoUpdate",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceSetUserInfoFields,
+				},
+				snacOut: oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: 0x01,
+								Val:   []byte{1, 2, 3, 4},
+							},
+						},
+					},
+				},
+			},
+			output: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServiceUserInfoUpdate,
+				},
+				snacOut: oscar.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName: "screen-name",
+					},
+				},
+			},
+		},
+		{
+			name: "receive OServicePauseReq, expect ErrUnsupportedSubGroup",
+			input: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: OSERVICE,
+					SubGroup:  oscar.OServicePauseReq,
+				},
+				snacOut: struct{}{}, // empty SNAC
+			},
+			output:    XMessage{}, // empty SNAC
+			expectErr: ErrUnsupportedSubGroup,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			svc := NewMockOServiceHandler(t)
+			svc.EXPECT().
+				ClientOnlineHandler(tc.input.snacOut, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
+				Return(tc.handlerErr).
+				Maybe()
+			svc.EXPECT().
+				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().
+				RateParamsSubAddHandler(tc.input.snacOut).
+				Maybe()
+			svc.EXPECT().
+				UserInfoQueryHandler(mock.Anything).
+				Return(tc.output).
+				Maybe()
+			svc.EXPECT().
+				IdleNotificationHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.snacOut).
+				Return(tc.handlerErr).
+				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()
+
+			router := OServiceRouter{
+				OServiceHandler: svc,
+			}
+
+			bufIn := &bytes.Buffer{}
+			assert.NoError(t, oscar.Marshal(tc.input.snacOut, bufIn))
+
+			bufOut := &bytes.Buffer{}
+			seq := uint32(1)
 
-			switch expectSNAC := tc.expectSNACBody.(type) {
-			case oscar.SNAC_0x01_0x05_OServiceServiceResponse:
-				assert.NoError(t, expectSNAC.SerializeInPlace())
-				outputSNAC := oscar.SNAC_0x01_0x05_OServiceServiceResponse{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, expectSNAC, outputSNAC)
-			case oscar.SnacOServiceErr:
-				outputSNAC := oscar.SnacOServiceErr{}
-				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
-				assert.Equal(t, expectSNAC, outputSNAC)
-			default:
-				t.Fatalf("unexpected output SNAC type")
+			err := router.RouteOService(Config{}, nil, nil, nil, nil, nil, tc.input.snacFrame, bufIn, bufOut, &seq)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
 			}
-			assert.Equalf(t, 0, output.Len(), "the rest of the buffer is unread")
+
+			if tc.output.snacFrame == (oscar.SnacFrame{}) {
+				return // handler doesn't return response
+			}
+
+			// 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, bufOut))
+			assert.Equal(t, uint16(1), flap.Sequence)
+
+			// verify the SNAC frame
+			snacFrame := oscar.SnacFrame{}
+			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())
 		})
 	}
 }

+ 7 - 5
server/protocol.go

@@ -122,7 +122,7 @@ func VerifyLogin(sm SessionManager, rw io.ReadWriter) (*Session, uint32, error)
 	}
 
 	var ok bool
-	ID, ok := flap.GetSlice(OserviceTlvTagsLoginCookie)
+	ID, ok := flap.GetSlice(oscar.OServiceTLVTagsLoginCookie)
 	if !ok {
 		return nil, 0, errors.New("unable to get session ID from payload")
 	}
@@ -145,7 +145,7 @@ func VerifyChatLogin(rw io.ReadWriter) (*ChatCookie, uint32, error) {
 	}
 
 	var ok bool
-	buf, ok := flap.GetSlice(OserviceTlvTagsLoginCookie)
+	buf, ok := flap.GetSlice(oscar.OServiceTLVTagsLoginCookie)
 	if !ok {
 		return nil, 0, errors.New("unable to get session ID from payload")
 	}
@@ -311,20 +311,22 @@ func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm SessionM
 
 func NewRouter() Router {
 	return Router{
-		ICBMRouter:   NewICBMRouter(),
-		LocateRouter: NewLocateRouter(),
+		ICBMRouter:     NewICBMRouter(),
+		LocateRouter:   NewLocateRouter(),
+		OServiceRouter: NewOServiceRouter(),
 	}
 }
 
 type Router struct {
 	ICBMRouter
 	LocateRouter
+	OServiceRouter
 }
 
 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 {
 	switch snac.FoodGroup {
 	case OSERVICE:
-		if err := routeOService(cfg, ready, cr, sm, fm, sess, snac, buf, rw, sequence); err != nil {
+		if err := rt.RouteOService(cfg, ready, cr, sm, fm, sess, snac, buf, rw, sequence); err != nil {
 			return err
 		}
 	case LOCATE: