buddylist_handler_test.go 42 KB

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