feedbag_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. package handler
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/mkaminski/goaim/oscar"
  6. "github.com/mkaminski/goaim/state"
  7. "github.com/stretchr/testify/assert"
  8. "github.com/stretchr/testify/mock"
  9. )
  10. func TestQueryHandler(t *testing.T) {
  11. cases := []struct {
  12. // name is the unit test name
  13. name string
  14. // screenName is the buddy list owner
  15. screenName string
  16. // feedbagItems is the list of items in user's buddy list
  17. feedbagItems []oscar.FeedbagItem
  18. // lastModified is the time the buddy list was last changed
  19. lastModified time.Time
  20. // inputSNAC is the SNAC sent by the sender client
  21. inputSNAC oscar.SNACMessage
  22. // expectOutput is the SNAC payload sent from the server to the
  23. // recipient client
  24. expectOutput oscar.SNACMessage
  25. }{
  26. {
  27. name: "retrieve empty feedbag",
  28. screenName: "user_screen_name",
  29. feedbagItems: []oscar.FeedbagItem{},
  30. lastModified: time.UnixMilli(0),
  31. inputSNAC: oscar.SNACMessage{
  32. Frame: oscar.SNACFrame{
  33. RequestID: 1234,
  34. },
  35. },
  36. expectOutput: oscar.SNACMessage{
  37. Frame: oscar.SNACFrame{
  38. FoodGroup: oscar.Feedbag,
  39. SubGroup: oscar.FeedbagReply,
  40. RequestID: 1234,
  41. },
  42. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  43. Items: []oscar.FeedbagItem{},
  44. },
  45. },
  46. },
  47. {
  48. name: "retrieve feedbag with items",
  49. screenName: "user_screen_name",
  50. feedbagItems: []oscar.FeedbagItem{
  51. {
  52. Name: "buddy_1",
  53. },
  54. {
  55. Name: "buddy_2",
  56. },
  57. },
  58. lastModified: time.UnixMilli(1696472198082),
  59. inputSNAC: oscar.SNACMessage{
  60. Frame: oscar.SNACFrame{
  61. RequestID: 1234,
  62. },
  63. },
  64. expectOutput: oscar.SNACMessage{
  65. Frame: oscar.SNACFrame{
  66. FoodGroup: oscar.Feedbag,
  67. SubGroup: oscar.FeedbagReply,
  68. RequestID: 1234,
  69. },
  70. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  71. Version: 0,
  72. Items: []oscar.FeedbagItem{
  73. {
  74. Name: "buddy_1",
  75. },
  76. {
  77. Name: "buddy_2",
  78. },
  79. },
  80. LastUpdate: uint32(time.UnixMilli(1696472198082).Unix()),
  81. },
  82. },
  83. },
  84. }
  85. for _, tc := range cases {
  86. t.Run(tc.name, func(t *testing.T) {
  87. //
  88. // initialize dependencies
  89. //
  90. feedbagManager := newMockFeedbagManager(t)
  91. feedbagManager.EXPECT().
  92. Retrieve(tc.screenName).
  93. Return(tc.feedbagItems, nil).
  94. Maybe()
  95. feedbagManager.EXPECT().
  96. LastModified(tc.screenName).
  97. Return(tc.lastModified, nil).
  98. Maybe()
  99. //
  100. // send input SNAC
  101. //
  102. senderSession := newTestSession(tc.screenName)
  103. svc := FeedbagService{
  104. feedbagManager: feedbagManager,
  105. }
  106. outputSNAC, err := svc.QueryHandler(nil, senderSession, tc.inputSNAC.Frame)
  107. assert.NoError(t, err)
  108. assert.Equal(t, tc.expectOutput, outputSNAC)
  109. })
  110. }
  111. }
  112. func TestQueryIfModifiedHandler(t *testing.T) {
  113. cases := []struct {
  114. // name is the unit test name
  115. name string
  116. // screenName is the buddy list owner
  117. screenName string
  118. // feedbagItems is the list of items in user's buddy list
  119. feedbagItems []oscar.FeedbagItem
  120. // lastModified is the time the buddy list was last changed
  121. lastModified time.Time
  122. // inputSNAC is the SNAC sent by the sender client
  123. inputSNAC oscar.SNACMessage
  124. // expectOutput is the SNAC payload sent from the server to the
  125. // recipient client
  126. expectOutput oscar.SNACMessage
  127. }{
  128. {
  129. name: "retrieve empty feedbag",
  130. screenName: "user_screen_name",
  131. feedbagItems: []oscar.FeedbagItem{},
  132. lastModified: time.UnixMilli(0),
  133. inputSNAC: oscar.SNACMessage{
  134. Frame: oscar.SNACFrame{
  135. RequestID: 1234,
  136. },
  137. Body: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
  138. LastUpdate: uint32(time.UnixMilli(100000).Unix()),
  139. },
  140. },
  141. expectOutput: oscar.SNACMessage{
  142. Frame: oscar.SNACFrame{
  143. FoodGroup: oscar.Feedbag,
  144. SubGroup: oscar.FeedbagReply,
  145. RequestID: 1234,
  146. },
  147. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  148. Items: []oscar.FeedbagItem{},
  149. },
  150. },
  151. },
  152. {
  153. name: "retrieve feedbag with items",
  154. screenName: "user_screen_name",
  155. feedbagItems: []oscar.FeedbagItem{
  156. {
  157. Name: "buddy_1",
  158. },
  159. {
  160. Name: "buddy_2",
  161. },
  162. },
  163. lastModified: time.UnixMilli(200000),
  164. inputSNAC: oscar.SNACMessage{
  165. Frame: oscar.SNACFrame{
  166. RequestID: 1234,
  167. },
  168. Body: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
  169. LastUpdate: uint32(time.UnixMilli(100000).Unix()),
  170. },
  171. },
  172. expectOutput: oscar.SNACMessage{
  173. Frame: oscar.SNACFrame{
  174. FoodGroup: oscar.Feedbag,
  175. SubGroup: oscar.FeedbagReply,
  176. RequestID: 1234,
  177. },
  178. Body: oscar.SNAC_0x13_0x06_FeedbagReply{
  179. Version: 0,
  180. Items: []oscar.FeedbagItem{
  181. {
  182. Name: "buddy_1",
  183. },
  184. {
  185. Name: "buddy_2",
  186. },
  187. },
  188. LastUpdate: uint32(time.UnixMilli(200000).Unix()),
  189. },
  190. },
  191. },
  192. {
  193. name: "retrieve not-modified response",
  194. screenName: "user_screen_name",
  195. feedbagItems: []oscar.FeedbagItem{
  196. {
  197. Name: "buddy_1",
  198. },
  199. {
  200. Name: "buddy_2",
  201. },
  202. },
  203. lastModified: time.UnixMilli(100000),
  204. inputSNAC: oscar.SNACMessage{
  205. Frame: oscar.SNACFrame{
  206. RequestID: 1234,
  207. },
  208. Body: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
  209. LastUpdate: uint32(time.UnixMilli(200000).Unix()),
  210. },
  211. },
  212. expectOutput: oscar.SNACMessage{
  213. Frame: oscar.SNACFrame{
  214. FoodGroup: oscar.Feedbag,
  215. SubGroup: oscar.FeedbagReplyNotModified,
  216. RequestID: 1234,
  217. },
  218. Body: oscar.SNAC_0x13_0x05_FeedbagQueryIfModified{
  219. LastUpdate: uint32(time.UnixMilli(100000).Unix()),
  220. Count: 2,
  221. },
  222. },
  223. },
  224. }
  225. for _, tc := range cases {
  226. t.Run(tc.name, func(t *testing.T) {
  227. //
  228. // initialize dependencies
  229. //
  230. feedbagManager := newMockFeedbagManager(t)
  231. feedbagManager.EXPECT().
  232. Retrieve(tc.screenName).
  233. Return(tc.feedbagItems, nil).
  234. Maybe()
  235. feedbagManager.EXPECT().
  236. LastModified(tc.screenName).
  237. Return(tc.lastModified, nil).
  238. Maybe()
  239. //
  240. // send input SNAC
  241. //
  242. senderSession := newTestSession(tc.screenName)
  243. svc := FeedbagService{
  244. feedbagManager: feedbagManager,
  245. }
  246. outputSNAC, err := svc.QueryIfModifiedHandler(nil, senderSession, tc.inputSNAC.Frame,
  247. tc.inputSNAC.Body.(oscar.SNAC_0x13_0x05_FeedbagQueryIfModified))
  248. assert.NoError(t, err)
  249. //
  250. // verify output
  251. //
  252. assert.Equal(t, tc.expectOutput, outputSNAC)
  253. })
  254. }
  255. }
  256. func TestInsertItemHandler(t *testing.T) {
  257. cases := []struct {
  258. // name is the unit test name
  259. name string
  260. // userSession is the session of the user managing buddy list
  261. userSession *state.Session
  262. // feedbagItems is the list of items in user's buddy list
  263. feedbagItems []oscar.FeedbagItem
  264. // inputSNAC is the SNAC sent by the sender client
  265. inputSNAC oscar.SNACMessage
  266. // screenNameLookups is the list of user's online buddies
  267. screenNameLookups map[string]struct {
  268. sess *state.Session
  269. err error
  270. }
  271. // clientResponse is the message returned to the client
  272. clientResponse oscar.SNACMessage
  273. // buddyMessages are events forwarded to buddy clients
  274. buddyMessages []struct {
  275. user string
  276. msg oscar.SNACMessage
  277. }
  278. }{
  279. {
  280. name: "user adds 2 online buddies, expect OK response",
  281. userSession: newTestSession("user_screen_name"),
  282. inputSNAC: oscar.SNACMessage{
  283. Frame: oscar.SNACFrame{
  284. RequestID: 1234,
  285. },
  286. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  287. Items: []oscar.FeedbagItem{
  288. {
  289. ClassID: 2,
  290. Name: "buddy_1_online",
  291. },
  292. {
  293. ClassID: 2,
  294. Name: "buddy_2_online",
  295. },
  296. },
  297. },
  298. },
  299. screenNameLookups: map[string]struct {
  300. sess *state.Session
  301. err error
  302. }{
  303. "user_screen_name": {
  304. sess: newTestSession("user_screen_name", sessOptCannedSignonTime),
  305. },
  306. "buddy_1_online": {
  307. sess: newTestSession("buddy_1_online", sessOptCannedSignonTime),
  308. },
  309. "buddy_2_online": {
  310. sess: newTestSession("buddy_2_online", sessOptCannedSignonTime),
  311. },
  312. },
  313. clientResponse: oscar.SNACMessage{
  314. Frame: oscar.SNACFrame{
  315. FoodGroup: oscar.Feedbag,
  316. SubGroup: oscar.FeedbagStatus,
  317. RequestID: 1234,
  318. },
  319. Body: oscar.SNAC_0x13_0x0E_FeedbagStatus{
  320. Results: []uint16{0x0000, 0x0000},
  321. },
  322. },
  323. buddyMessages: []struct {
  324. user string
  325. msg oscar.SNACMessage
  326. }{
  327. {
  328. user: "user_screen_name",
  329. msg: oscar.SNACMessage{
  330. Frame: oscar.SNACFrame{
  331. FoodGroup: oscar.Buddy,
  332. SubGroup: oscar.BuddyArrived,
  333. },
  334. Body: oscar.SNAC_0x03_0x0B_BuddyArrived{
  335. TLVUserInfo: oscar.TLVUserInfo{
  336. ScreenName: "buddy_1_online",
  337. TLVBlock: oscar.TLVBlock{
  338. TLVList: newTestSession("", sessOptCannedSignonTime).UserInfo(),
  339. },
  340. },
  341. },
  342. },
  343. },
  344. {
  345. user: "user_screen_name",
  346. msg: oscar.SNACMessage{
  347. Frame: oscar.SNACFrame{
  348. FoodGroup: oscar.Buddy,
  349. SubGroup: oscar.BuddyArrived,
  350. },
  351. Body: oscar.SNAC_0x03_0x0B_BuddyArrived{
  352. TLVUserInfo: oscar.TLVUserInfo{
  353. ScreenName: "buddy_2_online",
  354. TLVBlock: oscar.TLVBlock{
  355. TLVList: newTestSession("", sessOptCannedSignonTime).UserInfo(),
  356. },
  357. },
  358. },
  359. },
  360. },
  361. },
  362. },
  363. {
  364. name: "user adds an offline buddy, expect OK response and 0 buddy arrived events",
  365. userSession: newTestSession("user_screen_name"),
  366. inputSNAC: oscar.SNACMessage{
  367. Frame: oscar.SNACFrame{
  368. RequestID: 1234,
  369. },
  370. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  371. Items: []oscar.FeedbagItem{
  372. {
  373. ClassID: 2,
  374. Name: "buddy_offline",
  375. },
  376. },
  377. },
  378. },
  379. screenNameLookups: map[string]struct {
  380. sess *state.Session
  381. err error
  382. }{
  383. "buddy_offline": {
  384. sess: nil,
  385. },
  386. },
  387. clientResponse: oscar.SNACMessage{
  388. Frame: oscar.SNACFrame{
  389. FoodGroup: oscar.Feedbag,
  390. SubGroup: oscar.FeedbagStatus,
  391. RequestID: 1234,
  392. },
  393. Body: oscar.SNAC_0x13_0x0E_FeedbagStatus{
  394. Results: []uint16{0x0000},
  395. },
  396. },
  397. },
  398. {
  399. name: "users adds an invisible buddy, expect OK response and 0 buddy arrived events",
  400. userSession: newTestSession("user_screen_name"),
  401. inputSNAC: oscar.SNACMessage{
  402. Frame: oscar.SNACFrame{
  403. RequestID: 1234,
  404. },
  405. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  406. Items: []oscar.FeedbagItem{
  407. {
  408. ClassID: 2,
  409. Name: "invisible_buddy_online",
  410. },
  411. },
  412. },
  413. },
  414. screenNameLookups: map[string]struct {
  415. sess *state.Session
  416. err error
  417. }{
  418. "invisible_buddy_online": {
  419. sess: newTestSession("invisible_buddy_online", sessOptInvisible),
  420. },
  421. },
  422. clientResponse: oscar.SNACMessage{
  423. Frame: oscar.SNACFrame{
  424. FoodGroup: oscar.Feedbag,
  425. SubGroup: oscar.FeedbagStatus,
  426. RequestID: 1234,
  427. },
  428. Body: oscar.SNAC_0x13_0x0E_FeedbagStatus{
  429. Results: []uint16{0x0000},
  430. },
  431. },
  432. },
  433. {
  434. name: "user blocks buddy currently online, expect OK response, buddy departed event client, 1 buddy " +
  435. "departed event sent to buddy",
  436. userSession: newTestSession("user_screen_name"),
  437. inputSNAC: oscar.SNACMessage{
  438. Frame: oscar.SNACFrame{
  439. RequestID: 1234,
  440. },
  441. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  442. Items: []oscar.FeedbagItem{
  443. {
  444. ClassID: 3,
  445. Name: "buddy_1",
  446. },
  447. },
  448. },
  449. },
  450. screenNameLookups: map[string]struct {
  451. sess *state.Session
  452. err error
  453. }{
  454. "user_screen_name": {
  455. sess: newTestSession("user_screen_name"),
  456. },
  457. "buddy_1": {
  458. sess: newTestSession("buddy_1"),
  459. },
  460. },
  461. buddyMessages: []struct {
  462. user string
  463. msg oscar.SNACMessage
  464. }{
  465. {
  466. user: "buddy_1",
  467. msg: oscar.SNACMessage{
  468. Frame: oscar.SNACFrame{
  469. FoodGroup: oscar.Buddy,
  470. SubGroup: oscar.BuddyDeparted,
  471. },
  472. Body: oscar.SNAC_0x03_0x0C_BuddyDeparted{
  473. TLVUserInfo: oscar.TLVUserInfo{
  474. ScreenName: "user_screen_name",
  475. WarningLevel: 0,
  476. },
  477. },
  478. },
  479. },
  480. {
  481. user: "user_screen_name",
  482. msg: oscar.SNACMessage{
  483. Frame: oscar.SNACFrame{
  484. FoodGroup: oscar.Buddy,
  485. SubGroup: oscar.BuddyDeparted,
  486. },
  487. Body: oscar.SNAC_0x03_0x0C_BuddyDeparted{
  488. TLVUserInfo: oscar.TLVUserInfo{
  489. ScreenName: "buddy_1",
  490. WarningLevel: 0,
  491. },
  492. },
  493. },
  494. },
  495. },
  496. clientResponse: oscar.SNACMessage{
  497. Frame: oscar.SNACFrame{
  498. FoodGroup: oscar.Feedbag,
  499. SubGroup: oscar.FeedbagStatus,
  500. RequestID: 1234,
  501. },
  502. Body: oscar.SNAC_0x13_0x0E_FeedbagStatus{
  503. Results: []uint16{0x0000},
  504. },
  505. },
  506. },
  507. {
  508. name: "user blocks buddy currently offline, expect OK response and a superfluous buddy departed events",
  509. userSession: newTestSession("user_screen_name"),
  510. inputSNAC: oscar.SNACMessage{
  511. Frame: oscar.SNACFrame{
  512. RequestID: 1234,
  513. },
  514. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  515. Items: []oscar.FeedbagItem{
  516. {
  517. ClassID: 3,
  518. Name: "buddy_1",
  519. },
  520. },
  521. },
  522. },
  523. screenNameLookups: map[string]struct {
  524. sess *state.Session
  525. err error
  526. }{
  527. "user_screen_name": {
  528. sess: newTestSession("user_screen_name"),
  529. },
  530. "buddy_1": {
  531. sess: nil,
  532. },
  533. },
  534. clientResponse: oscar.SNACMessage{
  535. Frame: oscar.SNACFrame{
  536. FoodGroup: oscar.Feedbag,
  537. SubGroup: oscar.FeedbagStatus,
  538. RequestID: 1234,
  539. },
  540. Body: oscar.SNAC_0x13_0x0E_FeedbagStatus{
  541. Results: []uint16{0x0000},
  542. },
  543. },
  544. buddyMessages: []struct {
  545. user string
  546. msg oscar.SNACMessage
  547. }{
  548. {
  549. user: "buddy_1",
  550. msg: oscar.SNACMessage{
  551. Frame: oscar.SNACFrame{
  552. FoodGroup: oscar.Buddy,
  553. SubGroup: oscar.BuddyDeparted,
  554. },
  555. Body: oscar.SNAC_0x03_0x0C_BuddyDeparted{
  556. TLVUserInfo: oscar.TLVUserInfo{
  557. ScreenName: "user_screen_name",
  558. WarningLevel: 0,
  559. },
  560. },
  561. },
  562. },
  563. },
  564. },
  565. {
  566. name: "user tries to block themselves, expect feedback error",
  567. userSession: newTestSession("user_screen_name"),
  568. inputSNAC: oscar.SNACMessage{
  569. Frame: oscar.SNACFrame{
  570. RequestID: 1234,
  571. },
  572. Body: oscar.SNAC_0x13_0x08_FeedbagInsertItem{
  573. Items: []oscar.FeedbagItem{
  574. {
  575. ClassID: 3,
  576. Name: "user_screen_name",
  577. },
  578. },
  579. },
  580. },
  581. clientResponse: oscar.SNACMessage{
  582. Frame: oscar.SNACFrame{
  583. FoodGroup: oscar.Feedbag,
  584. SubGroup: oscar.FeedbagErr,
  585. RequestID: 1234,
  586. },
  587. Body: oscar.SNACError{
  588. Code: oscar.ErrorCodeNotSupportedByHost,
  589. },
  590. },
  591. },
  592. }
  593. for _, tc := range cases {
  594. t.Run(tc.name, func(t *testing.T) {
  595. //
  596. // initialize dependencies
  597. //
  598. feedbagManager := newMockFeedbagManager(t)
  599. feedbagManager.EXPECT().
  600. Upsert(tc.userSession.ScreenName(), tc.inputSNAC.Body.(oscar.SNAC_0x13_0x08_FeedbagInsertItem).Items).
  601. Return(nil).
  602. Maybe()
  603. feedbagManager.EXPECT().
  604. Buddies(tc.userSession.ScreenName()).
  605. Return([]string{}, nil).
  606. Maybe()
  607. messageRelayer := newMockMessageRelayer(t)
  608. for screenName, val := range tc.screenNameLookups {
  609. messageRelayer.EXPECT().
  610. RetrieveByScreenName(screenName).
  611. Return(val.sess).
  612. Maybe()
  613. }
  614. for _, n := range tc.buddyMessages {
  615. messageRelayer.EXPECT().
  616. SendToScreenName(mock.Anything, n.user, n.msg).
  617. Maybe()
  618. }
  619. //
  620. // send input SNAC
  621. //
  622. svc := FeedbagService{
  623. feedbagManager: feedbagManager,
  624. messageRelayer: messageRelayer,
  625. }
  626. output, err := svc.InsertItemHandler(nil, tc.userSession, tc.inputSNAC.Frame,
  627. tc.inputSNAC.Body.(oscar.SNAC_0x13_0x08_FeedbagInsertItem))
  628. assert.NoError(t, err)
  629. //
  630. // verify response
  631. //
  632. assert.Equal(t, output, tc.clientResponse)
  633. })
  634. }
  635. }