Josh Knight 2 лет назад
Родитель
Сommit
b7280a43f3

+ 7 - 1
.mockery.yaml

@@ -69,6 +69,9 @@ packages:
       PermitDenyService:
         config:
           filename: "mock_permit_deny_test.go"
+      AdminService:
+        config:
+          filename: "mock_admin_test.go"
   github.com/mk6i/retro-aim-server/foodgroup:
     interfaces:
       FeedbagManager:
@@ -106,4 +109,7 @@ packages:
           filename: "mock_cookie_baker_test.go"
       buddyBroadcaster:
         config:
-          filename: "mock_buddy_broadcaster_test.go"
+          filename: "mock_buddy_broadcaster_test.go"
+      AccountManager:
+        config:
+          filename: "mock_account_manager_test.go"

+ 390 - 0
foodgroup/admin_test.go

@@ -0,0 +1,390 @@
+package foodgroup
+
+import (
+	"testing"
+
+	"github.com/mk6i/retro-aim-server/config"
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestAdminService_ConfirmRequest(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// cfg is the app configuration
+		cfg config.Config
+		// inputSNAC is the SNAC sent from the client to the server
+		inputSNAC wire.SNACMessage
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+		// expectOutput is the SNAC sent from the server to client
+		expectOutput wire.SNACMessage
+		// expectErr is the expected error returned
+		expectErr error
+	}{
+		{
+			name: "user confirms their account",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminAcctConfirmRequest,
+				},
+				Body: wire.SNAC_0x07_0x06_AdminConfirmRequest{},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminAcctConfirmReply,
+				},
+				Body: wire.SNAC_0x07_0x07_AdminConfirmReply{
+					Status: wire.AdminAcctConfirmStatusEmailSent,
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			sessionManager := newMockSessionManager(t)
+			accountManager := newMockAccountManager(t)
+			buddyBroadcaster := newMockbuddyBroadcaster(t)
+			svc := AdminService{
+				sessionManager:         sessionManager,
+				accountManager:         accountManager,
+				buddyUpdateBroadcaster: buddyBroadcaster,
+			}
+			outputSNAC, err := svc.ConfirmRequest(nil, tc.inputSNAC.Frame)
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
+
+func TestAdminService_InfoQuery(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// cfg is the app configuration
+		cfg config.Config
+		// inputSNAC is the SNAC sent from the client to the server
+		inputSNAC wire.SNACMessage
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+		// userSession is the session of the user
+		userSession *state.Session
+		// expectOutput is the SNAC sent from the server to client
+		expectOutput wire.SNACMessage
+		// expectErr is the expected error returned
+		expectErr error
+	}{
+		{
+			name:        "user requests account registration status",
+			userSession: newTestSession("chattingchuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoQuery,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x02_AdminInfoQuery{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVRegistrationStatus, uint16(0x00))},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoReply,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x03_AdminInfoReply{
+					Permissions: wire.AdminInfoPermissionsReadWrite,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVRegistrationStatus, wire.AdminInfoRegStatusFullDisclosure),
+						},
+					},
+				},
+			},
+		},
+		{
+			name:        "user requests account email address",
+			userSession: newTestSession("chattingchuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoQuery,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x02_AdminInfoQuery{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVEmailAddress, uint16(0x00))},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoReply,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x03_AdminInfoReply{
+					Permissions: wire.AdminInfoPermissionsReadWrite,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVEmailAddress, "chattingchuck@aol.com"), // todo: get from session
+						},
+					},
+				},
+			},
+		},
+		{
+			name:        "user requests formatted screenname",
+			userSession: newTestSession("ChattingChuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoQuery,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x02_AdminInfoQuery{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, uint16(0x00))},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoReply,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x03_AdminInfoReply{
+					Permissions: wire.AdminInfoPermissionsReadWrite,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, "ChattingChuck"),
+						},
+					},
+				},
+			},
+		},
+		{
+			name:        "user requests invalid TLV",
+			userSession: newTestSession("ChattingChuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoQuery,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x02_AdminInfoQuery{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(uint16(0x99), uint16(0x00))},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminErr,
+					RequestID: 1337,
+				},
+				Body: wire.SNACError{
+					Code: wire.ErrorCodeNotSupportedByHost,
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			sessionManager := newMockSessionManager(t)
+			accountManager := newMockAccountManager(t)
+			buddyBroadcaster := newMockbuddyBroadcaster(t)
+			svc := AdminService{
+				sessionManager:         sessionManager,
+				accountManager:         accountManager,
+				buddyUpdateBroadcaster: buddyBroadcaster,
+			}
+			outputSNAC, err := svc.InfoQuery(nil, tc.userSession, tc.inputSNAC.Frame, tc.inputSNAC.Body.(wire.SNAC_0x07_0x02_AdminInfoQuery))
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}
+
+func TestAdminService_InfoChangeRequest(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// cfg is the app configuration
+		cfg config.Config
+		// inputSNAC is the SNAC sent from the client to the server
+		inputSNAC wire.SNACMessage
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+		// userSession is the session of the user
+		userSession *state.Session
+		// expectOutput is the SNAC sent from the server to client
+		expectOutput wire.SNACMessage
+		// expectErr is the expected error returned
+		expectErr error
+	}{
+		{
+			name:        "user changes screen name format successfully",
+			userSession: newTestSession("chattingchuck"),
+			mockParams: mockParams{
+				accountManagerParams: accountManagerParams{
+					accountManagerUpdateDisplayScreenNameParams: accountManagerUpdateDisplayScreenNameParams{
+						{
+							displayScreenName: state.DisplayScreenName("Chatting Chuck"),
+						},
+					},
+				},
+				buddyBroadcasterParams: buddyBroadcasterParams{
+					broadcastBuddyArrivedParams: broadcastBuddyArrivedParams{
+						{
+							screenName: state.NewIdentScreenName("Chatting Chuck"),
+						},
+					},
+				},
+			},
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoChangeRequest,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, "Chatting Chuck"),
+						},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoChangeReply,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+					Permissions: wire.AdminInfoPermissionsReadWrite,
+					TLVBlock: wire.TLVBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, "Chatting Chuck"),
+						},
+					},
+				},
+			},
+		},
+		{
+			name:        "proposed screen name is too long",
+			userSession: newTestSession("chattingchuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoChangeRequest,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, "c  h  a  t  t  i  n  g  c  h  u  c  k")},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminErr,
+					RequestID: 1337,
+				},
+				Body: wire.SNACError{
+					Code: wire.AdminInfoErrorInvalidNickNameLength,
+				},
+			},
+		},
+		{
+			name:        "proposed screen name does not match session's screen name (malicous client)",
+			userSession: newTestSession("chattingchuck"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminInfoChangeRequest,
+					RequestID: 1337,
+				},
+				Body: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.AdminTLVScreenNameFormatted, "QuietQuinton")},
+					},
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.Admin,
+					SubGroup:  wire.AdminErr,
+					RequestID: 1337,
+				},
+				Body: wire.SNACError{
+					Code: wire.AdminInfoErrorInvalidNickName,
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			sessionManager := newMockSessionManager(t)
+			accountManager := newMockAccountManager(t)
+			buddyBroadcaster := newMockbuddyBroadcaster(t)
+
+			for _, params := range tc.mockParams.accountManagerParams.accountManagerUpdateDisplayScreenNameParams {
+				accountManager.EXPECT().
+					UpdateDisplayScreenName(params.displayScreenName).
+					Return(params.err)
+			}
+
+			for _, params := range tc.mockParams.broadcastBuddyArrivedParams {
+				p := params
+				buddyBroadcaster.EXPECT().
+					BroadcastBuddyArrived(mock.Anything, mock.MatchedBy(func(s *state.Session) bool {
+						return s.IdentScreenName() == p.screenName
+					})).
+					Return(nil)
+			}
+
+			svc := AdminService{
+				sessionManager:         sessionManager,
+				accountManager:         accountManager,
+				buddyUpdateBroadcaster: buddyBroadcaster,
+			}
+			outputSNAC, err := svc.InfoChangeRequest(nil, tc.userSession, tc.inputSNAC.Frame, tc.inputSNAC.Body.(wire.SNAC_0x07_0x04_AdminInfoChangeRequest))
+			assert.ErrorIs(t, err, tc.expectErr)
+			if tc.expectErr != nil {
+				return
+			}
+			assert.Equal(t, tc.expectOutput, outputSNAC)
+		})
+	}
+}

