buddylist_test.go 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  1. package handlers
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "net/http"
  7. "net/http/httptest"
  8. "net/url"
  9. "strings"
  10. "testing"
  11. "time"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/mock"
  14. "github.com/mk6i/open-oscar-server/server/webapi/middleware"
  15. "github.com/mk6i/open-oscar-server/server/webapi/types"
  16. "github.com/mk6i/open-oscar-server/state"
  17. "github.com/mk6i/open-oscar-server/wire"
  18. )
  19. // MockWebAPISessionManager is a mock implementation of the WebAPISessionManager
  20. type MockWebAPISessionManager struct {
  21. mock.Mock
  22. }
  23. func (m *MockWebAPISessionManager) GetSession(ctx context.Context, aimsid string) (*state.WebAPISession, error) {
  24. args := m.Called(ctx, aimsid)
  25. if session := args.Get(0); session != nil {
  26. return session.(*state.WebAPISession), args.Error(1)
  27. }
  28. return nil, args.Error(1)
  29. }
  30. func (m *MockWebAPISessionManager) TouchSession(ctx context.Context, aimsid string) error {
  31. args := m.Called(ctx, aimsid)
  32. return args.Error(0)
  33. }
  34. func TestBuddyListHandler_AddTempBuddy(t *testing.T) {
  35. tests := []struct {
  36. name string
  37. queryParams map[string][]string
  38. session *state.WebAPISession
  39. expectedStatusCode int
  40. expectedResponse string
  41. checkSession func(*testing.T, *state.WebAPISession)
  42. }{
  43. {
  44. name: "Success_SingleBuddy",
  45. queryParams: map[string][]string{
  46. "aimsid": {"test-session-id"},
  47. "t": {"buddy1"},
  48. },
  49. session: &state.WebAPISession{
  50. AimSID: "test-session-id",
  51. ScreenName: state.DisplayScreenName("testuser"),
  52. EventQueue: types.NewEventQueue(100),
  53. TempBuddies: nil,
  54. LastAccessed: time.Now(),
  55. },
  56. expectedStatusCode: http.StatusOK,
  57. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyNames":["buddy1"],"resultCode":"success"}}}`,
  58. checkSession: func(t *testing.T, session *state.WebAPISession) {
  59. assert.NotNil(t, session.TempBuddies)
  60. assert.True(t, session.TempBuddies["buddy1"])
  61. assert.Equal(t, 1, len(session.TempBuddies))
  62. },
  63. },
  64. {
  65. name: "Success_MultipleBuddies",
  66. queryParams: map[string][]string{
  67. "aimsid": {"test-session-id"},
  68. "t": {"buddy1", "buddy2", "buddy3"},
  69. },
  70. session: &state.WebAPISession{
  71. AimSID: "test-session-id",
  72. ScreenName: state.DisplayScreenName("testuser"),
  73. EventQueue: types.NewEventQueue(100),
  74. TempBuddies: nil,
  75. LastAccessed: time.Now(),
  76. },
  77. expectedStatusCode: http.StatusOK,
  78. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyNames":["buddy1","buddy2","buddy3"],"resultCode":"success"}}}`,
  79. checkSession: func(t *testing.T, session *state.WebAPISession) {
  80. assert.NotNil(t, session.TempBuddies)
  81. assert.True(t, session.TempBuddies["buddy1"])
  82. assert.True(t, session.TempBuddies["buddy2"])
  83. assert.True(t, session.TempBuddies["buddy3"])
  84. assert.Equal(t, 3, len(session.TempBuddies))
  85. },
  86. },
  87. {
  88. name: "Success_AddToExistingTempBuddies",
  89. queryParams: map[string][]string{
  90. "aimsid": {"test-session-id"},
  91. "t": {"buddy2"},
  92. },
  93. session: &state.WebAPISession{
  94. AimSID: "test-session-id",
  95. ScreenName: state.DisplayScreenName("testuser"),
  96. EventQueue: types.NewEventQueue(100),
  97. TempBuddies: map[string]bool{
  98. "buddy1": true,
  99. },
  100. LastAccessed: time.Now(),
  101. },
  102. expectedStatusCode: http.StatusOK,
  103. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyNames":["buddy2"],"resultCode":"success"}}}`,
  104. checkSession: func(t *testing.T, session *state.WebAPISession) {
  105. assert.NotNil(t, session.TempBuddies)
  106. assert.True(t, session.TempBuddies["buddy1"])
  107. assert.True(t, session.TempBuddies["buddy2"])
  108. assert.Equal(t, 2, len(session.TempBuddies))
  109. },
  110. },
  111. {
  112. name: "Error_MissingBuddyNames",
  113. queryParams: map[string][]string{
  114. "aimsid": {"test-session-id"},
  115. },
  116. session: &state.WebAPISession{
  117. AimSID: "test-session-id",
  118. ScreenName: state.DisplayScreenName("testuser"),
  119. EventQueue: types.NewEventQueue(100),
  120. LastAccessed: time.Now(),
  121. },
  122. expectedStatusCode: http.StatusBadRequest,
  123. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing buddy names (t parameter)"}}`,
  124. },
  125. {
  126. name: "Success_WithWhitespace",
  127. queryParams: map[string][]string{
  128. "aimsid": {"test-session-id"},
  129. "t": {" buddy1 ", "buddy2 ", " buddy3"},
  130. },
  131. session: &state.WebAPISession{
  132. AimSID: "test-session-id",
  133. ScreenName: state.DisplayScreenName("testuser"),
  134. EventQueue: types.NewEventQueue(100),
  135. TempBuddies: nil,
  136. LastAccessed: time.Now(),
  137. },
  138. expectedStatusCode: http.StatusOK,
  139. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyNames":[" buddy1 ","buddy2 "," buddy3"],"resultCode":"success"}}}`,
  140. checkSession: func(t *testing.T, session *state.WebAPISession) {
  141. assert.NotNil(t, session.TempBuddies)
  142. assert.True(t, session.TempBuddies["buddy1"])
  143. assert.True(t, session.TempBuddies["buddy2"])
  144. assert.True(t, session.TempBuddies["buddy3"])
  145. assert.Equal(t, 3, len(session.TempBuddies))
  146. },
  147. },
  148. }
  149. for _, tt := range tests {
  150. t.Run(tt.name, func(t *testing.T) {
  151. handler := &BuddyListHandler{
  152. Logger: slog.Default(),
  153. }
  154. reqURL := "/aim/addTempBuddy"
  155. if len(tt.queryParams) > 0 {
  156. values := url.Values{}
  157. for key, vals := range tt.queryParams {
  158. for _, val := range vals {
  159. values.Add(key, val)
  160. }
  161. }
  162. reqURL += "?" + values.Encode()
  163. }
  164. req, err := http.NewRequest("GET", reqURL, nil)
  165. assert.NoError(t, err)
  166. rr := httptest.NewRecorder()
  167. handler.AddTempBuddy(rr, req, tt.session)
  168. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  169. assert.Equal(t, tt.expectedResponse, strings.TrimSpace(rr.Body.String()))
  170. if tt.checkSession != nil && tt.session != nil {
  171. tt.checkSession(t, tt.session)
  172. }
  173. })
  174. }
  175. }
  176. func TestBuddyListHandler_AddTempBuddy_DoesNotPushBuddyListEvent(t *testing.T) {
  177. handler := &BuddyListHandler{Logger: slog.Default()}
  178. eventQueue := types.NewEventQueue(100)
  179. session := &state.WebAPISession{
  180. AimSID: "test-session",
  181. ScreenName: state.DisplayScreenName("testuser"),
  182. EventQueue: eventQueue,
  183. TempBuddies: nil,
  184. LastAccessed: time.Now(),
  185. }
  186. req, err := http.NewRequest("GET", "/aim/addTempBuddy?aimsid=test-session&t=buddy1&t=buddy2", nil)
  187. assert.NoError(t, err)
  188. rr := httptest.NewRecorder()
  189. handler.AddTempBuddy(rr, req, session)
  190. assert.Equal(t, http.StatusOK, rr.Code)
  191. assert.Empty(t, eventQueue.GetAllEvents(), "addTempBuddy must not push buddylist events")
  192. }
  193. func TestBuddyListHandler_RemoveTempBuddy(t *testing.T) {
  194. handler := &BuddyListHandler{Logger: slog.Default()}
  195. session := &state.WebAPISession{
  196. AimSID: "test-session",
  197. ScreenName: state.DisplayScreenName("testuser"),
  198. TempBuddies: map[string]bool{
  199. "buddy1": true,
  200. "buddy2": true,
  201. },
  202. LastAccessed: time.Now(),
  203. }
  204. req, err := http.NewRequest("GET", "/aim/removeTempBuddy?aimsid=test-session&t=buddy1", nil)
  205. assert.NoError(t, err)
  206. rr := httptest.NewRecorder()
  207. handler.RemoveTempBuddy(rr, req, session)
  208. assert.Equal(t, http.StatusOK, rr.Code)
  209. assert.False(t, session.TempBuddies["buddy1"])
  210. assert.True(t, session.TempBuddies["buddy2"])
  211. }
  212. func TestFeedbagGroupMatchesRequested(t *testing.T) {
  213. assert.True(t, feedbagGroupMatchesRequested("Buddies", "Buddies"))
  214. assert.True(t, feedbagGroupMatchesRequested("", "Buddies"))
  215. assert.True(t, feedbagGroupMatchesRequested(" ", "Buddies"))
  216. assert.True(t, feedbagGroupMatchesRequested("Friends", "friends"))
  217. assert.False(t, feedbagGroupMatchesRequested("", "Friends"))
  218. }
  219. func TestStoredGroupNameForRequest(t *testing.T) {
  220. items := []wire.FeedbagItem{
  221. {ItemID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "", GroupID: 1},
  222. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "jon", GroupID: 1},
  223. }
  224. st, ok := storedGroupNameForRequest(items, "Buddies")
  225. assert.True(t, ok)
  226. assert.Equal(t, "", st)
  227. items2 := []wire.FeedbagItem{
  228. {ItemID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends", GroupID: 2},
  229. }
  230. st2, ok2 := storedGroupNameForRequest(items2, "Friends")
  231. assert.True(t, ok2)
  232. assert.Equal(t, "Friends", st2)
  233. }
  234. func TestBuddyListHandler_AddBuddy(t *testing.T) {
  235. tests := []struct {
  236. name string
  237. queryParams map[string][]string
  238. setupMocks func(*MockWebAPISessionManager, *MockFeedbagService, *MockFeedbagService, string) *state.WebAPISession
  239. expectedStatusCode int
  240. expectedResponse string
  241. }{
  242. {
  243. name: "Success_AddBuddyToExistingGroup",
  244. queryParams: map[string][]string{
  245. "aimsid": {"test-session"},
  246. "buddy": {"newbuddy"},
  247. "group": {"Friends"},
  248. },
  249. setupMocks: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  250. session := &state.WebAPISession{
  251. AimSID: aimsid,
  252. ScreenName: state.DisplayScreenName("testuser"),
  253. EventQueue: types.NewEventQueue(100),
  254. LastAccessed: time.Now(),
  255. }
  256. items := []wire.FeedbagItem{
  257. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  258. }
  259. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  260. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  261. // addBuddyToFeedbag calls UpsertItem twice: once for group order update, once for buddy insert
  262. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  263. Return((*wire.SNACMessage)(nil), nil)
  264. blmFs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  265. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  266. return session
  267. },
  268. expectedStatusCode: http.StatusOK,
  269. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyInfo":{"aimId":"newbuddy","displayId":"newbuddy","state":"offline","userType":"aim"},"resultCode":"success"}}}`,
  270. },
  271. {
  272. name: "Success_EventPushSkippedOnBLMError",
  273. queryParams: map[string][]string{
  274. "aimsid": {"test-session"},
  275. "buddy": {"newbuddy"},
  276. "group": {"Friends"},
  277. },
  278. setupMocks: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  279. session := &state.WebAPISession{
  280. AimSID: aimsid,
  281. ScreenName: state.DisplayScreenName("testuser"),
  282. EventQueue: types.NewEventQueue(100),
  283. LastAccessed: time.Now(),
  284. }
  285. items := []wire.FeedbagItem{
  286. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  287. }
  288. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  289. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  290. // addBuddyToFeedbag calls UpsertItem twice: once for group order update, once for buddy insert
  291. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  292. Return((*wire.SNACMessage)(nil), nil)
  293. blmFs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  294. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  295. return session
  296. },
  297. expectedStatusCode: http.StatusOK,
  298. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"buddyInfo":{"aimId":"newbuddy","displayId":"newbuddy","state":"offline","userType":"aim"},"resultCode":"success"}}}`,
  299. },
  300. {
  301. name: "Error_BuddyAlreadyExists",
  302. queryParams: map[string][]string{
  303. "aimsid": {"test-session"},
  304. "buddy": {"existingbuddy"},
  305. "group": {"Friends"},
  306. },
  307. setupMocks: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  308. session := &state.WebAPISession{
  309. AimSID: aimsid,
  310. ScreenName: state.DisplayScreenName("testuser"),
  311. EventQueue: types.NewEventQueue(100),
  312. LastAccessed: time.Now(),
  313. }
  314. // Friends group with existingbuddy already present
  315. items := []wire.FeedbagItem{
  316. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  317. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "existingbuddy"},
  318. }
  319. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  320. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  321. return session
  322. },
  323. expectedStatusCode: http.StatusOK,
  324. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"alreadyExists"}}}`,
  325. },
  326. {
  327. name: "Error_MissingBuddyParameter",
  328. queryParams: map[string][]string{
  329. "aimsid": {"test-session"},
  330. "group": {"Friends"},
  331. },
  332. setupMocks: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  333. return &state.WebAPISession{
  334. AimSID: aimsid,
  335. ScreenName: state.DisplayScreenName("testuser"),
  336. EventQueue: types.NewEventQueue(100),
  337. LastAccessed: time.Now(),
  338. }
  339. },
  340. expectedStatusCode: http.StatusBadRequest,
  341. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter"}}`,
  342. },
  343. }
  344. for _, tt := range tests {
  345. t.Run(tt.name, func(t *testing.T) {
  346. sessionManager := &MockWebAPISessionManager{}
  347. feedbagService := &MockFeedbagService{}
  348. blmFeedbagService := &MockFeedbagService{}
  349. blm := NewBuddyListManager(blmFeedbagService, &MockLocateService{}, newTestIconSource(), slog.Default())
  350. logger := slog.Default()
  351. handler := &BuddyListHandler{
  352. FeedbagService: feedbagService,
  353. BuddyListManager: blm,
  354. Logger: logger,
  355. }
  356. aimsid := ""
  357. if aimsids, ok := tt.queryParams["aimsid"]; ok && len(aimsids) > 0 {
  358. aimsid = aimsids[0]
  359. }
  360. session := tt.setupMocks(sessionManager, feedbagService, blmFeedbagService, aimsid)
  361. reqURL := "/buddylist/addBuddy"
  362. if len(tt.queryParams) > 0 {
  363. values := url.Values{}
  364. for key, vals := range tt.queryParams {
  365. for _, val := range vals {
  366. values.Add(key, val)
  367. }
  368. }
  369. reqURL += "?" + values.Encode()
  370. }
  371. req, err := http.NewRequest("GET", reqURL, nil)
  372. assert.NoError(t, err)
  373. rr := httptest.NewRecorder()
  374. handler.AddBuddy(rr, req, session)
  375. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  376. responseBody := strings.TrimSpace(rr.Body.String())
  377. assert.Equal(t, tt.expectedResponse, responseBody)
  378. feedbagService.AssertExpectations(t)
  379. blmFeedbagService.AssertExpectations(t)
  380. })
  381. }
  382. }
  383. func TestBuddyListHandler_AddGroup(t *testing.T) {
  384. type setupFunc func(*MockWebAPISessionManager, *MockFeedbagService, *MockFeedbagService, string) *state.WebAPISession
  385. newSession := func(aimsid string) *state.WebAPISession {
  386. return &state.WebAPISession{
  387. AimSID: aimsid,
  388. ScreenName: state.DisplayScreenName("testuser"),
  389. EventQueue: types.NewEventQueue(100),
  390. LastAccessed: time.Now(),
  391. }
  392. }
  393. tests := []struct {
  394. name string
  395. queryParams map[string][]string
  396. setup setupFunc
  397. expectedStatusCode int
  398. expectedResponse string
  399. }{
  400. {
  401. name: "Error_MissingGroupParam",
  402. queryParams: map[string][]string{"aimsid": {"sess"}},
  403. setup: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  404. return newSession(aimsid)
  405. },
  406. expectedStatusCode: http.StatusBadRequest,
  407. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing group parameter"}}`,
  408. },
  409. {
  410. name: "Success_GroupAdded",
  411. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  412. setup: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  413. sess := newSession(aimsid)
  414. // Empty feedbag — AddGroup will create root + NewGroup in pending
  415. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  416. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  417. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  418. Return((*wire.SNACMessage)(nil), nil).Once()
  419. blmFs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  420. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  421. return sess
  422. },
  423. expectedStatusCode: http.StatusOK,
  424. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  425. },
  426. {
  427. name: "Success_EventPushSkippedOnBLMError",
  428. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  429. setup: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  430. sess := newSession(aimsid)
  431. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  432. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  433. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  434. Return((*wire.SNACMessage)(nil), nil).Once()
  435. blmFs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  436. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  437. return sess
  438. },
  439. expectedStatusCode: http.StatusOK,
  440. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  441. },
  442. {
  443. name: "Success_GroupAlreadyExists",
  444. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  445. setup: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  446. sess := newSession(aimsid)
  447. items := []wire.FeedbagItem{
  448. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  449. }
  450. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  451. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  452. return sess
  453. },
  454. expectedStatusCode: http.StatusOK,
  455. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"alreadyExists"}}}`,
  456. },
  457. {
  458. name: "Error_FeedbagQueryFails",
  459. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NewGroup"}},
  460. setup: func(sm *MockWebAPISessionManager, fs *MockFeedbagService, blmFs *MockFeedbagService, aimsid string) *state.WebAPISession {
  461. sess := newSession(aimsid)
  462. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  463. Return(wire.SNACMessage{}, errors.New("feedbag error"))
  464. return sess
  465. },
  466. expectedStatusCode: http.StatusOK,
  467. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"error"}}}`,
  468. },
  469. }
  470. for _, tt := range tests {
  471. t.Run(tt.name, func(t *testing.T) {
  472. sm := &MockWebAPISessionManager{}
  473. fs := &MockFeedbagService{}
  474. blmFs := &MockFeedbagService{}
  475. blm := NewBuddyListManager(blmFs, &MockLocateService{}, newTestIconSource(), slog.Default())
  476. aimsid := ""
  477. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  478. aimsid = v[0]
  479. }
  480. session := tt.setup(sm, fs, blmFs, aimsid)
  481. handler := &BuddyListHandler{
  482. FeedbagService: fs,
  483. BuddyListManager: blm,
  484. Logger: slog.Default(),
  485. }
  486. values := url.Values{}
  487. for k, vs := range tt.queryParams {
  488. for _, v := range vs {
  489. values.Add(k, v)
  490. }
  491. }
  492. req, _ := http.NewRequest("GET", "/buddylist/addGroup?"+values.Encode(), nil)
  493. rr := httptest.NewRecorder()
  494. handler.AddGroup(rr, req, session)
  495. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  496. assert.Equal(t, tt.expectedResponse, strings.TrimSpace(rr.Body.String()))
  497. fs.AssertExpectations(t)
  498. blmFs.AssertExpectations(t)
  499. })
  500. }
  501. }
  502. func TestBuddyListHandler_RemoveBuddy(t *testing.T) {
  503. type setupFunc func(*MockWebAPISessionManager, *BuddyListManager, *MockFeedbagService, string) *state.WebAPISession
  504. newBuddyListManager := func(fs *MockFeedbagService) *BuddyListManager {
  505. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  506. }
  507. tests := []struct {
  508. name string
  509. queryParams map[string][]string
  510. setup setupFunc
  511. expectedStatusCode int
  512. expectedResponse string
  513. }{
  514. {
  515. name: "Error_MissingBuddyParam",
  516. queryParams: map[string][]string{"aimsid": {"sess"}},
  517. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  518. return &state.WebAPISession{AimSID: aimsid, ScreenName: state.DisplayScreenName("testuser"), LastAccessed: time.Now()}
  519. },
  520. expectedStatusCode: http.StatusBadRequest,
  521. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter"}}`,
  522. },
  523. {
  524. name: "Success_BuddyRemoved",
  525. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"Friends"}},
  526. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  527. sess := &state.WebAPISession{
  528. AimSID: aimsid,
  529. ScreenName: state.DisplayScreenName("testuser"),
  530. OSCARSession: state.NewSession().AddInstance(),
  531. EventQueue: types.NewEventQueue(100),
  532. LastAccessed: time.Now(),
  533. }
  534. items := []wire.FeedbagItem{
  535. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  536. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "someBuddy"},
  537. }
  538. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  539. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  540. fs.On("DeleteItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  541. Return((*wire.SNACMessage)(nil), nil).Once()
  542. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  543. Return((*wire.SNACMessage)(nil), nil).Once()
  544. // Second Query for GetBuddyListForUser event push
  545. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  546. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  547. return sess
  548. },
  549. expectedStatusCode: http.StatusOK,
  550. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  551. },
  552. {
  553. name: "Success_EventPushSkippedOnBLMError",
  554. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"Friends"}},
  555. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  556. sess := &state.WebAPISession{
  557. AimSID: aimsid,
  558. ScreenName: state.DisplayScreenName("testuser"),
  559. OSCARSession: state.NewSession().AddInstance(),
  560. EventQueue: types.NewEventQueue(100),
  561. LastAccessed: time.Now(),
  562. }
  563. items := []wire.FeedbagItem{
  564. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  565. {GroupID: 1, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, Name: "someBuddy"},
  566. }
  567. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  568. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  569. fs.On("DeleteItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  570. Return((*wire.SNACMessage)(nil), nil).Once()
  571. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  572. Return((*wire.SNACMessage)(nil), nil).Once()
  573. // BLM query fails — response should still be success
  574. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  575. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  576. return sess
  577. },
  578. expectedStatusCode: http.StatusOK,
  579. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  580. },
  581. {
  582. name: "Success_BuddyNotFound",
  583. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"ghost"}, "group": {"Friends"}},
  584. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  585. sess := &state.WebAPISession{
  586. AimSID: aimsid,
  587. ScreenName: state.DisplayScreenName("testuser"),
  588. OSCARSession: state.NewSession().AddInstance(),
  589. LastAccessed: time.Now(),
  590. }
  591. // Group exists but "ghost" is not in it
  592. items := []wire.FeedbagItem{
  593. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  594. }
  595. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  596. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil)
  597. return sess
  598. },
  599. expectedStatusCode: http.StatusOK,
  600. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  601. },
  602. {
  603. name: "Success_GroupNotFound",
  604. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"someBuddy"}, "group": {"NoSuchGroup"}},
  605. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  606. sess := &state.WebAPISession{
  607. AimSID: aimsid,
  608. ScreenName: state.DisplayScreenName("testuser"),
  609. OSCARSession: state.NewSession().AddInstance(),
  610. LastAccessed: time.Now(),
  611. }
  612. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  613. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil)
  614. return sess
  615. },
  616. expectedStatusCode: http.StatusOK,
  617. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  618. },
  619. }
  620. for _, tt := range tests {
  621. t.Run(tt.name, func(t *testing.T) {
  622. sm := &MockWebAPISessionManager{}
  623. fs := &MockFeedbagService{}
  624. blm := newBuddyListManager(fs)
  625. aimsid := ""
  626. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  627. aimsid = v[0]
  628. }
  629. session := tt.setup(sm, blm, fs, aimsid)
  630. handler := &BuddyListHandler{
  631. BuddyListManager: blm,
  632. Logger: slog.Default(),
  633. }
  634. values := url.Values{}
  635. for k, vs := range tt.queryParams {
  636. for _, v := range vs {
  637. values.Add(k, v)
  638. }
  639. }
  640. req, _ := http.NewRequest("GET", "/buddylist/removeBuddy?"+values.Encode(), nil)
  641. rr := httptest.NewRecorder()
  642. handler.RemoveBuddy(rr, req, session)
  643. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  644. assert.Equal(t, tt.expectedResponse, strings.TrimSpace(rr.Body.String()))
  645. fs.AssertExpectations(t)
  646. })
  647. }
  648. }
  649. func TestBuddyListHandler_RemoveGroup(t *testing.T) {
  650. type setupFunc func(*MockWebAPISessionManager, *BuddyListManager, *MockFeedbagService, string) *state.WebAPISession
  651. newBuddyListManager := func(fs *MockFeedbagService) *BuddyListManager {
  652. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  653. }
  654. tests := []struct {
  655. name string
  656. queryParams map[string][]string
  657. setup setupFunc
  658. expectedStatusCode int
  659. expectedResponse string
  660. }{
  661. {
  662. name: "Error_MissingGroupParam",
  663. queryParams: map[string][]string{"aimsid": {"sess"}},
  664. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  665. return &state.WebAPISession{AimSID: aimsid, ScreenName: state.DisplayScreenName("testuser"), LastAccessed: time.Now()}
  666. },
  667. expectedStatusCode: http.StatusBadRequest,
  668. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing group parameter"}}`,
  669. },
  670. {
  671. name: "Success_GroupRemoved",
  672. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  673. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  674. sess := &state.WebAPISession{
  675. AimSID: aimsid,
  676. ScreenName: state.DisplayScreenName("testuser"),
  677. OSCARSession: state.NewSession().AddInstance(),
  678. EventQueue: types.NewEventQueue(100),
  679. LastAccessed: time.Now(),
  680. }
  681. // Root order record + Friends group; DeleteGroup will delete Friends and update root.
  682. items := []wire.FeedbagItem{
  683. {
  684. GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "",
  685. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  686. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  687. }},
  688. },
  689. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  690. }
  691. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  692. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  693. fs.On("DeleteItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  694. Return((*wire.SNACMessage)(nil), nil).Once()
  695. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  696. Return((*wire.SNACMessage)(nil), nil).Once()
  697. // Second Query for GetBuddyListForUser event push
  698. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  699. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  700. return sess
  701. },
  702. expectedStatusCode: http.StatusOK,
  703. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  704. },
  705. {
  706. name: "Success_EventPushSkippedOnBLMError",
  707. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  708. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  709. sess := &state.WebAPISession{
  710. AimSID: aimsid,
  711. ScreenName: state.DisplayScreenName("testuser"),
  712. OSCARSession: state.NewSession().AddInstance(),
  713. EventQueue: types.NewEventQueue(100),
  714. LastAccessed: time.Now(),
  715. }
  716. items := []wire.FeedbagItem{
  717. {
  718. GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "",
  719. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  720. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  721. }},
  722. },
  723. {GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  724. }
  725. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  726. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  727. fs.On("DeleteItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  728. Return((*wire.SNACMessage)(nil), nil).Once()
  729. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  730. Return((*wire.SNACMessage)(nil), nil).Once()
  731. // BLM query fails — response should still be success
  732. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  733. Return(wire.SNACMessage{}, errors.New("feedbag unavailable")).Once()
  734. return sess
  735. },
  736. expectedStatusCode: http.StatusOK,
  737. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  738. },
  739. {
  740. name: "Success_GroupNotFound",
  741. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NoSuchGroup"}},
  742. setup: func(sm *MockWebAPISessionManager, blm *BuddyListManager, fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  743. sess := &state.WebAPISession{
  744. AimSID: aimsid,
  745. ScreenName: state.DisplayScreenName("testuser"),
  746. OSCARSession: state.NewSession().AddInstance(),
  747. LastAccessed: time.Now(),
  748. }
  749. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  750. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil)
  751. return sess
  752. },
  753. expectedStatusCode: http.StatusOK,
  754. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  755. },
  756. }
  757. for _, tt := range tests {
  758. t.Run(tt.name, func(t *testing.T) {
  759. sm := &MockWebAPISessionManager{}
  760. fs := &MockFeedbagService{}
  761. blm := newBuddyListManager(fs)
  762. aimsid := ""
  763. if v := tt.queryParams["aimsid"]; len(v) > 0 {
  764. aimsid = v[0]
  765. }
  766. session := tt.setup(sm, blm, fs, aimsid)
  767. handler := &BuddyListHandler{
  768. BuddyListManager: blm,
  769. Logger: slog.Default(),
  770. }
  771. values := url.Values{}
  772. for k, vs := range tt.queryParams {
  773. for _, v := range vs {
  774. values.Add(k, v)
  775. }
  776. }
  777. req, _ := http.NewRequest("GET", "/buddylist/removeGroup?"+values.Encode(), nil)
  778. rr := httptest.NewRecorder()
  779. handler.RemoveGroup(rr, req, session)
  780. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  781. assert.Equal(t, tt.expectedResponse, strings.TrimSpace(rr.Body.String()))
  782. fs.AssertExpectations(t)
  783. })
  784. }
  785. }
  786. func TestRequireSession(t *testing.T) {
  787. tests := []struct {
  788. name string
  789. aimsid string
  790. setupMocks func(*MockWebAPISessionManager, string)
  791. expectedStatusCode int
  792. expectedResponse string
  793. expectNextCalled bool
  794. }{
  795. {
  796. name: "Error_MissingAimsid",
  797. aimsid: "",
  798. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {},
  799. expectedStatusCode: http.StatusBadRequest,
  800. expectedResponse: `{"response":{"statusCode":400,"statusText":"missing aimsid parameter"}}`,
  801. expectNextCalled: false,
  802. },
  803. {
  804. name: "Error_SessionNotFound",
  805. aimsid: "unknown-session",
  806. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {
  807. sm.On("GetSession", mock.Anything, aimsid).Return(nil, state.ErrNoWebAPISession)
  808. },
  809. expectedStatusCode: http.StatusUnauthorized,
  810. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session"}}`,
  811. expectNextCalled: false,
  812. },
  813. {
  814. name: "Error_SessionExpired",
  815. aimsid: "expired-session",
  816. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {
  817. sm.On("GetSession", mock.Anything, aimsid).Return(nil, state.ErrWebAPISessionExpired)
  818. },
  819. expectedStatusCode: http.StatusUnauthorized,
  820. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session"}}`,
  821. expectNextCalled: false,
  822. },
  823. {
  824. name: "Error_InternalSessionError",
  825. aimsid: "some-session",
  826. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {
  827. sm.On("GetSession", mock.Anything, aimsid).Return(nil, errors.New("db error"))
  828. },
  829. expectedStatusCode: http.StatusUnauthorized,
  830. expectedResponse: `{"response":{"statusCode":401,"statusText":"invalid or expired session"}}`,
  831. expectNextCalled: false,
  832. },
  833. {
  834. name: "Error_NilOSCARSession",
  835. aimsid: "no-oscar-session",
  836. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {
  837. sess := &state.WebAPISession{
  838. AimSID: aimsid,
  839. ScreenName: state.DisplayScreenName("someuser"),
  840. LastAccessed: time.Now(),
  841. // OSCARSession is nil - a broken server invariant, since
  842. // startSession never creates a session without one.
  843. }
  844. sm.On("GetSession", mock.Anything, aimsid).Return(sess, nil)
  845. },
  846. expectedStatusCode: http.StatusInternalServerError,
  847. expectedResponse: `{"response":{"statusCode":500,"statusText":"internal server error"}}`,
  848. expectNextCalled: false,
  849. },
  850. {
  851. name: "Success_PassesSessionToNext",
  852. aimsid: "valid-session",
  853. setupMocks: func(sm *MockWebAPISessionManager, aimsid string) {
  854. sess := &state.WebAPISession{
  855. AimSID: aimsid,
  856. ScreenName: state.DisplayScreenName("testuser"),
  857. OSCARSession: state.NewSession().AddInstance(),
  858. LastAccessed: time.Now(),
  859. }
  860. sm.On("GetSession", mock.Anything, aimsid).Return(sess, nil)
  861. sm.On("TouchSession", mock.Anything, aimsid).Return(nil)
  862. },
  863. expectedStatusCode: http.StatusOK,
  864. expectedResponse: `{"response":{"statusCode":200,"statusText":"OK"}}`,
  865. expectNextCalled: true,
  866. },
  867. }
  868. for _, tt := range tests {
  869. t.Run(tt.name, func(t *testing.T) {
  870. sm := &MockWebAPISessionManager{}
  871. tt.setupMocks(sm, tt.aimsid)
  872. nextCalled := false
  873. next := func(w http.ResponseWriter, r *http.Request, session *state.WebAPISession) {
  874. nextCalled = true
  875. resp := BaseResponse{}
  876. resp.Response.StatusCode = 200
  877. resp.Response.StatusText = "OK"
  878. SendResponse(w, r, resp, slog.Default())
  879. }
  880. authMiddleware := middleware.NewAuthMiddleware(nil, slog.Default())
  881. wrapped := authMiddleware.RequireSession(sm, next)
  882. reqURL := "/buddylist/test"
  883. if tt.aimsid != "" {
  884. reqURL += "?aimsid=" + tt.aimsid
  885. }
  886. req, err := http.NewRequest("GET", reqURL, nil)
  887. assert.NoError(t, err)
  888. rr := httptest.NewRecorder()
  889. wrapped.ServeHTTP(rr, req)
  890. assert.Equal(t, tt.expectedStatusCode, rr.Code)
  891. assert.Equal(t, tt.expectedResponse, strings.TrimSpace(rr.Body.String()))
  892. assert.Equal(t, tt.expectNextCalled, nextCalled)
  893. sm.AssertExpectations(t)
  894. })
  895. }
  896. }
  897. func TestBuddyListHandler_RenameGroup(t *testing.T) {
  898. newBLM := func(fs *MockFeedbagService) *BuddyListManager {
  899. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  900. }
  901. sessWithOSCAR := func(aimsid string) *state.WebAPISession {
  902. return &state.WebAPISession{
  903. AimSID: aimsid,
  904. ScreenName: state.DisplayScreenName("testuser"),
  905. OSCARSession: state.NewSession().AddInstance(),
  906. EventQueue: types.NewEventQueue(100),
  907. LastAccessed: time.Now(),
  908. }
  909. }
  910. tests := []struct {
  911. name string
  912. queryParams map[string][]string
  913. setup func(*MockFeedbagService, string) *state.WebAPISession
  914. expectStatusCode int
  915. expectResponse string
  916. }{
  917. {
  918. name: "Error_MissingParam",
  919. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Friends"}},
  920. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  921. return &state.WebAPISession{AimSID: aimsid, LastAccessed: time.Now()}
  922. },
  923. expectStatusCode: http.StatusBadRequest,
  924. expectResponse: `{"response":{"statusCode":400,"statusText":"missing oldGroup or newGroup parameter"}}`,
  925. },
  926. {
  927. name: "Success",
  928. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Friends"}, "newGroup": {"Pals"}},
  929. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  930. items := []wire.FeedbagItem{
  931. {GroupID: 0, ClassID: wire.FeedbagClassIdGroup, Name: ""},
  932. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  933. }
  934. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  935. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  936. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  937. Return((*wire.SNACMessage)(nil), nil).Once()
  938. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  939. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  940. return sessWithOSCAR(aimsid)
  941. },
  942. expectStatusCode: http.StatusOK,
  943. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  944. },
  945. {
  946. name: "NotFound",
  947. queryParams: map[string][]string{"aimsid": {"sess"}, "oldGroup": {"Ghost"}, "newGroup": {"Pals"}},
  948. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  949. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  950. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  951. return sessWithOSCAR(aimsid)
  952. },
  953. expectStatusCode: http.StatusOK,
  954. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  955. },
  956. }
  957. for _, tt := range tests {
  958. t.Run(tt.name, func(t *testing.T) {
  959. fs := &MockFeedbagService{}
  960. session := tt.setup(fs, "sess")
  961. handler := &BuddyListHandler{BuddyListManager: newBLM(fs), Logger: slog.Default()}
  962. values := url.Values{}
  963. for k, vs := range tt.queryParams {
  964. for _, v := range vs {
  965. values.Add(k, v)
  966. }
  967. }
  968. req, _ := http.NewRequest("GET", "/buddylist/renameGroup?"+values.Encode(), nil)
  969. rr := httptest.NewRecorder()
  970. handler.RenameGroup(rr, req, session)
  971. assert.Equal(t, tt.expectStatusCode, rr.Code)
  972. assert.Equal(t, tt.expectResponse, strings.TrimSpace(rr.Body.String()))
  973. fs.AssertExpectations(t)
  974. })
  975. }
  976. }
  977. func TestBuddyListHandler_MoveBuddy(t *testing.T) {
  978. newBLM := func(fs *MockFeedbagService) *BuddyListManager {
  979. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  980. }
  981. sessWithOSCAR := func(aimsid string) *state.WebAPISession {
  982. return &state.WebAPISession{
  983. AimSID: aimsid,
  984. ScreenName: state.DisplayScreenName("testuser"),
  985. OSCARSession: state.NewSession().AddInstance(),
  986. EventQueue: types.NewEventQueue(100),
  987. LastAccessed: time.Now(),
  988. }
  989. }
  990. tests := []struct {
  991. name string
  992. queryParams map[string][]string
  993. setup func(*MockFeedbagService, string) *state.WebAPISession
  994. expectStatusCode int
  995. expectResponse string
  996. }{
  997. {
  998. name: "Error_MissingBuddy",
  999. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  1000. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1001. return &state.WebAPISession{AimSID: aimsid, LastAccessed: time.Now()}
  1002. },
  1003. expectStatusCode: http.StatusBadRequest,
  1004. expectResponse: `{"response":{"statusCode":400,"statusText":"missing buddy parameter"}}`,
  1005. },
  1006. {
  1007. name: "Success_Reorder",
  1008. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"bob"}, "group": {"Friends"}, "beforeBuddy": {"alice"}},
  1009. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1010. items := []wire.FeedbagItem{
  1011. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends",
  1012. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{
  1013. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{10, 20}),
  1014. }}},
  1015. {GroupID: 1, ItemID: 10, ClassID: wire.FeedbagClassIdBuddy, Name: "alice"},
  1016. {GroupID: 1, ItemID: 20, ClassID: wire.FeedbagClassIdBuddy, Name: "bob"},
  1017. }
  1018. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1019. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  1020. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  1021. Return((*wire.SNACMessage)(nil), nil).Once()
  1022. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1023. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  1024. return sessWithOSCAR(aimsid)
  1025. },
  1026. expectStatusCode: http.StatusOK,
  1027. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  1028. },
  1029. {
  1030. name: "NotFound_Buddy",
  1031. queryParams: map[string][]string{"aimsid": {"sess"}, "buddy": {"ghost"}, "group": {"Friends"}},
  1032. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1033. items := []wire.FeedbagItem{
  1034. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  1035. }
  1036. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1037. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  1038. return sessWithOSCAR(aimsid)
  1039. },
  1040. expectStatusCode: http.StatusOK,
  1041. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  1042. },
  1043. }
  1044. for _, tt := range tests {
  1045. t.Run(tt.name, func(t *testing.T) {
  1046. fs := &MockFeedbagService{}
  1047. session := tt.setup(fs, "sess")
  1048. handler := &BuddyListHandler{BuddyListManager: newBLM(fs), Logger: slog.Default()}
  1049. values := url.Values{}
  1050. for k, vs := range tt.queryParams {
  1051. for _, v := range vs {
  1052. values.Add(k, v)
  1053. }
  1054. }
  1055. req, _ := http.NewRequest("GET", "/buddylist/moveBuddy?"+values.Encode(), nil)
  1056. rr := httptest.NewRecorder()
  1057. handler.MoveBuddy(rr, req, session)
  1058. assert.Equal(t, tt.expectStatusCode, rr.Code)
  1059. assert.Equal(t, tt.expectResponse, strings.TrimSpace(rr.Body.String()))
  1060. fs.AssertExpectations(t)
  1061. })
  1062. }
  1063. }
  1064. func TestBuddyListHandler_SetBuddyAttribute(t *testing.T) {
  1065. newBLM := func(fs *MockFeedbagService) *BuddyListManager {
  1066. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  1067. }
  1068. sessWithOSCAR := func(aimsid string) *state.WebAPISession {
  1069. return &state.WebAPISession{
  1070. AimSID: aimsid,
  1071. ScreenName: state.DisplayScreenName("testuser"),
  1072. OSCARSession: state.NewSession().AddInstance(),
  1073. EventQueue: types.NewEventQueue(100),
  1074. LastAccessed: time.Now(),
  1075. }
  1076. }
  1077. tests := []struct {
  1078. name string
  1079. queryParams map[string][]string
  1080. setup func(*MockFeedbagService, string) *state.WebAPISession
  1081. expectStatusCode int
  1082. expectResponse string
  1083. }{
  1084. {
  1085. name: "Error_MissingT",
  1086. queryParams: map[string][]string{"aimsid": {"sess"}, "friendly": {"Al"}},
  1087. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1088. return &state.WebAPISession{AimSID: aimsid, LastAccessed: time.Now()}
  1089. },
  1090. expectStatusCode: http.StatusBadRequest,
  1091. expectResponse: `{"response":{"statusCode":400,"statusText":"missing t parameter"}}`,
  1092. },
  1093. {
  1094. name: "Success",
  1095. queryParams: map[string][]string{"aimsid": {"sess"}, "t": {"alice"}, "friendly": {"Al"}},
  1096. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1097. items := []wire.FeedbagItem{
  1098. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  1099. {GroupID: 1, ItemID: 10, ClassID: wire.FeedbagClassIdBuddy, Name: "alice"},
  1100. }
  1101. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1102. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  1103. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  1104. Return((*wire.SNACMessage)(nil), nil).Once()
  1105. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1106. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  1107. return sessWithOSCAR(aimsid)
  1108. },
  1109. expectStatusCode: http.StatusOK,
  1110. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  1111. },
  1112. {
  1113. name: "NotFound",
  1114. queryParams: map[string][]string{"aimsid": {"sess"}, "t": {"ghost"}, "friendly": {"Al"}},
  1115. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1116. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1117. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  1118. return sessWithOSCAR(aimsid)
  1119. },
  1120. expectStatusCode: http.StatusOK,
  1121. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  1122. },
  1123. }
  1124. for _, tt := range tests {
  1125. t.Run(tt.name, func(t *testing.T) {
  1126. fs := &MockFeedbagService{}
  1127. session := tt.setup(fs, "sess")
  1128. handler := &BuddyListHandler{BuddyListManager: newBLM(fs), Logger: slog.Default()}
  1129. values := url.Values{}
  1130. for k, vs := range tt.queryParams {
  1131. for _, v := range vs {
  1132. values.Add(k, v)
  1133. }
  1134. }
  1135. req, _ := http.NewRequest("GET", "/buddylist/setBuddyAttribute?"+values.Encode(), nil)
  1136. rr := httptest.NewRecorder()
  1137. handler.SetBuddyAttribute(rr, req, session)
  1138. assert.Equal(t, tt.expectStatusCode, rr.Code)
  1139. assert.Equal(t, tt.expectResponse, strings.TrimSpace(rr.Body.String()))
  1140. fs.AssertExpectations(t)
  1141. })
  1142. }
  1143. }
  1144. func TestBuddyListHandler_SetGroupAttribute(t *testing.T) {
  1145. newBLM := func(fs *MockFeedbagService) *BuddyListManager {
  1146. return NewBuddyListManager(fs, &MockLocateService{}, newTestIconSource(), slog.Default())
  1147. }
  1148. sessWithOSCAR := func(aimsid string) *state.WebAPISession {
  1149. return &state.WebAPISession{
  1150. AimSID: aimsid,
  1151. ScreenName: state.DisplayScreenName("testuser"),
  1152. OSCARSession: state.NewSession().AddInstance(),
  1153. EventQueue: types.NewEventQueue(100),
  1154. LastAccessed: time.Now(),
  1155. }
  1156. }
  1157. tests := []struct {
  1158. name string
  1159. queryParams map[string][]string
  1160. setup func(*MockFeedbagService, string) *state.WebAPISession
  1161. expectStatusCode int
  1162. expectResponse string
  1163. }{
  1164. {
  1165. name: "Error_MissingCollapsed",
  1166. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}},
  1167. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1168. return &state.WebAPISession{AimSID: aimsid, LastAccessed: time.Now()}
  1169. },
  1170. expectStatusCode: http.StatusBadRequest,
  1171. expectResponse: `{"response":{"statusCode":400,"statusText":"missing collapsed parameter"}}`,
  1172. },
  1173. {
  1174. name: "Success",
  1175. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"Friends"}, "collapsed": {"true"}},
  1176. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1177. items := []wire.FeedbagItem{
  1178. {GroupID: 0, ClassID: wire.FeedbagClassIdGroup, Name: ""},
  1179. {GroupID: 1, ClassID: wire.FeedbagClassIdGroup, Name: "Friends"},
  1180. }
  1181. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1182. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: items}}, nil).Once()
  1183. fs.On("UpsertItem", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
  1184. Return((*wire.SNACMessage)(nil), nil).Once()
  1185. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1186. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  1187. return sessWithOSCAR(aimsid)
  1188. },
  1189. expectStatusCode: http.StatusOK,
  1190. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"success"}}}`,
  1191. },
  1192. {
  1193. name: "NotFound",
  1194. queryParams: map[string][]string{"aimsid": {"sess"}, "group": {"NoSuch"}, "collapsed": {"true"}},
  1195. setup: func(fs *MockFeedbagService, aimsid string) *state.WebAPISession {
  1196. fs.On("Query", mock.Anything, mock.Anything, mock.Anything).
  1197. Return(wire.SNACMessage{Body: wire.SNAC_0x13_0x06_FeedbagReply{Items: nil}}, nil).Once()
  1198. return sessWithOSCAR(aimsid)
  1199. },
  1200. expectStatusCode: http.StatusOK,
  1201. expectResponse: `{"response":{"statusCode":200,"statusText":"OK","data":{"resultCode":"notFound"}}}`,
  1202. },
  1203. }
  1204. for _, tt := range tests {
  1205. t.Run(tt.name, func(t *testing.T) {
  1206. fs := &MockFeedbagService{}
  1207. session := tt.setup(fs, "sess")
  1208. handler := &BuddyListHandler{BuddyListManager: newBLM(fs), Logger: slog.Default()}
  1209. values := url.Values{}
  1210. for k, vs := range tt.queryParams {
  1211. for _, v := range vs {
  1212. values.Add(k, v)
  1213. }
  1214. }
  1215. req, _ := http.NewRequest("GET", "/buddylist/setGroupAttribute?"+values.Encode(), nil)
  1216. rr := httptest.NewRecorder()
  1217. handler.SetGroupAttribute(rr, req, session)
  1218. assert.Equal(t, tt.expectStatusCode, rr.Code)
  1219. assert.Equal(t, tt.expectResponse, strings.TrimSpace(rr.Body.String()))
  1220. fs.AssertExpectations(t)
  1221. })
  1222. }
  1223. }