Kaynağa Gözat

implement unit tests for SendAndReceiveChannelMsgTohost

Mike 2 yıl önce
ebeveyn
işleme
999dd2ee65

+ 11 - 0
.mockery.yaml

@@ -0,0 +1,11 @@
+with-expecter: True
+mockname: "Mock{{.InterfaceName}}"
+filename: "{{.MockName}}.go"
+inpackage: True
+dir: ./server
+packages:
+  github.com/mkaminski/goaim/server:
+    interfaces:
+      FeedbagManager:
+      SessionManager:
+# Lots more config...

+ 6 - 6
cmd/main.go

@@ -50,7 +50,7 @@ func main() {
 	}
 	}
 }
 }
 
 
-func listenBOS(cfg server.Config, sm *server.SessionManager, fm *server.FeedbagStore, cr *server.ChatRegistry) {
+func listenBOS(cfg server.Config, sm *server.InMemorySessionManager, fm *server.FeedbagStore, cr *server.ChatRegistry) {
 	listener, err := net.Listen("tcp", server.Address("", cfg.BOSPort))
 	listener, err := net.Listen("tcp", server.Address("", cfg.BOSPort))
 	if err != nil {
 	if err != nil {
 		log.Fatal(err)
 		log.Fatal(err)
@@ -88,7 +88,7 @@ func listenChat(cfg server.Config, fm *server.FeedbagStore, cr *server.ChatRegis
 	}
 	}
 }
 }
 
 
-func handleAuthConnection(cfg server.Config, sm *server.SessionManager, fm *server.FeedbagStore, conn net.Conn) {
+func handleAuthConnection(cfg server.Config, sm *server.InMemorySessionManager, fm *server.FeedbagStore, conn net.Conn) {
 	defer conn.Close()
 	defer conn.Close()
 	seq := uint32(100)
 	seq := uint32(100)
 	_, err := server.SendAndReceiveSignonFrame(conn, &seq)
 	_, err := server.SendAndReceiveSignonFrame(conn, &seq)
@@ -103,14 +103,14 @@ func handleAuthConnection(cfg server.Config, sm *server.SessionManager, fm *serv
 		return
 		return
 	}
 	}
 
 
-	err = server.ReceiveAndSendBUCPLoginRequest(cfg, sm, fm, conn, conn, &seq)
+	err = server.ReceiveAndSendBUCPLoginRequest(cfg, sm, fm, conn, conn, &seq, uuid.New)
 	if err != nil {
 	if err != nil {
 		log.Println(err)
 		log.Println(err)
 		return
 		return
 	}
 	}
 }
 }
 
 
