memberdir_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. package handlers
  2. import (
  3. "context"
  4. "encoding/json"
  5. "log/slog"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/mock"
  11. "github.com/stretchr/testify/require"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. type mockDirSearchService struct{ mock.Mock }
  16. func (m *mockDirSearchService) InfoQuery(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x0F_0x02_InfoQuery) (wire.SNACMessage, error) {
  17. args := m.Called(ctx, inFrame, inBody)
  18. return args.Get(0).(wire.SNACMessage), args.Error(1)
  19. }
  20. type mockMemberDirLocateService struct{ mock.Mock }
  21. func (m *mockMemberDirLocateService) DirInfo(ctx context.Context, inFrame wire.SNACFrame, inBody wire.SNAC_0x02_0x0B_LocateGetDirInfo) (wire.SNACMessage, error) {
  22. args := m.Called(ctx, inFrame, inBody)
  23. return args.Get(0).(wire.SNACMessage), args.Error(1)
  24. }
  25. func searchReply(status uint16, results ...wire.TLVBlock) wire.SNACMessage {
  26. body := wire.SNAC_0x0F_0x03_InfoReply{Status: status}
  27. body.Results.List = results
  28. return wire.SNACMessage{Body: body}
  29. }
  30. func result(screenName, firstName, lastName string) wire.TLVBlock {
  31. return wire.TLVBlock{TLVList: wire.TLVList{
  32. wire.NewTLVBE(wire.ODirTLVScreenName, screenName),
  33. wire.NewTLVBE(wire.ODirTLVFirstName, firstName),
  34. wire.NewTLVBE(wire.ODirTLVLastName, lastName),
  35. }}
  36. }
  37. // decodeInfoArray pulls infoArray out of the response envelope at the given
  38. // path ("results.infoArray" for search, "infoArray" for get).
  39. func decodeInfoArray(t *testing.T, body []byte, nested bool) []MemberDirInfo {
  40. t.Helper()
  41. var envelope struct {
  42. Response struct {
  43. StatusCode int `json:"statusCode"`
  44. Data struct {
  45. InfoArray []MemberDirInfo `json:"infoArray"`
  46. Results struct {
  47. InfoArray []MemberDirInfo `json:"infoArray"`
  48. } `json:"results"`
  49. } `json:"data"`
  50. } `json:"response"`
  51. }
  52. require.NoError(t, json.Unmarshal(body, &envelope))
  53. assert.Equal(t, 200, envelope.Response.StatusCode)
  54. if nested {
  55. return envelope.Response.Data.Results.InfoArray
  56. }
  57. return envelope.Response.Data.InfoArray
  58. }
  59. func TestMemberDirHandler_Search_Keyword(t *testing.T) {
  60. dirSvc := &mockDirSearchService{}
  61. // keyword=haha must map to the ODir interest TLV.
  62. dirSvc.On("InfoQuery", mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x0F_0x02_InfoQuery) bool {
  63. v, ok := q.String(wire.ODirTLVInterest)
  64. return ok && v == "haha"
  65. })).Return(searchReply(wire.ODirSearchResponseOK, result("FoundUser", "Found", "User")), nil)
  66. h := &MemberDirHandler{DirSearchService: dirSvc, Logger: slog.Default()}
  67. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("me")}
  68. req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dhaha&nToGet=200", nil)
  69. rr := httptest.NewRecorder()
  70. h.Search(rr, req, session)
  71. assert.Equal(t, http.StatusOK, rr.Code)
  72. infoArray := decodeInfoArray(t, rr.Body.Bytes(), true)
  73. require.Len(t, infoArray, 1)
  74. assert.Equal(t, "founduser", infoArray[0].Profile.AimID)
  75. assert.Equal(t, "FoundUser", infoArray[0].Profile.DisplayID)
  76. assert.Equal(t, "Found", infoArray[0].Profile.FirstName)
  77. dirSvc.AssertExpectations(t)
  78. }
  79. func TestMemberDirHandler_Search_FirstLastName(t *testing.T) {
  80. dirSvc := &mockDirSearchService{}
  81. // firstName/lastName must map to the ODir name TLVs, not interest.
  82. dirSvc.On("InfoQuery", mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x0F_0x02_InfoQuery) bool {
  83. first, hasFirst := q.String(wire.ODirTLVFirstName)
  84. last, hasLast := q.String(wire.ODirTLVLastName)
  85. _, hasInterest := q.String(wire.ODirTLVInterest)
  86. return hasFirst && first == "Bob" && hasLast && last == "Smith" && !hasInterest
  87. })).Return(searchReply(wire.ODirSearchResponseOK, result("Bob", "Bob", "Smith")), nil)
  88. h := &MemberDirHandler{DirSearchService: dirSvc, Logger: slog.Default()}
  89. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("me")}
  90. req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=firstName%3DBob%2ClastName%3DSmith", nil)
  91. rr := httptest.NewRecorder()
  92. h.Search(rr, req, session)
  93. infoArray := decodeInfoArray(t, rr.Body.Bytes(), true)
  94. require.Len(t, infoArray, 1)
  95. assert.Equal(t, "bob", infoArray[0].Profile.AimID)
  96. dirSvc.AssertExpectations(t)
  97. }
  98. func TestMemberDirHandler_Search_ExcludesSelf(t *testing.T) {
  99. dirSvc := &mockDirSearchService{}
  100. dirSvc.On("InfoQuery", mock.Anything, mock.Anything, mock.Anything).Return(
  101. searchReply(wire.ODirSearchResponseOK,
  102. result("Me", "", ""), // caller — must be filtered out
  103. result("Other", "", ""), // kept
  104. ), nil)
  105. h := &MemberDirHandler{DirSearchService: dirSvc, Logger: slog.Default()}
  106. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("M E")}
  107. req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dx", nil)
  108. rr := httptest.NewRecorder()
  109. h.Search(rr, req, session)
  110. infoArray := decodeInfoArray(t, rr.Body.Bytes(), true)
  111. require.Len(t, infoArray, 1)
  112. assert.Equal(t, "other", infoArray[0].Profile.AimID)
  113. }
  114. func TestMemberDirHandler_Search_RespectsJSONPCallback(t *testing.T) {
  115. dirSvc := &mockDirSearchService{}
  116. dirSvc.On("InfoQuery", mock.Anything, mock.Anything, mock.Anything).Return(
  117. searchReply(wire.ODirSearchResponseOK), nil)
  118. h := &MemberDirHandler{DirSearchService: dirSvc, Logger: slog.Default()}
  119. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("me")}
  120. req := httptest.NewRequest("GET", "/memberDir/search?aimsid=sid&match=keyword%3Dx&c=_callbacks_._abc", nil)
  121. rr := httptest.NewRecorder()
  122. h.Search(rr, req, session)
  123. // The web client loads this via a <script> tag, so the response must be
  124. // JavaScript (JSONP), not application/json — otherwise the browser CORB-blocks it.
  125. assert.Equal(t, "application/javascript", rr.Header().Get("Content-Type"))
  126. assert.Contains(t, rr.Body.String(), "_callbacks_._abc(")
  127. }
  128. func TestMemberDirHandler_Get_Self(t *testing.T) {
  129. reply := wire.SNAC_0x02_0x0C_LocateGetDirReply{Status: wire.LocateGetDirReplyOK}
  130. reply.Append(wire.NewTLVBE(wire.ODirTLVFirstName, "Me"))
  131. reply.Append(wire.NewTLVBE(wire.ODirTLVLastName, "Myself"))
  132. locSvc := &mockMemberDirLocateService{}
  133. locSvc.On("DirInfo", mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x02_0x0B_LocateGetDirInfo) bool {
  134. return q.ScreenName == "me"
  135. })).Return(wire.SNACMessage{Body: reply}, nil)
  136. h := &MemberDirHandler{LocateService: locSvc, Logger: slog.Default()}
  137. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("me")}
  138. // No "t" param: defaults to self.
  139. req := httptest.NewRequest("GET", "/memberDir/get?aimsid=sid", nil)
  140. rr := httptest.NewRecorder()
  141. h.Get(rr, req, session)
  142. infoArray := decodeInfoArray(t, rr.Body.Bytes(), false)
  143. require.Len(t, infoArray, 1)
  144. assert.Equal(t, "me", infoArray[0].Profile.AimID)
  145. assert.Equal(t, "Me", infoArray[0].Profile.FirstName)
  146. assert.Equal(t, "Myself", infoArray[0].Profile.LastName)
  147. locSvc.AssertExpectations(t)
  148. }
  149. func TestMemberDirHandler_Get_UsesSessionDisplayName(t *testing.T) {
  150. locSvc := &mockMemberDirLocateService{}
  151. // Client requests its own record by normalized name...
  152. locSvc.On("DirInfo", mock.Anything, mock.Anything, mock.MatchedBy(func(q wire.SNAC_0x02_0x0B_LocateGetDirInfo) bool {
  153. return q.ScreenName == "bobsmith"
  154. })).Return(wire.SNACMessage{Body: wire.SNAC_0x02_0x0C_LocateGetDirReply{Status: wire.LocateGetDirReplyOK}}, nil)
  155. h := &MemberDirHandler{LocateService: locSvc, Logger: slog.Default()}
  156. // ...but the session carries the formatted display name.
  157. session := &state.WebAPISession{AimSID: "sid", ScreenName: state.DisplayScreenName("Bob Smith")}
  158. req := httptest.NewRequest("GET", "/memberDir/get?aimsid=sid&t=bobsmith", nil)
  159. rr := httptest.NewRecorder()
  160. h.Get(rr, req, session)
  161. infoArray := decodeInfoArray(t, rr.Body.Bytes(), false)
  162. require.Len(t, infoArray, 1)
  163. assert.Equal(t, "bobsmith", infoArray[0].Profile.AimID)
  164. // displayId is the session's formatted name, not the raw "t" value.
  165. assert.Equal(t, "Bob Smith", infoArray[0].Profile.DisplayID)
  166. locSvc.AssertExpectations(t)
  167. }