+ 81 - 0
foodgroup/auth_test.go

@@ -991,6 +991,87 @@ func TestAuthService_RegisterBOSSession_HappyPath(t *testing.T) {
 	assert.Equal(t, sess, have)
 }
 
+func TestAuthService_RegisterBOSSession_SessionNotFound(t *testing.T) {
+	sess := newTestSession("screen-name")
+
+	sessionManager := newMockSessionManager(t)
+	sessionManager.EXPECT().
+		AddSession(sess.DisplayScreenName()).
+		Return(nil)
+
+	authCookie := []byte(`the-auth-cookie`)
+	cookieBaker := newMockCookieBaker(t)
+
+	cookieBaker.EXPECT().
+		Crack(authCookie).
+		Return([]byte("screen-name"), nil)
+
+	userManager := newMockUserManager(t)
+	userManager.EXPECT().
+		User(sess.IdentScreenName()).
+		Return(&state.User{DisplayScreenName: sess.DisplayScreenName()}, nil)
+
+	svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil)
+
+	have, err := svc.RegisterBOSSession(authCookie)
+	assert.NoError(t, err)
+	assert.Nil(t, have)
+}
+
+func TestAuthService_RetrieveBOSSession_HappyPath(t *testing.T) {
+	sess := newTestSession("screen-name")
+
+	sessionManager := newMockSessionManager(t)
+	sessionManager.EXPECT().
+		RetrieveSession(sess.IdentScreenName()).
+		Return(sess)
+
+	authCookie := []byte(`the-auth-cookie`)
+
+	cookieBaker := newMockCookieBaker(t)
+	cookieBaker.EXPECT().
+		Crack(authCookie).
+		Return([]byte("screen-name"), nil)
+
+	userManager := newMockUserManager(t)
+	userManager.EXPECT().
+		User(sess.IdentScreenName()).
+		Return(&state.User{IdentScreenName: sess.IdentScreenName()}, nil)
+
+	svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil)
+
+	have, err := svc.RetrieveBOSSession(authCookie)
+	assert.NoError(t, err)
+	assert.Equal(t, sess, have)
+}
+
+func TestAuthService_RetrieveBOSSession_SessionNotFound(t *testing.T) {
+	sess := newTestSession("screen-name")
+
+	sessionManager := newMockSessionManager(t)
+	sessionManager.EXPECT().
+		RetrieveSession(sess.IdentScreenName()).
+		Return(nil)
+
+	authCookie := []byte(`the-auth-cookie`)
+	cookieBaker := newMockCookieBaker(t)
+
+	cookieBaker.EXPECT().
+		Crack(authCookie).
+		Return([]byte("screen-name"), nil)
+
+	userManager := newMockUserManager(t)
+	userManager.EXPECT().
+		User(sess.IdentScreenName()).
+		Return(&state.User{IdentScreenName: sess.IdentScreenName()}, nil)
+
+	svc := NewAuthService(config.Config{}, sessionManager, nil, userManager, nil, cookieBaker, nil, nil, nil)
+
+	have, err := svc.RetrieveBOSSession(authCookie)
+	assert.NoError(t, err)
+	assert.Nil(t, have)
+}
+
 func TestAuthService_SignoutChat(t *testing.T) {
 	tests := []struct {
 		// name is the unit test name

+ 81 - 0
foodgroup/mock_account_manager_test.go

@@ -0,0 +1,81 @@
+// Code generated by mockery v2.43.2. DO NOT EDIT.
+
+package foodgroup
+
+import (
+	state "github.com/mk6i/retro-aim-server/state"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// mockAccountManager is an autogenerated mock type for the AccountManager type
+type mockAccountManager struct {
+	mock.Mock
+}
+
+type mockAccountManager_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *mockAccountManager) EXPECT() *mockAccountManager_Expecter {
+	return &mockAccountManager_Expecter{mock: &_m.Mock}
+}
+
+// UpdateDisplayScreenName provides a mock function with given fields: displayScreenName
+func (_m *mockAccountManager) UpdateDisplayScreenName(displayScreenName state.DisplayScreenName) error {
+	ret := _m.Called(displayScreenName)
+
+	if len(ret) == 0 {
+		panic("no return value specified for UpdateDisplayScreenName")
+	}
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(state.DisplayScreenName) error); ok {
+		r0 = rf(displayScreenName)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// mockAccountManager_UpdateDisplayScreenName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateDisplayScreenName'
+type mockAccountManager_UpdateDisplayScreenName_Call struct {
+	*mock.Call
+}
+
+// UpdateDisplayScreenName is a helper method to define mock.On call
+//   - displayScreenName state.DisplayScreenName
+func (_e *mockAccountManager_Expecter) UpdateDisplayScreenName(displayScreenName interface{}) *mockAccountManager_UpdateDisplayScreenName_Call {
+	return &mockAccountManager_UpdateDisplayScreenName_Call{Call: _e.mock.On("UpdateDisplayScreenName", displayScreenName)}
+}
+
+func (_c *mockAccountManager_UpdateDisplayScreenName_Call) Run(run func(displayScreenName state.DisplayScreenName)) *mockAccountManager_UpdateDisplayScreenName_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(state.DisplayScreenName))
+	})
+	return _c
+}
+
+func (_c *mockAccountManager_UpdateDisplayScreenName_Call) Return(_a0 error) *mockAccountManager_UpdateDisplayScreenName_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *mockAccountManager_UpdateDisplayScreenName_Call) RunAndReturn(run func(state.DisplayScreenName) error) *mockAccountManager_UpdateDisplayScreenName_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// newMockAccountManager creates a new instance of mockAccountManager. 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 newMockAccountManager(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *mockAccountManager {
+	mock := &mockAccountManager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 42 - 0
foodgroup/oservice_test.go

