Просмотр исходного кода

split sessionManager out of chat type

Mike 2 лет назад
Родитель
Сommit
299e0119e0

+ 3 - 0
.mockery.yaml

@@ -41,3 +41,6 @@ packages:
       SessionManager:
         config:
           filename: "session_manager_mock.go"
+      ChatSessionManager:
+        config:
+          filename: "chat_session_manager_mock.go"

+ 14 - 14
server/chat.go

@@ -10,7 +10,7 @@ import (
 )
 
 type ChatHandler interface {
-	ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)
+	ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, room ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)
 }
 
 func NewChatRouter(logger *slog.Logger) ChatRouter {
@@ -27,14 +27,14 @@ type ChatRouter struct {
 	RouteLogger
 }
 
-func (rt *ChatRouter) RouteChat(ctx context.Context, sess *user.Session, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt *ChatRouter) RouteChat(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.ChatChannelMsgToHost:
 		inSNAC := oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost{}
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sess, room, inSNAC)
+		outSNAC, err := rt.ChannelMsgToHostHandler(ctx, sess, chatSessMgr, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -52,7 +52,7 @@ func (rt *ChatRouter) RouteChat(ctx context.Context, sess *user.Session, room Ch
 type ChatService struct {
 }
 
-func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error) {
+func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error) {
 	snacFrameOut := oscar.SnacFrame{
 		FoodGroup: oscar.CHAT,
 		SubGroup:  oscar.ChatChannelMsgToClient,
@@ -75,7 +75,7 @@ func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *user.Ses
 	)
 
 	// send message to all the participants except sender
-	room.BroadcastExcept(ctx, sess, oscar.XMessage{
+	chatSessMgr.BroadcastExcept(ctx, sess, oscar.XMessage{
 		SnacFrame: snacFrameOut,
 		SnacOut:   snacPayloadOut,
 	})
@@ -92,9 +92,9 @@ func (s ChatService) ChannelMsgToHostHandler(ctx context.Context, sess *user.Ses
 	return ret, nil
 }
 
-func SetOnlineChatUsers(ctx context.Context, sess *user.Session, sm ChatRoom) {
+func SetOnlineChatUsers(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager) {
 	snacPayloadOut := oscar.SNAC_0x0E_0x03_ChatUsersJoined{}
-	sessions := sm.Participants()
+	sessions := chatSessMgr.Participants()
 
 	for _, uSess := range sessions {
 		snacPayloadOut.Users = append(snacPayloadOut.Users, oscar.TLVUserInfo{
@@ -106,7 +106,7 @@ func SetOnlineChatUsers(ctx context.Context, sess *user.Session, sm ChatRoom) {
 		})
 	}
 
-	sm.SendToScreenName(ctx, sess.ScreenName(), oscar.XMessage{
+	chatSessMgr.SendToScreenName(ctx, sess.ScreenName(), oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.CHAT,
 			SubGroup:  oscar.ChatUsersJoined,
@@ -115,8 +115,8 @@ func SetOnlineChatUsers(ctx context.Context, sess *user.Session, sm ChatRoom) {
 	})
 }
 
-func AlertUserJoined(ctx context.Context, sess *user.Session, sm SessionManager) {
-	sm.BroadcastExcept(ctx, sess, oscar.XMessage{
+func AlertUserJoined(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager) {
+	chatSessMgr.BroadcastExcept(ctx, sess, oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.CHAT,
 			SubGroup:  oscar.ChatUsersJoined,
@@ -135,8 +135,8 @@ func AlertUserJoined(ctx context.Context, sess *user.Session, sm SessionManager)
 	})
 }
 
-func AlertUserLeft(ctx context.Context, sess *user.Session, sm SessionManager) {
-	sm.BroadcastExcept(ctx, sess, oscar.XMessage{
+func AlertUserLeft(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager) {
+	chatSessMgr.BroadcastExcept(ctx, sess, oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.CHAT,
 			SubGroup:  oscar.ChatUsersLeft,
@@ -155,8 +155,8 @@ func AlertUserLeft(ctx context.Context, sess *user.Session, sm SessionManager) {
 	})
 }
 
-func SendChatRoomInfoUpdate(ctx context.Context, sess *user.Session, room ChatRoom) {
-	room.SendToScreenName(ctx, sess.ScreenName(), oscar.XMessage{
+func SendChatRoomInfoUpdate(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom) {
+	chatSessMgr.SendToScreenName(ctx, sess.ScreenName(), oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
 			FoodGroup: oscar.CHAT,
 			SubGroup:  oscar.ChatRoomInfoUpdate,

+ 8 - 8
server/chat_mock.go

@@ -25,15 +25,15 @@ func (_m *MockChatHandler) EXPECT() *MockChatHandler_Expecter {
 }
 
 // ChannelMsgToHostHandler provides a mock function with given fields: ctx, sess, room, snacPayloadIn
-func (_m *MockChatHandler) ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error) {
+func (_m *MockChatHandler) ChannelMsgToHostHandler(ctx context.Context, sess *user.Session, room ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error) {
 	ret := _m.Called(ctx, sess, room, snacPayloadIn)
 
 	var r0 *oscar.XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatSessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)); ok {
 		return rf(ctx, sess, room, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) *oscar.XMessage); ok {
+	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatSessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) *oscar.XMessage); ok {
 		r0 = rf(ctx, sess, room, snacPayloadIn)
 	} else {
 		if ret.Get(0) != nil {
@@ -41,7 +41,7 @@ func (_m *MockChatHandler) ChannelMsgToHostHandler(ctx context.Context, sess *us
 		}
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *user.Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) error); ok {
+	if rf, ok := ret.Get(1).(func(context.Context, *user.Session, ChatSessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) error); ok {
 		r1 = rf(ctx, sess, room, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
@@ -58,15 +58,15 @@ type MockChatHandler_ChannelMsgToHostHandler_Call struct {
 // ChannelMsgToHostHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *user.Session
-//   - room ChatRoom
+//   - room ChatSessionManager
 //   - snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost
 func (_e *MockChatHandler_Expecter) ChannelMsgToHostHandler(ctx interface{}, sess interface{}, room interface{}, snacPayloadIn interface{}) *MockChatHandler_ChannelMsgToHostHandler_Call {
 	return &MockChatHandler_ChannelMsgToHostHandler_Call{Call: _e.mock.On("ChannelMsgToHostHandler", ctx, sess, room, snacPayloadIn)}
 }
 
-func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sess *user.Session, room ChatRoom, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Run(run func(ctx context.Context, sess *user.Session, room ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost)) *MockChatHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*user.Session), args[2].(ChatRoom), args[3].(oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost))
+		run(args[0].(context.Context), args[1].(*user.Session), args[2].(ChatSessionManager), args[3].(oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost))
 	})
 	return _c
 }
@@ -76,7 +76,7 @@ func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) Return(_a0 *oscar.XMessa
 	return _c
 }
 
-func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, *user.Session, ChatRoom, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)) *MockChatHandler_ChannelMsgToHostHandler_Call {
+func (_c *MockChatHandler_ChannelMsgToHostHandler_Call) RunAndReturn(run func(context.Context, *user.Session, ChatSessionManager, oscar.SNAC_0x0E_0x05_ChatChannelMsgToHost) (*oscar.XMessage, error)) *MockChatHandler_ChannelMsgToHostHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 17 - 18
server/chat_nav.go

@@ -12,7 +12,7 @@ import (
 )
 
 type ChatNavHandler interface {
-	CreateRoomHandler(ctx context.Context, sess *user.Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)
+	CreateRoomHandler(ctx context.Context, sess *user.Session, newRoom func() ChatRoom, newChatSessMgr func() ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)
 	RequestChatRightsHandler(ctx context.Context) oscar.XMessage
 	RequestRoomInfoHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (oscar.XMessage, error)
 }
@@ -34,7 +34,7 @@ type ChatNavRouter struct {
 	RouteLogger
 }
 
-func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *user.Session, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *user.Session, newChatSessMgr func() ChatSessionManager, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.ChatNavRequestChatRights:
 		outSNAC := rt.RequestChatRightsHandler(ctx)
@@ -56,7 +56,13 @@ func (rt *ChatNavRouter) RouteChatNav(ctx context.Context, sess *user.Session, S
 		if err := oscar.Unmarshal(&inSNAC, r); err != nil {
 			return err
 		}
-		outSNAC, err := rt.CreateRoomHandler(ctx, sess, NewChatRoom, inSNAC)
+		newChatRoom := func() ChatRoom {
+			return ChatRoom{
+				Cookie:     uuid.New().String(),
+				CreateTime: time.Now(),
+			}
+		}
+		outSNAC, err := rt.CreateRoomHandler(ctx, sess, newChatRoom, newChatSessMgr, inSNAC)
 		if err != nil {
 			return err
 		}
@@ -110,31 +116,24 @@ func (s ChatNavService) RequestChatRightsHandler(context.Context) oscar.XMessage
 	}
 }
 
-func NewChatRoom(logger *slog.Logger) ChatRoom {
-	return ChatRoom{
-		Cookie:         uuid.New().String(),
-		CreateTime:     time.Now(),
-		SessionManager: user.NewSessionManager(logger),
-	}
-}
-
-type ChatRoomFactory func(logger *slog.Logger) ChatRoom
-
-func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *user.Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error) {
+func (s ChatNavService) CreateRoomHandler(ctx context.Context, sess *user.Session, newChatRoom func() ChatRoom, newChatSessMgr func() ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error) {
 	name, hasName := snacPayloadIn.GetString(oscar.ChatTLVRoomName)
 	if !hasName {
 		return oscar.XMessage{}, errors.New("unable to find chat name")
 	}
 
-	room := newChatRoom(s.Logger)
+	room := newChatRoom()
 	room.DetailLevel = snacPayloadIn.DetailLevel
 	room.Exchange = snacPayloadIn.Exchange
 	room.InstanceNumber = snacPayloadIn.InstanceNumber
 	room.Name = name
-	s.cr.Register(room)
+
+	chatSessMgr := newChatSessMgr()
+
+	s.cr.Register(room, chatSessMgr)
 
 	// add user to chat room
-	room.NewSessionWithSN(sess.ID(), sess.ScreenName())
+	chatSessMgr.NewSessionWithSN(sess.ID(), sess.ScreenName())
 
 	return oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
@@ -160,7 +159,7 @@ func (s ChatNavService) CreateRoomHandler(_ context.Context, sess *user.Session,
 }
 
 func (s ChatNavService) RequestRoomInfoHandler(_ context.Context, snacPayloadIn oscar.SNAC_0x0D_0x04_ChatNavRequestRoomInfo) (oscar.XMessage, error) {
-	room, err := s.cr.Retrieve(string(snacPayloadIn.Cookie))
+	room, _, err := s.cr.Retrieve(string(snacPayloadIn.Cookie))
 	if err != nil {
 		return oscar.XMessage{}, err
 	}

+ 16 - 15
server/chat_nav_mock.go

@@ -24,23 +24,23 @@ func (_m *MockChatNavHandler) EXPECT() *MockChatNavHandler_Expecter {
 	return &MockChatNavHandler_Expecter{mock: &_m.Mock}
 }
 
-// CreateRoomHandler provides a mock function with given fields: ctx, sess, newChatRoom, snacPayloadIn
-func (_m *MockChatNavHandler) CreateRoomHandler(ctx context.Context, sess *user.Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error) {
-	ret := _m.Called(ctx, sess, newChatRoom, snacPayloadIn)
+// CreateRoomHandler provides a mock function with given fields: ctx, sess, newRoom, newChatSessMgr, snacPayloadIn
+func (_m *MockChatNavHandler) CreateRoomHandler(ctx context.Context, sess *user.Session, newRoom func() ChatRoom, newChatSessMgr func() ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error) {
+	ret := _m.Called(ctx, sess, newRoom, newChatSessMgr, snacPayloadIn)
 
 	var r0 oscar.XMessage
 	var r1 error
-	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)); ok {
-		return rf(ctx, sess, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, func() ChatRoom, func() ChatSessionManager, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)); ok {
+		return rf(ctx, sess, newRoom, newChatSessMgr, snacPayloadIn)
 	}
-	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) oscar.XMessage); ok {
-		r0 = rf(ctx, sess, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(0).(func(context.Context, *user.Session, func() ChatRoom, func() ChatSessionManager, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) oscar.XMessage); ok {
+		r0 = rf(ctx, sess, newRoom, newChatSessMgr, snacPayloadIn)
 	} else {
 		r0 = ret.Get(0).(oscar.XMessage)
 	}
 
-	if rf, ok := ret.Get(1).(func(context.Context, *user.Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) error); ok {
-		r1 = rf(ctx, sess, newChatRoom, snacPayloadIn)
+	if rf, ok := ret.Get(1).(func(context.Context, *user.Session, func() ChatRoom, func() ChatSessionManager, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) error); ok {
+		r1 = rf(ctx, sess, newRoom, newChatSessMgr, snacPayloadIn)
 	} else {
 		r1 = ret.Error(1)
 	}
@@ -56,15 +56,16 @@ type MockChatNavHandler_CreateRoomHandler_Call struct {
 // CreateRoomHandler is a helper method to define mock.On call
 //   - ctx context.Context
 //   - sess *user.Session
-//   - newChatRoom ChatRoomFactory
+//   - newRoom func() ChatRoom
+//   - newChatSessMgr func() ChatSessionManager
 //   - snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate
-func (_e *MockChatNavHandler_Expecter) CreateRoomHandler(ctx interface{}, sess interface{}, newChatRoom interface{}, snacPayloadIn interface{}) *MockChatNavHandler_CreateRoomHandler_Call {
-	return &MockChatNavHandler_CreateRoomHandler_Call{Call: _e.mock.On("CreateRoomHandler", ctx, sess, newChatRoom, snacPayloadIn)}
+func (_e *MockChatNavHandler_Expecter) CreateRoomHandler(ctx interface{}, sess interface{}, newRoom interface{}, newChatSessMgr interface{}, snacPayloadIn interface{}) *MockChatNavHandler_CreateRoomHandler_Call {
+	return &MockChatNavHandler_CreateRoomHandler_Call{Call: _e.mock.On("CreateRoomHandler", ctx, sess, newRoom, newChatSessMgr, snacPayloadIn)}
 }
 
-func (_c *MockChatNavHandler_CreateRoomHandler_Call) Run(run func(ctx context.Context, sess *user.Session, newChatRoom ChatRoomFactory, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate)) *MockChatNavHandler_CreateRoomHandler_Call {
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) Run(run func(ctx context.Context, sess *user.Session, newRoom func() ChatRoom, newChatSessMgr func() ChatSessionManager, snacPayloadIn oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate)) *MockChatNavHandler_CreateRoomHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*user.Session), args[2].(ChatRoomFactory), args[3].(oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
+		run(args[0].(context.Context), args[1].(*user.Session), args[2].(func() ChatRoom), args[3].(func() ChatSessionManager), args[4].(oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate))
 	})
 	return _c
 }
@@ -74,7 +75,7 @@ func (_c *MockChatNavHandler_CreateRoomHandler_Call) Return(_a0 oscar.XMessage,
 	return _c
 }
 
-func (_c *MockChatNavHandler_CreateRoomHandler_Call) RunAndReturn(run func(context.Context, *user.Session, ChatRoomFactory, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)) *MockChatNavHandler_CreateRoomHandler_Call {
+func (_c *MockChatNavHandler_CreateRoomHandler_Call) RunAndReturn(run func(context.Context, *user.Session, func() ChatRoom, func() ChatSessionManager, oscar.SNAC_0x0E_0x02_ChatRoomInfoUpdate) (oscar.XMessage, error)) *MockChatNavHandler_CreateRoomHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 12 - 11
server/chat_nav_test.go

@@ -4,7 +4,6 @@ import (
 	"bytes"
 	"context"
 	"github.com/mkaminski/goaim/user"
-	"log/slog"
 	"testing"
 	"time"
 
@@ -21,15 +20,18 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 
 	cr := NewChatRegistry()
 
-	sm := NewMockSessionManager(t)
+	sm := NewMockChatSessionManager(t)
 	sm.EXPECT().NewSessionWithSN(userSess.ID(), userSess.ScreenName()).
 		Return(&user.Session{})
 
-	crf := func(logger *slog.Logger) ChatRoom {
+	chatSessMgrFactory := func() ChatSessionManager {
+		return sm
+	}
+
+	newChatRoom := func() ChatRoom {
 		return ChatRoom{
-			Cookie:         "dummy-cookie",
-			CreateTime:     time.UnixMilli(0),
-			SessionManager: sm,
+			Cookie:     "dummy-cookie",
+			CreateTime: time.UnixMilli(0),
 		}
 	}
 
@@ -50,14 +52,13 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 	svc := ChatNavService{
 		cr: cr,
 	}
-	outputSNAC, err := svc.CreateRoomHandler(context.Background(), userSess, crf, inputSNAC)
+	outputSNAC, err := svc.CreateRoomHandler(context.Background(), userSess, newChatRoom, chatSessMgrFactory, inputSNAC)
 	assert.NoError(t, err)
 
 	//
 	// verify chat room created by handler
 	//
 	expectChatRoom := ChatRoom{
-		SessionManager: sm,
 		Cookie:         "dummy-cookie",
 		CreateTime:     time.UnixMilli(0),
 		DetailLevel:    3,
@@ -65,7 +66,7 @@ func TestSendAndReceiveCreateRoom(t *testing.T) {
 		InstanceNumber: 2,
 		Name:           "the-chat-room-name",
 	}
-	chatRoom, err := cr.Retrieve("dummy-cookie")
+	chatRoom, _, err := cr.Retrieve("dummy-cookie")
 	assert.NoError(t, err)
 	assert.Equal(t, expectChatRoom, chatRoom)
 
@@ -212,7 +213,7 @@ func TestChatNavRouter_RouteChatNavRouter(t *testing.T) {
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svc.EXPECT().
-				CreateRoomHandler(mock.Anything, mock.Anything, mock.Anything, tc.input.SnacOut).
+				CreateRoomHandler(mock.Anything, mock.Anything, mock.Anything, mock.Anything, tc.input.SnacOut).
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 
@@ -229,7 +230,7 @@ func TestChatNavRouter_RouteChatNavRouter(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(0)
 
-			err := router.RouteChatNav(nil, nil, tc.input.SnacFrame, bufIn, bufOut, &seq)
+			err := router.RouteChatNav(nil, nil, nil, tc.input.SnacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 438 - 0
server/chat_session_manager_mock.go

@@ -0,0 +1,438 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import (
+	context "context"
+
+	oscar "github.com/mkaminski/goaim/oscar"
+	mock "github.com/stretchr/testify/mock"
+
+	user "github.com/mkaminski/goaim/user"
+)
+
+// MockChatSessionManager is an autogenerated mock type for the ChatSessionManager type
+type MockChatSessionManager struct {
+	mock.Mock
+}
+
+type MockChatSessionManager_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockChatSessionManager) EXPECT() *MockChatSessionManager_Expecter {
+	return &MockChatSessionManager_Expecter{mock: &_m.Mock}
+}
+
+// Broadcast provides a mock function with given fields: ctx, msg
+func (_m *MockChatSessionManager) Broadcast(ctx context.Context, msg oscar.XMessage) {
+	_m.Called(ctx, msg)
+}
+
+// MockChatSessionManager_Broadcast_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Broadcast'
+type MockChatSessionManager_Broadcast_Call struct {
+	*mock.Call
+}
+
+// Broadcast is a helper method to define mock.On call
+//   - ctx context.Context
+//   - msg oscar.XMessage
+func (_e *MockChatSessionManager_Expecter) Broadcast(ctx interface{}, msg interface{}) *MockChatSessionManager_Broadcast_Call {
+	return &MockChatSessionManager_Broadcast_Call{Call: _e.mock.On("Broadcast", ctx, msg)}
+}
+
+func (_c *MockChatSessionManager_Broadcast_Call) Run(run func(ctx context.Context, msg oscar.XMessage)) *MockChatSessionManager_Broadcast_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(oscar.XMessage))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_Broadcast_Call) Return() *MockChatSessionManager_Broadcast_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockChatSessionManager_Broadcast_Call) RunAndReturn(run func(context.Context, oscar.XMessage)) *MockChatSessionManager_Broadcast_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// BroadcastExcept provides a mock function with given fields: ctx, except, msg
+func (_m *MockChatSessionManager) BroadcastExcept(ctx context.Context, except *user.Session, msg oscar.XMessage) {
+	_m.Called(ctx, except, msg)
+}
+
+// MockChatSessionManager_BroadcastExcept_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BroadcastExcept'
+type MockChatSessionManager_BroadcastExcept_Call struct {
+	*mock.Call
+}
+
+// BroadcastExcept is a helper method to define mock.On call
+//   - ctx context.Context
+//   - except *user.Session
+//   - msg oscar.XMessage
+func (_e *MockChatSessionManager_Expecter) BroadcastExcept(ctx interface{}, except interface{}, msg interface{}) *MockChatSessionManager_BroadcastExcept_Call {
+	return &MockChatSessionManager_BroadcastExcept_Call{Call: _e.mock.On("BroadcastExcept", ctx, except, msg)}
+}
+
+func (_c *MockChatSessionManager_BroadcastExcept_Call) Run(run func(ctx context.Context, except *user.Session, msg oscar.XMessage)) *MockChatSessionManager_BroadcastExcept_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*user.Session), args[2].(oscar.XMessage))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_BroadcastExcept_Call) Return() *MockChatSessionManager_BroadcastExcept_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockChatSessionManager_BroadcastExcept_Call) RunAndReturn(run func(context.Context, *user.Session, oscar.XMessage)) *MockChatSessionManager_BroadcastExcept_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// BroadcastToScreenNames provides a mock function with given fields: ctx, screenNames, msg
+func (_m *MockChatSessionManager) BroadcastToScreenNames(ctx context.Context, screenNames []string, msg oscar.XMessage) {
+	_m.Called(ctx, screenNames, msg)
+}
+
+// MockChatSessionManager_BroadcastToScreenNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BroadcastToScreenNames'
+type MockChatSessionManager_BroadcastToScreenNames_Call struct {
+	*mock.Call
+}
+
+// BroadcastToScreenNames is a helper method to define mock.On call
+//   - ctx context.Context
+//   - screenNames []string
+//   - msg oscar.XMessage
+func (_e *MockChatSessionManager_Expecter) BroadcastToScreenNames(ctx interface{}, screenNames interface{}, msg interface{}) *MockChatSessionManager_BroadcastToScreenNames_Call {
+	return &MockChatSessionManager_BroadcastToScreenNames_Call{Call: _e.mock.On("BroadcastToScreenNames", ctx, screenNames, msg)}
+}
+
+func (_c *MockChatSessionManager_BroadcastToScreenNames_Call) Run(run func(ctx context.Context, screenNames []string, msg oscar.XMessage)) *MockChatSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].([]string), args[2].(oscar.XMessage))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_BroadcastToScreenNames_Call) Return() *MockChatSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockChatSessionManager_BroadcastToScreenNames_Call) RunAndReturn(run func(context.Context, []string, oscar.XMessage)) *MockChatSessionManager_BroadcastToScreenNames_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Empty provides a mock function with given fields:
+func (_m *MockChatSessionManager) 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
+}
+
+// MockChatSessionManager_Empty_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Empty'
+type MockChatSessionManager_Empty_Call struct {
+	*mock.Call
+}
+
+// Empty is a helper method to define mock.On call
+func (_e *MockChatSessionManager_Expecter) Empty() *MockChatSessionManager_Empty_Call {
+	return &MockChatSessionManager_Empty_Call{Call: _e.mock.On("Empty")}
+}
+
+func (_c *MockChatSessionManager_Empty_Call) Run(run func()) *MockChatSessionManager_Empty_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_Empty_Call) Return(_a0 bool) *MockChatSessionManager_Empty_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockChatSessionManager_Empty_Call) RunAndReturn(run func() bool) *MockChatSessionManager_Empty_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewSessionWithSN provides a mock function with given fields: sessID, screenName
+func (_m *MockChatSessionManager) NewSessionWithSN(sessID string, screenName string) *user.Session {
+	ret := _m.Called(sessID, screenName)
+
+	var r0 *user.Session
+	if rf, ok := ret.Get(0).(func(string, string) *user.Session); ok {
+		r0 = rf(sessID, screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*user.Session)
+		}
+	}
+
+	return r0
+}
+
+// MockChatSessionManager_NewSessionWithSN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewSessionWithSN'
+type MockChatSessionManager_NewSessionWithSN_Call struct {
+	*mock.Call
+}
+
+// NewSessionWithSN is a helper method to define mock.On call
+//   - sessID string
+//   - screenName string
+func (_e *MockChatSessionManager_Expecter) NewSessionWithSN(sessID interface{}, screenName interface{}) *MockChatSessionManager_NewSessionWithSN_Call {
+	return &MockChatSessionManager_NewSessionWithSN_Call{Call: _e.mock.On("NewSessionWithSN", sessID, screenName)}
+}
+
+func (_c *MockChatSessionManager_NewSessionWithSN_Call) Run(run func(sessID string, screenName string)) *MockChatSessionManager_NewSessionWithSN_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(string))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_NewSessionWithSN_Call) Return(_a0 *user.Session) *MockChatSessionManager_NewSessionWithSN_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockChatSessionManager_NewSessionWithSN_Call) RunAndReturn(run func(string, string) *user.Session) *MockChatSessionManager_NewSessionWithSN_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Participants provides a mock function with given fields:
+func (_m *MockChatSessionManager) Participants() []*user.Session {
+	ret := _m.Called()
+
+	var r0 []*user.Session
+	if rf, ok := ret.Get(0).(func() []*user.Session); ok {
+		r0 = rf()
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).([]*user.Session)
+		}
+	}
+
+	return r0
+}
+
+// MockChatSessionManager_Participants_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Participants'
+type MockChatSessionManager_Participants_Call struct {
+	*mock.Call
+}
+
+// Participants is a helper method to define mock.On call
+func (_e *MockChatSessionManager_Expecter) Participants() *MockChatSessionManager_Participants_Call {
+	return &MockChatSessionManager_Participants_Call{Call: _e.mock.On("Participants")}
+}
+
+func (_c *MockChatSessionManager_Participants_Call) Run(run func()) *MockChatSessionManager_Participants_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run()
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_Participants_Call) Return(_a0 []*user.Session) *MockChatSessionManager_Participants_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockChatSessionManager_Participants_Call) RunAndReturn(run func() []*user.Session) *MockChatSessionManager_Participants_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Remove provides a mock function with given fields: sess
+func (_m *MockChatSessionManager) Remove(sess *user.Session) {
+	_m.Called(sess)
+}
+
+// MockChatSessionManager_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
+type MockChatSessionManager_Remove_Call struct {
+	*mock.Call
+}
+
+// Remove is a helper method to define mock.On call
+//   - sess *user.Session
+func (_e *MockChatSessionManager_Expecter) Remove(sess interface{}) *MockChatSessionManager_Remove_Call {
+	return &MockChatSessionManager_Remove_Call{Call: _e.mock.On("Remove", sess)}
+}
+
+func (_c *MockChatSessionManager_Remove_Call) Run(run func(sess *user.Session)) *MockChatSessionManager_Remove_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(*user.Session))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_Remove_Call) Return() *MockChatSessionManager_Remove_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockChatSessionManager_Remove_Call) RunAndReturn(run func(*user.Session)) *MockChatSessionManager_Remove_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// Retrieve provides a mock function with given fields: ID
+func (_m *MockChatSessionManager) Retrieve(ID string) (*user.Session, bool) {
+	ret := _m.Called(ID)
+
+	var r0 *user.Session
+	var r1 bool
+	if rf, ok := ret.Get(0).(func(string) (*user.Session, bool)); ok {
+		return rf(ID)
+	}
+	if rf, ok := ret.Get(0).(func(string) *user.Session); ok {
+		r0 = rf(ID)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*user.Session)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(string) bool); ok {
+		r1 = rf(ID)
+	} else {
+		r1 = ret.Get(1).(bool)
+	}
+
+	return r0, r1
+}
+
+// MockChatSessionManager_Retrieve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Retrieve'
+type MockChatSessionManager_Retrieve_Call struct {
+	*mock.Call
+}
+
+// Retrieve is a helper method to define mock.On call
+//   - ID string
+func (_e *MockChatSessionManager_Expecter) Retrieve(ID interface{}) *MockChatSessionManager_Retrieve_Call {
+	return &MockChatSessionManager_Retrieve_Call{Call: _e.mock.On("Retrieve", ID)}
+}
+
+func (_c *MockChatSessionManager_Retrieve_Call) Run(run func(ID string)) *MockChatSessionManager_Retrieve_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_Retrieve_Call) Return(_a0 *user.Session, _a1 bool) *MockChatSessionManager_Retrieve_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockChatSessionManager_Retrieve_Call) RunAndReturn(run func(string) (*user.Session, bool)) *MockChatSessionManager_Retrieve_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// RetrieveByScreenName provides a mock function with given fields: screenName
+func (_m *MockChatSessionManager) RetrieveByScreenName(screenName string) *user.Session {
+	ret := _m.Called(screenName)
+
+	var r0 *user.Session
+	if rf, ok := ret.Get(0).(func(string) *user.Session); ok {
+		r0 = rf(screenName)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*user.Session)
+		}
+	}
+
+	return r0
+}
+
+// MockChatSessionManager_RetrieveByScreenName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByScreenName'
+type MockChatSessionManager_RetrieveByScreenName_Call struct {
+	*mock.Call
+}
+
+// RetrieveByScreenName is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockChatSessionManager_Expecter) RetrieveByScreenName(screenName interface{}) *MockChatSessionManager_RetrieveByScreenName_Call {
+	return &MockChatSessionManager_RetrieveByScreenName_Call{Call: _e.mock.On("RetrieveByScreenName", screenName)}
+}
+
+func (_c *MockChatSessionManager_RetrieveByScreenName_Call) Run(run func(screenName string)) *MockChatSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_RetrieveByScreenName_Call) Return(_a0 *user.Session) *MockChatSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockChatSessionManager_RetrieveByScreenName_Call) RunAndReturn(run func(string) *user.Session) *MockChatSessionManager_RetrieveByScreenName_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// SendToScreenName provides a mock function with given fields: ctx, screenName, msg
+func (_m *MockChatSessionManager) SendToScreenName(ctx context.Context, screenName string, msg oscar.XMessage) {
+	_m.Called(ctx, screenName, msg)
+}
+
+// MockChatSessionManager_SendToScreenName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendToScreenName'
+type MockChatSessionManager_SendToScreenName_Call struct {
+	*mock.Call
+}
+
+// SendToScreenName is a helper method to define mock.On call
+//   - ctx context.Context
+//   - screenName string
+//   - msg oscar.XMessage
+func (_e *MockChatSessionManager_Expecter) SendToScreenName(ctx interface{}, screenName interface{}, msg interface{}) *MockChatSessionManager_SendToScreenName_Call {
+	return &MockChatSessionManager_SendToScreenName_Call{Call: _e.mock.On("SendToScreenName", ctx, screenName, msg)}
+}
+
+func (_c *MockChatSessionManager_SendToScreenName_Call) Run(run func(ctx context.Context, screenName string, msg oscar.XMessage)) *MockChatSessionManager_SendToScreenName_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(string), args[2].(oscar.XMessage))
+	})
+	return _c
+}
+
+func (_c *MockChatSessionManager_SendToScreenName_Call) Return() *MockChatSessionManager_SendToScreenName_Call {
+	_c.Call.Return()
+	return _c
+}
+
+func (_c *MockChatSessionManager_SendToScreenName_Call) RunAndReturn(run func(context.Context, string, oscar.XMessage)) *MockChatSessionManager_SendToScreenName_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockChatSessionManager creates a new instance of MockChatSessionManager. 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 NewMockChatSessionManager(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockChatSessionManager {
+	mock := &MockChatSessionManager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 3 - 4
server/chat_test.go

@@ -120,15 +120,14 @@ func TestSendAndReceiveChatChannelMsgToHost(t *testing.T) {
 			//
 			// initialize dependencies
 			//
-			crm := NewMockSessionManager(t)
+			crm := NewMockChatSessionManager(t)
 			crm.EXPECT().
 				BroadcastExcept(mock.Anything, tc.userSession, tc.expectSNACToParticipants)
 			//
 			// send input SNAC
 			//
 			svc := ChatService{}
-			room := ChatRoom{SessionManager: crm}
-			outputSNAC, err := svc.ChannelMsgToHostHandler(context.Background(), tc.userSession, room, tc.inputSNAC)
+			outputSNAC, err := svc.ChannelMsgToHostHandler(context.Background(), tc.userSession, crm, tc.inputSNAC)
 			assert.NoError(t, err)
 
 			if tc.expectOutput.SnacFrame == (oscar.SnacFrame{}) {
@@ -222,7 +221,7 @@ func TestChatRouter_RouteChat(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(0)
 
-			err := router.RouteChat(nil, nil, ChatRoom{}, tc.input.SnacFrame, bufIn, bufOut, &seq)
+			err := router.RouteChat(nil, nil, nil, tc.input.SnacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 5 - 5
server/connection.go

@@ -124,13 +124,13 @@ func HandleChatConnection(ctx context.Context, cr *ChatRegistry, rw io.ReadWrite
 		return
 	}
 
-	room, err := cr.Retrieve(string(cookie.Cookie))
+	room, chatSessMgr, err := cr.Retrieve(string(cookie.Cookie))
 	if err != nil {
 		logger.ErrorContext(ctx, "unable to find chat room", "err", err.Error())
 		return
 	}
 
-	chatSess, found := room.Retrieve(cookie.SessID)
+	chatSess, found := chatSessMgr.Retrieve(cookie.SessID)
 	if !found {
 		logger.ErrorContext(ctx, "unable to find user for session", "sessID", cookie.SessID)
 		return
@@ -139,8 +139,8 @@ func HandleChatConnection(ctx context.Context, cr *ChatRegistry, rw io.ReadWrite
 	defer chatSess.Close()
 	go func() {
 		<-chatSess.Closed()
-		AlertUserLeft(ctx, chatSess, room)
-		room.Remove(chatSess)
+		AlertUserLeft(ctx, chatSess, chatSessMgr)
+		chatSessMgr.Remove(chatSess)
 		cr.MaybeRemoveRoom(room.Cookie)
 	}()
 
@@ -151,7 +151,7 @@ func HandleChatConnection(ctx context.Context, cr *ChatRegistry, rw io.ReadWrite
 	}
 
 	fnClientReqHandler := func(ctx context.Context, r io.Reader, w io.Writer, seq *uint32) error {
-		return router.Route(ctx, chatSess, r, w, seq, room)
+		return router.Route(ctx, chatSess, r, w, seq, chatSessMgr, room)
 	}
 	fnAlertHandler := func(ctx context.Context, msg oscar.XMessage, w io.Writer, seq *uint32) error {
 		return writeOutSNAC(oscar.SnacFrame{}, msg.SnacFrame, msg.SnacOut, seq, w)

+ 17 - 17
server/connection_test.go

@@ -20,11 +20,11 @@ func TestHandleChatConnection_Notification(t *testing.T) {
 	logger := NewLogger(cfg)
 
 	room := ChatRoom{
-		Name:           "test chat room!",
-		SessionManager: user.NewSessionManager(logger),
+		Name: "test chat room!",
 	}
-	bobSess := room.NewSessionWithSN("bob-sess-id", "bob")
-	cr.Register(room)
+	sm := user.NewSessionManager(logger)
+	sess := sm.NewSessionWithSN("bob-sess-id", "bob")
+	cr.Register(room, sm)
 
 	msgIn := []oscar.XMessage{
 		{
@@ -34,7 +34,7 @@ func TestHandleChatConnection_Notification(t *testing.T) {
 			},
 			SnacOut: oscar.SNAC_0x0E_0x03_ChatUsersJoined{
 				Users: []oscar.TLVUserInfo{
-					bobSess.TLVUserInfo(),
+					sess.TLVUserInfo(),
 				},
 			},
 		},
@@ -65,17 +65,17 @@ func TestHandleChatConnection_Notification(t *testing.T) {
 
 	go func() {
 		wg.Wait()
-		bobSess.Close()
+		sess.Close()
 	}()
 
 	pr, _ := io.Pipe()
 	rw := bufio.NewReadWriter(bufio.NewReader(pr), bufio.NewWriter(&bytes.Buffer{}))
 
 	for _, msg := range msgIn {
-		room.SendToScreenName(ctx, "bob", msg)
+		sm.SendToScreenName(ctx, "bob", msg)
 	}
 
-	dispatchIncomingMessages(ctx, bobSess, uint32(0), rw, logger, routeSig, alertHandler)
+	dispatchIncomingMessages(ctx, sess, uint32(0), rw, logger, routeSig, alertHandler)
 
 	assert.Equal(t, msgIn, msgOut)
 }
@@ -88,11 +88,11 @@ func TestHandleChatConnection_ClientRequestFLAP(t *testing.T) {
 	logger := NewLogger(cfg)
 
 	room := ChatRoom{
-		Name:           "test chat room!",
-		SessionManager: user.NewSessionManager(logger),
+		Name: "test chat room!",
 	}
-	bobSess := room.NewSessionWithSN("bob-sess-id", "bob")
-	cr.Register(room)
+	sm := user.NewSessionManager(logger)
+	sess := sm.NewSessionWithSN("bob-sess-id", "bob")
+	cr.Register(room, sm)
 
 	payloads := [][]byte{
 		{'a', 'b', 'c', 'd'},
@@ -135,7 +135,7 @@ func TestHandleChatConnection_ClientRequestFLAP(t *testing.T) {
 		pw.Close()
 	}()
 
-	dispatchIncomingMessages(ctx, bobSess, uint32(0), rw, logger, routeSig, alertHandler)
+	dispatchIncomingMessages(ctx, sess, uint32(0), rw, logger, routeSig, alertHandler)
 
 	assert.Equal(t, payloads, msgOut)
 }
@@ -148,11 +148,11 @@ func TestHandleChatConnection_SessionClosed(t *testing.T) {
 	logger := NewLogger(cfg)
 
 	room := ChatRoom{
-		Name:           "test chat room!",
-		SessionManager: user.NewSessionManager(logger),
+		Name: "test chat room!",
 	}
-	sess := room.NewSessionWithSN("bob-sess-id", "bob")
-	cr.Register(room)
+	sm := user.NewSessionManager(logger)
+	sess := sm.NewSessionWithSN("bob-sess-id", "bob")
+	cr.Register(room, sm)
 
 	routeSig := func(ctx context.Context, buf io.Reader, w io.Writer, u *uint32) error {
 		t.Fatal("not expecting any output")

+ 9 - 9
server/oservice.go

@@ -32,7 +32,7 @@ type OServiceChatHandler interface {
 	OServiceHandler
 	WriteOServiceHostOnline(w io.Writer, sequence *uint32) error
 	ServiceRequestHandler(ctx context.Context, sess *user.Session, snacPayloadIn oscar.SNAC_0x01_0x04_OServiceServiceRequest) (oscar.XMessage, error)
-	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, room ChatRoom) error
+	ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom) error
 }
 
 type OServiceRouter struct {
@@ -148,7 +148,7 @@ type OServiceChatRouter struct {
 	OServiceChatHandler
 }
 
-func (rt OServiceChatRouter) RouteOService(ctx context.Context, sess *user.Session, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func (rt OServiceChatRouter) RouteOService(ctx context.Context, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom, SNACFrame oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch SNACFrame.SubGroup {
 	case oscar.OServiceServiceRequest:
 		inSNAC := oscar.SNAC_0x01_0x04_OServiceServiceRequest{}
@@ -171,7 +171,7 @@ func (rt OServiceChatRouter) RouteOService(ctx context.Context, sess *user.Sessi
 		}
 		rt.Logger.InfoContext(ctx, "user signed on")
 		rt.logRequest(ctx, SNACFrame, inSNAC)
-		return rt.OServiceChatHandler.ClientOnlineHandler(ctx, inSNAC, sess, room)
+		return rt.OServiceChatHandler.ClientOnlineHandler(ctx, inSNAC, sess, chatSessMgr, room)
 	default:
 		return rt.OServiceRouter.RouteOService(ctx, sess, SNACFrame, r, w, sequence)
 	}
@@ -336,11 +336,11 @@ func (s OServiceServiceForBOS) ServiceRequestHandler(_ context.Context, sess *us
 		return oscar.XMessage{}, err
 	}
 
-	room, err := s.cr.Retrieve(string(roomSnac.Cookie))
+	room, chatSessMgr, err := s.cr.Retrieve(string(roomSnac.Cookie))
 	if err != nil {
 		return oscar.XMessage{}, ErrUnsupportedSubGroup
 	}
-	room.NewSessionWithSN(sess.ID(), sess.ScreenName())
+	chatSessMgr.NewSessionWithSN(sess.ID(), sess.ScreenName())
 
 	return oscar.XMessage{
 		SnacFrame: oscar.SnacFrame{
@@ -436,9 +436,9 @@ func (s OServiceServiceForChat) WriteOServiceHostOnline(w io.Writer, sequence *u
 	return writeOutSNAC(oscar.SnacFrame{}, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func (s OServiceServiceForChat) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, room ChatRoom) error {
-	SendChatRoomInfoUpdate(ctx, sess, room)
-	AlertUserJoined(ctx, sess, room)
-	SetOnlineChatUsers(ctx, sess, room)
+func (s OServiceServiceForChat) ClientOnlineHandler(ctx context.Context, _ oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom) error {
+	SendChatRoomInfoUpdate(ctx, sess, chatSessMgr, room)
+	AlertUserJoined(ctx, sess, chatSessMgr)
+	SetOnlineChatUsers(ctx, sess, chatSessMgr)
 	return nil
 }

+ 11 - 10
server/oservice_chat_mock.go

@@ -26,13 +26,13 @@ func (_m *MockOServiceChatHandler) EXPECT() *MockOServiceChatHandler_Expecter {
 	return &MockOServiceChatHandler_Expecter{mock: &_m.Mock}
 }
 
-// ClientOnlineHandler provides a mock function with given fields: ctx, snacPayloadIn, sess, room
-func (_m *MockOServiceChatHandler) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, room ChatRoom) error {
-	ret := _m.Called(ctx, snacPayloadIn, sess, room)
+// ClientOnlineHandler provides a mock function with given fields: ctx, snacPayloadIn, sess, chatSessMgr, room
+func (_m *MockOServiceChatHandler) ClientOnlineHandler(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom) error {
+	ret := _m.Called(ctx, snacPayloadIn, sess, chatSessMgr, room)
 
 	var r0 error
-	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *user.Session, ChatRoom) error); ok {
-		r0 = rf(ctx, snacPayloadIn, sess, room)
+	if rf, ok := ret.Get(0).(func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *user.Session, ChatSessionManager, ChatRoom) error); ok {
+		r0 = rf(ctx, snacPayloadIn, sess, chatSessMgr, room)
 	} else {
 		r0 = ret.Error(0)
 	}
@@ -49,14 +49,15 @@ type MockOServiceChatHandler_ClientOnlineHandler_Call struct {
 //   - ctx context.Context
 //   - snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline
 //   - sess *user.Session
+//   - chatSessMgr ChatSessionManager
 //   - room ChatRoom
-func (_e *MockOServiceChatHandler_Expecter) ClientOnlineHandler(ctx interface{}, snacPayloadIn interface{}, sess interface{}, room interface{}) *MockOServiceChatHandler_ClientOnlineHandler_Call {
-	return &MockOServiceChatHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", ctx, snacPayloadIn, sess, room)}
+func (_e *MockOServiceChatHandler_Expecter) ClientOnlineHandler(ctx interface{}, snacPayloadIn interface{}, sess interface{}, chatSessMgr interface{}, room interface{}) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+	return &MockOServiceChatHandler_ClientOnlineHandler_Call{Call: _e.mock.On("ClientOnlineHandler", ctx, snacPayloadIn, sess, chatSessMgr, room)}
 }
 
-func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, room ChatRoom)) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) Run(run func(ctx context.Context, snacPayloadIn oscar.SNAC_0x01_0x02_OServiceClientOnline, sess *user.Session, chatSessMgr ChatSessionManager, room ChatRoom)) *MockOServiceChatHandler_ClientOnlineHandler_Call {
 	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[2].(*user.Session), args[3].(ChatRoom))
+		run(args[0].(context.Context), args[1].(oscar.SNAC_0x01_0x02_OServiceClientOnline), args[2].(*user.Session), args[3].(ChatSessionManager), args[4].(ChatRoom))
 	})
 	return _c
 }
@@ -66,7 +67,7 @@ func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) Return(_a0 error) *M
 	return _c
 }
 
-func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *user.Session, ChatRoom) error) *MockOServiceChatHandler_ClientOnlineHandler_Call {
+func (_c *MockOServiceChatHandler_ClientOnlineHandler_Call) RunAndReturn(run func(context.Context, oscar.SNAC_0x01_0x02_OServiceClientOnline, *user.Session, ChatSessionManager, ChatRoom) error) *MockOServiceChatHandler_ClientOnlineHandler_Call {
 	_c.Call.Return(run)
 	return _c
 }

+ 4 - 5
server/oservice_test.go

@@ -115,15 +115,14 @@ func TestReceiveAndSendServiceRequest(t *testing.T) {
 			//
 			// initialize dependencies
 			//
-			sm := NewMockSessionManager(t)
+			sm := NewMockChatSessionManager(t)
 			cr := NewChatRegistry()
 			if tc.chatRoom != nil {
 				sm.EXPECT().
 					NewSessionWithSN(tc.userSession.ID(), tc.userSession.ScreenName()).
 					Return(&user.Session{}).
 					Maybe()
-				tc.chatRoom.SessionManager = sm
-				cr.Register(*tc.chatRoom)
+				cr.Register(*tc.chatRoom, sm)
 			}
 			//
 			// send input SNAC
@@ -682,7 +681,7 @@ func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
 				Return(tc.output, tc.handlerErr).
 				Maybe()
 			svcBOS.EXPECT().
-				ClientOnlineHandler(mock.Anything, tc.input.SnacOut, mock.Anything, mock.Anything).
+				ClientOnlineHandler(mock.Anything, tc.input.SnacOut, mock.Anything, mock.Anything, mock.Anything).
 				Return(tc.handlerErr).
 				Maybe()
 
@@ -702,7 +701,7 @@ func TestOServiceRouter_RouteOService_ForChat(t *testing.T) {
 			bufOut := &bytes.Buffer{}
 			seq := uint32(1)
 
-			err := router.RouteOService(nil, nil, ChatRoom{}, tc.input.SnacFrame, bufIn, bufOut, &seq)
+			err := router.RouteOService(nil, nil, nil, ChatRoom{}, tc.input.SnacFrame, bufIn, bufOut, &seq)
 			assert.ErrorIs(t, err, tc.expectErr)
 			if tc.expectErr != nil {
 				return

+ 6 - 4
server/router.go

@@ -28,6 +28,7 @@ func NewBOSServiceRouter(logger *slog.Logger, cfg Config, fm FeedbagManager, sm
 		RouteLogger: RouteLogger{
 			Logger: logger,
 		},
+		NewChatSessMgr: func() ChatSessionManager { return user.NewSessionManager(logger) },
 	}
 }
 
@@ -54,6 +55,7 @@ type BOSServiceRouter struct {
 	fm  FeedbagManager
 	cfg Config
 	RouteLogger
+	NewChatSessMgr func() ChatSessionManager
 }
 
 func (rt *BOSServiceRouter) Route(ctx context.Context, sess *user.Session, r io.Reader, w io.Writer, sequence *uint32) error {
@@ -73,7 +75,7 @@ func (rt *BOSServiceRouter) Route(ctx context.Context, sess *user.Session, r io.
 		case oscar.ICBM:
 			return rt.RouteICBM(ctx, sess, snac, r, w, sequence)
 		case oscar.CHAT_NAV:
-			return rt.RouteChatNav(ctx, sess, snac, r, w, sequence)
+			return rt.RouteChatNav(ctx, sess, rt.NewChatSessMgr, snac, r, w, sequence)
 		case oscar.FEEDBAG:
 			return rt.RouteFeedbag(ctx, sess, snac, r, w, sequence)
 		case oscar.BUCP:
@@ -185,7 +187,7 @@ type ChatServiceRouter struct {
 	RouteLogger
 }
 
-func (rt *ChatServiceRouter) Route(ctx context.Context, sess *user.Session, r io.Reader, w io.Writer, sequence *uint32, room ChatRoom) error {
+func (rt *ChatServiceRouter) Route(ctx context.Context, sess *user.Session, r io.Reader, w io.Writer, sequence *uint32, chatSessMgr ChatSessionManager, room ChatRoom) error {
 	snac := oscar.SnacFrame{}
 	if err := oscar.Unmarshal(&snac, r); err != nil {
 		return err
@@ -194,9 +196,9 @@ func (rt *ChatServiceRouter) Route(ctx context.Context, sess *user.Session, r io
 	err := func() error {
 		switch snac.FoodGroup {
 		case oscar.OSERVICE:
-			return rt.RouteOService(ctx, sess, room, snac, r, w, sequence)
+			return rt.RouteOService(ctx, sess, chatSessMgr, room, snac, r, w, sequence)
 		case oscar.CHAT:
-			return rt.RouteChat(ctx, sess, room, snac, r, w, sequence)
+			return rt.RouteChat(ctx, sess, chatSessMgr, snac, r, w, sequence)
 		default:
 			return ErrUnsupportedSubGroup
 		}

+ 20 - 14
server/session.go

@@ -17,7 +17,6 @@ type ChatRoom struct {
 	Cookie         string
 	InstanceNumber uint16
 	Name           string
-	SessionManager
 }
 
 func (c ChatRoom) TLVList() []oscar.TLV {
@@ -33,38 +32,45 @@ func (c ChatRoom) TLVList() []oscar.TLV {
 }
 
 type ChatRegistry struct {
-	store    map[string]ChatRoom
-	mapMutex sync.RWMutex
+	chatRoomStore map[string]ChatRoom
+	smStore       map[string]ChatSessionManager
+	mapMutex      sync.RWMutex
 }
 
 func NewChatRegistry() *ChatRegistry {
 	return &ChatRegistry{
-		store: make(map[string]ChatRoom),
+		chatRoomStore: make(map[string]ChatRoom),
+		smStore:       make(map[string]ChatSessionManager),
 	}
 }
 
-func (c *ChatRegistry) Register(room ChatRoom) {
+func (c *ChatRegistry) Register(room ChatRoom, sm ChatSessionManager) {
 	c.mapMutex.Lock()
 	defer c.mapMutex.Unlock()
-	c.store[room.Cookie] = room
+	c.chatRoomStore[room.Cookie] = room
+	c.smStore[room.Cookie] = sm
 }
 
-func (c *ChatRegistry) Retrieve(chatID string) (ChatRoom, error) {
+func (c *ChatRegistry) Retrieve(chatID string) (ChatRoom, ChatSessionManager, error) {
 	c.mapMutex.RLock()
 	defer c.mapMutex.RUnlock()
-	sm, found := c.store[chatID]
+	cr, found := c.chatRoomStore[chatID]
 	if !found {
-		return sm, errors.New("unable to find session manager for chat")
+		return ChatRoom{}, nil, errors.New("unable to find chat room")
 	}
-	return sm, nil
+	sm, found := c.smStore[chatID]
+	if !found {
+		panic("unable to find session manager for chat")
+	}
+	return cr, sm, nil
 }
 
 func (c *ChatRegistry) MaybeRemoveRoom(chatID string) {
 	c.mapMutex.Lock()
 	defer c.mapMutex.Unlock()
-
-	room, found := c.store[chatID]
-	if found && room.Empty() {
-		delete(c.store, chatID)
+	sm, found := c.smStore[chatID]
+	if found && sm.Empty() {
+		delete(c.chatRoomStore, chatID)
+		delete(c.smStore, chatID)
 	}
 }

+ 0 - 78
server/session_manager_mock.go

@@ -58,41 +58,6 @@ func (_c *MockSessionManager_Broadcast_Call) RunAndReturn(run func(context.Conte
 	return _c
 }
 
-// BroadcastExcept provides a mock function with given fields: ctx, except, msg
-func (_m *MockSessionManager) BroadcastExcept(ctx context.Context, except *user.Session, msg oscar.XMessage) {
-	_m.Called(ctx, 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
-//   - ctx context.Context
-//   - except *user.Session
-//   - msg oscar.XMessage
-func (_e *MockSessionManager_Expecter) BroadcastExcept(ctx interface{}, except interface{}, msg interface{}) *MockSessionManager_BroadcastExcept_Call {
-	return &MockSessionManager_BroadcastExcept_Call{Call: _e.mock.On("BroadcastExcept", ctx, except, msg)}
-}
-
-func (_c *MockSessionManager_BroadcastExcept_Call) Run(run func(ctx context.Context, except *user.Session, msg oscar.XMessage)) *MockSessionManager_BroadcastExcept_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run(args[0].(context.Context), args[1].(*user.Session), args[2].(oscar.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(context.Context, *user.Session, oscar.XMessage)) *MockSessionManager_BroadcastExcept_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // BroadcastToScreenNames provides a mock function with given fields: ctx, screenNames, msg
 func (_m *MockSessionManager) BroadcastToScreenNames(ctx context.Context, screenNames []string, msg oscar.XMessage) {
 	_m.Called(ctx, screenNames, msg)
@@ -214,49 +179,6 @@ func (_c *MockSessionManager_NewSessionWithSN_Call) RunAndReturn(run func(string
 	return _c
 }
 
-// Participants provides a mock function with given fields:
-func (_m *MockSessionManager) Participants() []*user.Session {
-	ret := _m.Called()
-
-	var r0 []*user.Session
-	if rf, ok := ret.Get(0).(func() []*user.Session); ok {
-		r0 = rf()
-	} else {
-		if ret.Get(0) != nil {
-			r0 = ret.Get(0).([]*user.Session)
-		}
-	}
-
-	return r0
-}
-
-// MockSessionManager_Participants_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Participants'
-type MockSessionManager_Participants_Call struct {
-	*mock.Call
-}
-
-// Participants is a helper method to define mock.On call
-func (_e *MockSessionManager_Expecter) Participants() *MockSessionManager_Participants_Call {
-	return &MockSessionManager_Participants_Call{Call: _e.mock.On("Participants")}
-}
-
-func (_c *MockSessionManager_Participants_Call) Run(run func()) *MockSessionManager_Participants_Call {
-	_c.Call.Run(func(args mock.Arguments) {
-		run()
-	})
-	return _c
-}
-
-func (_c *MockSessionManager_Participants_Call) Return(_a0 []*user.Session) *MockSessionManager_Participants_Call {
-	_c.Call.Return(_a0)
-	return _c
-}
-
-func (_c *MockSessionManager_Participants_Call) RunAndReturn(run func() []*user.Session) *MockSessionManager_Participants_Call {
-	_c.Call.Return(run)
-	return _c
-}
-
 // Remove provides a mock function with given fields: sess
 func (_m *MockSessionManager) Remove(sess *user.Session) {
 	_m.Called(sess)

+ 6 - 2
server/types.go

@@ -19,17 +19,21 @@ type FeedbagManager interface {
 
 type SessionManager interface {
 	Broadcast(ctx context.Context, msg oscar.XMessage)
-	BroadcastExcept(ctx context.Context, except *user.Session, msg oscar.XMessage)
 	BroadcastToScreenNames(ctx context.Context, screenNames []string, msg oscar.XMessage)
 	Empty() bool
 	NewSessionWithSN(sessID string, screenName string) *user.Session
-	Participants() []*user.Session
 	Remove(sess *user.Session)
 	Retrieve(ID string) (*user.Session, bool)
 	RetrieveByScreenName(screenName string) *user.Session
 	SendToScreenName(ctx context.Context, screenName string, msg oscar.XMessage)
 }
 
+type ChatSessionManager interface {
+	SessionManager
+	BroadcastExcept(ctx context.Context, except *user.Session, msg oscar.XMessage)
+	Participants() []*user.Session
+}
+
 type ProfileManager interface {
 	RetrieveProfile(screenName string) (string, error)
 	UpsertProfile(screenName string, body string) error