feedbag_test.go 14 KB

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