@@ -142,6 +142,48 @@ func TestOServiceServiceForBOS_ServiceRequest(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "request info for connecting to admin svc, return admin svc connection metadata",
+			cfg: config.Config{
+				OSCARHost: "127.0.0.1",
+				AdminPort: "1234",
+			},
+			userSession: newTestSession("user_screen_name"),
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
+					FoodGroup: wire.Admin,
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.OService,
+					SubGroup:  wire.OServiceServiceResponse,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLV(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
+							wire.NewTLV(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
+							wire.NewTLV(wire.OServiceTLVTagsGroupID, wire.Admin),
+							wire.NewTLV(wire.OServiceTLVTagsSSLCertName, ""),
+							wire.NewTLV(wire.OServiceTLVTagsSSLState, uint8(0x00)),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				cookieIssuerParams: cookieIssuerParams{
+					{
+						data:   []byte("user_screen_name"),
+						cookie: []byte("the-cookie"),
+					},
+				},
+			},
+		},
 		{
 			name: "request info for connecting to chat room, return chat service and chat room metadata",
 			cfg: config.Config{

+ 14 - 0
foodgroup/test_helpers.go

@@ -23,6 +23,7 @@ type mockParams struct {
 	cookieIssuerParams
 	buddyBroadcasterParams
 	chatRoomRegistryParams
+	accountManagerParams
 }
 
 // bartManagerParams is a helper struct that contains mock parameters for
@@ -297,6 +298,19 @@ type cookieIssuerParams []struct {
 	err    error
 }
 
+// accountManagerParams is a helper struct that contains mock parameters for
+// accountManager methods
+type accountManagerParams struct {
+	accountManagerUpdateDisplayScreenNameParams
+}
+
+// accountManagerUpdateDisplayScreenNameParams is the list of parameters passed at the mock
+// accountManager.UpdateDisplayScreenName call site
+type accountManagerUpdateDisplayScreenNameParams []struct {
+	displayScreenName state.DisplayScreenName
+	err               error
+}
+
 // buddyBroadcasterParams is a helper struct that contains mock parameters for
 // buddyBroadcaster methods
 type buddyBroadcasterParams struct {

+ 140 - 0
server/oscar/handler/admin_test.go

@@ -0,0 +1,140 @@
+package handler
+
+import (
+	"bytes"
+	"log/slog"
+	"testing"
+
+	"github.com/mk6i/retro-aim-server/wire"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestAdminHandler_ConfirmRequest(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminAcctConfirmRequest,
+		},
+		Body: wire.SNAC_0x07_0x06_AdminConfirmRequest{},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminAcctConfirmReply,
+		},
+		Body: wire.SNAC_0x07_0x07_AdminConfirmReply{
+			Status: wire.AdminAcctConfirmStatusEmailSent,
+		},
+	}
+
+	svc := newMockAdminService(t)
+	svc.EXPECT().
+		ConfirmRequest(mock.Anything, input.Frame).
+		Return(output, nil)
+
+	h := NewAdminHandler(slog.Default(), svc)
+
+	responseWriter := newMockResponseWriter(t)
+	responseWriter.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.Marshal(input.Body, buf))
+
+	assert.NoError(t, h.ConfirmRequest(nil, nil, input.Frame, buf, responseWriter))
+}
+
+func TestAdminHandler_InfoQuery_RegistrationStatus(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminInfoQuery,
+		},
+		Body: wire.SNAC_0x07_0x02_AdminInfoQuery{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.AdminTLVRegistrationStatus, uint16(0x00))},
+			},
+		},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminInfoReply,
+		},
+		Body: wire.SNAC_0x07_0x03_AdminInfoReply{
+			Permissions: wire.AdminInfoPermissionsReadWrite,
+			TLVBlock: wire.TLVBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.AdminTLVRegistrationStatus, wire.AdminInfoRegStatusFullDisclosure),
+				},
+			},
+		},
+	}
+
+	svc := newMockAdminService(t)
+	svc.EXPECT().
+		ConfirmRequest(mock.Anything, input.Frame).
+		Return(output, nil)
+
+	h := NewAdminHandler(slog.Default(), svc)
+
+	responseWriter := newMockResponseWriter(t)
+	responseWriter.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.Marshal(input.Body, buf))
+
+	assert.NoError(t, h.ConfirmRequest(nil, nil, input.Frame, buf, responseWriter))
+}
+
+func TestAdminHandler_InfoChangeRequest_ScreenNameFormatted(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminInfoQuery,
+		},
+		Body: wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.AdminTLVScreenNameFormatted, "Chatting Chuck")},
+			},
+		},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.Admin,
+			SubGroup:  wire.AdminInfoChangeReply,
+		},
+		Body: wire.SNAC_0x07_0x05_AdminChangeReply{
+			Permissions: wire.AdminInfoPermissionsReadWrite,
+			TLVBlock: wire.TLVBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLV(wire.AdminTLVScreenNameFormatted, "Chatting Chuck"),
+				},
+			},
+		},
+	}
+
+	svc := newMockAdminService(t)
+	svc.EXPECT().
+		InfoChangeRequest(mock.Anything, mock.Anything, input.Frame, input.Body).
+		Return(output, nil)
+
+	h := NewAdminHandler(slog.Default(), svc)
+
+	responseWriter := newMockResponseWriter(t)
+	responseWriter.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.Marshal(input.Body, buf))
+
+	assert.NoError(t, h.InfoChangeRequest(nil, nil, input.Frame, buf, responseWriter))
+}

