Răsfoiți Sursa

implement UserLookup food group

Makes user search by email work in AIM 1.0.
Mike 1 an în urmă
părinte
comite
093cd2a070

+ 3 - 0
.mockery.yaml

@@ -81,6 +81,9 @@ packages:
       ODirService:
         config:
           filename: "mock_odir_service_test.go"
+      UserLookupService:
+        config:
+          filename: "mock_user_lookup_service_test.go"
   github.com/mk6i/retro-aim-server/foodgroup:
     interfaces:
       FeedbagManager:

+ 2 - 0
cmd/server/factory.go

@@ -159,6 +159,7 @@ func BOS(deps Container) oscar.BOSServer {
 		deps.adjListBuddyListStore)
 	oServiceService := foodgroup.NewOServiceServiceForBOS(deps.cfg, deps.inMemorySessionManager,
 		deps.adjListBuddyListStore, logger, deps.hmacCookieBaker, deps.sqLiteUserStore, deps.sqLiteUserStore)
+	userLookupService := foodgroup.NewUserLookupService(deps.sqLiteUserStore)
 
 	return oscar.BOSServer{
 		AuthService: authService,
@@ -174,6 +175,7 @@ func BOS(deps Container) oscar.BOSServer {
 			LocateHandler:     handler.NewLocateHandler(locateService, logger),
 			OServiceHandler:   handler.NewOServiceHandler(logger, oServiceService),
 			PermitDenyHandler: handler.NewPermitDenyHandler(logger, permitDenyService),
+			UserLookupHandler: handler.NewUserLookupHandler(logger, userLookupService),
 		}),
 		Logger:         logger,
 		OnlineNotifier: oServiceService,

+ 5 - 0
foodgroup/oservice.go

@@ -351,6 +351,9 @@ func init() {
 			wire.ODirKeywordListQuery,
 			wire.ODirKeywordListReply,
 		},
+		wire.UserLookup: {
+			wire.UserLookupFindByEmail,
+		},
 	}
 
 	for _, foodGroup := range []uint16{
@@ -367,6 +370,7 @@ func init() {
 		wire.ICQ,
 		wire.PermitDeny,
 		wire.ODir,
+		wire.UserLookup,
 	} {
 		subGroups := foodGroupToSubgroup[foodGroup]
 		for _, subGroup := range subGroups {
@@ -556,6 +560,7 @@ func NewOServiceServiceForBOS(
 				wire.Locate,
 				wire.OService,
 				wire.PermitDeny,
+				wire.UserLookup,
 			},
 		},
 	}

+ 5 - 0
foodgroup/oservice_test.go

@@ -1465,6 +1465,10 @@ func TestOServiceService_RateParamsQuery(t *testing.T) {
 							FoodGroup: wire.ODir,
 							SubGroup:  wire.ODirKeywordListReply,
 						},
+						{
+							FoodGroup: wire.UserLookup,
+							SubGroup:  wire.UserLookupFindByEmail,
+						},
 					},
 				},
 			},
@@ -1494,6 +1498,7 @@ func TestOServiceServiceForBOS_OServiceHostOnline(t *testing.T) {
 				wire.Locate,
 				wire.OService,
 				wire.PermitDeny,
+				wire.UserLookup,
 			},
 		},
 	}

+ 55 - 0
foodgroup/user_lookup.go

@@ -0,0 +1,55 @@
+package foodgroup
+
+import (
+	"context"
+	"errors"
+
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+// NewUserLookupService returns a new instance of UserLookupService.
+func NewUserLookupService(profileManager ProfileManager) UserLookupService {
+	return UserLookupService{
+		profileManager: profileManager,
+	}
+}
+
+// UserLookupService implements the UserLookup food group.
+type UserLookupService struct {
+	profileManager ProfileManager
+}
+
+// FindByEmail searches for a user by email address.
+func (s UserLookupService) FindByEmail(_ context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0A_0x02_UserLookupFindByEmail) (wire.SNACMessage, error) {
+	user, err := s.profileManager.FindByAIMEmail(string(inBody.Email))
+
+	switch {
+	case errors.Is(err, state.ErrNoUser):
+		return wire.SNACMessage{
+			Frame: wire.SNACFrame{
+				FoodGroup: wire.UserLookup,
+				SubGroup:  wire.UserLookupErr,
+				RequestID: inFrame.RequestID,
+			},
+			Body: wire.UserLookupErrNoUserFound,
+		}, nil
+	case err != nil:
+		return wire.SNACMessage{}, err
+	}
+
+	return wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.UserLookup,
+			SubGroup:  wire.UserLookupFindReply,
+			RequestID: inFrame.RequestID,
+		},
+		Body: wire.SNAC_0x0A_0x03_UserLookupFindReply{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLVBE(wire.UserLookupTLVEmailAddress, user.DisplayScreenName),
+				},
+			},
+		},
+	}, nil
+}

