buddylist_handler_test.go 38 KB

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