-func handleBOSConnection(cfg server.Config, sm *server.SessionManager, fm *server.FeedbagStore, cr *server.ChatRegistry, conn net.Conn) {
+func handleBOSConnection(cfg server.Config, sm *server.InMemorySessionManager, fm *server.FeedbagStore, cr *server.ChatRegistry, conn net.Conn) {
 	sess, seq, err := server.VerifyLogin(sm, conn)
 	sess, seq, err := server.VerifyLogin(sm, conn)
 	if err != nil {
 	if err != nil {
 		fmt.Printf("user disconnected with error: %s\n", err.Error())
 		fmt.Printf("user disconnected with error: %s\n", err.Error())
@@ -124,7 +124,7 @@ func handleBOSConnection(cfg server.Config, sm *server.SessionManager, fm *serve
 		conn.Close()
 		conn.Close()
 	}()
 	}()
 
 
-	onClientReady := func(sess *server.Session, sm *server.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+	onClientReady := func(sess *server.Session, sm *server.InMemorySessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
 		if err := server.NotifyArrival(sess, sm, fm); err != nil {
 		if err := server.NotifyArrival(sess, sm, fm); err != nil {
 			return err
 			return err
 		}
 		}
@@ -167,7 +167,7 @@ func handleChatConnection(cfg server.Config, fm *server.FeedbagStore, cr *server
 
 
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D, 0x000E}
 	foodGroups := []uint16{0x0001, 0x0002, 0x0003, 0x0004, 0x0009, 0x0013, 0x000D, 0x000E}
 
 
-	onClientReady := func(sess *server.Session, sm *server.SessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
+	onClientReady := func(sess *server.Session, sm *server.InMemorySessionManager, r io.Reader, w io.Writer, sequence *uint32) error {
 		if err := server.SendChatRoomInfoUpdate(room, w, sequence); err != nil {
 		if err := server.SendChatRoomInfoUpdate(room, w, sequence); err != nil {
 			return err
 			return err
 		}
 		}

+ 1 - 0
go.mod

@@ -12,5 +12,6 @@ require (
 require (
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/objx v0.5.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
 )

+ 8 - 0
go.sum

@@ -1,3 +1,4 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -8,9 +9,16 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 390 - 0
server/MockFeedbagManager.go

@@ -0,0 +1,390 @@
+// Code generated by mockery v2.34.2. DO NOT EDIT.
+
+package server
+
+import (
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+
+	time "time"
+)
+
+// MockFeedbagManager is an autogenerated mock type for the FeedbagManager type
+type MockFeedbagManager struct {
+	mock.Mock
+}
+
+type MockFeedbagManager_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockFeedbagManager) EXPECT() *MockFeedbagManager_Expecter {
+	return &MockFeedbagManager_Expecter{mock: &_m.Mock}
+}
+
+// Blocked provides a mock function with given fields: sn1, sn2
+func (_m *MockFeedbagManager) Blocked(sn1 string, sn2 string) (BlockedState, error) {
+	ret := _m.Called(sn1, sn2)
+
+	var r0 BlockedState
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string, string) (BlockedState, error)); ok {
+		return rf(sn1, sn2)
+	}
+	if rf, ok := ret.Get(0).(func(string, string) BlockedState); ok {
+		r0 = rf(sn1, sn2)
+	} else {
+		r0 = ret.Get(0).(BlockedState)
+	}
+
+	if rf, ok := ret.Get(1).(func(string, string) error); ok {
+		r1 = rf(sn1, sn2)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagManager_Blocked_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Blocked'
+type MockFeedbagManager_Blocked_Call struct {
+	*mock.Call
+}
+
+// Blocked is a helper method to define mock.On call
+//   - sn1 string
+//   - sn2 string
+func (_e *MockFeedbagManager_Expecter) Blocked(sn1 interface{}, sn2 interface{}) *MockFeedbagManager_Blocked_Call {
+	return &MockFeedbagManager_Blocked_Call{Call: _e.mock.On("Blocked", sn1, sn2)}
+}
+
+func (_c *MockFeedbagManager_Blocked_Call) Run(run func(sn1 string, sn2 string)) *MockFeedbagManager_Blocked_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(string))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_Blocked_Call) Return(_a0 BlockedState, _a1 error) *MockFeedbagManager_Blocked_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagManager_Blocked_Call) RunAndReturn(run func(string, string) (BlockedState, error)) *MockFeedbagManager_Blocked_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Buddies provides a mock function with given fields: screenName
+func (_m *MockFeedbagManager) Buddies(screenName string) ([]string, error) {
+	ret := _m.Called(screenName)
+
+	var r0 []string
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) []string); ok {
+		r0 = rf(screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]string)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagManager_Buddies_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Buddies'
+type MockFeedbagManager_Buddies_Call struct {
+	*mock.Call
+}
+
+// Buddies is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockFeedbagManager_Expecter) Buddies(screenName interface{}) *MockFeedbagManager_Buddies_Call {
+	return &MockFeedbagManager_Buddies_Call{Call: _e.mock.On("Buddies", screenName)}
+}
+
+func (_c *MockFeedbagManager_Buddies_Call) Run(run func(screenName string)) *MockFeedbagManager_Buddies_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_Buddies_Call) Return(_a0 []string, _a1 error) *MockFeedbagManager_Buddies_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagManager_Buddies_Call) RunAndReturn(run func(string) ([]string, error)) *MockFeedbagManager_Buddies_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Delete provides a mock function with given fields: screenName, items
+func (_m *MockFeedbagManager) Delete(screenName string, items []oscar.FeedbagItem) error {
+	ret := _m.Called(screenName, items)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, []oscar.FeedbagItem) error); ok {
+		r0 = rf(screenName, items)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockFeedbagManager_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
+type MockFeedbagManager_Delete_Call struct {
+	*mock.Call
+}
+
+// Delete is a helper method to define mock.On call
+//   - screenName string
+//   - items []oscar.FeedbagItem
+func (_e *MockFeedbagManager_Expecter) Delete(screenName interface{}, items interface{}) *MockFeedbagManager_Delete_Call {
+	return &MockFeedbagManager_Delete_Call{Call: _e.mock.On("Delete", screenName, items)}
+}
+
+func (_c *MockFeedbagManager_Delete_Call) Run(run func(screenName string, items []oscar.FeedbagItem)) *MockFeedbagManager_Delete_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].([]oscar.FeedbagItem))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_Delete_Call) Return(_a0 error) *MockFeedbagManager_Delete_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFeedbagManager_Delete_Call) RunAndReturn(run func(string, []oscar.FeedbagItem) error) *MockFeedbagManager_Delete_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// InterestedUsers provides a mock function with given fields: screenName
+func (_m *MockFeedbagManager) InterestedUsers(screenName string) ([]string, error) {
+	ret := _m.Called(screenName)
+
+	var r0 []string
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) []string); ok {
+		r0 = rf(screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]string)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagManager_InterestedUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InterestedUsers'
+type MockFeedbagManager_InterestedUsers_Call struct {
+	*mock.Call
+}
+
+// InterestedUsers is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockFeedbagManager_Expecter) InterestedUsers(screenName interface{}) *MockFeedbagManager_InterestedUsers_Call {
+	return &MockFeedbagManager_InterestedUsers_Call{Call: _e.mock.On("InterestedUsers", screenName)}
+}
+
+func (_c *MockFeedbagManager_InterestedUsers_Call) Run(run func(screenName string)) *MockFeedbagManager_InterestedUsers_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_InterestedUsers_Call) Return(_a0 []string, _a1 error) *MockFeedbagManager_InterestedUsers_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagManager_InterestedUsers_Call) RunAndReturn(run func(string) ([]string, error)) *MockFeedbagManager_InterestedUsers_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// LastModified provides a mock function with given fields: screenName
+func (_m *MockFeedbagManager) LastModified(screenName string) (time.Time, error) {
+	ret := _m.Called(screenName)
+
+	var r0 time.Time
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) (time.Time, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) time.Time); ok {
+		r0 = rf(screenName)
+	} else {
+		r0 = ret.Get(0).(time.Time)
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagManager_LastModified_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LastModified'
+type MockFeedbagManager_LastModified_Call struct {
+	*mock.Call
+}
+
+// LastModified is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockFeedbagManager_Expecter) LastModified(screenName interface{}) *MockFeedbagManager_LastModified_Call {
+	return &MockFeedbagManager_LastModified_Call{Call: _e.mock.On("LastModified", screenName)}
+}
+
+func (_c *MockFeedbagManager_LastModified_Call) Run(run func(screenName string)) *MockFeedbagManager_LastModified_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_LastModified_Call) Return(_a0 time.Time, _a1 error) *MockFeedbagManager_LastModified_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagManager_LastModified_Call) RunAndReturn(run func(string) (time.Time, error)) *MockFeedbagManager_LastModified_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Retrieve provides a mock function with given fields: screenName
+func (_m *MockFeedbagManager) Retrieve(screenName string) ([]oscar.FeedbagItem, error) {
+	ret := _m.Called(screenName)
+
+	var r0 []oscar.FeedbagItem
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) ([]oscar.FeedbagItem, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) []oscar.FeedbagItem); ok {
+		r0 = rf(screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]oscar.FeedbagItem)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockFeedbagManager_Retrieve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Retrieve'
+type MockFeedbagManager_Retrieve_Call struct {
+	*mock.Call
+}
+
+// Retrieve is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockFeedbagManager_Expecter) Retrieve(screenName interface{}) *MockFeedbagManager_Retrieve_Call {
+	return &MockFeedbagManager_Retrieve_Call{Call: _e.mock.On("Retrieve", screenName)}
+}
+
+func (_c *MockFeedbagManager_Retrieve_Call) Run(run func(screenName string)) *MockFeedbagManager_Retrieve_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_Retrieve_Call) Return(_a0 []oscar.FeedbagItem, _a1 error) *MockFeedbagManager_Retrieve_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockFeedbagManager_Retrieve_Call) RunAndReturn(run func(string) ([]oscar.FeedbagItem, error)) *MockFeedbagManager_Retrieve_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Upsert provides a mock function with given fields: screenName, items
+func (_m *MockFeedbagManager) Upsert(screenName string, items []oscar.FeedbagItem) error {
+	ret := _m.Called(screenName, items)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, []oscar.FeedbagItem) error); ok {
+		r0 = rf(screenName, items)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockFeedbagManager_Upsert_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Upsert'
+type MockFeedbagManager_Upsert_Call struct {
+	*mock.Call
+}
+
+// Upsert is a helper method to define mock.On call
+//   - screenName string
+//   - items []oscar.FeedbagItem
+func (_e *MockFeedbagManager_Expecter) Upsert(screenName interface{}, items interface{}) *MockFeedbagManager_Upsert_Call {
+	return &MockFeedbagManager_Upsert_Call{Call: _e.mock.On("Upsert", screenName, items)}
+}
+
+func (_c *MockFeedbagManager_Upsert_Call) Run(run func(screenName string, items []oscar.FeedbagItem)) *MockFeedbagManager_Upsert_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].([]oscar.FeedbagItem))
+	})
+	return _c
+}
+
+func (_c *MockFeedbagManager_Upsert_Call) Return(_a0 error) *MockFeedbagManager_Upsert_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockFeedbagManager_Upsert_Call) RunAndReturn(run func(string, []oscar.FeedbagItem) error) *MockFeedbagManager_Upsert_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockFeedbagManager creates a new instance of MockFeedbagManager. 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 NewMockFeedbagManager(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockFeedbagManager {
+	mock := &MockFeedbagManager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 437 - 0
server/MockSessionManager.go

@@ -0,0 +1,437 @@
+// Code generated by mockery v2.34.2. DO NOT EDIT.
+
+package server
+
+import mock "github.com/stretchr/testify/mock"
+
+// MockSessionManager is an autogenerated mock type for the SessionManager type
+type MockSessionManager struct {
+	mock.Mock
+}
+
+type MockSessionManager_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockSessionManager) EXPECT() *MockSessionManager_Expecter {
+	return &MockSessionManager_Expecter{mock: &_m.Mock}
+}
+
+// All provides a mock function with given fields:
+func (_m *MockSessionManager) All() []*Session {
+	ret := _m.Called()
+
+	var r0 []*Session
+	if rf, ok := ret.Get(0).(func() []*Session); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*Session)
+		}
+	}
+
+	return r0
+}
+
+// MockSessionManager_All_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'All'
+type MockSessionManager_All_Call struct {
+	*mock.Call
+}
+
+// All is a helper method to define mock.On call
+func (_e *MockSessionManager_Expecter) All() *MockSessionManager_All_Call {
+	return &MockSessionManager_All_Call{Call: _e.mock.On("All")}
+}
+
+func (_c *MockSessionManager_All_Call) Run(run func()) *MockSessionManager_All_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_All_Call) Return(_a0 []*Session) *MockSessionManager_All_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockSessionManager_All_Call) RunAndReturn(run func() []*Session) *MockSessionManager_All_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Broadcast provides a mock function with given fields: msg
+func (_m *MockSessionManager) Broadcast(msg XMessage) {
+	_m.Called(msg)
+}
+
+// MockSessionManager_Broadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Broadcast'
+type MockSessionManager_Broadcast_Call struct {
+	*mock.Call
+}
+
+// Broadcast is a helper method to define mock.On call
+//   - msg XMessage
+func (_e *MockSessionManager_Expecter) Broadcast(msg interface{}) *MockSessionManager_Broadcast_Call {
+	return &MockSessionManager_Broadcast_Call{Call: _e.mock.On("Broadcast", msg)}
+}
+
+func (_c *MockSessionManager_Broadcast_Call) Run(run func(msg XMessage)) *MockSessionManager_Broadcast_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(XMessage))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_Broadcast_Call) Return() *MockSessionManager_Broadcast_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockSessionManager_Broadcast_Call) RunAndReturn(run func(XMessage)) *MockSessionManager_Broadcast_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// BroadcastExcept provides a mock function with given fields: except, msg
+func (_m *MockSessionManager) BroadcastExcept(except *Session, msg XMessage) {
+	_m.Called(except, msg)
+}
+
+// MockSessionManager_BroadcastExcept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BroadcastExcept'
+type MockSessionManager_BroadcastExcept_Call struct {
+	*mock.Call
+}
+
+// BroadcastExcept is a helper method to define mock.On call
+//   - except *Session
+//   - msg XMessage
+func (_e *MockSessionManager_Expecter) BroadcastExcept(except interface{}, msg interface{}) *MockSessionManager_BroadcastExcept_Call {
+	return &MockSessionManager_BroadcastExcept_Call{Call: _e.mock.On("BroadcastExcept", except, msg)}
+}
+
+func (_c *MockSessionManager_BroadcastExcept_Call) Run(run func(except *Session, msg XMessage)) *MockSessionManager_BroadcastExcept_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session), args[1].(XMessage))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_BroadcastExcept_Call) Return() *MockSessionManager_BroadcastExcept_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockSessionManager_BroadcastExcept_Call) RunAndReturn(run func(*Session, XMessage)) *MockSessionManager_BroadcastExcept_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// BroadcastToScreenNames provides a mock function with given fields: screenNames, msg
+func (_m *MockSessionManager) BroadcastToScreenNames(screenNames []string, msg XMessage) {
+	_m.Called(screenNames, msg)
+}
+
+// MockSessionManager_BroadcastToScreenNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BroadcastToScreenNames'
+type MockSessionManager_BroadcastToScreenNames_Call struct {
+	*mock.Call
+}
+
+// BroadcastToScreenNames is a helper method to define mock.On call
+//   - screenNames []string
+//   - msg XMessage
+func (_e *MockSessionManager_Expecter) BroadcastToScreenNames(screenNames interface{}, msg interface{}) *MockSessionManager_BroadcastToScreenNames_Call {
+	return &MockSessionManager_BroadcastToScreenNames_Call{Call: _e.mock.On("BroadcastToScreenNames", screenNames, msg)}
+}
+
+func (_c *MockSessionManager_BroadcastToScreenNames_Call) Run(run func(screenNames []string, msg XMessage)) *MockSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].([]string), args[1].(XMessage))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_BroadcastToScreenNames_Call) Return() *MockSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockSessionManager_BroadcastToScreenNames_Call) RunAndReturn(run func([]string, XMessage)) *MockSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Empty provides a mock function with given fields:
+func (_m *MockSessionManager) Empty() bool {
+	ret := _m.Called()
+
+	var r0 bool
+	if rf, ok := ret.Get(0).(func() bool); ok {
+		r0 = rf()
+	} else {
+		r0 = ret.Get(0).(bool)
+	}
+
+	return r0
+}
+
+// MockSessionManager_Empty_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Empty'
+type MockSessionManager_Empty_Call struct {
+	*mock.Call
+}
+
+// Empty is a helper method to define mock.On call
+func (_e *MockSessionManager_Expecter) Empty() *MockSessionManager_Empty_Call {
+	return &MockSessionManager_Empty_Call{Call: _e.mock.On("Empty")}
+}
+
+func (_c *MockSessionManager_Empty_Call) Run(run func()) *MockSessionManager_Empty_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_Empty_Call) Return(_a0 bool) *MockSessionManager_Empty_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockSessionManager_Empty_Call) RunAndReturn(run func() bool) *MockSessionManager_Empty_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewSessionWithSN provides a mock function with given fields: sessID, screenName
+func (_m *MockSessionManager) NewSessionWithSN(sessID string, screenName string) *Session {
+	ret := _m.Called(sessID, screenName)
+
+	var r0 *Session
+	if rf, ok := ret.Get(0).(func(string, string) *Session); ok {
+		r0 = rf(sessID, screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*Session)
+		}
+	}
+
+	return r0
+}
+
+// MockSessionManager_NewSessionWithSN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewSessionWithSN'
+type MockSessionManager_NewSessionWithSN_Call struct {
+	*mock.Call
+}
+
+// NewSessionWithSN is a helper method to define mock.On call
+//   - sessID string
+//   - screenName string
+func (_e *MockSessionManager_Expecter) NewSessionWithSN(sessID interface{}, screenName interface{}) *MockSessionManager_NewSessionWithSN_Call {
+	return &MockSessionManager_NewSessionWithSN_Call{Call: _e.mock.On("NewSessionWithSN", sessID, screenName)}
+}
+
+func (_c *MockSessionManager_NewSessionWithSN_Call) Run(run func(sessID string, screenName string)) *MockSessionManager_NewSessionWithSN_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(string))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_NewSessionWithSN_Call) Return(_a0 *Session) *MockSessionManager_NewSessionWithSN_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockSessionManager_NewSessionWithSN_Call) RunAndReturn(run func(string, string) *Session) *MockSessionManager_NewSessionWithSN_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Remove provides a mock function with given fields: sess
+func (_m *MockSessionManager) Remove(sess *Session) {
+	_m.Called(sess)
+}
+
+// MockSessionManager_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
+type MockSessionManager_Remove_Call struct {
+	*mock.Call
+}
+
+// Remove is a helper method to define mock.On call
+//   - sess *Session
+func (_e *MockSessionManager_Expecter) Remove(sess interface{}) *MockSessionManager_Remove_Call {
+	return &MockSessionManager_Remove_Call{Call: _e.mock.On("Remove", sess)}
+}
+
+func (_c *MockSessionManager_Remove_Call) Run(run func(sess *Session)) *MockSessionManager_Remove_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*Session))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_Remove_Call) Return() *MockSessionManager_Remove_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockSessionManager_Remove_Call) RunAndReturn(run func(*Session)) *MockSessionManager_Remove_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Retrieve provides a mock function with given fields: ID
+func (_m *MockSessionManager) Retrieve(ID string) (*Session, bool) {
+	ret := _m.Called(ID)
+
+	var r0 *Session
+	var r1 bool
+	if rf, ok := ret.Get(0).(func(string) (*Session, bool)); ok {
+		return rf(ID)
+	}
+	if rf, ok := ret.Get(0).(func(string) *Session); ok {
+		r0 = rf(ID)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*Session)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) bool); ok {
+		r1 = rf(ID)
+	} else {
+		r1 = ret.Get(1).(bool)
+	}
+
+	return r0, r1
+}
+
+// MockSessionManager_Retrieve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Retrieve'
+type MockSessionManager_Retrieve_Call struct {
+	*mock.Call
+}
+
+// Retrieve is a helper method to define mock.On call
+//   - ID string
+func (_e *MockSessionManager_Expecter) Retrieve(ID interface{}) *MockSessionManager_Retrieve_Call {
+	return &MockSessionManager_Retrieve_Call{Call: _e.mock.On("Retrieve", ID)}
+}
+
+func (_c *MockSessionManager_Retrieve_Call) Run(run func(ID string)) *MockSessionManager_Retrieve_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_Retrieve_Call) Return(_a0 *Session, _a1 bool) *MockSessionManager_Retrieve_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockSessionManager_Retrieve_Call) RunAndReturn(run func(string) (*Session, bool)) *MockSessionManager_Retrieve_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RetrieveByScreenName provides a mock function with given fields: screenName
+func (_m *MockSessionManager) RetrieveByScreenName(screenName string) (*Session, error) {
+	ret := _m.Called(screenName)
+
+	var r0 *Session
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) (*Session, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) *Session); ok {
+		r0 = rf(screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*Session)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockSessionManager_RetrieveByScreenName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByScreenName'
+type MockSessionManager_RetrieveByScreenName_Call struct {
+	*mock.Call
+}
+
+// RetrieveByScreenName is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockSessionManager_Expecter) RetrieveByScreenName(screenName interface{}) *MockSessionManager_RetrieveByScreenName_Call {
+	return &MockSessionManager_RetrieveByScreenName_Call{Call: _e.mock.On("RetrieveByScreenName", screenName)}
+}
+
+func (_c *MockSessionManager_RetrieveByScreenName_Call) Run(run func(screenName string)) *MockSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_RetrieveByScreenName_Call) Return(_a0 *Session, _a1 error) *MockSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockSessionManager_RetrieveByScreenName_Call) RunAndReturn(run func(string) (*Session, error)) *MockSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SendToScreenName provides a mock function with given fields: screenName, msg
+func (_m *MockSessionManager) SendToScreenName(screenName string, msg XMessage) {
+	_m.Called(screenName, msg)
+}
+
+// MockSessionManager_SendToScreenName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendToScreenName'
+type MockSessionManager_SendToScreenName_Call struct {
+	*mock.Call
+}
+
+// SendToScreenName is a helper method to define mock.On call
+//   - screenName string
+//   - msg XMessage
+func (_e *MockSessionManager_Expecter) SendToScreenName(screenName interface{}, msg interface{}) *MockSessionManager_SendToScreenName_Call {
+	return &MockSessionManager_SendToScreenName_Call{Call: _e.mock.On("SendToScreenName", screenName, msg)}
+}
+
+func (_c *MockSessionManager_SendToScreenName_Call) Run(run func(screenName string, msg XMessage)) *MockSessionManager_SendToScreenName_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(XMessage))
+	})
+	return _c
+}
+
+func (_c *MockSessionManager_SendToScreenName_Call) Return() *MockSessionManager_SendToScreenName_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockSessionManager_SendToScreenName_Call) RunAndReturn(run func(string, XMessage)) *MockSessionManager_SendToScreenName_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockSessionManager creates a new instance of MockSessionManager. 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 NewMockSessionManager(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockSessionManager {
+	mock := &MockSessionManager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 1 - 1
server/bucp.go

@@ -102,7 +102,7 @@ func ReceiveAndSendAuthChallenge(cfg Config, fm *FeedbagStore, r io.Reader, w io
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func ReceiveAndSendBUCPLoginRequest(cfg Config, sm *SessionManager, fm *FeedbagStore, r io.Reader, w io.Writer, sequence *uint32, newUUID func() uuid.UUID) error {
+func ReceiveAndSendBUCPLoginRequest(cfg Config, sm *InMemorySessionManager, fm *FeedbagStore, r io.Reader, w io.Writer, sequence *uint32, newUUID func() uuid.UUID) error {
 	flap := oscar.FlapFrame{}
 	flap := oscar.FlapFrame{}
 	if err := oscar.Unmarshal(&flap, r); err != nil {
 	if err := oscar.Unmarshal(&flap, r); err != nil {
 		return err
 		return err

+ 2 - 2
server/buddy.go

@@ -92,7 +92,7 @@ func SendAndReceiveBuddyRights(snac oscar.SnacFrame, r io.Reader, w io.Writer, s
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func NotifyArrival(sess *Session, sm *SessionManager, fm *FeedbagStore) error {
+func NotifyArrival(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore) error {
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -117,7 +117,7 @@ func NotifyArrival(sess *Session, sm *SessionManager, fm *FeedbagStore) error {
 	return nil
 	return nil
 }
 }
 
 
-func NotifyDeparture(sess *Session, sm *SessionManager, fm *FeedbagStore) error {
+func NotifyDeparture(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore) error {
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	screenNames, err := fm.InterestedUsers(sess.ScreenName)
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 5 - 5
server/chat.go

@@ -51,7 +51,7 @@ const (
 	ChatRoomInfoOwner             = 0x0030
 	ChatRoomInfoOwner             = 0x0030
 )
 )
 
 
-func routeChat(sess *Session, sm *SessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeChat(sess *Session, sm *InMemorySessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	switch snac.SubGroup {
 	case ChatErr:
 	case ChatErr:
 		panic("not implemented")
 		panic("not implemented")
@@ -141,7 +141,7 @@ func routeChat(sess *Session, sm *SessionManager, snac oscar.SnacFrame, r io.Rea
 	return nil
 	return nil
 }
 }
 
 
-func SendAndReceiveChatChannelMsgTohost(sess *Session, sm *SessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveChatChannelMsgTohost(sess *Session, sm *InMemorySessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveChatChannelMsgTohost read SNAC frame: %+v\n", snac)
 	fmt.Printf("SendAndReceiveChatChannelMsgTohost read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
 	snacPayloadIn := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
@@ -186,7 +186,7 @@ func SendAndReceiveChatChannelMsgTohost(sess *Session, sm *SessionManager, snac
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func SetOnlineChatUsers(sm *SessionManager, w io.Writer, sequence *uint32) error {
+func SetOnlineChatUsers(sm *InMemorySessionManager, w io.Writer, sequence *uint32) error {
 	snacFrameOut := oscar.SnacFrame{
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: CHAT,
 		FoodGroup: CHAT,
 		SubGroup:  ChatUsersJoined,
 		SubGroup:  ChatUsersJoined,
@@ -208,7 +208,7 @@ func SetOnlineChatUsers(sm *SessionManager, w io.Writer, sequence *uint32) error
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func AlertUserJoined(sess *Session, sm *SessionManager) {
+func AlertUserJoined(sess *Session, sm *InMemorySessionManager) {
 	sm.BroadcastExcept(sess, XMessage{
 	sm.BroadcastExcept(sess, XMessage{
 		snacFrame: oscar.SnacFrame{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
 			FoodGroup: CHAT,
@@ -228,7 +228,7 @@ func AlertUserJoined(sess *Session, sm *SessionManager) {
 	})
 	})
 }
 }
 
 
-func AlertUserLeft(sess *Session, sm *SessionManager) {
+func AlertUserLeft(sess *Session, sm *InMemorySessionManager) {
 	sm.BroadcastExcept(sess, XMessage{
 	sm.BroadcastExcept(sess, XMessage{
 		snacFrame: oscar.SnacFrame{
 		snacFrame: oscar.SnacFrame{
 			FoodGroup: CHAT,
 			FoodGroup: CHAT,

+ 5 - 5
server/feedbag.go

@@ -144,7 +144,7 @@ const (
 	FeedbagClassIdMin                     = 0x0400
 	FeedbagClassIdMin                     = 0x0400
 )
 )
 
 
-func routeFeedbag(sm *SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeFeedbag(sm *InMemorySessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	switch snac.SubGroup {
 	case FeedbagErr:
 	case FeedbagErr:
 		panic("not implemented")
 		panic("not implemented")
@@ -389,7 +389,7 @@ func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm *FeedbagStore, snac
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveInsertItem(sm *InMemorySessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveInsertItem read SNAC frame: %+v\n", snac)
 	fmt.Printf("ReceiveInsertItem read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x13_0x08_FeedbagInsertItem{}
 	snacPayloadIn := oscar.SNAC_0x13_0x08_FeedbagInsertItem{}
@@ -441,7 +441,7 @@ func ReceiveInsertItem(sm *SessionManager, sess *Session, fm *FeedbagStore, snac
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 }
 }
 
 
-func blockBuddy(sm *SessionManager, sess *Session, screenName string, sequence *uint32, w io.Writer) error {
+func blockBuddy(sm *InMemorySessionManager, sess *Session, screenName string, sequence *uint32, w io.Writer) error {
 	// tell the blocked buddy you've signed off
 	// tell the blocked buddy you've signed off
 	sm.SendToScreenName(screenName, XMessage{
 	sm.SendToScreenName(screenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 		snacFrame: oscar.SnacFrame{
@@ -480,7 +480,7 @@ func blockBuddy(sm *SessionManager, sess *Session, screenName string, sequence *
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func ReceiveUpdateItem(sm *SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveUpdateItem(sm *InMemorySessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x13_0x09_FeedbagUpdateItem{}
 	snacPayloadIn := oscar.SNAC_0x13_0x09_FeedbagUpdateItem{}
@@ -511,7 +511,7 @@ func ReceiveUpdateItem(sm *SessionManager, sess *Session, fm *FeedbagStore, snac
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 	return GetOnlineBuddies(w, sess, sm, fm, sequence)
 }
 }
 
 
-func ReceiveDeleteItem(sm *SessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveDeleteItem(sm *InMemorySessionManager, sess *Session, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{}
 	snacPayloadIn := oscar.SNAC_0x13_0x0A_FeedbagDeleteItem{}

+ 23 - 0
server/feedbag_store.go

@@ -390,3 +390,26 @@ func (f *FeedbagStore) UpsertProfile(screenName string, body string) error {
 	_, err := f.db.Exec(q, screenName, body)
 	_, err := f.db.Exec(q, screenName, body)
 	return err
 	return err
 }
 }
+
+type FeedbagManager interface {
+	Delete(screenName string, items []oscar.FeedbagItem) error
+	Retrieve(screenName string) ([]oscar.FeedbagItem, error)
+	LastModified(screenName string) (time.Time, error)
+	Upsert(screenName string, items []oscar.FeedbagItem) error
+	InterestedUsers(screenName string) ([]string, error)
+	Buddies(screenName string) ([]string, error)
+	Blocked(sn1, sn2 string) (BlockedState, error)
+}
+
+type SessionManager interface {
+	Broadcast(msg XMessage)
+	Empty() bool
+	All() []*Session
+	BroadcastExcept(except *Session, msg XMessage)
+	Retrieve(ID string) (*Session, bool)
+	RetrieveByScreenName(screenName string) (*Session, error)
+	SendToScreenName(screenName string, msg XMessage)
+	BroadcastToScreenNames(screenNames []string, msg XMessage)
+	NewSessionWithSN(sessID string, screenName string) *Session
+	Remove(sess *Session)
+}

+ 14 - 20
server/icbm.go

@@ -31,7 +31,12 @@ const (
 	ICBMSinReply                  = 0x0017
 	ICBMSinReply                  = 0x0017
 )
 )
 
 
-func routeICBM(sm *SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+const (
+	ICBMTLVTagRequestHostAck uint16 = 0x03
+	ICBMTLVTagsWantEvents    uint16 = 0x0B
+)
+
+func routeICBM(sm *InMemorySessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	switch snac.SubGroup {
 	case ICBMErr:
 	case ICBMErr:
 		panic("not implemented")
 		panic("not implemented")
@@ -95,7 +100,7 @@ func SendAndReceiveICBMParameterReply(snac oscar.SnacFrame, _ io.Reader, w io.Wr
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveChannelMsgTohost(sm SessionManager, fm FeedbagManager, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveChannelMsgTohost read SNAC frame: %+v\n", snac)
 	fmt.Printf("SendAndReceiveChannelMsgTohost read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{}
 	snacPayloadIn := oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{}
@@ -152,21 +157,10 @@ func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *
 			},
 			},
 		},
 		},
 	}
 	}
-	if messagePayload, found := snacPayloadIn.TLVRestBlock.GetSlice(0x02); found {
-		clientIM.TLVRestBlock.AddTLV(oscar.TLV{
-			TType: 0x02,
-			Val:   messagePayload,
-		})
-	}
-	if messagePayload, found := snacPayloadIn.TLVRestBlock.GetSlice(0x05); found {
-		clientIM.TLVRestBlock.AddTLV(oscar.TLV{
-			TType: 0x05,
-			Val:   messagePayload,
-		})
-	}
-	if t, hasAutoResp := snacPayloadIn.GetTLV(0x04); hasAutoResp {
-		clientIM.TLVRestBlock.AddTLV(t)
-	}
+	// copy over TLVs from sender SNAC to recipient SNAC verbatim. this
+	// includes ICBMTLVTagRequestHostAck, which is ignored by the client, as
+	// far as I can tell.
+	clientIM.AddTLVList(snacPayloadIn.TLVRestBlock.TLVList)
 
 
 	sm.SendToScreenName(recipSess.ScreenName, XMessage{
 	sm.SendToScreenName(recipSess.ScreenName, XMessage{
 		snacFrame: oscar.SnacFrame{
 		snacFrame: oscar.SnacFrame{
@@ -176,7 +170,7 @@ func SendAndReceiveChannelMsgTohost(sm *SessionManager, fm *FeedbagStore, sess *
 		snacOut: clientIM,
 		snacOut: clientIM,
 	})
 	})
 
 
-	if _, requestedConfirmation := snacPayloadIn.TLVRestBlock.GetSlice(0x03); !requestedConfirmation {
+	if _, requestedConfirmation := snacPayloadIn.TLVRestBlock.GetSlice(ICBMTLVTagRequestHostAck); !requestedConfirmation {
 		// don't ack message
 		// don't ack message
 		return nil
 		return nil
 	}
 	}
@@ -219,7 +213,7 @@ func ReceiveClientErr(snac oscar.SnacFrame, r io.Reader) error {
 	return nil
 	return nil
 }
 }
 
 
-func SendAndReceiveClientEvent(sm *SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader) error {
+func SendAndReceiveClientEvent(sm *InMemorySessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader) error {
 	fmt.Printf("SendAndReceiveClientEvent read SNAC frame: %+v\n", snac)
 	fmt.Printf("SendAndReceiveClientEvent read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x04_0x14_ICBMClientEvent{}
 	snacPayloadIn := oscar.SNAC_0x04_0x14_ICBMClientEvent{}
@@ -256,7 +250,7 @@ const (
 	evilDeltaAnon = uint16(30)
 	evilDeltaAnon = uint16(30)
 )
 )
 
 
-func SendAndReceiveEvilRequest(sm *SessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveEvilRequest(sm *InMemorySessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveEvilRequest read SNAC frame: %+v\n", snac)
 	fmt.Printf("SendAndReceiveEvilRequest read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}
 	snacPayloadIn := oscar.SNAC_0x04_0x08_ICBMEvilRequest{}

+ 270 - 0
server/icbm_test.go

@@ -0,0 +1,270 @@
+package server
+
+import (
+	"bytes"
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestSendAndReceiveChannelMsgTohost(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// blockedState is the response to the sender/recipient block check
+		blockedState BlockedState
+		// recipRetrieveErr is the error returned by the recipient session
+		// lookup
+		recipRetrieveErr error
+		// senderScreenName is the screen name of the user sending the IM
+		senderScreenName string
+		// senderWarning is the warning level of the user sending the IM
+		senderWarning uint16
+		// recipientScreenName is the screen name of the user receiving the IM
+		recipientScreenName string
+		// recipientWarning is the warning level of the user receiving the IM
+		recipientWarning uint16
+		// inputSNAC is the SNAC sent by the sender client
+		inputSNAC oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost
+		// expectSNACToClient is the SNAC sent from the server to the
+		// recipient client
+		expectSNACToClient XMessage
+		// inputSNAC 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
+	}{
+		{
+			name:                "transmit message from sender to recipient, ack message back to sender",
+			blockedState:        BlockedNo,
+			senderScreenName:    "sender-screen-name",
+			senderWarning:       10,
+			recipientScreenName: "recipient-screen-name",
+			recipientWarning:    20,
+			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ScreenName: "recipient-screen-name",
+				TLVRestBlock: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: ICBMTLVTagRequestHostAck,
+							Val:   []byte{},
+						},
+					},
+				},
+			},
+			expectSNACToClient: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  ICBMChannelMsgToclient,
+				},
+				snacOut: oscar.SNAC_0x04_0x07_ICBMChannelMsgToClient{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName:   "sender-screen-name",
+						WarningLevel: 10,
+					},
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: ICBMTLVTagsWantEvents,
+								Val:   []byte{},
+							},
+							{
+								TType: ICBMTLVTagRequestHostAck,
+								Val:   []byte{},
+							},
+						},
+					},
+				},
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  ICBMHostAck,
+			},
+			expectSNACBody: oscar.SNAC_0x04_0x0C_ICBMHostAck{
+				ScreenName: "recipient-screen-name",
+			},
+		},
+		{
+			name:                "transmit message from sender to recipient, don't ack message back to sender",
+			blockedState:        BlockedNo,
+			senderScreenName:    "sender-screen-name",
+			senderWarning:       10,
+			recipientScreenName: "recipient-screen-name",
+			recipientWarning:    20,
+			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ScreenName: "recipient-screen-name",
+				TLVRestBlock: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{},
+				},
+			},
+			expectSNACToClient: XMessage{
+				snacFrame: oscar.SnacFrame{
+					FoodGroup: ICBM,
+					SubGroup:  ICBMChannelMsgToclient,
+				},
+				snacOut: oscar.SNAC_0x04_0x07_ICBMChannelMsgToClient{
+					TLVUserInfo: oscar.TLVUserInfo{
+						ScreenName:   "sender-screen-name",
+						WarningLevel: 10,
+					},
+					TLVRestBlock: oscar.TLVRestBlock{
+						TLVList: oscar.TLVList{
+							{
+								TType: ICBMTLVTagsWantEvents,
+								Val:   []byte{},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name:                "don't transmit message from sender to recipient because sender has blocked recipient",
+			blockedState:        BlockedA,
+			senderScreenName:    "sender-screen-name",
+			senderWarning:       10,
+			recipientScreenName: "recipient-screen-name",
+			recipientWarning:    20,
+			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ScreenName: "recipient-screen-name",
+				TLVRestBlock: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: ICBMTLVTagRequestHostAck,
+							Val:   []byte{},
+						},
+					},
+				},
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  ICBMErr,
+			},
+			expectSNACBody: oscar.SnacError{
+				Code: ErrorCodeInLocalPermitDeny,
+			},
+		},
+		{
+			name:                "don't transmit message from sender to recipient because recipient has blocked sender",
+			blockedState:        BlockedB,
+			senderScreenName:    "sender-screen-name",
+			senderWarning:       10,
+			recipientScreenName: "recipient-screen-name",
+			recipientWarning:    20,
+			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ScreenName: "recipient-screen-name",
+				TLVRestBlock: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: ICBMTLVTagRequestHostAck,
+							Val:   []byte{},
+						},
+					},
+				},
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  ICBMErr,
+			},
+			expectSNACBody: oscar.SnacError{
+				Code: ErrorCodeNotLoggedOn,
+			},
+		},
+		{
+			name:                "don't transmit message from sender to recipient because recipient doesn't exist",
+			blockedState:        BlockedNo,
+			recipRetrieveErr:    errSessNotFound,
+			senderScreenName:    "sender-screen-name",
+			senderWarning:       10,
+			recipientScreenName: "recipient-screen-name",
+			recipientWarning:    20,
+			inputSNAC: oscar.SNAC_0x04_0x06_ICBMChannelMsgToHost{
+				ScreenName: "recipient-screen-name",
+				TLVRestBlock: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: ICBMTLVTagRequestHostAck,
+							Val:   []byte{},
+						},
+					},
+				},
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: ICBM,
+				SubGroup:  ICBMErr,
+			},
+			expectSNACBody: 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.senderScreenName, tc.recipientScreenName).
+				Return(tc.blockedState, nil).
+				Maybe()
+			sm := NewMockSessionManager(t)
+			sm.EXPECT().
+				RetrieveByScreenName(tc.recipientScreenName).
+				Return(&Session{
+					ScreenName: tc.recipientScreenName,
+					Warning:    tc.recipientWarning,
+				}, tc.recipRetrieveErr).
+				Maybe()
+			sm.EXPECT().
+				SendToScreenName(tc.recipientScreenName, tc.expectSNACToClient).
+				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: ICBM,
+				SubGroup:  ICBMChannelMsgTohost,
+			}
+			senderSession := &Session{
+				ScreenName: tc.senderScreenName,
+				Warning:    tc.senderWarning,
+			}
+			assert.NoError(t, SendAndReceiveChannelMsgTohost(sm, fm, senderSession, snac, input, output, &seq))
+			//
+			// verify output
+			//
+			if tc.expectSNACFrame.FoodGroup == 0 {
+				// no ack was sent
+				return
+			}
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, output))
+			SnacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&SnacFrame, output))
+			assert.Equal(t, tc.expectSNACFrame, SnacFrame)
+			//
+			// verify output SNAC body
+			//
+			switch v := tc.expectSNACBody.(type) {
+			case oscar.SNAC_0x04_0x0C_ICBMHostAck:
+				outputSNAC := oscar.SNAC_0x04_0x0C_ICBMHostAck{}
+				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")
+			}
+		})
+	}
+}

+ 3 - 3
server/locate.go

@@ -31,7 +31,7 @@ const (
 	LocateUserInfoQuery2              = 0x0015
 	LocateUserInfoQuery2              = 0x0015
 )
 )
 
 
-func routeLocate(sess *Session, sm *SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeLocate(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	switch snac.SubGroup {
 	case LocateErr:
 	case LocateErr:
 		panic("not implemented")
 		panic("not implemented")
@@ -125,7 +125,7 @@ var (
 	LocateTlvTagsInfoHtmlInfoType    = uint16(0x0D)
 	LocateTlvTagsInfoHtmlInfoType    = uint16(0x0D)
 )
 )
 
 
-func ReceiveSetInfo(sess *Session, sm *SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
+func ReceiveSetInfo(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
 	fmt.Printf("ReceiveSetInfo read SNAC frame: %+v\n", snac)
 	fmt.Printf("ReceiveSetInfo read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x02_0x04_LocateSetInfo{}
 	snacPayloadIn := oscar.SNAC_0x02_0x04_LocateSetInfo{}
@@ -166,7 +166,7 @@ func ReceiveLocateGetDirInfo(snac oscar.SnacFrame, r io.Reader) error {
 	return nil
 	return nil
 }
 }
 
 
-func SendAndReceiveUserInfoQuery2(sess *Session, sm *SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func SendAndReceiveUserInfoQuery2(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("SendAndReceiveUserInfoQuery2 read SNAC frame: %+v\n", snac)
 	fmt.Printf("SendAndReceiveUserInfoQuery2 read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}
 	snacPayloadIn := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}

+ 6 - 6
server/oservice.go

@@ -47,7 +47,7 @@ const (
 	OServiceBartReply2               = 0x0023
 	OServiceBartReply2               = 0x0023
 )
 )
 
 
-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 {
+func routeOService(cfg Config, ready OnReadyCB, cr *ChatRegistry, sm *InMemorySessionManager, fm *FeedbagStore, sess *Session, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	switch snac.SubGroup {
 	case OServiceErr:
 	case OServiceErr:
 		panic("not implemented")
 		panic("not implemented")
@@ -250,9 +250,9 @@ func ReceiveRateParamsSubAdd(snac oscar.SnacFrame, r io.Reader) error {
 	return nil
 	return nil
 }
 }
 
 
-type OnReadyCB func(sess *Session, sm *SessionManager, r io.Reader, w io.Writer, sequence *uint32) error
+type OnReadyCB func(sess *Session, sm *InMemorySessionManager, 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 {
+func ReceiveClientOnline(onReadyCB OnReadyCB, sess *Session, sm *InMemorySessionManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveClientOnline read SNAC frame: %+v\n", snac)
 	fmt.Printf("receiveClientOnline read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
 	snacPayloadIn := oscar.SNAC_0x01_0x02_OServiceClientOnline{}
@@ -267,7 +267,7 @@ func ReceiveClientOnline(onReadyCB OnReadyCB, sess *Session, sm *SessionManager,
 	return onReadyCB(sess, sm, r, w, sequence)
 	return onReadyCB(sess, sm, r, w, sequence)
 }
 }
 
 
-func GetOnlineBuddies(w io.Writer, sess *Session, sm *SessionManager, fm *FeedbagStore, sequence *uint32) error {
+func GetOnlineBuddies(w io.Writer, sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, sequence *uint32) error {
 	screenNames, err := fm.Buddies(sess.ScreenName)
 	screenNames, err := fm.Buddies(sess.ScreenName)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -307,7 +307,7 @@ func GetOnlineBuddies(w io.Writer, sess *Session, sm *SessionManager, fm *Feedba
 	return nil
 	return nil
 }
 }
 
 
-func ReceiveSetUserInfoFields(sess *Session, sm *SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveSetUserInfoFields(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveSetUserInfoFields read SNAC frame: %+v\n", snac)
 	fmt.Printf("receiveSetUserInfoFields read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{}
 	snacPayloadIn := oscar.SNAC_0x01_0x1E_OServiceSetUserInfoFields{}
@@ -349,7 +349,7 @@ func ReceiveSetUserInfoFields(sess *Session, sm *SessionManager, fm *FeedbagStor
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 }
 
 
-func ReceiveIdleNotification(sess *Session, sm *SessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
+func ReceiveIdleNotification(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
 	fmt.Printf("receiveIdleNotification read SNAC frame: %+v\n", snac)
 	fmt.Printf("receiveIdleNotification read SNAC frame: %+v\n", snac)
 
 
 	snacPayloadIn := oscar.SNAC_0x01_0x11_OServiceIdleNotification{}
 	snacPayloadIn := oscar.SNAC_0x01_0x11_OServiceIdleNotification{}

+ 4 - 4
server/protocol.go

@@ -109,7 +109,7 @@ func SendAndReceiveSignonFrame(rw io.ReadWriter, sequence *uint32) (oscar.FlapSi
 	return flapSignonFrameIn, nil
 	return flapSignonFrameIn, nil
 }
 }
 
 
-func VerifyLogin(sm *SessionManager, rw io.ReadWriter) (*Session, uint32, error) {
+func VerifyLogin(sm *InMemorySessionManager, rw io.ReadWriter) (*Session, uint32, error) {
 	seq := uint32(100)
 	seq := uint32(100)
 	fmt.Println("VerifyLogin...")
 	fmt.Println("VerifyLogin...")
 
 
@@ -248,14 +248,14 @@ func readIncomingRequests(rw io.Reader, msCh chan IncomingMessage, errCh chan er
 	}
 	}
 }
 }
 
 
-func Signout(sess *Session, sm *SessionManager, fm *FeedbagStore) {
+func Signout(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore) {
 	if err := NotifyDeparture(sess, sm, fm); err != nil {
 	if err := NotifyDeparture(sess, sm, fm); err != nil {
 		fmt.Printf("error notifying departure: %s", err.Error())
 		fmt.Printf("error notifying departure: %s", err.Error())
 	}
 	}
 	sm.Remove(sess)
 	sm.Remove(sess)
 }
 }
 
 
-func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm *SessionManager, fm *FeedbagStore, cr *ChatRegistry, rwc io.ReadWriter, foodGroups []uint16) error {
+func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm *InMemorySessionManager, fm *FeedbagStore, cr *ChatRegistry, rwc io.ReadWriter, foodGroups []uint16) error {
 	if err := WriteOServiceHostOnline(foodGroups, rwc, &seq); err != nil {
 	if err := WriteOServiceHostOnline(foodGroups, rwc, &seq); err != nil {
 		return err
 		return err
 	}
 	}
@@ -281,7 +281,7 @@ func ReadBos(cfg Config, ready OnReadyCB, sess *Session, seq uint32, sm *Session
 	}
 	}
 }
 }
 
 
-func 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 {
+func routeIncomingRequests(cfg Config, ready OnReadyCB, sm *InMemorySessionManager, sess *Session, fm *FeedbagStore, cr *ChatRegistry, rw io.ReadWriter, sequence *uint32, snac oscar.SnacFrame, buf io.Reader) error {
 	switch snac.FoodGroup {
 	switch snac.FoodGroup {
 	case OSERVICE:
 	case OSERVICE:
 		if err := routeOService(cfg, ready, cr, sm, fm, sess, snac, buf, rw, sequence); err != nil {
 		if err := routeOService(cfg, ready, cr, sm, fm, sess, snac, buf, rw, sequence); err != nil {

+ 17 - 17
server/session.go

@@ -180,18 +180,18 @@ func (s *Session) Closed() <-chan struct{} {
 	return s.stopCh
 	return s.stopCh
 }
 }
 
 
-type SessionManager struct {
+type InMemorySessionManager struct {
 	store    map[string]*Session
 	store    map[string]*Session
 	mapMutex sync.RWMutex
 	mapMutex sync.RWMutex
 }
 }
 
 
-func NewSessionManager() *SessionManager {
-	return &SessionManager{
+func NewSessionManager() *InMemorySessionManager {
+	return &InMemorySessionManager{
 		store: make(map[string]*Session),
 		store: make(map[string]*Session),
 	}
 	}
 }
 }
 
 
-func (s *SessionManager) Broadcast(msg XMessage) {
+func (s *InMemorySessionManager) Broadcast(msg XMessage) {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	for _, sess := range s.store {
 	for _, sess := range s.store {
@@ -199,7 +199,7 @@ func (s *SessionManager) Broadcast(msg XMessage) {
 	}
 	}
 }
 }
 
 
-func (s *SessionManager) maybeSendMessage(msg XMessage, sess *Session) {
+func (s *InMemorySessionManager) maybeSendMessage(msg XMessage, sess *Session) {
 	switch sess.SendMessage(msg) {
 	switch sess.SendMessage(msg) {
 	case SessSendClosed:
 	case SessSendClosed:
 		fmt.Printf("message to %s was blocked, removing session\n", sess.ScreenName)
 		fmt.Printf("message to %s was blocked, removing session\n", sess.ScreenName)
@@ -209,13 +209,13 @@ func (s *SessionManager) maybeSendMessage(msg XMessage, sess *Session) {
 	}
 	}
 }
 }
 
 
-func (s *SessionManager) Empty() bool {
+func (s *InMemorySessionManager) Empty() bool {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	return len(s.store) == 0
 	return len(s.store) == 0
 }
 }
 
 
-func (s *SessionManager) All() []*Session {
+func (s *InMemorySessionManager) All() []*Session {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	var sessions []*Session
 	var sessions []*Session
@@ -225,7 +225,7 @@ func (s *SessionManager) All() []*Session {
 	return sessions
 	return sessions
 }
 }
 
 
-func (s *SessionManager) BroadcastExcept(except *Session, msg XMessage) {
+func (s *InMemorySessionManager) BroadcastExcept(except *Session, msg XMessage) {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	for _, sess := range s.store {
 	for _, sess := range s.store {
@@ -236,14 +236,14 @@ func (s *SessionManager) BroadcastExcept(except *Session, msg XMessage) {
 	}
 	}
 }
 }
 
 
-func (s *SessionManager) Retrieve(ID string) (*Session, bool) {
+func (s *InMemorySessionManager) Retrieve(ID string) (*Session, bool) {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	sess, found := s.store[ID]
 	sess, found := s.store[ID]
 	return sess, found
 	return sess, found
 }
 }
 
 
-func (s *SessionManager) RetrieveByScreenName(screenName string) (*Session, error) {
+func (s *InMemorySessionManager) RetrieveByScreenName(screenName string) (*Session, error) {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	for _, sess := range s.store {
 	for _, sess := range s.store {
@@ -254,7 +254,7 @@ func (s *SessionManager) RetrieveByScreenName(screenName string) (*Session, erro
 	return nil, fmt.Errorf("%w: %s", errSessNotFound, screenName)
 	return nil, fmt.Errorf("%w: %s", errSessNotFound, screenName)
 }
 }
 
 
-func (s *SessionManager) RetrieveByScreenNames(screenNames []string) []*Session {
+func (s *InMemorySessionManager) retrieveByScreenNames(screenNames []string) []*Session {
 	s.mapMutex.RLock()
 	s.mapMutex.RLock()
 	defer s.mapMutex.RUnlock()
 	defer s.mapMutex.RUnlock()
 	var ret []*Session
 	var ret []*Session
@@ -268,7 +268,7 @@ func (s *SessionManager) RetrieveByScreenNames(screenNames []string) []*Session
 	return ret
 	return ret
 }
 }
 
 
-func (s *SessionManager) SendToScreenName(screenName string, msg XMessage) {
+func (s *InMemorySessionManager) SendToScreenName(screenName string, msg XMessage) {
 	sess, err := s.RetrieveByScreenName(screenName)
 	sess, err := s.RetrieveByScreenName(screenName)
 	if err != nil {
 	if err != nil {
 		fmt.Printf("error sending to screen name: %s\n", screenName)
 		fmt.Printf("error sending to screen name: %s\n", screenName)
@@ -277,8 +277,8 @@ func (s *SessionManager) SendToScreenName(screenName string, msg XMessage) {
 	go s.maybeSendMessage(msg, sess)
 	go s.maybeSendMessage(msg, sess)
 }
 }
 
 
-func (s *SessionManager) BroadcastToScreenNames(screenNames []string, msg XMessage) {
-	for _, sess := range s.RetrieveByScreenNames(screenNames) {
+func (s *InMemorySessionManager) BroadcastToScreenNames(screenNames []string, msg XMessage) {
+	for _, sess := range s.retrieveByScreenNames(screenNames) {
 		go s.maybeSendMessage(msg, sess)
 		go s.maybeSendMessage(msg, sess)
 	}
 	}
 }
 }
@@ -292,7 +292,7 @@ func makeSession() *Session {
 	}
 	}
 }
 }
 
 
-func (s *SessionManager) NewSessionWithSN(sessID string, screenName string) *Session {
+func (s *InMemorySessionManager) NewSessionWithSN(sessID string, screenName string) *Session {
 	s.mapMutex.Lock()
 	s.mapMutex.Lock()
 	defer s.mapMutex.Unlock()
 	defer s.mapMutex.Unlock()
 	sess := makeSession()
 	sess := makeSession()
@@ -302,7 +302,7 @@ func (s *SessionManager) NewSessionWithSN(sessID string, screenName string) *Ses
 	return sess
 	return sess
 }
 }
 
 
-func (s *SessionManager) Remove(sess *Session) {
+func (s *InMemorySessionManager) Remove(sess *Session) {
 	s.mapMutex.Lock()
 	s.mapMutex.Lock()
 	defer s.mapMutex.Unlock()
 	defer s.mapMutex.Unlock()
 	delete(s.store, sess.ID)
 	delete(s.store, sess.ID)
@@ -310,7 +310,7 @@ func (s *SessionManager) Remove(sess *Session) {
 
 
 type ChatRoom struct {
 type ChatRoom struct {
 	ID             string
 	ID             string
-	SessionManager *SessionManager
+	SessionManager *InMemorySessionManager
 	CreateTime     time.Time
 	CreateTime     time.Time
 	Name           string
 	Name           string
 }
 }