presence_handler_test.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. package webapi
  2. import (
  3. "context"
  4. "encoding/json"
  5. "log/slog"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/mock"
  12. "github.com/stretchr/testify/require"
  13. "github.com/mk6i/open-oscar-server/state"
  14. "github.com/mk6i/open-oscar-server/wire"
  15. )
  16. // onlineUserInfoReply builds a locate UserInfoReply for an online user,
  17. // optionally marking them idle by the given number of minutes (0 = not idle).
  18. func onlineUserInfoReply(screenName string, idleMinutes uint16) wire.SNACMessage {
  19. info := wire.TLVUserInfo{ScreenName: screenName}
  20. if idleMinutes > 0 {
  21. info.Append(wire.NewTLVBE(wire.OServiceUserInfoIdleTime, idleMinutes))
  22. }
  23. return wire.SNACMessage{
  24. Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{TLVUserInfo: info},
  25. }
  26. }
  27. // screenNameMatcher matches a UserInfoQuery request body by its target screen name.
  28. func screenNameMatcher(screenName string) any {
  29. return mock.MatchedBy(func(b wire.SNAC_0x02_0x05_LocateUserInfoQuery) bool {
  30. return b.ScreenName == screenName
  31. })
  32. }
  33. func TestPresenceHandler_GetPresence(t *testing.T) {
  34. tests := []struct {
  35. name string
  36. queryParams string
  37. setupMocks func(*mockFeedbagService, *mockLocateService)
  38. expectedStatusCode int
  39. checkResponse func(*testing.T, string)
  40. }{
  41. {
  42. name: "Success_BuddyList",
  43. queryParams: "bl=1",
  44. setupMocks: func(fr *mockFeedbagService, ls *mockLocateService) {
  45. // Return feedbag with a group and buddy
  46. fr.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  47. Return(wire.SNACMessage{
  48. Body: wire.SNAC_0x13_0x06_FeedbagReply{
  49. Items: []wire.FeedbagItem{
  50. {ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends", GroupID: 1},
  51. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "buddy1", GroupID: 1},
  52. },
  53. },
  54. }, nil)
  55. ls.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("buddy1")).
  56. Return(onlineUserInfoReply("buddy1", 0), nil)
  57. },
  58. expectedStatusCode: http.StatusOK,
  59. checkResponse: func(t *testing.T, body string) {
  60. assert.Contains(t, body, `"statusCode":200`)
  61. assert.Contains(t, body, `"groups"`)
  62. assert.Contains(t, body, `"Friends"`)
  63. assert.Contains(t, body, `"buddy1"`)
  64. assert.Contains(t, body, `"online"`)
  65. },
  66. },
  67. {
  68. name: "Success_TargetUsers",
  69. queryParams: "t=user1,user2",
  70. setupMocks: func(fr *mockFeedbagService, ls *mockLocateService) {
  71. ls.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("user1")).
  72. Return(onlineUserInfoReply("user1", 0), nil)
  73. // user2 is idle for 7 minutes.
  74. ls.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("user2")).
  75. Return(onlineUserInfoReply("user2", 7), nil)
  76. },
  77. expectedStatusCode: http.StatusOK,
  78. checkResponse: func(t *testing.T, body string) {
  79. assert.Contains(t, body, `"statusCode":200`)
  80. assert.Contains(t, body, `"users"`)
  81. assert.Contains(t, body, `"user1"`)
  82. assert.Contains(t, body, `"user2"`)
  83. assert.Contains(t, body, `"idle"`)
  84. },
  85. },
  86. {
  87. name: "Success_BlockedOrOfflineUser",
  88. queryParams: "t=blockeduser",
  89. setupMocks: func(fr *mockFeedbagService, ls *mockLocateService) {
  90. // A blocked or offline user comes back as a locate error.
  91. ls.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("blockeduser")).
  92. Return(wire.SNACMessage{Body: wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}}, nil)
  93. },
  94. expectedStatusCode: http.StatusOK,
  95. checkResponse: func(t *testing.T, body string) {
  96. assert.Contains(t, body, `"statusCode":200`)
  97. assert.Contains(t, body, `"blockeduser"`)
  98. assert.Contains(t, body, `"offline"`)
  99. },
  100. },
  101. {
  102. name: "Success_EmptyRequest",
  103. queryParams: "",
  104. setupMocks: func(fr *mockFeedbagService, ls *mockLocateService) {},
  105. expectedStatusCode: http.StatusOK,
  106. checkResponse: func(t *testing.T, body string) {
  107. assert.Contains(t, body, `"statusCode":200`)
  108. },
  109. },
  110. {
  111. name: "Error_TooManyTargets",
  112. queryParams: "t=u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11",
  113. // No UserInfoQuery should be issued; the request is rejected up front.
  114. setupMocks: func(fr *mockFeedbagService, ls *mockLocateService) {},
  115. expectedStatusCode: http.StatusBadRequest,
  116. checkResponse: func(t *testing.T, body string) {
  117. assert.Contains(t, body, "too many screen names requested")
  118. },
  119. },
  120. }
  121. for _, tt := range tests {
  122. t.Run(tt.name, func(t *testing.T) {
  123. feedbagService := newMockFeedbagService(t)
  124. locateService := newMockLocateService(t)
  125. oscarInstance := state.NewSession().AddInstance()
  126. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  127. handler := &PresenceHandler{
  128. SessionManager: sessionMgr,
  129. FeedbagService: feedbagService,
  130. LocateService: locateService,
  131. Logger: slog.Default(),
  132. }
  133. tt.setupMocks(feedbagService, locateService)
  134. // Presence payloads carry the viewer's alias, so GetPresence reads the
  135. // feedbag. Registered last so a case's own Query stub takes precedence.
  136. feedbagService.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  137. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{}}, nil).Maybe()
  138. reqURL := "/presence/get?aimsid=" + aimsid
  139. if tt.queryParams != "" {
  140. reqURL += "&" + tt.queryParams
  141. }
  142. req, err := http.NewRequest("GET", reqURL, nil)
  143. assert.NoError(t, err)
  144. rr := httptest.NewRecorder()
  145. requireSession(handler.SessionManager, handler.GetPresence).ServeHTTP(rr, req)
  146. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  147. if tt.checkResponse != nil {
  148. responseBody := strings.TrimSpace(rr.Body.String())
  149. tt.checkResponse(t, responseBody)
  150. }
  151. })
  152. }
  153. }
  154. // TestPresenceHandler_GetPresence_PublishesIconForOnlineBuddiesOnly verifies that
  155. // the icon is published only for an online, non-blocking user, and that an
  156. // offline or blocking user is never even looked up — so neither their icon nor
  157. // its hash leaks to a caller they are invisible to.
  158. func TestPresenceHandler_GetPresence_PublishesIconForOnlineBuddiesOnly(t *testing.T) {
  159. ctx := context.Background()
  160. feedbagService := newMockFeedbagService(t)
  161. feedbagService.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  162. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{}}, nil).Maybe()
  163. locateService := newMockLocateService(t)
  164. locateService.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("onlineuser")).
  165. Return(onlineUserInfoReply("onlineuser", 0), nil)
  166. locateService.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("offlineuser")).
  167. Return(wire.SNACMessage{Body: wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}}, nil)
  168. iconRetriever := newMockBuddyIconRetriever(t)
  169. iconRetriever.EXPECT().BuddyIconMetadata(mock.Anything, state.NewIdentScreenName("onlineuser")).
  170. Return(bartID([]byte{0xab, 0xcd}), nil).Once()
  171. oscarInstance := state.NewSession().AddInstance()
  172. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  173. sess, err := sessionMgr.GetSession(ctx, aimsid)
  174. require.NoError(t, err)
  175. sess.BaseURL = "http://api.example.com"
  176. handler := &PresenceHandler{
  177. SessionManager: sessionMgr,
  178. FeedbagService: feedbagService,
  179. LocateService: locateService,
  180. IconSource: BuddyIconSource{IconRetriever: iconRetriever, Logger: slog.Default()},
  181. Logger: slog.Default(),
  182. }
  183. req, err := http.NewRequest("GET", "/presence/get?aimsid="+aimsid+"&t=onlineuser,offlineuser", nil)
  184. require.NoError(t, err)
  185. rr := httptest.NewRecorder()
  186. requireSession(handler.SessionManager, handler.GetPresence).ServeHTTP(rr, req)
  187. require.Equal(t, http.StatusOK, rr.Code)
  188. var got struct {
  189. Response struct {
  190. Data struct {
  191. Users []struct {
  192. AimID string `json:"aimId"`
  193. State string `json:"state"`
  194. BuddyIcon string `json:"buddyIcon"`
  195. } `json:"users"`
  196. } `json:"data"`
  197. } `json:"response"`
  198. }
  199. require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &got))
  200. icons := map[string]string{}
  201. states := map[string]string{}
  202. for _, u := range got.Response.Data.Users {
  203. icons[u.AimID] = u.BuddyIcon
  204. states[u.AimID] = u.State
  205. }
  206. assert.Equal(t, "online", states["onlineuser"])
  207. assert.Equal(t,
  208. "http://api.example.com/expressions/get?t=onlineuser&type=buddyIcon&bartId=abcd",
  209. icons["onlineuser"])
  210. assert.Equal(t, "offline", states["offlineuser"])
  211. assert.Empty(t, icons["offlineuser"])
  212. iconRetriever.AssertNotCalled(t, "BuddyIconMetadata", mock.Anything, state.NewIdentScreenName("offlineuser"))
  213. }
  214. // TestPresenceHandler_GetPresence_BuddyListGrouping verifies that bl=1 places
  215. // each buddy under its own group using realistic feedbag data, where group rows
  216. // carry ItemID 0 and a distinct nonzero GroupID, and buddy rows reference those
  217. // GroupIDs. This is the shape the OSCAR feedbag actually stores.
  218. func TestPresenceHandler_GetPresence_BuddyListGrouping(t *testing.T) {
  219. feedbagService := newMockFeedbagService(t)
  220. locateService := newMockLocateService(t)
  221. items := []wire.FeedbagItem{
  222. // Root order group: ItemID 0, GroupID 0, empty name — not a real buddy group.
  223. {ItemID: 0, GroupID: 0, ClassID: wire.FeedbagClassIdGroup, Name: ""},
  224. // Named groups: ItemID 0, distinct nonzero GroupIDs.
  225. {ItemID: 0, GroupID: 10, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  226. {ItemID: 0, GroupID: 20, ClassID: wire.FeedbagClassIdGroup, Name: "Work"},
  227. // Buddies reference their group's GroupID.
  228. {ItemID: 101, GroupID: 10, ClassID: wire.FeedbagClassIdBuddy, Name: "alice"},
  229. {ItemID: 201, GroupID: 20, ClassID: wire.FeedbagClassIdBuddy, Name: "bob"},
  230. }
  231. feedbagService.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  232. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  233. locateService.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("alice")).
  234. Return(onlineUserInfoReply("alice", 0), nil)
  235. locateService.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("bob")).
  236. Return(onlineUserInfoReply("bob", 0), nil)
  237. oscarInstance := state.NewSession().AddInstance()
  238. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  239. handler := &PresenceHandler{
  240. SessionManager: sessionMgr,
  241. FeedbagService: feedbagService,
  242. LocateService: locateService,
  243. Logger: slog.Default(),
  244. }
  245. req, err := http.NewRequest("GET", "/presence/get?aimsid="+aimsid+"&bl=1", nil)
  246. assert.NoError(t, err)
  247. rr := httptest.NewRecorder()
  248. requireSession(handler.SessionManager, handler.GetPresence).ServeHTTP(rr, req)
  249. assert.Equal(t, http.StatusOK, rr.Code)
  250. var parsed struct {
  251. Response struct {
  252. Data struct {
  253. Groups []struct {
  254. Name string `json:"name"`
  255. Buddies []struct {
  256. AimID string `json:"aimId"`
  257. } `json:"buddies"`
  258. } `json:"groups"`
  259. } `json:"data"`
  260. } `json:"response"`
  261. }
  262. assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &parsed))
  263. // Build name -> set of buddy aimIds.
  264. byGroup := map[string][]string{}
  265. for _, g := range parsed.Response.Data.Groups {
  266. for _, b := range g.Buddies {
  267. byGroup[g.Name] = append(byGroup[g.Name], b.AimID)
  268. }
  269. }
  270. // Exactly the two named groups appear; the root group is excluded.
  271. assert.Len(t, parsed.Response.Data.Groups, 2)
  272. assert.Equal(t, []string{"alice"}, byGroup["Friends"])
  273. assert.Equal(t, []string{"bob"}, byGroup["Work"])
  274. }
  275. func TestPresenceHandler_GetPresence_MissingAimsid(t *testing.T) {
  276. handler := &PresenceHandler{
  277. SessionManager: NewSessionManager(),
  278. Logger: slog.Default(),
  279. }
  280. req, err := http.NewRequest("GET", "/presence/get", nil)
  281. assert.NoError(t, err)
  282. rr := httptest.NewRecorder()
  283. requireSession(handler.SessionManager, handler.GetPresence).ServeHTTP(rr, req)
  284. assert.Equal(t, http.StatusBadRequest, rr.Code)
  285. assert.Contains(t, rr.Body.String(), "missing aimsid parameter")
  286. }
  287. func TestPresenceHandler_GetPresence_SessionNotFound(t *testing.T) {
  288. handler := &PresenceHandler{
  289. SessionManager: NewSessionManager(),
  290. Logger: slog.Default(),
  291. }
  292. req, err := http.NewRequest("GET", "/presence/get?aimsid=nonexistent", nil)
  293. assert.NoError(t, err)
  294. rr := httptest.NewRecorder()
  295. requireSession(handler.SessionManager, handler.GetPresence).ServeHTTP(rr, req)
  296. assert.Equal(t, http.StatusUnauthorized, rr.Code)
  297. assert.Contains(t, rr.Body.String(), "invalid or expired session")
  298. }
  299. func TestPresenceHandler_SetState_MissingAimsid(t *testing.T) {
  300. handler := &PresenceHandler{
  301. SessionManager: NewSessionManager(),
  302. Logger: slog.Default(),
  303. }
  304. req, err := http.NewRequest("GET", "/presence/setState", nil)
  305. assert.NoError(t, err)
  306. rr := httptest.NewRecorder()
  307. requireSession(handler.SessionManager, handler.SetState).ServeHTTP(rr, req)
  308. assert.Equal(t, http.StatusBadRequest, rr.Code)
  309. assert.Contains(t, rr.Body.String(), "missing aimsid parameter")
  310. }
  311. func TestPresenceHandler_SetState_InvalidState(t *testing.T) {
  312. oscarInstance := state.NewSession().AddInstance()
  313. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  314. handler := &PresenceHandler{
  315. SessionManager: sessionMgr,
  316. Logger: slog.Default(),
  317. }
  318. req, err := http.NewRequest("GET", "/presence/setState?aimsid="+aimsid+"&state=bogus", nil)
  319. assert.NoError(t, err)
  320. rr := httptest.NewRecorder()
  321. requireSession(handler.SessionManager, handler.SetState).ServeHTTP(rr, req)
  322. assert.Equal(t, http.StatusBadRequest, rr.Code)
  323. assert.Contains(t, rr.Body.String(), "invalid state parameter")
  324. }
  325. func TestPresenceHandler_SetState_EmitsMyInfoEvent(t *testing.T) {
  326. // The AIM client re-renders its own status badge only from "myInfo" events,
  327. // so setState must queue one on the user's own session for the change to be
  328. // visible in their UI.
  329. oscarInstance := state.NewSession().AddInstance()
  330. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  331. broadcaster := newMockBuddyBroadcaster(t)
  332. broadcaster.EXPECT().BroadcastBuddyArrived(mock.Anything, mock.Anything, mock.Anything).Return(nil)
  333. handler := &PresenceHandler{
  334. SessionManager: sessionMgr,
  335. BuddyBroadcaster: broadcaster,
  336. Logger: slog.Default(),
  337. }
  338. req, err := http.NewRequest("GET", "/presence/setState?aimsid="+aimsid+"&state=away&awayMsg=brb", nil)
  339. assert.NoError(t, err)
  340. rr := httptest.NewRecorder()
  341. requireSession(handler.SessionManager, handler.SetState).ServeHTTP(rr, req)
  342. assert.Equal(t, http.StatusOK, rr.Code)
  343. session, err := sessionMgr.GetSession(context.Background(), aimsid)
  344. assert.NoError(t, err)
  345. myInfo := queuedMyInfo(session)
  346. assert.NotNil(t, myInfo, "expected a myInfo event to be queued")
  347. assert.Equal(t, "away", myInfo.State)
  348. assert.Equal(t, "brb", myInfo.AwayMsg)
  349. assert.Equal(t, "testuser", myInfo.AimID)
  350. }
  351. func TestPresenceHandler_SetState_MyInfoNormalizesAimID(t *testing.T) {
  352. // The client shallow-merges myInfo onto the shared user object, so aimId must
  353. // be the normalized id while displayId and friendly keep the user's own
  354. // casing and spacing.
  355. oscarInstance := state.NewSession().AddInstance()
  356. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("Mike Kelly", oscarInstance)
  357. broadcaster := newMockBuddyBroadcaster(t)
  358. broadcaster.EXPECT().BroadcastBuddyArrived(mock.Anything, mock.Anything, mock.Anything).Return(nil)
  359. handler := &PresenceHandler{
  360. SessionManager: sessionMgr,
  361. BuddyBroadcaster: broadcaster,
  362. Logger: slog.Default(),
  363. }
  364. req, err := http.NewRequest("GET", "/presence/setState?aimsid="+aimsid+"&state=away", nil)
  365. assert.NoError(t, err)
  366. rr := httptest.NewRecorder()
  367. requireSession(handler.SessionManager, handler.SetState).ServeHTTP(rr, req)
  368. assert.Equal(t, http.StatusOK, rr.Code)
  369. // The setState response body carries the same identity fields.
  370. var resp struct {
  371. Response struct {
  372. Data map[string]interface{} `json:"data"`
  373. } `json:"response"`
  374. }
  375. assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &resp))
  376. assert.Equal(t, "mikekelly", resp.Response.Data["aimId"])
  377. assert.Equal(t, "Mike Kelly", resp.Response.Data["displayId"])
  378. session, err := sessionMgr.GetSession(context.Background(), aimsid)
  379. assert.NoError(t, err)
  380. myInfo := queuedMyInfo(session)
  381. require.NotNil(t, myInfo, "expected a myInfo event to be queued")
  382. assert.Equal(t, "mikekelly", myInfo.AimID)
  383. assert.Equal(t, "Mike Kelly", myInfo.DisplayID)
  384. assert.Equal(t, "Mike Kelly", myInfo.Friendly)
  385. }
  386. func TestIsICQScreenName(t *testing.T) {
  387. tests := []struct {
  388. name string
  389. screenName string
  390. expected bool
  391. }{
  392. {"ICQ_Number", "123456789", true},
  393. {"AIM_Name", "cooluser", false},
  394. {"AIM_WithNumbers", "cool123", false},
  395. {"Empty", "", false},
  396. {"Single_Digit", "5", true},
  397. {"Mixed_Chars", "12abc34", false},
  398. }
  399. for _, tt := range tests {
  400. t.Run(tt.name, func(t *testing.T) {
  401. assert.Equal(t, tt.expected, isICQScreenName(tt.screenName))
  402. })
  403. }
  404. }
  405. func TestPresenceHandler_Icon(t *testing.T) {
  406. tests := []struct {
  407. name string
  408. queryParams string
  409. expectedStatusCode int
  410. checkRedirect func(*testing.T, *httptest.ResponseRecorder)
  411. }{
  412. {
  413. name: "Redirect_OfflineUser",
  414. // No aimsid, so there is no OSCAR session to query on behalf of and
  415. // the target resolves to offline.
  416. queryParams: "name=offlineuser",
  417. expectedStatusCode: http.StatusFound,
  418. checkRedirect: func(t *testing.T, rr *httptest.ResponseRecorder) {
  419. location := rr.Header().Get("Location")
  420. assert.Contains(t, location, "offline")
  421. },
  422. },
  423. {
  424. name: "Error_MissingName",
  425. queryParams: "",
  426. expectedStatusCode: http.StatusBadRequest,
  427. },
  428. }
  429. for _, tt := range tests {
  430. t.Run(tt.name, func(t *testing.T) {
  431. handler := &PresenceHandler{
  432. SessionManager: NewSessionManager(),
  433. LocateService: newMockLocateService(t),
  434. Logger: slog.Default(),
  435. }
  436. reqURL := "/presence/icon"
  437. if tt.queryParams != "" {
  438. reqURL += "?" + tt.queryParams
  439. }
  440. req, err := http.NewRequest("GET", reqURL, nil)
  441. assert.NoError(t, err)
  442. rr := httptest.NewRecorder()
  443. handler.Icon(rr, req)
  444. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  445. if tt.checkRedirect != nil {
  446. tt.checkRedirect(t, rr)
  447. }
  448. })
  449. }
  450. }
  451. func TestPresenceHandler_SetProfile(t *testing.T) {
  452. oscarInstance := state.NewSession().AddInstance()
  453. tests := []struct {
  454. name string
  455. queryParams string
  456. setupMocks func(*mockLocateService)
  457. expectedStatusCode int
  458. checkResponse func(*testing.T, string)
  459. }{
  460. {
  461. name: "Success_SetProfile",
  462. queryParams: "profile=Hello+World",
  463. setupMocks: func(ls *mockLocateService) {
  464. ls.EXPECT().SetInfo(mock.Anything, oscarInstance, mock.AnythingOfType("wire.SNAC_0x02_0x04_LocateSetInfo")).Return(nil)
  465. },
  466. expectedStatusCode: http.StatusOK,
  467. checkResponse: func(t *testing.T, body string) {
  468. assert.Contains(t, body, `"statusCode":200`)
  469. },
  470. },
  471. {
  472. name: "Error_ProfileTooLarge",
  473. queryParams: "profile=" + strings.Repeat("x", 4097),
  474. setupMocks: func(ls *mockLocateService) {},
  475. expectedStatusCode: http.StatusBadRequest,
  476. checkResponse: func(t *testing.T, body string) {
  477. assert.Contains(t, body, "profile too large")
  478. },
  479. },
  480. }
  481. for _, tt := range tests {
  482. t.Run(tt.name, func(t *testing.T) {
  483. locateService := newMockLocateService(t)
  484. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  485. handler := &PresenceHandler{
  486. SessionManager: sessionMgr,
  487. LocateService: locateService,
  488. Logger: slog.Default(),
  489. }
  490. tt.setupMocks(locateService)
  491. reqURL := "/presence/setProfile?aimsid=" + aimsid + "&" + tt.queryParams
  492. req, err := http.NewRequest("GET", reqURL, nil)
  493. assert.NoError(t, err)
  494. rr := httptest.NewRecorder()
  495. requireSession(handler.SessionManager, handler.SetProfile).ServeHTTP(rr, req)
  496. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  497. if tt.checkResponse != nil {
  498. responseBody := strings.TrimSpace(rr.Body.String())
  499. tt.checkResponse(t, responseBody)
  500. }
  501. })
  502. }
  503. }
  504. func TestPresenceHandler_GetProfile(t *testing.T) {
  505. locateService := newMockLocateService(t)
  506. oscarInstance := state.NewSession().AddInstance()
  507. sessionMgr, aimsid := createTestSessionManagerWithOSCAR("testuser", oscarInstance)
  508. handler := &PresenceHandler{
  509. SessionManager: sessionMgr,
  510. LocateService: locateService,
  511. Logger: slog.Default(),
  512. }
  513. locateService.EXPECT().UserInfoQuery(mock.Anything, mock.Anything, mock.Anything, screenNameMatcher("testuser")).
  514. Return(wire.SNACMessage{
  515. Body: wire.SNAC_0x02_0x06_LocateUserInfoReply{
  516. LocateInfo: wire.TLVRestBlock{
  517. TLVList: wire.TLVList{
  518. wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, "My profile"),
  519. },
  520. },
  521. },
  522. }, nil)
  523. req, err := http.NewRequest("GET", "/presence/getProfile?aimsid="+aimsid, nil)
  524. assert.NoError(t, err)
  525. rr := httptest.NewRecorder()
  526. requireSession(handler.SessionManager, handler.GetProfile).ServeHTTP(rr, req)
  527. assert.Equal(t, http.StatusOK, rr.Code)
  528. body := rr.Body.String()
  529. assert.Contains(t, body, `"statusCode":200`)
  530. assert.Contains(t, body, `"My profile"`)
  531. assert.Contains(t, body, `"testuser"`)
  532. }
  533. // queuedMyInfo returns the myInfo event the session has queued, if any.
  534. func queuedMyInfo(session *Session) *MyInfo {
  535. var myInfo *MyInfo
  536. for _, event := range session.EventQueue.GetAllEvents() {
  537. if event.Type == "myInfo" {
  538. myInfo, _ = event.Data.(*MyInfo)
  539. }
  540. }
  541. return myInfo
  542. }