+ 214 - 0
server/oscar/handler/mock_admin_test.go

@@ -0,0 +1,214 @@
+// Code generated by mockery v2.43.2. DO NOT EDIT.
+
+package handler
+
+import (
+	context "context"
+
+	state "github.com/mk6i/retro-aim-server/state"
+	mock "github.com/stretchr/testify/mock"
+
+	wire "github.com/mk6i/retro-aim-server/wire"
+)
+
+// mockAdminService is an autogenerated mock type for the AdminService type
+type mockAdminService struct {
+	mock.Mock
+}
+
+type mockAdminService_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *mockAdminService) EXPECT() *mockAdminService_Expecter {
+	return &mockAdminService_Expecter{mock: &_m.Mock}
+}
+
+// ConfirmRequest provides a mock function with given fields: _a0, frame
+func (_m *mockAdminService) ConfirmRequest(_a0 context.Context, frame wire.SNACFrame) (wire.SNACMessage, error) {
+	ret := _m.Called(_a0, frame)
+
+	if len(ret) == 0 {
+		panic("no return value specified for ConfirmRequest")
+	}
+
+	var r0 wire.SNACMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame) (wire.SNACMessage, error)); ok {
+		return rf(_a0, frame)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame) wire.SNACMessage); ok {
+		r0 = rf(_a0, frame)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, wire.SNACFrame) error); ok {
+		r1 = rf(_a0, frame)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockAdminService_ConfirmRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConfirmRequest'
+type mockAdminService_ConfirmRequest_Call struct {
+	*mock.Call
+}
+
+// ConfirmRequest is a helper method to define mock.On call
+//   - _a0 context.Context
+//   - frame wire.SNACFrame
+func (_e *mockAdminService_Expecter) ConfirmRequest(_a0 interface{}, frame interface{}) *mockAdminService_ConfirmRequest_Call {
+	return &mockAdminService_ConfirmRequest_Call{Call: _e.mock.On("ConfirmRequest", _a0, frame)}
+}
+
+func (_c *mockAdminService_ConfirmRequest_Call) Run(run func(_a0 context.Context, frame wire.SNACFrame)) *mockAdminService_ConfirmRequest_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(wire.SNACFrame))
+	})
+	return _c
+}
+
+func (_c *mockAdminService_ConfirmRequest_Call) Return(_a0 wire.SNACMessage, _a1 error) *mockAdminService_ConfirmRequest_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockAdminService_ConfirmRequest_Call) RunAndReturn(run func(context.Context, wire.SNACFrame) (wire.SNACMessage, error)) *mockAdminService_ConfirmRequest_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// InfoChangeRequest provides a mock function with given fields: ctx, sess, frame, body
+func (_m *mockAdminService) InfoChangeRequest(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error) {
+	ret := _m.Called(ctx, sess, frame, body)
+
+	if len(ret) == 0 {
+		panic("no return value specified for InfoChangeRequest")
+	}
+
+	var r0 wire.SNACMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)); ok {
+		return rf(ctx, sess, frame, body)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) wire.SNACMessage); ok {
+		r0 = rf(ctx, sess, frame, body)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) error); ok {
+		r1 = rf(ctx, sess, frame, body)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockAdminService_InfoChangeRequest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoChangeRequest'
+type mockAdminService_InfoChangeRequest_Call struct {
+	*mock.Call
+}
+
+// InfoChangeRequest is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *state.Session
+//   - frame wire.SNACFrame
+//   - body wire.SNAC_0x07_0x04_AdminInfoChangeRequest
+func (_e *mockAdminService_Expecter) InfoChangeRequest(ctx interface{}, sess interface{}, frame interface{}, body interface{}) *mockAdminService_InfoChangeRequest_Call {
+	return &mockAdminService_InfoChangeRequest_Call{Call: _e.mock.On("InfoChangeRequest", ctx, sess, frame, body)}
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) Run(run func(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x04_AdminInfoChangeRequest)) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*state.Session), args[2].(wire.SNACFrame), args[3].(wire.SNAC_0x07_0x04_AdminInfoChangeRequest))
+	})
+	return _c
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) Return(_a0 wire.SNACMessage, _a1 error) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockAdminService_InfoChangeRequest_Call) RunAndReturn(run func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x04_AdminInfoChangeRequest) (wire.SNACMessage, error)) *mockAdminService_InfoChangeRequest_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// InfoQuery provides a mock function with given fields: ctx, sess, frame, body
+func (_m *mockAdminService) InfoQuery(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error) {
+	ret := _m.Called(ctx, sess, frame, body)
+
+	if len(ret) == 0 {
+		panic("no return value specified for InfoQuery")
+	}
+
+	var r0 wire.SNACMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error)); ok {
+		return rf(ctx, sess, frame, body)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x02_AdminInfoQuery) wire.SNACMessage); ok {
+		r0 = rf(ctx, sess, frame, body)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x02_AdminInfoQuery) error); ok {
+		r1 = rf(ctx, sess, frame, body)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockAdminService_InfoQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoQuery'
+type mockAdminService_InfoQuery_Call struct {
+	*mock.Call
+}
+
+// InfoQuery is a helper method to define mock.On call
+//   - ctx context.Context
+//   - sess *state.Session
+//   - frame wire.SNACFrame
+//   - body wire.SNAC_0x07_0x02_AdminInfoQuery
+func (_e *mockAdminService_Expecter) InfoQuery(ctx interface{}, sess interface{}, frame interface{}, body interface{}) *mockAdminService_InfoQuery_Call {
+	return &mockAdminService_InfoQuery_Call{Call: _e.mock.On("InfoQuery", ctx, sess, frame, body)}
+}
+
+func (_c *mockAdminService_InfoQuery_Call) Run(run func(ctx context.Context, sess *state.Session, frame wire.SNACFrame, body wire.SNAC_0x07_0x02_AdminInfoQuery)) *mockAdminService_InfoQuery_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(*state.Session), args[2].(wire.SNACFrame), args[3].(wire.SNAC_0x07_0x02_AdminInfoQuery))
+	})
+	return _c
+}
+
+func (_c *mockAdminService_InfoQuery_Call) Return(_a0 wire.SNACMessage, _a1 error) *mockAdminService_InfoQuery_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockAdminService_InfoQuery_Call) RunAndReturn(run func(context.Context, *state.Session, wire.SNACFrame, wire.SNAC_0x07_0x02_AdminInfoQuery) (wire.SNACMessage, error)) *mockAdminService_InfoQuery_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// newMockAdminService creates a new instance of mockAdminService. 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 newMockAdminService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *mockAdminService {
+	mock := &mockAdminService{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 58 - 0
server/oscar/mock_auth_test.go

@@ -313,6 +313,64 @@ func (_c *mockAuthService_RegisterChatSession_Call) RunAndReturn(run func([]byte
 	return _c
 }
 
+// RetrieveBOSSession provides a mock function with given fields: authCookie
+func (_m *mockAuthService) RetrieveBOSSession(authCookie []byte) (*state.Session, error) {
+	ret := _m.Called(authCookie)
+
+	if len(ret) == 0 {
+		panic("no return value specified for RetrieveBOSSession")
+	}
+
+	var r0 *state.Session
+	var r1 error
+	if rf, ok := ret.Get(0).(func([]byte) (*state.Session, error)); ok {
+		return rf(authCookie)
+	}
+	if rf, ok := ret.Get(0).(func([]byte) *state.Session); ok {
+		r0 = rf(authCookie)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*state.Session)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func([]byte) error); ok {
+		r1 = rf(authCookie)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockAuthService_RetrieveBOSSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveBOSSession'
+type mockAuthService_RetrieveBOSSession_Call struct {
+	*mock.Call
+}
+
+// RetrieveBOSSession is a helper method to define mock.On call
+//   - authCookie []byte
+func (_e *mockAuthService_Expecter) RetrieveBOSSession(authCookie interface{}) *mockAuthService_RetrieveBOSSession_Call {
+	return &mockAuthService_RetrieveBOSSession_Call{Call: _e.mock.On("RetrieveBOSSession", authCookie)}
+}
+
+func (_c *mockAuthService_RetrieveBOSSession_Call) Run(run func(authCookie []byte)) *mockAuthService_RetrieveBOSSession_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].([]byte))
+	})
+	return _c
+}
+
+func (_c *mockAuthService_RetrieveBOSSession_Call) Return(_a0 *state.Session, _a1 error) *mockAuthService_RetrieveBOSSession_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockAuthService_RetrieveBOSSession_Call) RunAndReturn(run func([]byte) (*state.Session, error)) *mockAuthService_RetrieveBOSSession_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // Signout provides a mock function with given fields: ctx, sess
 func (_m *mockAuthService) Signout(ctx context.Context, sess *state.Session) error {
 	ret := _m.Called(ctx, sess)

+ 38 - 0
state/user_store_test.go

@@ -862,3 +862,41 @@ func TestSQLiteUserStore_CreateChatRoom_ErrChatRoomExists(t *testing.T) {
 		})
 	}
 }