+ 135 - 0
foodgroup/user_lookup_test.go

@@ -0,0 +1,135 @@
+package foodgroup
+
+import (
+	"io"
+	"testing"
+
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUserLookupService_FindByEmail(t *testing.T) {
+	cases := []struct {
+		// name is the unit test name
+		name string
+		// inputSNAC is the SNAC sent by the sender client
+		inputSNAC wire.SNACMessage
+		// expectSNACFrame is the SNAC frame sent from the server to the recipient
+		// client
+		expectOutput wire.SNACMessage
+		// mockParams is the list of params sent to mocks that satisfy this
+		// method's dependencies
+		mockParams mockParams
+		// expectErr is the expected error returned by the handler
+		expectErr error
+	}{
+		{
+			name: "search by email address - results found",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0A_0x02_UserLookupFindByEmail{
+					Email: []byte("user@aol.com"),
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.UserLookup,
+					SubGroup:  wire.UserLookupFindReply,
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0A_0x03_UserLookupFindReply{
+					TLVRestBlock: wire.TLVRestBlock{
+						TLVList: wire.TLVList{
+							wire.NewTLVBE(wire.UserLookupTLVEmailAddress, "ChattingChuck"),
+						},
+					},
+				},
+			},
+			mockParams: mockParams{
+				profileManagerParams: profileManagerParams{
+					findByAIMEmailParams: findByAIMEmailParams{
+						{
+							email: "user@aol.com",
+							result: state.User{
+								DisplayScreenName: "ChattingChuck",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "search by email address - no results found",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0A_0x02_UserLookupFindByEmail{
+					Email: []byte("user@aol.com"),
+				},
+			},
+			expectOutput: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					FoodGroup: wire.UserLookup,
+					SubGroup:  wire.UserLookupErr,
+					RequestID: 1234,
+				},
+				Body: wire.UserLookupErrNoUserFound,
+			},
+			mockParams: mockParams{
+				profileManagerParams: profileManagerParams{
+					findByAIMEmailParams: findByAIMEmailParams{
+						{
+							email: "user@aol.com",
+							err:   state.ErrNoUser,
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "search by email address - search error",
+			inputSNAC: wire.SNACMessage{
+				Frame: wire.SNACFrame{
+					RequestID: 1234,
+				},
+				Body: wire.SNAC_0x0A_0x02_UserLookupFindByEmail{
+					Email: []byte("user@aol.com"),
+				},
+			},
+			expectOutput: wire.SNACMessage{},
+			expectErr:    io.EOF,
+			mockParams: mockParams{
+				profileManagerParams: profileManagerParams{
+					findByAIMEmailParams: findByAIMEmailParams{
+						{
+							email: "user@aol.com",
+							err:   io.EOF,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			profileManager := newMockProfileManager(t)
+
+			for _, params := range tc.mockParams.findByAIMEmailParams {
+				profileManager.EXPECT().
+					FindByAIMEmail(params.email).
+					Return(params.result, params.err)
+			}
+
+			svc := NewUserLookupService(profileManager)
+			actual, err := svc.FindByEmail(nil, tc.inputSNAC.Frame, tc.inputSNAC.Body.(wire.SNAC_0x0A_0x02_UserLookupFindByEmail))
+			assert.ErrorIs(t, err, tc.expectErr)
+			assert.Equal(t, tc.expectOutput, actual)
+		})
+	}
+}

+ 95 - 0
server/oscar/handler/mock_user_lookup_service_test.go

@@ -0,0 +1,95 @@
+// Code generated by mockery v2.46.3. DO NOT EDIT.
+
+package handler
+
+import (
+	context "context"
+
+	wire "github.com/mk6i/retro-aim-server/wire"
+	mock "github.com/stretchr/testify/mock"
+)
+
+// mockUserLookupService is an autogenerated mock type for the UserLookupService type
+type mockUserLookupService struct {
+	mock.Mock
+}
+
+type mockUserLookupService_Expecter struct {
+	mock *mock.Mock
+}
+
+func (_m *mockUserLookupService) EXPECT() *mockUserLookupService_Expecter {
+	return &mockUserLookupService_Expecter{mock: &_m.Mock}
+}
+
+// FindByEmail provides a mock function with given fields: ctx, inFrame, inBody
+func (_m *mockUserLookupService) FindByEmail(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0A_0x02_UserLookupFindByEmail) (wire.SNACMessage, error) {
+	ret := _m.Called(ctx, inFrame, inBody)
+
+	if len(ret) == 0 {
+		panic("no return value specified for FindByEmail")
+	}
+
+	var r0 wire.SNACMessage
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame, wire.SNAC_0x0A_0x02_UserLookupFindByEmail) (wire.SNACMessage, error)); ok {
+		return rf(ctx, inFrame, inBody)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, wire.SNACFrame, wire.SNAC_0x0A_0x02_UserLookupFindByEmail) wire.SNACMessage); ok {
+		r0 = rf(ctx, inFrame, inBody)
+	} else {
+		r0 = ret.Get(0).(wire.SNACMessage)
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, wire.SNACFrame, wire.SNAC_0x0A_0x02_UserLookupFindByEmail) error); ok {
+		r1 = rf(ctx, inFrame, inBody)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// mockUserLookupService_FindByEmail_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindByEmail'
+type mockUserLookupService_FindByEmail_Call struct {
+	*mock.Call
+}
+
+// FindByEmail is a helper method to define mock.On call
+//   - ctx context.Context
+//   - inFrame wire.SNACFrame
+//   - inBody wire.SNAC_0x0A_0x02_UserLookupFindByEmail
+func (_e *mockUserLookupService_Expecter) FindByEmail(ctx interface{}, inFrame interface{}, inBody interface{}) *mockUserLookupService_FindByEmail_Call {
+	return &mockUserLookupService_FindByEmail_Call{Call: _e.mock.On("FindByEmail", ctx, inFrame, inBody)}
+}
+
+func (_c *mockUserLookupService_FindByEmail_Call) Run(run func(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0A_0x02_UserLookupFindByEmail)) *mockUserLookupService_FindByEmail_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(wire.SNACFrame), args[2].(wire.SNAC_0x0A_0x02_UserLookupFindByEmail))
+	})
+	return _c
+}
+
+func (_c *mockUserLookupService_FindByEmail_Call) Return(_a0 wire.SNACMessage, _a1 error) *mockUserLookupService_FindByEmail_Call {
+	_c.Call.Return(_a0, _a1)
+	return _c
+}
+
+func (_c *mockUserLookupService_FindByEmail_Call) RunAndReturn(run func(context.Context, wire.SNACFrame, wire.SNAC_0x0A_0x02_UserLookupFindByEmail) (wire.SNACMessage, error)) *mockUserLookupService_FindByEmail_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
+// newMockUserLookupService creates a new instance of mockUserLookupService. 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 newMockUserLookupService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *mockUserLookupService {
+	mock := &mockUserLookupService{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}

+ 3 - 0
server/oscar/handler/routes.go

@@ -23,6 +23,7 @@ type Handlers struct {
 	ODirHandler
 	OServiceHandler
 	PermitDenyHandler
+	UserLookupHandler
 }
 
 // NewBOSRouter initializes and configures a new Router instance for handling
@@ -88,6 +89,8 @@ func NewBOSRouter(h Handlers) oscar.Router {
 	router.Register(wire.PermitDeny, wire.PermitDenyAddPermListEntries, h.PermitDenyHandler.AddPermListEntries)
 	router.Register(wire.PermitDeny, wire.PermitDenySetGroupPermitMask, h.PermitDenyHandler.SetGroupPermitMask)
 
+	router.Register(wire.UserLookup, wire.UserLookupFindByEmail, h.UserLookupHandler.FindByEmail)
+
 	return router
 }
 

+ 43 - 0
server/oscar/handler/user_lookup.go

@@ -0,0 +1,43 @@
+package handler
+
+import (
+	"context"
+	"io"
+	"log/slog"
+
+	"github.com/mk6i/retro-aim-server/server/oscar"
+	"github.com/mk6i/retro-aim-server/server/oscar/middleware"
+	"github.com/mk6i/retro-aim-server/state"
+	"github.com/mk6i/retro-aim-server/wire"
+)
+
+type UserLookupService interface {
+	FindByEmail(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0A_0x02_UserLookupFindByEmail) (wire.SNACMessage, error)
+}
+
+func NewUserLookupHandler(logger *slog.Logger, userLookupService UserLookupService) UserLookupHandler {
+	return UserLookupHandler{
+		UserLookupService: userLookupService,
+		RouteLogger: middleware.RouteLogger{
+			Logger: logger,
+		},
+	}
+}
+
+type UserLookupHandler struct {
+	UserLookupService
+	middleware.RouteLogger
+}
+
+func (h UserLookupHandler) FindByEmail(ctx context.Context, _ *state.Session, inFrame wire.SNACFrame, r io.Reader, rw oscar.ResponseWriter) error {
+	inBody := wire.SNAC_0x0A_0x02_UserLookupFindByEmail{}
+	if err := wire.UnmarshalBE(&inBody, r); err != nil {
+		return err
+	}
+	outSNAC, err := h.UserLookupService.FindByEmail(ctx, inFrame, inBody)
+	if err != nil {
+		return err
+	}
+	h.LogRequestAndResponse(ctx, inFrame, inBody, outSNAC.Frame, outSNAC.Body)
+	return rw.SendSNAC(outSNAC.Frame, outSNAC.Body)
+}

+ 54 - 0
server/oscar/handler/user_lookup_test.go

@@ -0,0 +1,54 @@
+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 TestUserLookupHandler_FindByEmail(t *testing.T) {
+	input := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.UserLookup,
+			SubGroup:  wire.UserLookupFindByEmail,
+		},
+		Body: wire.SNAC_0x0A_0x02_UserLookupFindByEmail{
+			Email: []byte("haha@aol.com"),
+		},
+	}
+	output := wire.SNACMessage{
+		Frame: wire.SNACFrame{
+			FoodGroup: wire.UserLookup,
+			SubGroup:  wire.UserLookupFindReply,
+		},
+		Body: wire.SNAC_0x0A_0x03_UserLookupFindReply{
+			TLVRestBlock: wire.TLVRestBlock{
+				TLVList: wire.TLVList{
+					wire.NewTLVBE(0x01, uint16(0x02)),
+				},
+			},
+		},
+	}
+
+	svc := newMockUserLookupService(t)
+	svc.EXPECT().
+		FindByEmail(mock.Anything, input.Frame, input.Body).
+		Return(output, nil)
+
+	h := NewUserLookupHandler(slog.Default(), svc)
+
+	ss := newMockResponseWriter(t)
+	ss.EXPECT().
+		SendSNAC(output.Frame, output.Body).
+		Return(nil)
+
+	buf := &bytes.Buffer{}
+	assert.NoError(t, wire.MarshalBE(input.Body, buf))
+
+	assert.NoError(t, h.FindByEmail(nil, nil, input.Frame, buf, ss))
+}

+ 22 - 0
wire/snacs.go

@@ -848,6 +848,28 @@ type SNAC_0x09_0x03_PermitDenyRightsReply struct {
 	TLVRestBlock
 }
 
+//
+// 0x0A: UserLookup
+//
+
+const (
+	UserLookupErr         uint16 = 0x0001
+	UserLookupFindByEmail uint16 = 0x0002
+	UserLookupFindReply   uint16 = 0x0003
+
+	UserLookupErrNoUserFound uint16 = 0x0014
+
+	UserLookupTLVEmailAddress uint16 = 0x0001
+)
+
+type SNAC_0x0A_0x02_UserLookupFindByEmail struct {
+	Email []byte
+}
+
+type SNAC_0x0A_0x03_UserLookupFindReply struct {
+	TLVRestBlock
+}
+
 //
 // 0x0D: ChatNav
 //