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

test SendAndReceiveUserInfoQuery2

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

+ 1 - 0
.mockery.yaml

@@ -8,4 +8,5 @@ packages:
     interfaces:
       FeedbagManager:
       SessionManager:
+      ProfileManager:
 # Lots more config...

+ 17 - 2
oscar/snacs.go

@@ -91,6 +91,14 @@ type SNAC_0x01_0x1E_OServiceSetUserInfoFields struct {
 // 0x02: Locate
 //
 
+const (
+	LocateType2Sig          uint32 = 0x00000001
+	LocateType2Unavailable  uint32 = 0x00000002
+	LocateType2Capabilities uint32 = 0x00000004
+	LocateType2Certs        uint32 = 0x00000008
+	LocateType2HtmlInfo     uint32 = 0x00000400
+)
+
 type SNAC_0x02_0x03_LocateRightsReply struct {
 	TLVRestBlock
 }
@@ -101,8 +109,7 @@ type SNAC_0x02_0x04_LocateSetInfo struct {
 
 type SNAC_0x02_0x06_LocateUserInfoReply struct {
 	TLVUserInfo
-	ClientProfile TLVRestBlock
-	AwayMessage   TLVRestBlock
+	LocateInfo TLVRestBlock
 }
 
 type SNAC_0x02_0x09_LocateSetDirInfo struct {
@@ -130,6 +137,14 @@ type SNAC_0x02_0x15_LocateUserInfoQuery2 struct {
 	ScreenName string `len_prefix:"uint8"`
 }
 
+func (s SNAC_0x02_0x15_LocateUserInfoQuery2) RequestProfile() bool {
+	return s.Type2&LocateType2Sig == LocateType2Sig
+}
+
+func (s SNAC_0x02_0x15_LocateUserInfoQuery2) RequestAwayMessage() bool {
+	return s.Type2&LocateType2Unavailable == LocateType2Unavailable
+}
+
 //
 // 0x03: Buddy
 //

+ 1 - 1
server/MockFeedbagManager.go

@@ -1,4 +1,4 @@
-// Code generated by mockery v2.34.2. DO NOT EDIT.
+// Code generated by mockery v2.35.2. DO NOT EDIT.
 
 package server
 

+ 127 - 0
server/MockProfileManager.go

@@ -0,0 +1,127 @@
+// Code generated by mockery v2.35.2. DO NOT EDIT.
+
+package server
+
+import mock "github.com/stretchr/testify/mock"
+
+// MockProfileManager is an autogenerated mock type for the ProfileManager type
+type MockProfileManager struct {
+	mock.Mock
+}
+
+type MockProfileManager_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *MockProfileManager) EXPECT() *MockProfileManager_Expecter {
+	return &MockProfileManager_Expecter{mock: &_m.Mock}
+}
+
+// RetrieveProfile provides a mock function with given fields: screenName
+func (_m *MockProfileManager) RetrieveProfile(screenName string) (string, error) {
+	ret := _m.Called(screenName)
+
+	var r0 string
+	var r1 error
+	if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
+		return rf(screenName)
+	}
+	if rf, ok := ret.Get(0).(func(string) string); ok {
+		r0 = rf(screenName)
+	} else {
+		r0 = ret.Get(0).(string)
+	}
+
+	if rf, ok := ret.Get(1).(func(string) error); ok {
+		r1 = rf(screenName)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// MockProfileManager_RetrieveProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveProfile'
+type MockProfileManager_RetrieveProfile_Call struct {
+	*mock.Call
+}
+
+// RetrieveProfile is a helper method to define mock.On call
+//   - screenName string
+func (_e *MockProfileManager_Expecter) RetrieveProfile(screenName interface{}) *MockProfileManager_RetrieveProfile_Call {
+	return &MockProfileManager_RetrieveProfile_Call{Call: _e.mock.On("RetrieveProfile", screenName)}
+}
+
+func (_c *MockProfileManager_RetrieveProfile_Call) Run(run func(screenName string)) *MockProfileManager_RetrieveProfile_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string))
+	})
+	return _c
+}
+
+func (_c *MockProfileManager_RetrieveProfile_Call) Return(_a0 string, _a1 error) *MockProfileManager_RetrieveProfile_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *MockProfileManager_RetrieveProfile_Call) RunAndReturn(run func(string) (string, error)) *MockProfileManager_RetrieveProfile_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// UpsertProfile provides a mock function with given fields: screenName, body
+func (_m *MockProfileManager) UpsertProfile(screenName string, body string) error {
+	ret := _m.Called(screenName, body)
+
+	var r0 error
+	if rf, ok := ret.Get(0).(func(string, string) error); ok {
+		r0 = rf(screenName, body)
+	} else {
+		r0 = ret.Error(0)
+	}
+
+	return r0
+}
+
+// MockProfileManager_UpsertProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertProfile'
+type MockProfileManager_UpsertProfile_Call struct {
+	*mock.Call
+}
+
+// UpsertProfile is a helper method to define mock.On call
+//   - screenName string
+//   - body string
+func (_e *MockProfileManager_Expecter) UpsertProfile(screenName interface{}, body interface{}) *MockProfileManager_UpsertProfile_Call {
+	return &MockProfileManager_UpsertProfile_Call{Call: _e.mock.On("UpsertProfile", screenName, body)}
+}
+
+func (_c *MockProfileManager_UpsertProfile_Call) Run(run func(screenName string, body string)) *MockProfileManager_UpsertProfile_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(string), args[1].(string))
+	})
+	return _c
+}
+
+func (_c *MockProfileManager_UpsertProfile_Call) Return(_a0 error) *MockProfileManager_UpsertProfile_Call {
+	_c.Call.Return(_a0)
+	return _c
+}
+
+func (_c *MockProfileManager_UpsertProfile_Call) RunAndReturn(run func(string, string) error) *MockProfileManager_UpsertProfile_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// NewMockProfileManager creates a new instance of MockProfileManager. 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 NewMockProfileManager(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *MockProfileManager {
+	mock := &MockProfileManager{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 1 - 1
server/MockSessionManager.go

@@ -1,4 +1,4 @@
-// Code generated by mockery v2.34.2. DO NOT EDIT.
+// Code generated by mockery v2.35.2. DO NOT EDIT.
 
 package server
 

+ 12 - 10
server/feedbag_store.go

@@ -13,8 +13,6 @@ import (
 	"github.com/mkaminski/goaim/oscar"
 )
 
-var errUserNotExist = errors.New("user does not exist")
-
 var feedbagDDL = `
 	CREATE TABLE IF NOT EXISTS user
 	(
@@ -335,6 +333,7 @@ func (f *FeedbagStore) Blocked(sn1, sn2 string) (BlockedState, error) {
 	var blockedA bool
 	row, err := f.db.Query(q, sn1, sn2, sn2, sn1)
 	if err != nil {
+		// todo check to make sure there's no runtime error here...
 		return BlockedNo, err
 	}
 	defer row.Close()
@@ -363,21 +362,19 @@ func (f *FeedbagStore) Blocked(sn1, sn2 string) (BlockedState, error) {
 }
 
 // RetrieveProfile fetches a user profile. Return empty string if the user
-// exists but has no profile. Return errUserNotExist if the user does not
-// exist.
+// does not exist or has no profile.
 func (f *FeedbagStore) RetrieveProfile(screenName string) (string, error) {
 	q := `
 		SELECT IFNULL(body, '')
-		FROM user u
-		LEFT JOIN profile p ON p.ScreenName = u.ScreenName
-		WHERE u.ScreenName = ?
+		FROM profile
+		WHERE ScreenName = ?
 	`
 	var profile string
 	err := f.db.QueryRow(q, screenName).Scan(&profile)
-	if err == sql.ErrNoRows {
-		return "", errUserNotExist
+	if err != nil && !errors.Is(err, sql.ErrNoRows) {
+		return "", err
 	}
-	return profile, err
+	return profile, nil
 }
 
 func (f *FeedbagStore) UpsertProfile(screenName string, body string) error {
@@ -413,3 +410,8 @@ type SessionManager interface {
 	NewSessionWithSN(sessID string, screenName string) *Session
 	Remove(sess *Session)
 }
+
+type ProfileManager interface {
+	RetrieveProfile(screenName string) (string, error)
+	UpsertProfile(screenName string, body string) error
+}

+ 4 - 4
server/feedbag_store_test.go

@@ -2,6 +2,7 @@ package server
 
 import (
 	"github.com/mkaminski/goaim/oscar"
+	"github.com/stretchr/testify/assert"
 	"os"
 	"reflect"
 	"testing"
@@ -258,10 +259,9 @@ func TestProfileNonExistent(t *testing.T) {
 		t.Fatalf("failed to create new feedbag store: %s", err.Error())
 	}
 
-	_, err = f.RetrieveProfile(screenName)
-	if err != errUserNotExist {
-		t.Fatalf("failed to get error on non-existing profile: %v", err)
-	}
+	prof, err := f.RetrieveProfile(screenName)
+	assert.NoError(t, err)
+	assert.Empty(t, prof)
 }
 
 func TestInterestedUsers(t *testing.T) {

+ 29 - 50
server/locate.go

@@ -31,6 +31,20 @@ const (
 	LocateUserInfoQuery2              = 0x0015
 )
 
+var (
+	LocateTLVTagsInfoSigMime         uint16 = 0x01
+	LocateTLVTagsInfoSigData         uint16 = 0x02
+	LocateTLVTagsInfoUnavailableMime uint16 = 0x03
+	LocateTLVTagsInfoUnavailableData uint16 = 0x04
+	LocateTLVTagsInfoCapabilities    uint16 = 0x05
+	LocateTLVTagsInfoCerts           uint16 = 0x06
+	LocateTLVTagsInfoSigTime         uint16 = 0x0A
+	LocateTLVTagsInfoUnavailableTime uint16 = 0x0B
+	LocateTLVTagsInfoSupportHostSig  uint16 = 0x0C
+	LocateTLVTagsInfoHtmlInfoData    uint16 = 0x0E
+	LocateTLVTagsInfoHtmlInfoType    uint16 = 0x0D
+)
+
 func routeLocate(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.SubGroup {
 	case LocateErr:
@@ -68,7 +82,7 @@ func routeLocate(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, sn
 	case LocateFindListReply:
 		panic("not implemented")
 	case LocateUserInfoQuery2:
-		return SendAndReceiveUserInfoQuery2(sess, sm, fm, snac, r, w, sequence)
+		return SendAndReceiveUserInfoQuery2(sess, sm, fm, fm, snac, r, w, sequence)
 	}
 
 	return nil
@@ -111,20 +125,6 @@ func SendAndReceiveLocateRights(snac oscar.SnacFrame, w io.Writer, sequence *uin
 	return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-var (
-	LocateTlvTagsInfoSigMime         = uint16(0x01)
-	LocateTlvTagsInfoSigData         = uint16(0x02)
-	LocateTlvTagsInfoUnavailableMime = uint16(0x03)
-	LocateTlvTagsInfoUnavailableData = uint16(0x04)
-	LocateTlvTagsInfoCapabilities    = uint16(0x05)
-	LocateTlvTagsInfoCerts           = uint16(0x06)
-	LocateTlvTagsInfoSigTime         = uint16(0x0A)
-	LocateTlvTagsInfoUnavailableTime = uint16(0x0B)
-	LocateTlvTagsInfoSupportHostSig  = uint16(0x0C)
-	LocateTlvTagsInfoHtmlInfoData    = uint16(0x0E)
-	LocateTlvTagsInfoHtmlInfoType    = uint16(0x0D)
-)
-
 func ReceiveSetInfo(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader) error {
 	fmt.Printf("ReceiveSetInfo read SNAC frame: %+v\n", snac)
 
@@ -134,14 +134,14 @@ func ReceiveSetInfo(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore,
 	}
 
 	// update profile
-	if profile, hasProfile := snacPayloadIn.GetString(LocateTlvTagsInfoSigData); hasProfile {
+	if profile, hasProfile := snacPayloadIn.GetString(LocateTLVTagsInfoSigData); hasProfile {
 		if err := fm.UpsertProfile(sess.ScreenName, profile); err != nil {
 			return err
 		}
 	}
 
 	// broadcast away message change to buddies
-	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(LocateTlvTagsInfoUnavailableData); hasAwayMsg {
+	if awayMsg, hasAwayMsg := snacPayloadIn.GetString(LocateTLVTagsInfoUnavailableData); hasAwayMsg {
 		sess.SetAwayMessage(awayMsg)
 		if err := NotifyArrival(sess, sm, fm); err != nil {
 			return err
@@ -166,9 +166,7 @@ func ReceiveLocateGetDirInfo(snac oscar.SnacFrame, r io.Reader) error {
 	return nil
 }
 
-func SendAndReceiveUserInfoQuery2(sess *Session, sm *InMemorySessionManager, fm *FeedbagStore, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
-	fmt.Printf("SendAndReceiveUserInfoQuery2 read SNAC frame: %+v\n", snac)
-
+func SendAndReceiveUserInfoQuery2(sess *Session, sm SessionManager, fm FeedbagManager, pm ProfileManager, snac oscar.SnacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	snacPayloadIn := oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{}
 	if err := oscar.Unmarshal(&snacPayloadIn, r); err != nil {
 		return err
@@ -201,6 +199,7 @@ func SendAndReceiveUserInfoQuery2(sess *Session, sm *InMemorySessionManager, fm
 			}
 			return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
 		}
+		return err
 	}
 
 	snacFrameOut := oscar.SnacFrame{
@@ -208,54 +207,34 @@ func SendAndReceiveUserInfoQuery2(sess *Session, sm *InMemorySessionManager, fm
 		SubGroup:  LocateUserInfoReply,
 	}
 	snacPayloadOut := oscar.SNAC_0x02_0x06_LocateUserInfoReply{
-		TLVUserInfo: oscar.TLVUserInfo{
-			ScreenName:   snacPayloadIn.ScreenName,
-			WarningLevel: buddySess.GetWarning(),
-			TLVBlock: oscar.TLVBlock{
-				TLVList: buddySess.GetUserInfo(),
-			},
-		},
-		ClientProfile: oscar.TLVRestBlock{},
-		AwayMessage:   oscar.TLVRestBlock{},
+		TLVUserInfo: buddySess.GetTLVUserInfo(),
 	}
 
-	// profile
-	if snacPayloadIn.Type2&1 == 1 {
-		profile, err := fm.RetrieveProfile(snacPayloadIn.ScreenName)
+	if snacPayloadIn.RequestProfile() {
+		profile, err := pm.RetrieveProfile(snacPayloadIn.ScreenName)
 		if err != nil {
-			if err == errUserNotExist {
-				snacFrameOut := oscar.SnacFrame{
-					FoodGroup: LOCATE,
-					SubGroup:  LocateErr,
-				}
-				snacPayloadOut := oscar.SnacError{
-					Code: ErrorCodeNotLoggedOn,
-				}
-				return writeOutSNAC(snac, snacFrameOut, snacPayloadOut, sequence, w)
-			}
 			return err
 		}
-		snacPayloadOut.ClientProfile.TLVList = append(snacPayloadOut.ClientProfile.TLVList, []oscar.TLV{
+		snacPayloadOut.LocateInfo.TLVList = append(snacPayloadOut.LocateInfo.TLVList, []oscar.TLV{
 			{
-				TType: 0x01,
+				TType: LocateTLVTagsInfoSigMime,
 				Val:   `text/aolrtf; charset="us-ascii"`,
 			},
 			{
-				TType: 0x02,
+				TType: LocateTLVTagsInfoSigData,
 				Val:   profile,
 			},
 		}...)
 	}
 
-	// away message
-	if snacPayloadIn.Type2&2 == 2 {
-		snacPayloadOut.ClientProfile.TLVList = append(snacPayloadOut.ClientProfile.TLVList, []oscar.TLV{
+	if snacPayloadIn.RequestAwayMessage() {
+		snacPayloadOut.LocateInfo.TLVList = append(snacPayloadOut.LocateInfo.TLVList, []oscar.TLV{
 			{
-				TType: 0x03,
+				TType: LocateTLVTagsInfoUnavailableMime,
 				Val:   `text/aolrtf; charset="us-ascii"`,
 			},
 			{
-				TType: 0x04,
+				TType: LocateTLVTagsInfoUnavailableData,
 				Val:   buddySess.GetAwayMessage(),
 			},
 		}...)

+ 337 - 0
server/locate_test.go

@@ -0,0 +1,337 @@
+package server
+
+import (
+	"bytes"
+	"github.com/mkaminski/goaim/oscar"
+	"github.com/stretchr/testify/assert"
+	"testing"
+	"time"
+)
+
+// newTestSession returns a copy of the prototype session with fixed time
+//
+//goland:noinspection GoVetCopyLock
+func newTestSession(prototype Session) *Session {
+	prototype.SignonTime = time.UnixMilli(1696790127565)
+	return &prototype
+}
+
+func TestSendAndReceiveUserInfoQuery2(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// blockedState is the response to the sender/recipient block check
+		blockedState BlockedState
+		// screenNameLookups is the list of user session lookups
+		screenNameLookups map[string]struct {
+			sess *Session
+			err  error
+		}
+		// screenNameLookups is the list of user session lookups
+		profileLookups map[string]struct {
+			payload string
+			err     error
+		}
+		// userSession is the session of the user requesting the user info
+		userSession *Session
+		// expectSNACFrame is the SNAC frame sent from the server to the recipient
+		// client
+		expectSNACFrame oscar.SnacFrame
+		// inputSNAC is the SNAC sent by the sender client
+		inputSNAC oscar.SNAC_0x02_0x15_LocateUserInfoQuery2
+		// expectSNACBody is the SNAC payload sent from the server to the
+		// recipient client
+		expectSNACBody any
+	}{
+		{
+			name:         "request user info, expect user info response",
+			blockedState: BlockedNo,
+			screenNameLookups: map[string]struct {
+				sess *Session
+				err  error
+			}{
+				"requested-user": {
+					sess: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}),
+				},
+			},
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateUserInfoReply,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				Type2:      0,
+				ScreenName: "requested-user",
+			},
+			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+				TLVUserInfo: newTestSession(Session{
+					ScreenName:  "requested-user",
+					AwayMessage: "this is my away message!",
+				}).GetTLVUserInfo(),
+				LocateInfo: oscar.TLVRestBlock{},
+			},
+		},
+		{
+			name:         "request user info + profile, expect user info response + profile",
+			blockedState: BlockedNo,
+			screenNameLookups: map[string]struct {
+				sess *Session
+				err  error
+			}{
+				"requested-user": {
+					sess: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}),
+				},
+			},
+			profileLookups: map[string]struct {
+				payload string
+				err     error
+			}{
+				"requested-user": {
+					payload: "this is my profile!",
+				},
+			},
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateUserInfoReply,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				// 2048 is a dummy to make sure bitmask check works
+				Type2:      oscar.LocateType2Sig | 2048,
+				ScreenName: "requested-user",
+			},
+			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+				TLVUserInfo: newTestSession(Session{
+					ScreenName:  "requested-user",
+					AwayMessage: "this is my away message!",
+				}).GetTLVUserInfo(),
+				LocateInfo: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: LocateTLVTagsInfoSigMime,
+							Val:   `text/aolrtf; charset="us-ascii"`,
+						},
+						{
+							TType: LocateTLVTagsInfoSigData,
+							Val:   "this is my profile!",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:         "request user info + profile, expect user info response + profile",
+			blockedState: BlockedNo,
+			screenNameLookups: map[string]struct {
+				sess *Session
+				err  error
+			}{
+				"requested-user": {
+					sess: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}),
+				},
+			},
+			profileLookups: map[string]struct {
+				payload string
+				err     error
+			}{
+				"requested-user": {
+					payload: "this is my profile!",
+				},
+			},
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateUserInfoReply,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				// 2048 is a dummy to make sure bitmask check works
+				Type2:      oscar.LocateType2Sig | 2048,
+				ScreenName: "requested-user",
+			},
+			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+				TLVUserInfo: newTestSession(Session{
+					ScreenName:  "requested-user",
+					AwayMessage: "this is my away message!",
+				}).GetTLVUserInfo(),
+				LocateInfo: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: LocateTLVTagsInfoSigMime,
+							Val:   `text/aolrtf; charset="us-ascii"`,
+						},
+						{
+							TType: LocateTLVTagsInfoSigData,
+							Val:   "this is my profile!",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:         "request user info + away message, expect user info response + away message",
+			blockedState: BlockedNo,
+			screenNameLookups: map[string]struct {
+				sess *Session
+				err  error
+			}{
+				"requested-user": {
+					sess: newTestSession(Session{
+						ScreenName:  "requested-user",
+						AwayMessage: "this is my away message!",
+					}),
+				},
+			},
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateUserInfoReply,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				// 2048 is a dummy to make sure bitmask check works
+				Type2:      oscar.LocateType2Unavailable | 2048,
+				ScreenName: "requested-user",
+			},
+			expectSNACBody: oscar.SNAC_0x02_0x06_LocateUserInfoReply{
+				TLVUserInfo: newTestSession(Session{
+					ScreenName:  "requested-user",
+					AwayMessage: "this is my away message!",
+				}).GetTLVUserInfo(),
+				LocateInfo: oscar.TLVRestBlock{
+					TLVList: oscar.TLVList{
+						{
+							TType: LocateTLVTagsInfoUnavailableMime,
+							Val:   `text/aolrtf; charset="us-ascii"`,
+						},
+						{
+							TType: LocateTLVTagsInfoUnavailableData,
+							Val:   "this is my away message!",
+						},
+					},
+				},
+			},
+		},
+		{
+			name:         "request user info of user who blocked requester, expect not logged in error",
+			blockedState: BlockedB,
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateErr,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				ScreenName: "requested-user",
+			},
+			expectSNACBody: oscar.SnacError{
+				Code: ErrorCodeNotLoggedOn,
+			},
+		},
+		{
+			name:         "request user info of user who does not exist, expect not logged in error",
+			blockedState: BlockedNo,
+			screenNameLookups: map[string]struct {
+				sess *Session
+				err  error
+			}{
+				"non_existent_requested_user": {
+					err: errSessNotFound,
+				},
+			},
+			userSession: &Session{
+				ScreenName: "user_screen_name",
+			},
+			expectSNACFrame: oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateErr,
+			},
+			inputSNAC: oscar.SNAC_0x02_0x15_LocateUserInfoQuery2{
+				ScreenName: "non_existent_requested_user",
+			},
+			expectSNACBody: oscar.SnacError{
+				Code: ErrorCodeNotLoggedOn,
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			//
+			// initialize dependencies
+			//
+			fm := NewMockFeedbagManager(t)
+			fm.EXPECT().
+				Blocked(tc.userSession.ScreenName, tc.inputSNAC.ScreenName).
+				Return(tc.blockedState, nil).
+				Maybe()
+			sm := NewMockSessionManager(t)
+			for screenName, val := range tc.screenNameLookups {
+				sm.EXPECT().
+					RetrieveByScreenName(screenName).
+					Return(val.sess, val.err).
+					Maybe()
+			}
+			pm := NewMockProfileManager(t)
+			for screenName, val := range tc.profileLookups {
+				pm.EXPECT().
+					RetrieveProfile(screenName).
+					Return(val.payload, val.err).
+					Maybe()
+			}
+			//
+			// send input SNAC
+			//
+			input := &bytes.Buffer{}
+			var seq uint32
+			assert.NoError(t, oscar.Marshal(tc.inputSNAC, input))
+			output := &bytes.Buffer{}
+			snac := oscar.SnacFrame{
+				FoodGroup: LOCATE,
+				SubGroup:  LocateUserInfoQuery2,
+			}
+			assert.NoError(t, SendAndReceiveUserInfoQuery2(tc.userSession, sm, fm, pm, snac, input, output, &seq))
+			//
+			// verify output
+			//
+			flap := oscar.FlapFrame{}
+			assert.NoError(t, oscar.Unmarshal(&flap, output))
+			snacFrame := oscar.SnacFrame{}
+			assert.NoError(t, oscar.Unmarshal(&snacFrame, output))
+			assert.Equal(t, tc.expectSNACFrame, snacFrame)
+			//
+			// verify output SNAC body
+			//
+			switch v := tc.expectSNACBody.(type) {
+			case oscar.SNAC_0x02_0x06_LocateUserInfoReply:
+				assert.NoError(t, v.SerializeInPlace())
+				assert.NoError(t, v.LocateInfo.SerializeInPlace())
+				outputSNAC := oscar.SNAC_0x02_0x06_LocateUserInfoReply{}
+				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
+				assert.Equal(t, v, outputSNAC)
+			case oscar.SnacError:
+				outputSNAC := oscar.SnacError{}
+				assert.NoError(t, oscar.Unmarshal(&outputSNAC, output))
+				assert.Equal(t, v, outputSNAC)
+			default:
+				t.Fatalf("unexpected output SNAC type")
+			}
+		})
+	}
+}

+ 10 - 0
server/session.go

@@ -89,6 +89,16 @@ func (s *Session) GetAwayMessage() string {
 	return s.AwayMessage
 }
 
+func (s *Session) GetTLVUserInfo() oscar.TLVUserInfo {
+	return oscar.TLVUserInfo{
+		ScreenName:   s.ScreenName,
+		WarningLevel: s.GetWarning(),
+		TLVBlock: oscar.TLVBlock{
+			TLVList: s.GetUserInfo(),
+		},
+	}
+}
+
 func (s *Session) GetUserInfo() []oscar.TLV {
 	s.Mutex.RLock()
 	defer s.Mutex.RUnlock()