feedbag_test.go 15 KB

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