buddylist_handler_test.go 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040
  1. package webapi
  2. import (
  3. "errors"
  4. "log/slog"
  5. "net/http"
  6. "net/http/httptest"
  7. "net/url"
  8. "testing"
  9. "time"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/mock"
  12. "github.com/mk6i/open-oscar-server/state"
  13. "github.com/mk6i/open-oscar-server/wire"
  14. )
  15. func TestBuddyListHandler_AddBuddy(t *testing.T) {
  16. tests := []struct {
  17. name string
  18. queryParams map[string][]string
  19. setupMocks func(*mockSessionResolver, *mockFeedbagService, *mockFeedbagService, string) *Session
  20. expectedStatusCode int
  21. expectedResponse string
  22. }{
  23. {
  24. name: "Success_AddBuddyToExistingGroup",
  25. queryParams: map[string][]string{
  26. "aimsid": {"test-session"},
  27. "buddy": {"newbuddy"},
  28. "group": {"Friends"},
  29. },
  30. setupMocks: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  31. session := &Session{
  32. AimSID: aimsid,
  33. ScreenName: state.DisplayScreenName("testuser"),
  34. EventQueue: NewEventQueue(100),
  35. LastAccessed: time.Now(),
  36. }
  37. items := []wire.FeedbagItem{
  38. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  39. }
  40. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  41. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  42. // addBuddyToFeedbag calls UpsertItem twice: once for group order update, once for buddy insert
  43. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  44. Return((*wire.SNACMessage)(nil), nil)
  45. blmFs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  46. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  47. return session
  48. },
  49. expectedStatusCode: http.StatusOK,
  50. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyInfo":{"aimId":"newbuddy","displayId":"newbuddy","state":"offline","userType":"aim"},"resultCode":"success"}}}`,
  51. },
  52. {
  53. name: "Success_EventPushSkippedOnBLMError",
  54. queryParams: map[string][]string{
  55. "aimsid": {"test-session"},
  56. "buddy": {"newbuddy"},
  57. "group": {"Friends"},
  58. },
  59. setupMocks: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  60. session := &Session{
  61. AimSID: aimsid,
  62. ScreenName: state.DisplayScreenName("testuser"),
  63. EventQueue: NewEventQueue(100),
  64. LastAccessed: time.Now(),
  65. }
  66. items := []wire.FeedbagItem{
  67. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  68. }
  69. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  70. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  71. // addBuddyToFeedbag calls UpsertItem twice: once for group order update, once for buddy insert
  72. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  73. Return((*wire.SNACMessage)(nil), nil)
  74. blmFs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  75. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  76. return session
  77. },
  78. expectedStatusCode: http.StatusOK,
  79. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyInfo":{"aimId":"newbuddy","displayId":"newbuddy","state":"offline","userType":"aim"},"resultCode":"success"}}}`,
  80. },
  81. {
  82. name: "Error_BuddyAlreadyExists",
  83. queryParams: map[string][]string{
  84. "aimsid": {"test-session"},
  85. "buddy": {"existingbuddy"},
  86. "group": {"Friends"},
  87. },
  88. setupMocks: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  89. session := &Session{
  90. AimSID: aimsid,
  91. ScreenName: state.DisplayScreenName("testuser"),
  92. EventQueue: NewEventQueue(100),
  93. LastAccessed: time.Now(),
  94. }
  95. // Friends group with existingbuddy already present
  96. items := []wire.FeedbagItem{
  97. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  98. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "existingbuddy"},
  99. }
  100. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  101. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  102. return session
  103. },
  104. expectedStatusCode: http.StatusOK,
  105. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"alreadyExists"}}}`,
  106. },
  107. {
  108. name: "Error_MissingBuddyParameter",
  109. queryParams: map[string][]string{
  110. "aimsid": {"test-session"},
  111. "group": {"Friends"},
  112. },
  113. setupMocks: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  114. return &Session{
  115. AimSID: aimsid,
  116. ScreenName: state.DisplayScreenName("testuser"),
  117. EventQueue: NewEventQueue(100),
  118. LastAccessed: time.Now(),
  119. }
  120. },
  121. expectedStatusCode: http.StatusBadRequest,
  122. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter","data":{}}}`,
  123. },
  124. }
  125. for _, tt := range tests {
  126. t.Run(tt.name, func(t *testing.T) {
  127. sessionManager := newMockSessionResolver(t)
  128. feedbagService := newMockFeedbagService(t)
  129. blmFeedbagService := newMockFeedbagService(t)
  130. blm := NewBuddyListManager(blmFeedbagService, newMockLocateService(t), newTestIconSource(t), slog.Default())
  131. logger := slog.Default()
  132. handler := &BuddyListHandler{
  133. FeedbagService: feedbagService,
  134. BuddyListManager: blm,
  135. Logger: logger,
  136. }
  137. aimsid := ""
  138. if aimsids, ok := tt.queryParams["aimsid"]; ok && len(aimsids) > 0 {
  139. aimsid = aimsids[0]
  140. }
  141. session := tt.setupMocks(sessionManager, feedbagService, blmFeedbagService, aimsid)
  142. reqURL := "/buddylist/addBuddy"
  143. if len(tt.queryParams) > 0 {
  144. values := url.Values{}
  145. for key, vals := range tt.queryParams {
  146. for _, val := range vals {
  147. values.Add(key, val)
  148. }
  149. }
  150. reqURL += "?" + values.Encode()
  151. }
  152. req, err := http.NewRequest("GET", reqURL, nil)
  153. assert.NoError(t, err)
  154. rr := httptest.NewRecorder()
  155. handler.AddBuddy(rr, req, session)
  156. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  157. assert.JSONEq(t, tt.expectedResponse, rr.Body.String())
  158. })
  159. }
  160. }
  161. func TestBuddyListHandler_AddGroup(t *testing.T) {
  162. type setupFunc func(*mockSessionResolver, *mockFeedbagService, *mockFeedbagService, string) *Session
  163. newSession := func(aimsid string) *Session {
  164. return &Session{
  165. AimSID: aimsid,
  166. ScreenName: state.DisplayScreenName("testuser"),
  167. EventQueue: NewEventQueue(100),
  168. LastAccessed: time.Now(),
  169. }
  170. }
  171. tests := []struct {
  172. name string
  173. queryParams map[string][]string
  174. setup setupFunc
  175. expectedStatusCode int
  176. expectedResponse string
  177. }{
  178. {
  179. name: "Error_MissingGroupParam",
  180. queryParams: map[string][]string{"aimsid": {"sess"}},
  181. setup: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  182. return newSession(aimsid)
  183. },
  184. expectedStatusCode: http.StatusBadRequest,
  185. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing group parameter","data":{}}}`,
  186. },
  187. {
  188. name: "Success_GroupAdded",
  189. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  190. setup: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  191. sess := newSession(aimsid)
  192. // Empty feedbag — AddGroup will create root + NewGroup in pending
  193. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  194. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  195. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  196. Return((*wire.SNACMessage)(nil), nil).Once()
  197. blmFs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  198. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  199. return sess
  200. },
  201. expectedStatusCode: http.StatusOK,
  202. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  203. },
  204. {
  205. name: "Success_EventPushSkippedOnBLMError",
  206. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  207. setup: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  208. sess := newSession(aimsid)
  209. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  210. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  211. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  212. Return((*wire.SNACMessage)(nil), nil).Once()
  213. blmFs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  214. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  215. return sess
  216. },
  217. expectedStatusCode: http.StatusOK,
  218. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  219. },
  220. {
  221. name: "Success_GroupAlreadyExists",
  222. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  223. setup: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  224. sess := newSession(aimsid)
  225. items := []wire.FeedbagItem{
  226. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  227. }
  228. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  229. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  230. return sess
  231. },
  232. expectedStatusCode: http.StatusOK,
  233. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"alreadyExists"}}}`,
  234. },
  235. {
  236. name: "Error_FeedbagQueryFails",
  237. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  238. setup: func(sm *mockSessionResolver, fs *mockFeedbagService, blmFs *mockFeedbagService, aimsid string) *Session {
  239. sess := newSession(aimsid)
  240. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  241. Return(wire.SNACMessage{}, errors.New("feedbag error"))
  242. return sess
  243. },
  244. expectedStatusCode: http.StatusOK,
  245. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"error"}}}`,
  246. },
  247. }
  248. for _, tt := range tests {
  249. t.Run(tt.name, func(t *testing.T) {
  250. sm := newMockSessionResolver(t)
  251. fs := newMockFeedbagService(t)
  252. blmFs := newMockFeedbagService(t)
  253. blm := NewBuddyListManager(blmFs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  254. aimsid := ""
  255. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  256. aimsid = v[0]
  257. }
  258. session := tt.setup(sm, fs, blmFs, aimsid)
  259. handler := &BuddyListHandler{
  260. FeedbagService: fs,
  261. BuddyListManager: blm,
  262. Logger: slog.Default(),
  263. }
  264. values := url.Values{}
  265. for k, vs := range tt.queryParams {
  266. for _, v := range vs {
  267. values.Add(k, v)
  268. }
  269. }
  270. req, _ := http.NewRequest("GET", "/buddylist/addGroup?"+values.Encode(), nil)
  271. rr := httptest.NewRecorder()
  272. handler.AddGroup(rr, req, session)
  273. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  274. assert.JSONEq(t, tt.expectedResponse, rr.Body.String())
  275. })
  276. }
  277. }
  278. func TestBuddyListHandler_RemoveBuddy(t *testing.T) {
  279. type setupFunc func(*mockSessionResolver, *BuddyListManager, *mockFeedbagService, string) *Session
  280. newBuddyListManager := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  281. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  282. }
  283. tests := []struct {
  284. name string
  285. queryParams map[string][]string
  286. setup setupFunc
  287. expectedStatusCode int
  288. expectedResponse string
  289. }{
  290. {
  291. name: "Error_MissingBuddyParam",
  292. queryParams: map[string][]string{"aimsid": {"sess"}},
  293. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  294. return &Session{AimSID: aimsid, ScreenName: state.DisplayScreenName("testuser"), LastAccessed: time.Now()}
  295. },
  296. expectedStatusCode: http.StatusBadRequest,
  297. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter","data":{}}}`,
  298. },
  299. {
  300. name: "Success_BuddyRemoved",
  301. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"Friends"}},
  302. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  303. sess := &Session{
  304. AimSID: aimsid,
  305. ScreenName: state.DisplayScreenName("testuser"),
  306. OSCARSession: state.NewSession().AddInstance(),
  307. EventQueue: NewEventQueue(100),
  308. LastAccessed: time.Now(),
  309. }
  310. items := []wire.FeedbagItem{
  311. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  312. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "someBuddy"},
  313. }
  314. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  315. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  316. fs.EXPECT().DeleteItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  317. Return((*wire.SNACMessage)(nil), nil).Once()
  318. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  319. Return((*wire.SNACMessage)(nil), nil).Once()
  320. // Second Query for GetBuddyListForUser event push
  321. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  322. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  323. return sess
  324. },
  325. expectedStatusCode: http.StatusOK,
  326. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  327. },
  328. {
  329. name: "Success_EventPushSkippedOnBLMError",
  330. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"Friends"}},
  331. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  332. sess := &Session{
  333. AimSID: aimsid,
  334. ScreenName: state.DisplayScreenName("testuser"),
  335. OSCARSession: state.NewSession().AddInstance(),
  336. EventQueue: NewEventQueue(100),
  337. LastAccessed: time.Now(),
  338. }
  339. items := []wire.FeedbagItem{
  340. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  341. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "someBuddy"},
  342. }
  343. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  344. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  345. fs.EXPECT().DeleteItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  346. Return((*wire.SNACMessage)(nil), nil).Once()
  347. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  348. Return((*wire.SNACMessage)(nil), nil).Once()
  349. // BLM query fails — response should still be success
  350. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  351. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  352. return sess
  353. },
  354. expectedStatusCode: http.StatusOK,
  355. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  356. },
  357. {
  358. name: "Success_BuddyNotFound",
  359. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"ghost"}, "group": {"Friends"}},
  360. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  361. sess := &Session{
  362. AimSID: aimsid,
  363. ScreenName: state.DisplayScreenName("testuser"),
  364. OSCARSession: state.NewSession().AddInstance(),
  365. LastAccessed: time.Now(),
  366. }
  367. // Group exists but "ghost" is not in it
  368. items := []wire.FeedbagItem{
  369. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  370. }
  371. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  372. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  373. return sess
  374. },
  375. expectedStatusCode: http.StatusOK,
  376. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  377. },
  378. {
  379. name: "Success_GroupNotFound",
  380. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"NoSuchGroup"}},
  381. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  382. sess := &Session{
  383. AimSID: aimsid,
  384. ScreenName: state.DisplayScreenName("testuser"),
  385. OSCARSession: state.NewSession().AddInstance(),
  386. LastAccessed: time.Now(),
  387. }
  388. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  389. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil)
  390. return sess
  391. },
  392. expectedStatusCode: http.StatusOK,
  393. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  394. },
  395. }
  396. for _, tt := range tests {
  397. t.Run(tt.name, func(t *testing.T) {
  398. sm := newMockSessionResolver(t)
  399. fs := newMockFeedbagService(t)
  400. blm := newBuddyListManager(t, fs)
  401. aimsid := ""
  402. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  403. aimsid = v[0]
  404. }
  405. session := tt.setup(sm, blm, fs, aimsid)
  406. handler := &BuddyListHandler{
  407. BuddyListManager: blm,
  408. Logger: slog.Default(),
  409. }
  410. values := url.Values{}
  411. for k, vs := range tt.queryParams {
  412. for _, v := range vs {
  413. values.Add(k, v)
  414. }
  415. }
  416. req, _ := http.NewRequest("GET", "/buddylist/removeBuddy?"+values.Encode(), nil)
  417. rr := httptest.NewRecorder()
  418. handler.RemoveBuddy(rr, req, session)
  419. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  420. assert.JSONEq(t, tt.expectedResponse, rr.Body.String())
  421. })
  422. }
  423. }
  424. func TestBuddyListHandler_RemoveGroup(t *testing.T) {
  425. type setupFunc func(*mockSessionResolver, *BuddyListManager, *mockFeedbagService, string) *Session
  426. newBuddyListManager := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  427. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  428. }
  429. tests := []struct {
  430. name string
  431. queryParams map[string][]string
  432. setup setupFunc
  433. expectedStatusCode int
  434. expectedResponse string
  435. }{
  436. {
  437. name: "Error_MissingGroupParam",
  438. queryParams: map[string][]string{"aimsid": {"sess"}},
  439. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  440. return &Session{AimSID: aimsid, ScreenName: state.DisplayScreenName("testuser"), LastAccessed: time.Now()}
  441. },
  442. expectedStatusCode: http.StatusBadRequest,
  443. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing group parameter","data":{}}}`,
  444. },
  445. {
  446. name: "Success_GroupRemoved",
  447. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  448. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  449. sess := &Session{
  450. AimSID: aimsid,
  451. ScreenName: state.DisplayScreenName("testuser"),
  452. OSCARSession: state.NewSession().AddInstance(),
  453. EventQueue: NewEventQueue(100),
  454. LastAccessed: time.Now(),
  455. }
  456. // Root order record + Friends group; DeleteGroup will delete Friends and update root.
  457. items := []wire.FeedbagItem{
  458. {
  459. GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "",
  460. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  461. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  462. }},
  463. },
  464. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  465. }
  466. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  467. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  468. fs.EXPECT().DeleteItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  469. Return((*wire.SNACMessage)(nil), nil).Once()
  470. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  471. Return((*wire.SNACMessage)(nil), nil).Once()
  472. // Second Query for GetBuddyListForUser event push
  473. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  474. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  475. return sess
  476. },
  477. expectedStatusCode: http.StatusOK,
  478. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  479. },
  480. {
  481. name: "Success_EventPushSkippedOnBLMError",
  482. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  483. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  484. sess := &Session{
  485. AimSID: aimsid,
  486. ScreenName: state.DisplayScreenName("testuser"),
  487. OSCARSession: state.NewSession().AddInstance(),
  488. EventQueue: NewEventQueue(100),
  489. LastAccessed: time.Now(),
  490. }
  491. items := []wire.FeedbagItem{
  492. {
  493. GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "",
  494. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  495. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  496. }},
  497. },
  498. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  499. }
  500. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  501. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  502. fs.EXPECT().DeleteItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  503. Return((*wire.SNACMessage)(nil), nil).Once()
  504. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  505. Return((*wire.SNACMessage)(nil), nil).Once()
  506. // BLM query fails — response should still be success
  507. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  508. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  509. return sess
  510. },
  511. expectedStatusCode: http.StatusOK,
  512. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  513. },
  514. {
  515. name: "Success_GroupNotFound",
  516. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NoSuchGroup"}},
  517. setup: func(sm *mockSessionResolver, blm *BuddyListManager, fs *mockFeedbagService, aimsid string) *Session {
  518. sess := &Session{
  519. AimSID: aimsid,
  520. ScreenName: state.DisplayScreenName("testuser"),
  521. OSCARSession: state.NewSession().AddInstance(),
  522. LastAccessed: time.Now(),
  523. }
  524. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  525. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil)
  526. return sess
  527. },
  528. expectedStatusCode: http.StatusOK,
  529. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  530. },
  531. }
  532. for _, tt := range tests {
  533. t.Run(tt.name, func(t *testing.T) {
  534. sm := newMockSessionResolver(t)
  535. fs := newMockFeedbagService(t)
  536. blm := newBuddyListManager(t, fs)
  537. aimsid := ""
  538. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  539. aimsid = v[0]
  540. }
  541. session := tt.setup(sm, blm, fs, aimsid)
  542. handler := &BuddyListHandler{
  543. BuddyListManager: blm,
  544. Logger: slog.Default(),
  545. }
  546. values := url.Values{}
  547. for k, vs := range tt.queryParams {
  548. for _, v := range vs {
  549. values.Add(k, v)
  550. }
  551. }
  552. req, _ := http.NewRequest("GET", "/buddylist/removeGroup?"+values.Encode(), nil)
  553. rr := httptest.NewRecorder()
  554. handler.RemoveGroup(rr, req, session)
  555. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  556. assert.JSONEq(t, tt.expectedResponse, rr.Body.String())
  557. })
  558. }
  559. }
  560. func TestRequireSession(t *testing.T) {
  561. tests := []struct {
  562. name string
  563. aimsid string
  564. setupMocks func(*mockSessionResolver, string)
  565. expectedStatusCode int
  566. expectedResponse string
  567. expectNextCalled bool
  568. }{
  569. {
  570. name: "Error_MissingAimsid",
  571. aimsid: "",
  572. setupMocks: func(sm *mockSessionResolver, aimsid string) {},
  573. expectedStatusCode: http.StatusBadRequest,
  574. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing aimsid parameter","data":{}}}`,
  575. expectNextCalled: false,
  576. },
  577. {
  578. name: "Error_SessionNotFound",
  579. aimsid: "unknown-session",
  580. setupMocks: func(sm *mockSessionResolver, aimsid string) {
  581. sm.EXPECT().GetSession(mock.Anything, aimsid).Return(nil, ErrNoWebAPISession)
  582. },
  583. expectedStatusCode: http.StatusUnauthorized,
  584. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session","data":{}}}`,
  585. expectNextCalled: false,
  586. },
  587. {
  588. name: "Error_SessionExpired",
  589. aimsid: "expired-session",
  590. setupMocks: func(sm *mockSessionResolver, aimsid string) {
  591. sm.EXPECT().GetSession(mock.Anything, aimsid).Return(nil, ErrWebAPISessionExpired)
  592. },
  593. expectedStatusCode: http.StatusUnauthorized,
  594. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session","data":{}}}`,
  595. expectNextCalled: false,
  596. },
  597. {
  598. name: "Error_InternalSessionError",
  599. aimsid: "some-session",
  600. setupMocks: func(sm *mockSessionResolver, aimsid string) {
  601. sm.EXPECT().GetSession(mock.Anything, aimsid).Return(nil, errors.New("db error"))
  602. },
  603. expectedStatusCode: http.StatusUnauthorized,
  604. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session","data":{}}}`,
  605. expectNextCalled: false,
  606. },
  607. {
  608. name: "Success_PassesSessionToNext",
  609. aimsid: "valid-session",
  610. setupMocks: func(sm *mockSessionResolver, aimsid string) {
  611. sess := &Session{
  612. AimSID: aimsid,
  613. ScreenName: state.DisplayScreenName("testuser"),
  614. OSCARSession: state.NewSession().AddInstance(),
  615. LastAccessed: time.Now(),
  616. }
  617. sm.EXPECT().GetSession(mock.Anything, aimsid).Return(sess, nil)
  618. sm.EXPECT().TouchSession(mock.Anything, aimsid).Return(nil)
  619. },
  620. expectedStatusCode: http.StatusOK,
  621. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{}}}`,
  622. expectNextCalled: true,
  623. },
  624. }
  625. for _, tt := range tests {
  626. t.Run(tt.name, func(t *testing.T) {
  627. sm := newMockSessionResolver(t)
  628. tt.setupMocks(sm, tt.aimsid)
  629. nextCalled := false
  630. next := func(w http.ResponseWriter, r *http.Request, session *Session) {
  631. nextCalled = true
  632. resp := BaseResponse{}
  633. resp.Response.StatusCode = 200
  634. resp.Response.StatusText = "OK"
  635. SendResponse(w, r, resp, slog.Default())
  636. }
  637. authMiddleware := NewAuthMiddleware(nil, slog.Default())
  638. wrapped := authMiddleware.RequireSession(sm, next)
  639. reqURL := "/buddylist/test"
  640. if tt.aimsid != "" {
  641. reqURL += "?aimsid=" + tt.aimsid
  642. }
  643. req, err := http.NewRequest("GET", reqURL, nil)
  644. assert.NoError(t, err)
  645. rr := httptest.NewRecorder()
  646. wrapped.ServeHTTP(rr, req)
  647. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  648. assert.JSONEq(t, tt.expectedResponse, rr.Body.String())
  649. assert.Equal(t, tt.expectNextCalled, nextCalled)
  650. })
  651. }
  652. }
  653. func TestBuddyListHandler_RenameGroup(t *testing.T) {
  654. newBLM := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  655. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  656. }
  657. sessWithOSCAR := func(aimsid string) *Session {
  658. return &Session{
  659. AimSID: aimsid,
  660. ScreenName: state.DisplayScreenName("testuser"),
  661. OSCARSession: state.NewSession().AddInstance(),
  662. EventQueue: NewEventQueue(100),
  663. LastAccessed: time.Now(),
  664. }
  665. }
  666. tests := []struct {
  667. name string
  668. queryParams map[string][]string
  669. setup func(*mockFeedbagService, string) *Session
  670. expectStatusCode int
  671. expectResponse string
  672. }{
  673. {
  674. name: "Error_MissingParam",
  675. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Friends"}},
  676. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  677. return &Session{AimSID: aimsid, LastAccessed: time.Now()}
  678. },
  679. expectStatusCode: http.StatusBadRequest,
  680. expectResponse: `{"response":{"statusCode":400,"statusText":"missing oldGroup or newGroup parameter","data":{}}}`,
  681. },
  682. {
  683. name: "Success",
  684. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Friends"}, "newGroup": {"Pals"}},
  685. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  686. items := []wire.FeedbagItem{
  687. {GroupID: 0, ClassID: wire.FeedbagClassIdGroup, Name: ""},
  688. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  689. }
  690. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  691. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  692. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  693. Return((*wire.SNACMessage)(nil), nil).Once()
  694. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  695. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  696. return sessWithOSCAR(aimsid)
  697. },
  698. expectStatusCode: http.StatusOK,
  699. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  700. },
  701. {
  702. name: "NotFound",
  703. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Ghost"}, "newGroup": {"Pals"}},
  704. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  705. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  706. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  707. return sessWithOSCAR(aimsid)
  708. },
  709. expectStatusCode: http.StatusOK,
  710. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  711. },
  712. }
  713. for _, tt := range tests {
  714. t.Run(tt.name, func(t *testing.T) {
  715. fs := newMockFeedbagService(t)
  716. session := tt.setup(fs, "sess")
  717. handler := &BuddyListHandler{BuddyListManager: newBLM(t, fs), Logger: slog.Default()}
  718. values := url.Values{}
  719. for k, vs := range tt.queryParams {
  720. for _, v := range vs {
  721. values.Add(k, v)
  722. }
  723. }
  724. req, _ := http.NewRequest("GET", "/buddylist/renameGroup?"+values.Encode(), nil)
  725. rr := httptest.NewRecorder()
  726. handler.RenameGroup(rr, req, session)
  727. assert.Equal(t, tt.expectStatusCode, rr.Code)
  728. assert.JSONEq(t, tt.expectResponse, rr.Body.String())
  729. })
  730. }
  731. }
  732. func TestBuddyListHandler_MoveBuddy(t *testing.T) {
  733. newBLM := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  734. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  735. }
  736. sessWithOSCAR := func(aimsid string) *Session {
  737. return &Session{
  738. AimSID: aimsid,
  739. ScreenName: state.DisplayScreenName("testuser"),
  740. OSCARSession: state.NewSession().AddInstance(),
  741. EventQueue: NewEventQueue(100),
  742. LastAccessed: time.Now(),
  743. }
  744. }
  745. tests := []struct {
  746. name string
  747. queryParams map[string][]string
  748. setup func(*mockFeedbagService, string) *Session
  749. expectStatusCode int
  750. expectResponse string
  751. }{
  752. {
  753. name: "Error_MissingBuddy",
  754. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  755. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  756. return &Session{AimSID: aimsid, LastAccessed: time.Now()}
  757. },
  758. expectStatusCode: http.StatusBadRequest,
  759. expectResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter","data":{}}}`,
  760. },
  761. {
  762. name: "Success_Reorder",
  763. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"bob"}, "group": {"Friends"}, "beforeBuddy": {"alice"}},
  764. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  765. items := []wire.FeedbagItem{
  766. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends",
  767. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  768. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20}),
  769. }}},
  770. {GroupID: 1, ItemID: 10, ClassID: wire.FeedbagClassIdBuddy, Name: "alice"},
  771. {GroupID: 1, ItemID: 20, ClassID: wire.FeedbagClassIdBuddy, Name: "bob"},
  772. }
  773. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  774. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  775. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  776. Return((*wire.SNACMessage)(nil), nil).Once()
  777. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  778. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  779. return sessWithOSCAR(aimsid)
  780. },
  781. expectStatusCode: http.StatusOK,
  782. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  783. },
  784. {
  785. name: "NotFound_Buddy",
  786. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"ghost"}, "group": {"Friends"}},
  787. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  788. items := []wire.FeedbagItem{
  789. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  790. }
  791. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  792. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  793. return sessWithOSCAR(aimsid)
  794. },
  795. expectStatusCode: http.StatusOK,
  796. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  797. },
  798. }
  799. for _, tt := range tests {
  800. t.Run(tt.name, func(t *testing.T) {
  801. fs := newMockFeedbagService(t)
  802. session := tt.setup(fs, "sess")
  803. handler := &BuddyListHandler{BuddyListManager: newBLM(t, fs), Logger: slog.Default()}
  804. values := url.Values{}
  805. for k, vs := range tt.queryParams {
  806. for _, v := range vs {
  807. values.Add(k, v)
  808. }
  809. }
  810. req, _ := http.NewRequest("GET", "/buddylist/moveBuddy?"+values.Encode(), nil)
  811. rr := httptest.NewRecorder()
  812. handler.MoveBuddy(rr, req, session)
  813. assert.Equal(t, tt.expectStatusCode, rr.Code)
  814. assert.JSONEq(t, tt.expectResponse, rr.Body.String())
  815. })
  816. }
  817. }
  818. func TestBuddyListHandler_SetBuddyAttribute(t *testing.T) {
  819. newBLM := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  820. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  821. }
  822. sessWithOSCAR := func(aimsid string) *Session {
  823. return &Session{
  824. AimSID: aimsid,
  825. ScreenName: state.DisplayScreenName("testuser"),
  826. OSCARSession: state.NewSession().AddInstance(),
  827. EventQueue: NewEventQueue(100),
  828. LastAccessed: time.Now(),
  829. }
  830. }
  831. tests := []struct {
  832. name string
  833. queryParams map[string][]string
  834. setup func(*mockFeedbagService, string) *Session
  835. expectStatusCode int
  836. expectResponse string
  837. }{
  838. {
  839. name: "Error_MissingT",
  840. queryParams: map[string][]string{"aimsid": {"sess"}, "friendly": {"Al"}},
  841. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  842. return &Session{AimSID: aimsid, LastAccessed: time.Now()}
  843. },
  844. expectStatusCode: http.StatusBadRequest,
  845. expectResponse: `{"response":{"statusCode":400,"statusText":"missing t parameter","data":{}}}`,
  846. },
  847. {
  848. name: "Success",
  849. queryParams: map[string][]string{"aimsid": {"sess"}, "t": {"alice"}, "friendly": {"Al"}},
  850. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  851. items := []wire.FeedbagItem{
  852. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  853. {GroupID: 1, ItemID: 10, ClassID: wire.FeedbagClassIdBuddy, Name: "alice"},
  854. }
  855. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  856. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  857. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  858. Return((*wire.SNACMessage)(nil), nil).Once()
  859. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  860. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  861. return sessWithOSCAR(aimsid)
  862. },
  863. expectStatusCode: http.StatusOK,
  864. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  865. },
  866. {
  867. name: "NotFound",
  868. queryParams: map[string][]string{"aimsid": {"sess"}, "t": {"ghost"}, "friendly": {"Al"}},
  869. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  870. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  871. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  872. return sessWithOSCAR(aimsid)
  873. },
  874. expectStatusCode: http.StatusOK,
  875. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  876. },
  877. }
  878. for _, tt := range tests {
  879. t.Run(tt.name, func(t *testing.T) {
  880. fs := newMockFeedbagService(t)
  881. session := tt.setup(fs, "sess")
  882. handler := &BuddyListHandler{BuddyListManager: newBLM(t, fs), Logger: slog.Default()}
  883. values := url.Values{}
  884. for k, vs := range tt.queryParams {
  885. for _, v := range vs {
  886. values.Add(k, v)
  887. }
  888. }
  889. req, _ := http.NewRequest("GET", "/buddylist/setBuddyAttribute?"+values.Encode(), nil)
  890. rr := httptest.NewRecorder()
  891. handler.SetBuddyAttribute(rr, req, session)
  892. assert.Equal(t, tt.expectStatusCode, rr.Code)
  893. assert.JSONEq(t, tt.expectResponse, rr.Body.String())
  894. })
  895. }
  896. }
  897. func TestBuddyListHandler_SetGroupAttribute(t *testing.T) {
  898. newBLM := func(t *testing.T, fs *mockFeedbagService) *BuddyListManager {
  899. return NewBuddyListManager(fs, newMockLocateService(t), newTestIconSource(t), slog.Default())
  900. }
  901. sessWithOSCAR := func(aimsid string) *Session {
  902. return &Session{
  903. AimSID: aimsid,
  904. ScreenName: state.DisplayScreenName("testuser"),
  905. OSCARSession: state.NewSession().AddInstance(),
  906. EventQueue: NewEventQueue(100),
  907. LastAccessed: time.Now(),
  908. }
  909. }
  910. tests := []struct {
  911. name string
  912. queryParams map[string][]string
  913. setup func(*mockFeedbagService, string) *Session
  914. expectStatusCode int
  915. expectResponse string
  916. }{
  917. {
  918. name: "Error_MissingCollapsed",
  919. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  920. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  921. return &Session{AimSID: aimsid, LastAccessed: time.Now()}
  922. },
  923. expectStatusCode: http.StatusBadRequest,
  924. expectResponse: `{"response":{"statusCode":400,"statusText":"missing collapsed parameter","data":{}}}`,
  925. },
  926. {
  927. name: "Success",
  928. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}, "collapsed": {"true"}},
  929. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  930. items := []wire.FeedbagItem{
  931. {GroupID: 0, ClassID: wire.FeedbagClassIdGroup, Name: ""},
  932. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  933. }
  934. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  935. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  936. fs.EXPECT().UpsertItem(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  937. Return((*wire.SNACMessage)(nil), nil).Once()
  938. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  939. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  940. return sessWithOSCAR(aimsid)
  941. },
  942. expectStatusCode: http.StatusOK,
  943. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  944. },
  945. {
  946. name: "NotFound",
  947. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NoSuch"}, "collapsed": {"true"}},
  948. setup: func(fs *mockFeedbagService, aimsid string) *Session {
  949. fs.EXPECT().Query(mock.Anything, mock.Anything, mock.Anything).
  950. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  951. return sessWithOSCAR(aimsid)
  952. },
  953. expectStatusCode: http.StatusOK,
  954. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  955. },
  956. }
  957. for _, tt := range tests {
  958. t.Run(tt.name, func(t *testing.T) {
  959. fs := newMockFeedbagService(t)
  960. session := tt.setup(fs, "sess")
  961. handler := &BuddyListHandler{BuddyListManager: newBLM(t, fs), Logger: slog.Default()}
  962. values := url.Values{}
  963. for k, vs := range tt.queryParams {
  964. for _, v := range vs {
  965. values.Add(k, v)
  966. }
  967. }
  968. req, _ := http.NewRequest("GET", "/buddylist/setGroupAttribute?"+values.Encode(), nil)
  969. rr := httptest.NewRecorder()
  970. handler.SetGroupAttribute(rr, req, session)
  971. assert.Equal(t, tt.expectStatusCode, rr.Code)
  972. assert.JSONEq(t, tt.expectResponse, rr.Body.String())
  973. })
  974. }
  975. }