package webapi import ( "encoding/json" "fmt" "io" "log/slog" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/mk6i/open-oscar-server/state" "github.com/mk6i/open-oscar-server/wire" ) func searchReply(status uint16, results ...wire.TLVBlock) wire.SNACMessage { body := wire.SNAC_0x0F_0x03_InfoReply{Status: status} body.Results.List = results return wire.SNACMessage{Body: body} } func result(screenName, firstName, lastName string) wire.TLVBlock { return wire.TLVBlock{TLVList: wire.TLVList{ wire.NewTLVBE(wire.ODirTLVScreenName, screenName), wire.NewTLVBE(wire.ODirTLVFirstName, firstName), wire.NewTLVBE(wire.ODirTLVLastName, lastName), }} } // decodeInfoArray pulls infoArray out of the response envelope at the given // path ("results.infoArray" for search, "infoArray" for get). func decodeInfoArray(t *testing.T, body []byte, nested bool) []MemberDirInfo { t.Helper() var envelope struct { Response struct { StatusCode int `json:"statusCode"` Data struct { InfoArray []MemberDirInfo `json:"infoArray"` Results struct { InfoArray []MemberDirInfo `json:"infoArray"` } `json:"results"` } `json:"data"` } `json:"response"` } require.NoError(t, json.Unmarshal(body, &envelope)) assert.Equal(t, 200, envelope.Response.StatusCode) if nested { return envelope.Response.Data.Results.InfoArray } return envelope.Response.Data.InfoArray } // stubNoDirUser answers DirInfo with no directory TLVs, which is what the service // returns for a name that belongs to no user. Every search test needs one, since // Search does an identity lookup alongside the directory query. func stubNoDirUser(t *testing.T) *mockLocateService { ls := newMockLocateService(t) ls.EXPECT().DirInfo(mock.Anything, mock.Anything, mock.Anything). Return(wire.SNACMessage{Body: wire.SNAC_0x02_0x0C_LocateGetDirReply{ Status: wire.LocateGetDirReplyOK, }}, nil).Maybe() return ls } func TestMemberDirHandler_Search_Keyword(t *testing.T) { dirSvc := newMockDirSearchService(t) // keyword=haha must map to the ODir interest TLV. dirSvc.EXPECT().InfoQuery(mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x0F_0x02_InfoQuery) bool { v, ok := q.String(wire.ODirTLVInterest) return ok && v == "haha" })).Return(searchReply(wire.ODirSearchResponseOK, result("FoundUser", "Found", "User")), nil) h := &MemberDirHandler{DirSearchService: dirSvc, LocateService: stubNoDirUser(t), Logger: slog.Default()} session := &Session{AimSID: "sid", ScreenName: state.DisplayScreenName("me")} req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dhaha&nToGet=200", nil) rr := httptest.NewRecorder() h.Search(rr, req, session) assert.Equal(t, http.StatusOK, rr.Code) infoArray := decodeInfoArray(t, rr.Body.Bytes(), true) require.Len(t, infoArray, 1) assert.Equal(t, "founduser", infoArray[0].Profile.AimID) assert.Equal(t, "FoundUser", infoArray[0].Profile.DisplayID) assert.Equal(t, "Found", infoArray[0].Profile.FirstName) } func TestMemberDirHandler_Search_FirstLastName(t *testing.T) { dirSvc := newMockDirSearchService(t) // firstName/lastName must map to the ODir name TLVs, not interest. dirSvc.EXPECT().InfoQuery(mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x0F_0x02_InfoQuery) bool { first, hasFirst := q.String(wire.ODirTLVFirstName) last, hasLast := q.String(wire.ODirTLVLastName) _, hasInterest := q.String(wire.ODirTLVInterest) return hasFirst && first == "Bob" && hasLast && last == "Smith" && !hasInterest })).Return(searchReply(wire.ODirSearchResponseOK, result("Bob", "Bob", "Smith")), nil) h := &MemberDirHandler{DirSearchService: dirSvc, LocateService: stubNoDirUser(t), Logger: slog.Default()} session := &Session{AimSID: "sid", ScreenName: state.DisplayScreenName("me")} req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=firstName%3DBob%2ClastName%3DSmith", nil) rr := httptest.NewRecorder() h.Search(rr, req, session) infoArray := decodeInfoArray(t, rr.Body.Bytes(), true) require.Len(t, infoArray, 1) assert.Equal(t, "bob", infoArray[0].Profile.AimID) } func TestMemberDirHandler_Search_ExcludesSelf(t *testing.T) { dirSvc := newMockDirSearchService(t) dirSvc.EXPECT().InfoQuery(mock.Anything, mock.Anything, mock.Anything).Return( searchReply(wire.ODirSearchResponseOK, result("Me", "", ""), // caller — must be filtered out result("Other", "", ""), // kept ), nil) h := &MemberDirHandler{DirSearchService: dirSvc, LocateService: stubNoDirUser(t), Logger: slog.Default()} session := &Session{AimSID: "sid", ScreenName: state.DisplayScreenName("M E")} req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dx", nil) rr := httptest.NewRecorder() h.Search(rr, req, session) infoArray := decodeInfoArray(t, rr.Body.Bytes(), true) require.Len(t, infoArray, 1) assert.Equal(t, "other", infoArray[0].Profile.AimID) } func TestMemberDirHandler_Search_RespectsJSONPCallback(t *testing.T) { dirSvc := newMockDirSearchService(t) dirSvc.EXPECT().InfoQuery(mock.Anything, mock.Anything, mock.Anything).Return( searchReply(wire.ODirSearchResponseOK), nil) h := &MemberDirHandler{DirSearchService: dirSvc, LocateService: stubNoDirUser(t), Logger: slog.Default()} session := &Session{AimSID: "sid", ScreenName: state.DisplayScreenName("me")} req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dx&c=_callbacks_._abc", nil) rr := httptest.NewRecorder() h.Search(rr, req, session) // The web client loads this via a