package foodgroup import ( "bytes" "context" "errors" "fmt" "io" "log/slog" "testing" "time" "github.com/mk6i/open-oscar-server/config" "github.com/mk6i/open-oscar-server/state" "github.com/mk6i/open-oscar-server/wire" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestAuthService_BUCPLoginRequest(t *testing.T) { user := state.User{ IdentScreenName: state.NewIdentScreenName("screenName"), DisplayScreenName: "screenName", AuthKey: "auth_key", } assert.NoError(t, user.HashPassword("the_password")) cases := []struct { // name is the unit test name name string // advertisedHost is the BOS host the client will connect to upon successful login advertisedHost string // cfg is the app configuration cfg config.Config // inputSNAC is the SNAC sent from the client to the server inputSNAC wire.SNAC_0x17_0x02_BUCPLoginRequest // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams // createAccount is the function that creates a new user account createAccount state.CreateAccountFunc // expectOutput is the SNAC sent from the server to client expectOutput wire.SNACMessage // wantErr is the error we expect from the method wantErr error // maxConcurrentLoginsPerUser is the maximum concurrent logins per user (only set for MultiConnFlagsRecentClient tests) maxConcurrentLoginsPerUser int }{ { name: "AIM account exists, correct password, login OK, no concurrent logins", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, MultiConnFlag: uint8(wire.MultiConnFlagsRecentClient), } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, sessionRetrieverParams: sessionRetrieverParams{ retrieveSessionParams: retrieveSessionParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, maxConcurrentLoginsPerUser: 2, }, { name: "AIM account exists, correct password, login OK, concurrent logins under limit", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, MultiConnFlag: uint8(wire.MultiConnFlagsRecentClient), } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, sessionRetrieverParams: sessionRetrieverParams{ retrieveSessionParams: retrieveSessionParams{ { screenName: user.IdentScreenName, result: func() *state.Session { // Create a session with 1 instance, under the limit sess := state.NewSession() sess.SetIdentScreenName(user.IdentScreenName) sess.SetDisplayScreenName(user.DisplayScreenName) sess.AddInstance() return sess }(), }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, maxConcurrentLoginsPerUser: 2, }, { name: "login fails when concurrent login limit is reached", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, sessionRetrieverParams: sessionRetrieverParams{ retrieveSessionParams: retrieveSessionParams{ { screenName: user.IdentScreenName, result: func() *state.Session { // Create a session with 2 instances (the max allowed) // This will cause InstanceCount() to return 2, which equals the limit of 2 sess := state.NewSession() sess.SetIdentScreenName(user.IdentScreenName) sess.SetDisplayScreenName(user.DisplayScreenName) sess.AddInstance() sess.AddInstance() return sess }(), }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrRateLimitExceeded), }, }, }, }, maxConcurrentLoginsPerUser: 2, }, { name: "ICQ account exists, correct password, login OK", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, ClientID: "ICQ 2000b", } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, }, { name: "AIM account exists, incorrect password, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("bad_password")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword), }, }, }, }, }, { name: "AIM account doesn't exist, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("password")), wire.NewTLVBE(wire.LoginTLVTagsScreenName, []byte("non_existent_screen_name")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("non_existent_screen_name"), result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_screen_name")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword), }, }, }, }, }, { name: "AIM account is suspended", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("password")), wire.NewTLVBE(wire.LoginTLVTagsScreenName, []byte("suspended_screen_name")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("suspended_screen_name"), result: &state.User{ SuspendedStatus: wire.LoginErrSuspendedAccount, }, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("suspended_screen_name")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrSuspendedAccount), }, }, }, }, }, { name: "ICQ account doesn't exist, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("password")), wire.NewTLVBE(wire.LoginTLVTagsScreenName, []byte("100003")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("100003"), result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("100003")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrICQUserErr), }, }, }, }, }, { name: "account doesn't exist, authentication is disabled, account is created, login succeeds", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, createAccount: func(ctx context.Context, screenName state.DisplayScreenName, password string) error { assert.Equal(t, user.DisplayScreenName, screenName) assert.Equal(t, "welcome1", password) return nil }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, }, { name: "AIM account doesn't exist, authentication is disabled, screen name has bad format, login fails", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "2coolforschool"), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("2coolforschool"), result: nil, }, }, }, }, createAccount: func(ctx context.Context, screenName state.DisplayScreenName, password string) error { assert.Equal(t, state.DisplayScreenName("2coolforschool"), screenName) assert.Equal(t, "welcome1", password) return state.ErrAIMHandleInvalidFormat }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("2coolforschool")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword), }, }, }, }, }, { name: "ICQ account doesn't exist, authentication is disabled, UIN has bad format, login fails", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "99"), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("99"), result: nil, }, }, }, }, createAccount: func(ctx context.Context, screenName state.DisplayScreenName, password string) error { assert.Equal(t, state.DisplayScreenName("99"), screenName) assert.Equal(t, "welcome1", password) return state.ErrICQUINInvalidFormat }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("99")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrICQUserErr), }, }, }, }, }, { name: "account exists, password is invalid, authentication is disabled, login succeeds", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, []byte("bad-password-hash")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, }, { name: "login fails on user manager lookup", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, err: io.EOF, }, }, }, }, wantErr: io.EOF, }, { name: "login with TOC client - success", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, wire.RoastTOCPassword([]byte("the_password"))), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, }, }, { name: "AIM account exists, correct password, linked accounts in response", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, feedbagManagerParams: feedbagManagerParams{ feedbagParams: feedbagParams{ { screenName: user.IdentScreenName, results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdAlInfo, Name: "linked1"}}, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), wire.NewTLVBE(wire.OServiceTLVTagsLinkedAccounts, `screennamelinked1`), }, }, }, }, }, { name: "feedbag error during login, returns error", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsPasswordHash, user.StrongMD5Pass), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, feedbagManagerParams: feedbagManagerParams{ feedbagParams: feedbagParams{ { screenName: user.IdentScreenName, err: io.EOF, }, }, }, }, wantErr: io.EOF, }, { name: "login with TOC client - failed", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x02_BUCPLoginRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, wire.RoastTOCPassword([]byte("the_wrong_password"))), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "screenName"), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword), }, }, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { userManager := newMockUserManager(t) for _, params := range tc.mockParams.userManagerParams.getUserParams { userManager.EXPECT(). User(matchContext(), params.screenName). Return(params.result, params.err) } cookieBaker := newMockCookieBaker(t) for _, params := range tc.mockParams.cookieIssueParams { cookieBaker.EXPECT(). Issue(params.dataIn). Return(params.cookieOut, params.err) } sessionRetriever := newMockSessionRetriever(t) for _, params := range tc.mockParams.retrieveSessionParams { sessionRetriever.EXPECT(). RetrieveSession(params.screenName). Return(params.result) } feedbagManager := newMockFeedbagManager(t) for _, params := range tc.mockParams.feedbagParams { feedbagManager.EXPECT(). Feedbag(matchContext(), params.screenName). Return(params.results, params.err) } feedbagManager.EXPECT().Feedbag(matchContext(), mock.Anything).Return(nil, nil).Maybe() svc := AuthService{ config: tc.cfg, cookieBaker: cookieBaker, userManager: userManager, sessionRetriever: sessionRetriever, feedbagManager: feedbagManager, maxConcurrentLoginsPerUser: 2, createAccount: tc.createAccount, logger: slog.Default(), } outputSNAC, err := svc.BUCPLogin(context.Background(), tc.inputSNAC, tc.advertisedHost) assert.ErrorIs(t, err, tc.wantErr) assert.Equal(t, tc.expectOutput, outputSNAC) }) } } func TestAuthService_FLAPLogin(t *testing.T) { user := state.User{ AuthKey: "auth_key", DisplayScreenName: "screenName", IdentScreenName: state.NewIdentScreenName("screenName"), } assert.NoError(t, user.HashPassword("the_password")) cases := []struct { // name is the unit test name name string // advertisedHost is the BOS host the client will connect to upon successful login advertisedHost string // cfg is the app configuration cfg config.Config // inputSNAC is the authentication FLAP frame sent from the client to the server inputSNAC wire.FLAPSignonFrame // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams // createAccount is the function that creates a new user account createAccount state.CreateAccountFunc // expectOutput is the response sent from the server to client expectOutput wire.TLVRestBlock // wantErr is the error we expect from the method wantErr error }{ { name: "AIM account exists, correct password, login OK", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, { name: "ICQ account exists, correct password, login OK", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"), wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, ClientID: "ICQ 2000b", } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, { name: "AIM account exists, incorrect password, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, []byte("bad_roasted_password")), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword), }, }, }, { name: "AIM account doesn't exist, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, []byte("non_existent_screen_name")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("non_existent_screen_name"), result: nil, }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("non_existent_screen_name")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword), }, }, }, { name: "ICQ account doesn't exist, login fails", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "ICQ 2000b"), wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, []byte("100003")), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("100003"), result: nil, }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: []wire.TLV{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, state.NewIdentScreenName("100003")), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrICQUserErr), }, }, }, { name: "account doesn't exist, authentication is disabled, account is created, login succeeds", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, createAccount: func(ctx context.Context, screenName state.DisplayScreenName, password string) error { assert.Equal(t, user.DisplayScreenName, screenName) assert.Equal(t, "welcome1", password) return nil }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, { name: "account exists, password is invalid, authentication is disabled, login succeeds", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, "bad-roasted-password"), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, { name: "feedbag error during login, returns error", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, feedbagManagerParams: feedbagManagerParams{ feedbagParams: feedbagParams{ { screenName: user.IdentScreenName, err: io.EOF, }, }, }, }, wantErr: io.EOF, }, { name: "login fails on user manager lookup", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARPassword([]byte("the_password"))), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, err: io.EOF, }, }, }, }, wantErr: io.EOF, }, { name: "login with AIM 1.1.19 for Java - success", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "AOL Instant Messenger (TM) version 1.1.19 for Java"), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARJavaPassword([]byte("the_password"))), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ ScreenName: user.DisplayScreenName, ClientID: "AOL Instant Messenger (TM) version 1.1.19 for Java", } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsReconnectHere, "127.0.0.1:5190"), wire.NewTLVBE(wire.LoginTLVTagsAuthorizationCookie, []byte("the-cookie")), wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)), }, }, }, { name: "login with AIM 1.1.19 for Java - failed", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.FLAPSignonFrame{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsClientIdentity, "AOL Instant Messenger (TM) version 1.1.19 for Java"), wire.NewTLVBE(wire.LoginTLVTagsScreenName, user.DisplayScreenName), wire.NewTLVBE(wire.LoginTLVTagsRoastedPassword, wire.RoastOSCARJavaPassword([]byte("the_wrong_password"))), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, }, expectOutput: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "screenName"), wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidPassword), }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { userManager := newMockUserManager(t) for _, params := range tc.mockParams.userManagerParams.getUserParams { userManager.EXPECT(). User(matchContext(), params.screenName). Return(params.result, params.err) } cookieBaker := newMockCookieBaker(t) for _, params := range tc.mockParams.cookieIssueParams { cookieBaker.EXPECT(). Issue(params.dataIn). Return(params.cookieOut, params.err) } feedbagManager := newMockFeedbagManager(t) for _, params := range tc.mockParams.feedbagParams { feedbagManager.EXPECT(). Feedbag(matchContext(), params.screenName). Return(params.results, params.err) } feedbagManager.EXPECT().Feedbag(matchContext(), mock.Anything).Return(nil, nil).Maybe() svc := AuthService{ config: tc.cfg, cookieBaker: cookieBaker, userManager: userManager, feedbagManager: feedbagManager, createAccount: tc.createAccount, logger: slog.Default(), } outputSNAC, err := svc.FLAPLogin(context.Background(), tc.inputSNAC, tc.advertisedHost) assert.ErrorIs(t, err, tc.wantErr) assert.Equal(t, tc.expectOutput, outputSNAC) }) } } func TestAuthService_KerberosLogin(t *testing.T) { user := state.User{ AuthKey: "auth_key", DisplayScreenName: "screenName", IdentScreenName: state.NewIdentScreenName("screenName"), } assert.NoError(t, user.HashPassword("the_password")) cases := []struct { // name is the unit test name name string // advertisedHost is the BOS host the client will connect to upon successful login advertisedHost string // cfg is the app configuration cfg config.Config // inputSNAC is the kerberos SNAC sent from the client to the server inputSNAC wire.SNAC_0x050C_0x0002_KerberosLoginRequest // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams // createAccount is the function that creates a new user account createAccount state.CreateAccountFunc // expectOutput is the response sent from the server to client expectOutput wire.SNACMessage // wantErr is the error we expect from the method wantErr error // timeNow returns a canned time value timeNow func() time.Time }{ { name: "AIM account exists, correct password, login OK", advertisedHost: "127.0.0.1:5190", timeNow: func() time.Time { return time.Unix(1000, 0) }, inputSNAC: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{ RequestID: 54321, ClientPrincipal: user.DisplayScreenName.String(), TicketRequestMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVTicketRequest, wire.KerberosLoginRequestTicket{ Password: []byte("the_password"), }), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ Service: wire.BOS, ScreenName: user.DisplayScreenName, ClientID: "", MultiConnFlag: uint8(wire.MultiConnFlagsRecentClient), KerberosAuth: 1, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, sessionRetrieverParams: sessionRetrieverParams{ retrieveSessionParams: retrieveSessionParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Kerberos, SubGroup: wire.KerberosLoginSuccessResponse, }, Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{ RequestID: 54321, Epoch: 1000, ClientPrincipal: user.DisplayScreenName.String(), ClientRealm: "AOL", Tickets: []wire.KerberosTicket{ { PVNO: 0x5, EncTicket: []uint8{}, TicketRealm: "AOL", ServicePrincipal: "im/boss", ClientRealm: "AOL", ClientPrincipal: user.DisplayScreenName.String(), AuthTime: 1000, StartTime: 1000, EndTime: 87400, Unknown4: 0x60000000, Unknown5: 0x40000000, ConnectionMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVBOSServerInfo, wire.KerberosBOSServerInfo{ Unknown: 1, ConnectionInfo: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVHostname, "127.0.0.1:5190"), wire.NewTLVBE(wire.KerberosTLVCookie, []byte("the-cookie")), wire.NewTLVBE(wire.KerberosTLVConnSettings, wire.KerberosConnUseSSL), }, }, }), }, }, }, }, }, }, }, { name: "AIM account exists, incorrect password, login failed", advertisedHost: "127.0.0.1:5190", timeNow: func() time.Time { return time.Unix(1000, 0) }, inputSNAC: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{ RequestID: 54321, ClientPrincipal: user.DisplayScreenName.String(), TicketRequestMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVTicketRequest, wire.KerberosLoginRequestTicket{ Password: []byte("the_WRONG_password"), }), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Kerberos, SubGroup: wire.KerberosKerberosLoginErrResponse, }, Body: wire.SNAC_0x050C_0x0004_KerberosLoginErrResponse{ KerbRequestID: 54321, ScreenName: user.DisplayScreenName.String(), ErrCode: wire.KerberosErrAuthFailure, Message: "Auth failure", }, }, }, { name: "AIM account exists, correct roasted password, login OK", advertisedHost: "127.0.0.1:5190", timeNow: func() time.Time { return time.Unix(1000, 0) }, inputSNAC: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{ RequestID: 54321, ClientPrincipal: user.DisplayScreenName.String(), TicketRequestMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVTicketRequest, wire.KerberosLoginRequestTicket{ Version: 4, Password: wire.RoastKerberosPassword([]byte("the_password")), }), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, cookieBakerParams: cookieBakerParams{ cookieIssueParams: cookieIssueParams{ { dataIn: func() []byte { loginCookie := state.ServerCookie{ Service: wire.BOS, ScreenName: user.DisplayScreenName, ClientID: "", MultiConnFlag: uint8(wire.MultiConnFlagsRecentClient), KerberosAuth: 1, } buf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(loginCookie, buf)) return buf.Bytes() }(), cookieOut: []byte("the-cookie"), }, }, }, sessionRetrieverParams: sessionRetrieverParams{ retrieveSessionParams: retrieveSessionParams{ { screenName: user.IdentScreenName, result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Kerberos, SubGroup: wire.KerberosLoginSuccessResponse, }, Body: wire.SNAC_0x050C_0x0003_KerberosLoginSuccessResponse{ RequestID: 54321, Epoch: 1000, ClientPrincipal: user.DisplayScreenName.String(), ClientRealm: "AOL", Tickets: []wire.KerberosTicket{ { PVNO: 0x5, EncTicket: []uint8{}, TicketRealm: "AOL", ServicePrincipal: "im/boss", ClientRealm: "AOL", ClientPrincipal: user.DisplayScreenName.String(), AuthTime: 1000, StartTime: 1000, EndTime: 87400, Unknown4: 0x60000000, Unknown5: 0x40000000, ConnectionMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVBOSServerInfo, wire.KerberosBOSServerInfo{ Unknown: 1, ConnectionInfo: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVHostname, "127.0.0.1:5190"), wire.NewTLVBE(wire.KerberosTLVCookie, []byte("the-cookie")), wire.NewTLVBE(wire.KerberosTLVConnSettings, wire.KerberosConnUseSSL), }, }, }), }, }, }, }, }, }, }, { name: "AIM account exists, incorrect roasted password, login failed", advertisedHost: "127.0.0.1:5190", timeNow: func() time.Time { return time.Unix(1000, 0) }, inputSNAC: wire.SNAC_0x050C_0x0002_KerberosLoginRequest{ RequestID: 54321, ClientPrincipal: user.DisplayScreenName.String(), TicketRequestMetadata: wire.TLVBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.KerberosTLVTicketRequest, wire.KerberosLoginRequestTicket{ Version: 4, Password: wire.RoastKerberosPassword([]byte("the_WRONG_password")), }), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: user.IdentScreenName, result: &user, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Kerberos, SubGroup: wire.KerberosKerberosLoginErrResponse, }, Body: wire.SNAC_0x050C_0x0004_KerberosLoginErrResponse{ KerbRequestID: 54321, ScreenName: user.DisplayScreenName.String(), ErrCode: wire.KerberosErrAuthFailure, Message: "Auth failure", }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { userManager := newMockUserManager(t) for _, params := range tc.mockParams.userManagerParams.getUserParams { userManager.EXPECT(). User(matchContext(), params.screenName). Return(params.result, params.err) } cookieBaker := newMockCookieBaker(t) for _, params := range tc.mockParams.cookieIssueParams { cookieBaker.EXPECT(). Issue(params.dataIn). Return(params.cookieOut, params.err) } sessionRetriever := newMockSessionRetriever(t) for _, params := range tc.mockParams.retrieveSessionParams { sessionRetriever.EXPECT(). RetrieveSession(params.screenName). Return(params.result) } feedbagManager := newMockFeedbagManager(t) feedbagManager.EXPECT().Feedbag(matchContext(), mock.Anything).Return(nil, nil).Maybe() svc := AuthService{ config: tc.cfg, cookieBaker: cookieBaker, userManager: userManager, sessionRetriever: sessionRetriever, feedbagManager: feedbagManager, timeNow: tc.timeNow, maxConcurrentLoginsPerUser: 2, createAccount: tc.createAccount, logger: slog.Default(), } outputSNAC, err := svc.KerberosLogin(context.Background(), tc.inputSNAC, tc.advertisedHost) assert.ErrorIs(t, err, tc.wantErr) assert.Equal(t, tc.expectOutput, outputSNAC) }) } } func TestAuthService_BUCPChallengeRequest(t *testing.T) { sessUUID := uuid.UUID{1, 2, 3} cases := []struct { // name is the unit test name name string // advertisedHost is the BOS host the client will connect to upon successful login advertisedHost string // cfg is the app configuration cfg config.Config // inputSNAC is the SNAC sent from the client to the server inputSNAC wire.SNAC_0x17_0x06_BUCPChallengeRequest // 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 // wantErr is the error we expect from the method wantErr error }{ { name: "login with valid username, expect OK login response", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x06_BUCPChallengeRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "sn_user_a"), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("sn_user_a"), result: &state.User{ IdentScreenName: state.NewIdentScreenName("sn_user_a"), AuthKey: "auth_key_user_a", }, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPChallengeResponse, }, Body: wire.SNAC_0x17_0x07_BUCPChallengeResponse{ AuthKey: "auth_key_user_a", }, }, }, { name: "login with invalid username, expect OK login response (Cfg.DisableAuth=true)", advertisedHost: "127.0.0.1:5190", cfg: config.Config{ DisableAuth: true, }, inputSNAC: wire.SNAC_0x17_0x06_BUCPChallengeRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "sn_user_b"), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("sn_user_b"), result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPChallengeResponse, }, Body: wire.SNAC_0x17_0x07_BUCPChallengeResponse{ AuthKey: sessUUID.String(), }, }, }, { name: "login with invalid username, expect failed login response (Cfg.DisableAuth=false)", advertisedHost: "127.0.0.1:5190", inputSNAC: wire.SNAC_0x17_0x06_BUCPChallengeRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "sn_user_b"), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("sn_user_b"), result: nil, }, }, }, }, expectOutput: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginResponse, }, Body: wire.SNAC_0x17_0x03_BUCPLoginResponse{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, uint16(0x01)), }, }, }, }, }, { name: "login fails on user manager lookup", inputSNAC: wire.SNAC_0x17_0x06_BUCPChallengeRequest{ TLVRestBlock: wire.TLVRestBlock{ TLVList: wire.TLVList{ wire.NewTLVBE(wire.LoginTLVTagsScreenName, "sn_user_b"), }, }, }, mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: state.NewIdentScreenName("sn_user_b"), err: io.EOF, }, }, }, }, wantErr: io.EOF, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { userManager := newMockUserManager(t) for _, params := range tc.mockParams.userManagerParams.getUserParams { userManager.EXPECT(). User(matchContext(), params.screenName). Return(params.result, params.err) } svc := AuthService{ config: tc.cfg, userManager: userManager, logger: slog.Default(), } fnNewUUID := func() uuid.UUID { return sessUUID } outputSNAC, err := svc.BUCPChallenge(context.Background(), tc.inputSNAC, fnNewUUID) assert.ErrorIs(t, err, tc.wantErr) assert.Equal(t, tc.expectOutput, outputSNAC) }) } } func TestAuthService_RegisterChatSession_HappyPath(t *testing.T) { instance := newTestInstance("ScreenName") serverCookie := state.ServerCookie{ ChatCookie: "the-chat-cookie", ScreenName: instance.DisplayScreenName(), } chatSessionRegistry := newMockChatSessionRegistry(t) chatSessionRegistry.EXPECT(). AddSession(mock.Anything, serverCookie.ChatCookie, instance.DisplayScreenName(), mock.Anything). Return(instance, nil) chatCookieBuf := &bytes.Buffer{} assert.NoError(t, wire.MarshalBE(serverCookie, chatCookieBuf)) svc := NewAuthService(config.Config{}, nil, nil, chatSessionRegistry, nil, nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), nil, slog.Default()) have, err := svc.RegisterChatSession(context.Background(), serverCookie, nil) assert.NoError(t, err) assert.Equal(t, instance, have) } func TestAuthService_RegisterBOSSession(t *testing.T) { screenName := state.DisplayScreenName("UserScreenName") aimAuthCookie := state.ServerCookie{ ScreenName: screenName, } uin := state.DisplayScreenName("100003") icqAuthCookie := state.ServerCookie{ ScreenName: uin, } cases := []struct { // name is the unit test name name string // cookieOut is the auth cookieOut that contains session information cookie state.ServerCookie // cfg is the server config cfg config.Config // createAccount is called to auto-create a missing user when DisableAuth is set createAccount state.CreateAccountFunc // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams // wantSess asserts the values of one or more session properties wantSess func(*state.SessionInstance) bool // wantErr is the error we expect from the method wantErr error }{ { name: "successfully register an AIM session", cookie: aimAuthCookie, mockParams: mockParams{ sessionRegistryParams: sessionRegistryParams{ addSessionParams: addSessionParams{ { screenName: screenName, doMultiSess: false, result: newTestInstance(screenName), }, }, }, userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: screenName.IdentScreenName(), result: &state.User{ IdentScreenName: screenName.IdentScreenName(), DisplayScreenName: screenName, }, }, }, }, accountManagerParams: accountManagerParams{ accountManagerConfirmStatusParams: accountManagerConfirmStatusParams{ { screenName: screenName.IdentScreenName(), confirmStatus: true, }, }, }, bartItemManagerParams: bartItemManagerParams{ buddyIconMetadataParams: buddyIconMetadataParams{ { screenName: screenName.IdentScreenName(), result: &wire.BARTID{ Type: wire.BARTTypesBuddyIcon, BARTInfo: wire.BARTInfo{ Flags: wire.BARTFlagsKnown, Hash: []byte{'m', 'y', 'i', 'c', 'o', 'n'}, }, }, }, }, }, }, wantSess: func(instance *state.SessionInstance) bool { want := wire.BARTID{ Type: wire.BARTTypesBuddyIcon, BARTInfo: wire.BARTInfo{ Flags: wire.BARTFlagsKnown, Hash: []byte{'m', 'y', 'i', 'c', 'o', 'n'}, }, } has, hasIcon := instance.Session().BuddyIcon() return assert.True(t, hasIcon) && assert.Equal(t, want, has) }, }, { name: "successfully register an AIM bot session", cookie: aimAuthCookie, mockParams: mockParams{ sessionRegistryParams: sessionRegistryParams{ addSessionParams: addSessionParams{ { screenName: screenName, doMultiSess: false, result: newTestInstance(screenName), }, }, }, userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: screenName.IdentScreenName(), result: &state.User{ IdentScreenName: screenName.IdentScreenName(), DisplayScreenName: screenName, IsBot: true, }, }, }, }, accountManagerParams: accountManagerParams{ accountManagerConfirmStatusParams: accountManagerConfirmStatusParams{ { screenName: screenName.IdentScreenName(), confirmStatus: true, }, }, }, bartItemManagerParams: bartItemManagerParams{ buddyIconMetadataParams: buddyIconMetadataParams{ { screenName: screenName.IdentScreenName(), result: nil, }, }, }, }, wantSess: func(instance *state.SessionInstance) bool { return instance.Session().AllUserInfoBitmask(wire.OServiceUserFlagBot) }, }, { name: "successfully register an ICQ session", cookie: icqAuthCookie, mockParams: mockParams{ sessionRegistryParams: sessionRegistryParams{ addSessionParams: addSessionParams{ { screenName: uin, result: newTestInstance(uin), }, }, }, userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: uin.IdentScreenName(), result: &state.User{ IdentScreenName: uin.IdentScreenName(), DisplayScreenName: uin, }, }, }, }, accountManagerParams: accountManagerParams{ accountManagerConfirmStatusParams: accountManagerConfirmStatusParams{ { screenName: uin.IdentScreenName(), confirmStatus: true, }, }, }, bartItemManagerParams: bartItemManagerParams{ buddyIconMetadataParams: buddyIconMetadataParams{ { screenName: uin.IdentScreenName(), result: nil, }, }, }, }, wantSess: func(instance *state.SessionInstance) bool { uinMatches := fmt.Sprintf("%d", instance.UIN()) == uin.String() flagsMatch := instance.Session().AllUserInfoBitmask(wire.OServiceUserFlagICQ) return uinMatches && flagsMatch }, }, { name: "user not found, DisableAuth false, return error", cookie: aimAuthCookie, cfg: config.Config{DisableAuth: false}, wantErr: errors.New("user not found"), mockParams: mockParams{ userManagerParams: userManagerParams{ getUserParams: getUserParams{ { screenName: screenName.IdentScreenName(), result: nil, }, }, }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { sessionRegistry := newMockSessionRegistry(t) for _, params := range tc.mockParams.addSessionParams { sessionRegistry.EXPECT(). AddSession(mock.Anything, params.screenName, params.doMultiSess, mock.Anything, mock.Anything). Return(params.result, params.err) } userManager := newMockUserManager(t) for _, params := range tc.mockParams.userManagerParams.getUserParams { userManager.EXPECT(). User(matchContext(), params.screenName). Return(params.result, nil).Once() } accountManager := newMockAccountManager(t) for _, params := range tc.mockParams.accountManagerConfirmStatusParams { accountManager.EXPECT(). ConfirmStatus(matchContext(), params.screenName). Return(params.confirmStatus, nil) } bartItemManager := newMockBARTItemManager(t) for _, params := range tc.mockParams.buddyIconMetadataParams { bartItemManager.EXPECT(). BuddyIconMetadata(matchContext(), params.screenName). Return(params.result, params.err) } svc := NewAuthService(tc.cfg, sessionRegistry, nil, nil, userManager, nil, nil, accountManager, bartItemManager, nil, wire.DefaultRateLimitClasses(), tc.createAccount, slog.Default()) have, err := svc.RegisterBOSSession(context.Background(), tc.cookie, nil) if tc.wantErr != nil { assert.ErrorContains(t, err, tc.wantErr.Error()) return } assert.NoError(t, err) if tc.wantSess != nil { assert.True(t, tc.wantSess(have)) } }) } } func TestAuthService_RetrieveBOSSession_HappyPath(t *testing.T) { instance := newTestInstance("screenName", sessOptSignonComplete) aimAuthCookie := state.ServerCookie{ ScreenName: instance.DisplayScreenName(), SessionNum: instance.Num(), } sessionRetriever := newMockSessionRetriever(t) sessionRetriever.EXPECT(). RetrieveSession(instance.IdentScreenName()). Return(instance.Session()) userManager := newMockUserManager(t) userManager.EXPECT(). User(matchContext(), instance.IdentScreenName()). Return(&state.User{IdentScreenName: instance.IdentScreenName()}, nil) svc := NewAuthService(config.Config{}, nil, sessionRetriever, nil, userManager, nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), nil, slog.Default()) have, err := svc.RetrieveBOSSession(context.Background(), aimAuthCookie) assert.NoError(t, err) assert.Equal(t, instance, have) } func TestAuthService_RetrieveBOSSession_SessionNotFound(t *testing.T) { instance := newTestInstance("screenName") aimAuthCookie := state.ServerCookie{ ScreenName: instance.DisplayScreenName(), SessionNum: instance.Num(), } sessionRetriever := newMockSessionRetriever(t) sessionRetriever.EXPECT(). RetrieveSession(instance.IdentScreenName()). Return(nil) userManager := newMockUserManager(t) userManager.EXPECT(). User(matchContext(), instance.IdentScreenName()). Return(&state.User{IdentScreenName: instance.IdentScreenName()}, nil) svc := NewAuthService(config.Config{}, nil, sessionRetriever, nil, userManager, nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), nil, slog.Default()) have, err := svc.RetrieveBOSSession(context.Background(), aimAuthCookie) assert.NoError(t, err) assert.Nil(t, have) } func TestAuthService_SignoutChat(t *testing.T) { tests := []struct { // name is the unit test name name string // instance is the session of the user signing out instance *state.SessionInstance // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams }{ { name: "user signs out of chat room, room is empty after user leaves", instance: newTestInstance("me", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")), mockParams: mockParams{ chatMessageRelayerParams: chatMessageRelayerParams{ chatRelayToAllExceptParams: chatRelayToAllExceptParams{ { screenName: state.NewIdentScreenName("me"), message: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Chat, SubGroup: wire.ChatUsersLeft, }, Body: wire.SNAC_0x0E_0x04_ChatUsersLeft{ Users: []wire.TLVUserInfo{ newTestInstance("me", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")).Session().TLVUserInfo(), }, }, }, }, }, }, sessionRegistryParams: sessionRegistryParams{ removeSessionParams: removeSessionParams{ { screenName: state.NewIdentScreenName("me"), }, }, }, }, }, { name: "user signs out of chat room, room is not empty after user leaves", instance: newTestInstance("me", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")), mockParams: mockParams{ chatMessageRelayerParams: chatMessageRelayerParams{ chatRelayToAllExceptParams: chatRelayToAllExceptParams{ { screenName: state.NewIdentScreenName("me"), message: wire.SNACMessage{ Frame: wire.SNACFrame{ FoodGroup: wire.Chat, SubGroup: wire.ChatUsersLeft, }, Body: wire.SNAC_0x0E_0x04_ChatUsersLeft{ Users: []wire.TLVUserInfo{ newTestInstance("me", sessOptCannedSignonTime, sessOptChatRoomCookie("the-chat-cookie")).Session().TLVUserInfo(), }, }, }, }, }, }, sessionRegistryParams: sessionRegistryParams{ removeSessionParams: removeSessionParams{ { screenName: state.NewIdentScreenName("me"), }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { chatMessageRelayer := newMockChatMessageRelayer(t) for _, params := range tt.mockParams.chatRelayToAllExceptParams { chatMessageRelayer.EXPECT(). RelayToAllExcept(matchContext(), tt.instance.ChatRoomCookie(), params.screenName, params.message) } sessionManager := newMockChatSessionRegistry(t) for _, params := range tt.mockParams.removeSessionParams { sessionManager.EXPECT(). RemoveSession(matchUserSession(params.screenName)) } svc := NewAuthService(config.Config{}, nil, nil, sessionManager, nil, nil, chatMessageRelayer, nil, nil, nil, wire.DefaultRateLimitClasses(), nil, slog.Default()) svc.SignoutChat(context.Background(), tt.instance.Session()) }) } } func TestAuthService_Signout(t *testing.T) { tests := []struct { // name is the unit test name name string // instance is the session of the user signing out instance *state.SessionInstance // wantErr is the error we expect from the method wantErr error // mockParams is the list of params sent to mocks that satisfy this // method's dependencies mockParams mockParams }{ { name: "user signs out of chat room, room is empty after user leaves", instance: newTestInstance("me", sessOptCannedSignonTime), mockParams: mockParams{ buddyBroadcasterParams: buddyBroadcasterParams{ broadcastBuddyDepartedParams: broadcastBuddyDepartedParams{ { screenName: state.NewIdentScreenName("me"), }, }, }, sessionRegistryParams: sessionRegistryParams{ removeSessionParams: removeSessionParams{ { screenName: state.NewIdentScreenName("me"), }, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sessionManager := newMockSessionRegistry(t) for _, params := range tt.mockParams.removeSessionParams { sessionManager.EXPECT().RemoveSession(matchUserSession(params.screenName)) } svc := NewAuthService(config.Config{}, sessionManager, nil, nil, nil, nil, nil, nil, nil, nil, wire.DefaultRateLimitClasses(), nil, slog.Default()) svc.Signout(context.Background(), tt.instance.Session()) }) } } func TestBuildLinkedAccountsXML(t *testing.T) { cases := []struct { name string screenName state.IdentScreenName linkedNames []state.IdentScreenName wantXML string wantErr bool }{ { name: "primary account with no linked accounts", screenName: state.NewIdentScreenName("PrimaryUser"), linkedNames: []state.IdentScreenName{}, wantXML: `primaryuser`, }, { name: "primary account with one linked account", screenName: state.NewIdentScreenName("PrimaryUser"), linkedNames: []state.IdentScreenName{ state.NewIdentScreenName("LinkedUser1"), }, wantXML: `primaryuserlinkeduser1`, }, { name: "primary account with multiple linked accounts", screenName: state.NewIdentScreenName("PrimaryUser"), linkedNames: []state.IdentScreenName{ state.NewIdentScreenName("LinkedUser1"), state.NewIdentScreenName("LinkedUser2"), state.NewIdentScreenName("LinkedUser3"), }, wantXML: `primaryuserlinkeduser1linkeduser2linkeduser3`, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := buildLinkedAccountsXML(tc.screenName, tc.linkedNames) if tc.wantErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Equal(t, tc.wantXML, got) }) } } func TestAuthService_addLinkedAccountsTLV(t *testing.T) { cases := []struct { name string screenName state.DisplayScreenName // feedbagItems is what feedbagManager.Feedbag returns feedbagItems []wire.FeedbagItem feedbagErr error // wantTLVCount is the expected number of TLVs after the call wantTLVCount int wantErr bool }{ { name: "no linked accounts, TLV list unchanged", screenName: "PrimaryUser", feedbagItems: nil, wantTLVCount: 0, }, { name: "one linked account, TLV appended", screenName: "PrimaryUser", feedbagItems: []wire.FeedbagItem{ {ClassID: wire.FeedbagClassIdAlInfo, Name: "linkeduser1"}, }, wantTLVCount: 1, }, { name: "multiple linked accounts, single TLV appended", screenName: "PrimaryUser", feedbagItems: []wire.FeedbagItem{ {ClassID: wire.FeedbagClassIdAlInfo, Name: "linkeduser1"}, {ClassID: wire.FeedbagClassIdAlInfo, Name: "linkeduser2"}, }, wantTLVCount: 1, }, { name: "feedbagManager returns error, error propagated", screenName: "PrimaryUser", feedbagErr: io.EOF, wantErr: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { feedbagManager := newMockFeedbagManager(t) feedbagManager.EXPECT(). Feedbag(matchContext(), state.NewIdentScreenName(string(tc.screenName))). Return(tc.feedbagItems, tc.feedbagErr) svc := AuthService{feedbagManager: feedbagManager} tlvs := wire.TLVList{} err := svc.addLinkedAccountsTLV(context.Background(), tc.screenName, &tlvs) if tc.wantErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Len(t, tlvs, tc.wantTLVCount) if tc.wantTLVCount > 0 { assert.Equal(t, wire.OServiceTLVTagsLinkedAccounts, tlvs[0].Tag) } }) } }