+
+func TestUpdateDisplayScreenName(t *testing.T) {
+
+	screenNameOriginal := DisplayScreenName("chattingchuck")
+	screenNameFormatted := DisplayScreenName("Chatting Chuck")
+
+	defer func() {
+		assert.NoError(t, os.Remove(testFile))
+	}()
+
+	f, err := NewSQLiteUserStore(testFile)
+	assert.NoError(t, err)
+
+	user := User{
+		DisplayScreenName: screenNameOriginal,
+		IdentScreenName:   screenNameOriginal.IdentScreenName(),
+	}
+	userFormatted := User{
+		DisplayScreenName: screenNameFormatted,
+		IdentScreenName:   screenNameFormatted.IdentScreenName(),
+	}
+	if err := f.InsertUser(user); err != nil {
+		t.Fatalf("failed to upsert new user: %s", err.Error())
+	}
+
+	err = f.UpdateDisplayScreenName(screenNameFormatted)
+	if err != nil {
+		t.Fatalf("failed to update display screen name: %s", err.Error())
+	}
+
+	dbUser, err := f.User(screenNameOriginal.IdentScreenName())
+	if err != nil {
+		t.Fatalf("failed to retrieve screen name: %s", err.Error())
+	}
+	if !reflect.DeepEqual(userFormatted, *dbUser) {
+		t.Fatalf("users did not match:\n expected: %v\n actual: %v", userFormatted, dbUser)
+	}
+}