4
0

cmd_server_test.go 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808
  1. package toc
  2. import (
  3. "context"
  4. "log/slog"
  5. "net"
  6. "sync"
  7. "testing"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/mk6i/open-oscar-server/wire"
  10. )
  11. func TestOSCARProxy_RecvBOS_ChatIn(t *testing.T) {
  12. cases := []struct {
  13. // name is the unit test name
  14. name string
  15. // me is the TOC user session
  16. me *tocConn
  17. // chatID is the chat ID
  18. chatID int
  19. // givenMsg is the incoming SNAC
  20. givenMsg wire.SNACMessage
  21. // wantCmd is the expected TOC response
  22. wantCmd string
  23. }{
  24. {
  25. name: "send chat message - plain CHAT_IN",
  26. me: testConn(),
  27. chatID: 0,
  28. givenMsg: wire.SNACMessage{
  29. Body: wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
  30. TLVRestBlock: wire.TLVRestBlock{
  31. TLVList: wire.TLVList{
  32. wire.NewTLVBE(wire.ChatTLVSenderInformation, wire.TLVUserInfo{
  33. ScreenName: "them",
  34. }),
  35. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  36. TLVList: wire.TLVList{
  37. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "<p>hello world!</p>"),
  38. },
  39. }),
  40. },
  41. },
  42. },
  43. },
  44. wantCmd: "CHAT_IN:0:them:F:<p>hello world!</p>",
  45. },
  46. {
  47. name: "send chat message - TOC2 encoded CHAT_IN_ENC",
  48. me: testConn(withTOC2(true)),
  49. chatID: 0,
  50. givenMsg: wire.SNACMessage{
  51. Body: wire.SNAC_0x0E_0x06_ChatChannelMsgToClient{
  52. TLVRestBlock: wire.TLVRestBlock{
  53. TLVList: wire.TLVList{
  54. wire.NewTLVBE(wire.ChatTLVSenderInformation, wire.TLVUserInfo{
  55. ScreenName: "them",
  56. }),
  57. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  58. TLVList: wire.TLVList{
  59. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "<p>hello world!</p>"),
  60. },
  61. }),
  62. },
  63. },
  64. },
  65. },
  66. wantCmd: "CHAT_IN_ENC:0:them:F:A:en:<p>hello world!</p>",
  67. },
  68. }
  69. for _, tc := range cases {
  70. t.Run(tc.name, func(t *testing.T) {
  71. ctx, cancel := context.WithCancel(context.Background())
  72. svc := OSCARProxy{
  73. Logger: slog.Default(),
  74. }
  75. ch := make(chan []string)
  76. tc.me.toCh = ch
  77. chatConn := newFakeOSCARConn()
  78. wg := &sync.WaitGroup{}
  79. wg.Add(1)
  80. go func() {
  81. defer wg.Done()
  82. svc.RecvChat(ctx, tc.me, chatConn, tc.chatID)
  83. }()
  84. chatConn.msgs <- tc.givenMsg
  85. gotCmd := <-ch
  86. assert.Equal(t, tc.wantCmd, gotCmd[0])
  87. cancel()
  88. wg.Wait()
  89. })
  90. }
  91. }
  92. func TestOSCARProxy_RecvBOS_ChatUpdateBuddyArrived(t *testing.T) {
  93. cases := []struct {
  94. // name is the unit test name
  95. name string
  96. // me is the TOC user session
  97. me *tocConn
  98. // chatID is the chat ID
  99. chatID int
  100. // givenMsg is the incoming SNAC
  101. givenMsg wire.SNACMessage
  102. // wantCmd is the expected TOC response
  103. wantCmd []string
  104. }{
  105. {
  106. name: "send chat participant arrival",
  107. me: testConn(),
  108. givenMsg: wire.SNACMessage{
  109. Body: wire.SNAC_0x0E_0x03_ChatUsersJoined{
  110. Users: []wire.TLVUserInfo{
  111. {ScreenName: "user1"},
  112. {ScreenName: "user2"},
  113. },
  114. },
  115. },
  116. wantCmd: []string{"CHAT_UPDATE_BUDDY:0:T:user1:user2"},
  117. },
  118. }
  119. for _, tc := range cases {
  120. t.Run(tc.name, func(t *testing.T) {
  121. ctx, cancel := context.WithCancel(context.Background())
  122. svc := OSCARProxy{
  123. Logger: slog.Default(),
  124. }
  125. ch := make(chan []string)
  126. tc.me.toCh = ch
  127. chatConn := newFakeOSCARConn()
  128. wg := &sync.WaitGroup{}
  129. wg.Add(1)
  130. go func() {
  131. defer wg.Done()
  132. svc.RecvChat(ctx, tc.me, chatConn, tc.chatID)
  133. }()
  134. chatConn.msgs <- tc.givenMsg
  135. gotCmd := <-ch
  136. assert.Equal(t, tc.wantCmd[0], gotCmd[0])
  137. cancel()
  138. wg.Wait()
  139. })
  140. }
  141. }
  142. func TestOSCARProxy_RecvBOS_ChatUpdateBuddyLeft(t *testing.T) {
  143. cases := []struct {
  144. // name is the unit test name
  145. name string
  146. // me is the TOC user session
  147. me *tocConn
  148. // chatID is the chat ID
  149. chatID int
  150. // givenMsg is the incoming SNAC
  151. givenMsg wire.SNACMessage
  152. // wantCmd is the expected TOC response
  153. wantCmd []string
  154. }{
  155. {
  156. name: "send chat participant departure",
  157. me: testConn(),
  158. givenMsg: wire.SNACMessage{
  159. Body: wire.SNAC_0x0E_0x04_ChatUsersLeft{
  160. Users: []wire.TLVUserInfo{
  161. {ScreenName: "user1"},
  162. {ScreenName: "user2"},
  163. },
  164. },
  165. },
  166. wantCmd: []string{"CHAT_UPDATE_BUDDY:0:F:user1:user2"},
  167. },
  168. }
  169. for _, tc := range cases {
  170. t.Run(tc.name, func(t *testing.T) {
  171. ctx, cancel := context.WithCancel(context.Background())
  172. svc := OSCARProxy{
  173. Logger: slog.Default(),
  174. }
  175. ch := make(chan []string)
  176. tc.me.toCh = ch
  177. chatConn := newFakeOSCARConn()
  178. wg := &sync.WaitGroup{}
  179. wg.Add(1)
  180. go func() {
  181. defer wg.Done()
  182. svc.RecvChat(ctx, tc.me, chatConn, tc.chatID)
  183. }()
  184. chatConn.msgs <- tc.givenMsg
  185. gotCmd := <-ch
  186. assert.Equal(t, tc.wantCmd[0], gotCmd[0])
  187. cancel()
  188. wg.Wait()
  189. })
  190. }
  191. }
  192. func TestOSCARProxy_RecvBOS_Eviled(t *testing.T) {
  193. cases := []struct {
  194. // name is the unit test name
  195. name string
  196. // me is the TOC user session
  197. me *tocConn
  198. // givenMsg is the incoming SNAC
  199. givenMsg wire.SNACMessage
  200. // chatRegistry is the chat registry for the current session
  201. chatRegistry *ChatRegistry
  202. // wantCmd is the expected TOC response
  203. wantCmd []string
  204. }{
  205. {
  206. name: "anonymous warning - 10%",
  207. me: testConn(),
  208. givenMsg: wire.SNACMessage{
  209. Body: wire.SNAC_0x01_0x10_OServiceEvilNotification{
  210. NewEvil: 100,
  211. },
  212. },
  213. wantCmd: []string{"EVILED:10:"},
  214. },
  215. {
  216. name: "normal warning - 10%",
  217. me: testConn(),
  218. givenMsg: wire.SNACMessage{
  219. Body: wire.SNAC_0x01_0x10_OServiceEvilNotification{
  220. NewEvil: 100,
  221. Snitcher: &struct {
  222. wire.TLVUserInfo
  223. }{
  224. TLVUserInfo: wire.TLVUserInfo{
  225. ScreenName: "them",
  226. },
  227. },
  228. },
  229. },
  230. wantCmd: []string{"EVILED:10:them"},
  231. },
  232. }
  233. for _, tc := range cases {
  234. t.Run(tc.name, func(t *testing.T) {
  235. ctx, cancel := context.WithCancel(context.Background())
  236. svc := testOSCARProxy(t)
  237. ch := make(chan []string)
  238. tc.me.toCh = ch
  239. wg := &sync.WaitGroup{}
  240. wg.Add(1)
  241. go func() {
  242. defer wg.Done()
  243. err := svc.RecvBOS(ctx, tc.me)
  244. assert.NoError(t, err)
  245. }()
  246. tc.me.bos.(*fakeOSCARConn).msgs <- tc.givenMsg
  247. gotCmd := <-ch
  248. assert.Equal(t, tc.wantCmd[0], gotCmd[0])
  249. cancel()
  250. wg.Wait()
  251. })
  252. }
  253. }
  254. func TestOSCARProxy_RecvBOS_IMIn(t *testing.T) {
  255. cases := []struct {
  256. // name is the unit test name
  257. name string
  258. // me is the TOC user session
  259. me *tocConn
  260. // givenMsg is the incoming SNAC
  261. givenMsg wire.SNACMessage
  262. // wantCmd is the expected TOC response
  263. wantCmd []string
  264. }{
  265. {
  266. name: "send IM",
  267. me: testConn(),
  268. givenMsg: wire.SNACMessage{
  269. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  270. ChannelID: wire.ICBMChannelIM,
  271. TLVUserInfo: wire.TLVUserInfo{
  272. ScreenName: "them",
  273. },
  274. TLVRestBlock: wire.TLVRestBlock{
  275. TLVList: wire.TLVList{
  276. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  277. {
  278. ID: 0x5,
  279. Version: 0x1,
  280. Payload: []uint8{0x1, 0x1, 0x2},
  281. },
  282. {
  283. ID: 0x1,
  284. Version: 0x1,
  285. Payload: []uint8{
  286. 0x0, 0x0, // charset
  287. 0x0, 0x0, // lang
  288. 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!',
  289. },
  290. },
  291. }),
  292. },
  293. },
  294. },
  295. },
  296. wantCmd: []string{"IM_IN:them:F:hello world!"},
  297. },
  298. {
  299. name: "send IM - auto-response",
  300. me: testConn(),
  301. givenMsg: wire.SNACMessage{
  302. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  303. ChannelID: wire.ICBMChannelIM,
  304. TLVUserInfo: wire.TLVUserInfo{
  305. ScreenName: "them",
  306. },
  307. TLVRestBlock: wire.TLVRestBlock{
  308. TLVList: wire.TLVList{
  309. wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}),
  310. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  311. {
  312. ID: 0x5,
  313. Version: 0x1,
  314. Payload: []uint8{0x1, 0x1, 0x2},
  315. },
  316. {
  317. ID: 0x1,
  318. Version: 0x1,
  319. Payload: []uint8{
  320. 0x0, 0x0, // charset
  321. 0x0, 0x0, // lang
  322. 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!',
  323. },
  324. },
  325. }),
  326. },
  327. },
  328. },
  329. },
  330. wantCmd: []string{"IM_IN:them:T:hello world!"},
  331. },
  332. {
  333. name: "send IM - TOC2 (IM_IN2)",
  334. me: testConn(withTOC2(false)),
  335. givenMsg: wire.SNACMessage{
  336. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  337. ChannelID: wire.ICBMChannelIM,
  338. TLVUserInfo: wire.TLVUserInfo{
  339. ScreenName: "them",
  340. },
  341. TLVRestBlock: wire.TLVRestBlock{
  342. TLVList: wire.TLVList{
  343. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  344. {ID: 0x5, Version: 0x1, Payload: []uint8{0x1, 0x1, 0x2}},
  345. {ID: 0x1, Version: 0x1, Payload: []uint8{0x0, 0x0, 0x0, 0x0, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!'}},
  346. }),
  347. },
  348. },
  349. },
  350. },
  351. wantCmd: []string{"IM_IN2:them:F:F:hello world!"},
  352. },
  353. {
  354. name: "send IM - TOC2 with encoded messaging (IM_IN_ENC2)",
  355. me: testConn(withTOC2(true)),
  356. givenMsg: wire.SNACMessage{
  357. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  358. ChannelID: wire.ICBMChannelIM,
  359. TLVUserInfo: wire.TLVUserInfo{
  360. ScreenName: "them",
  361. TLVBlock: wire.TLVBlock{
  362. TLVList: wire.TLVList{
  363. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  364. },
  365. },
  366. },
  367. TLVRestBlock: wire.TLVRestBlock{
  368. TLVList: wire.TLVList{
  369. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  370. {ID: 0x5, Version: 0x1, Payload: []uint8{0x1, 0x1, 0x2}},
  371. {ID: 0x1, Version: 0x1, Payload: []uint8{0x0, 0x0, 0x0, 0x0, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!'}},
  372. }),
  373. },
  374. },
  375. },
  376. },
  377. wantCmd: []string{"IM_IN_ENC2:them:F:F:T: O :F:L:en:hello world!"},
  378. },
  379. {
  380. name: "send IM - TOC2 encoded messaging missing OServiceUserInfoUserFlags returns empty",
  381. me: testConn(withTOC2(true)),
  382. givenMsg: wire.SNACMessage{
  383. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  384. ChannelID: wire.ICBMChannelIM,
  385. TLVUserInfo: wire.TLVUserInfo{
  386. ScreenName: "them",
  387. // no TLVList / OServiceUserInfoUserFlags
  388. },
  389. TLVRestBlock: wire.TLVRestBlock{
  390. TLVList: wire.TLVList{
  391. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  392. {ID: 0x5, Version: 0x1, Payload: []uint8{0x1, 0x1, 0x2}},
  393. {ID: 0x1, Version: 0x1, Payload: []uint8{0x0, 0x0, 0x0, 0x0, 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!'}},
  394. }),
  395. },
  396. },
  397. },
  398. },
  399. wantCmd: []string{"IM_IN_ENC2:them:F:F:T: :F:L:en:hello world!"},
  400. },
  401. {
  402. name: "send chat invitation",
  403. me: testConn(),
  404. givenMsg: wire.SNACMessage{
  405. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  406. ChannelID: wire.ICBMChannelRendezvous,
  407. TLVUserInfo: wire.TLVUserInfo{
  408. ScreenName: "them",
  409. },
  410. TLVRestBlock: wire.TLVRestBlock{
  411. TLVList: wire.TLVList{
  412. wire.NewTLVBE(wire.ICBMTLVData, []wire.ICBMCh2Fragment{
  413. {
  414. Capability: wire.CapChat,
  415. TLVRestBlock: wire.TLVRestBlock{
  416. TLVList: wire.TLVList{
  417. wire.NewTLVBE(wire.ICBMRdvTLVTagsInvitation, "join my chat!"),
  418. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, wire.ICBMRoomInfo{
  419. Cookie: "a-b-the room",
  420. }),
  421. },
  422. },
  423. },
  424. }),
  425. },
  426. },
  427. },
  428. },
  429. wantCmd: []string{"CHAT_INVITE:the room:0:them:join my chat!"},
  430. },
  431. {
  432. name: "receive file transfer rendezvous IM",
  433. me: testConn(),
  434. givenMsg: wire.SNACMessage{
  435. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  436. ChannelID: wire.ICBMChannelRendezvous,
  437. TLVUserInfo: newTestSession("them").Session().TLVUserInfo(),
  438. TLVRestBlock: wire.TLVRestBlock{
  439. TLVList: wire.TLVList{
  440. wire.NewTLVBE(wire.ICBMTLVWantEvents, []byte{}),
  441. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  442. Cookie: [8]byte{'h', 'a', 'h', 'a', 'h', 'a', 'h', 'a'},
  443. Type: wire.ICBMRdvMessagePropose,
  444. Capability: wire.CapFileTransfer,
  445. TLVRestBlock: wire.TLVRestBlock{
  446. TLVList: wire.TLVList{
  447. wire.NewTLVBE(wire.ICBMRdvTLVTagsSeqNum, uint16(1)),
  448. wire.NewTLVBE(wire.ICBMRdvTLVTagsPort, uint16(4000)),
  449. wire.NewTLVBE(wire.ICBMRdvTLVTagsRdvIP, net.ParseIP("129.168.0.1").To4()),
  450. wire.NewTLVBE(wire.ICBMRdvTLVTagsRequesterIP, net.ParseIP("129.168.0.2").To4()),
  451. wire.NewTLVBE(wire.ICBMRdvTLVTagsVerifiedIP, net.ParseIP("129.168.0.3").To4()),
  452. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, []byte{'l', 'o', 'l'}),
  453. },
  454. },
  455. }),
  456. },
  457. },
  458. },
  459. },
  460. wantCmd: []string{"RVOUS_PROPOSE:them:09461343-4C7F-11D1-8222-444553540000:aGFoYWhhaGE=:1:129.168.0.1:129.168.0.2:129.168.0.3:4000:10001:bG9s"},
  461. },
  462. }
  463. for _, tc := range cases {
  464. t.Run(tc.name, func(t *testing.T) {
  465. ctx, cancel := context.WithCancel(context.Background())
  466. svc := testOSCARProxy(t)
  467. ch := make(chan []string)
  468. tc.me.toCh = ch
  469. wg := &sync.WaitGroup{}
  470. wg.Add(1)
  471. go func() {
  472. defer wg.Done()
  473. err := svc.RecvBOS(ctx, tc.me)
  474. assert.NoError(t, err)
  475. }()
  476. tc.me.bos.(*fakeOSCARConn).msgs <- tc.givenMsg
  477. gotCmd := <-ch
  478. assert.Equal(t, tc.wantCmd[0], gotCmd[0])
  479. cancel()
  480. wg.Wait()
  481. })
  482. }
  483. }
  484. func TestOSCARProxy_RecvBOS_UpdateBuddyArrival(t *testing.T) {
  485. cases := []struct {
  486. // name is the unit test name
  487. name string
  488. // me is the TOC user session
  489. me *tocConn
  490. // givenMsg is the incoming SNAC
  491. givenMsg wire.SNACMessage
  492. // wantCmd is the expected TOC response
  493. wantCmd []string
  494. }{
  495. {
  496. name: "send buddy arrival - buddy online",
  497. me: testConn(),
  498. givenMsg: wire.SNACMessage{
  499. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  500. TLVUserInfo: wire.TLVUserInfo{
  501. ScreenName: "me",
  502. WarningLevel: 0,
  503. TLVBlock: wire.TLVBlock{
  504. TLVList: wire.TLVList{
  505. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  506. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  507. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  508. },
  509. },
  510. },
  511. },
  512. },
  513. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678: O "},
  514. },
  515. {
  516. name: "send buddy arrival - buddy warned 10%",
  517. me: testConn(),
  518. givenMsg: wire.SNACMessage{
  519. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  520. TLVUserInfo: wire.TLVUserInfo{
  521. ScreenName: "me",
  522. WarningLevel: 100,
  523. TLVBlock: wire.TLVBlock{
  524. TLVList: wire.TLVList{
  525. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  526. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  527. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  528. },
  529. },
  530. },
  531. },
  532. },
  533. wantCmd: []string{"UPDATE_BUDDY:me:T:10:1234:5678: O "},
  534. },
  535. {
  536. name: "send buddy arrival - buddy away",
  537. me: testConn(),
  538. givenMsg: wire.SNACMessage{
  539. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  540. TLVUserInfo: wire.TLVUserInfo{
  541. ScreenName: "me",
  542. WarningLevel: 0,
  543. TLVBlock: wire.TLVBlock{
  544. TLVList: wire.TLVList{
  545. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  546. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  547. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree|wire.OServiceUserFlagUnavailable),
  548. },
  549. },
  550. },
  551. },
  552. },
  553. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678: OU"},
  554. },
  555. {
  556. name: "send buddy arrival - user class AOL (userClassString uc[0])",
  557. me: testConn(),
  558. givenMsg: wire.SNACMessage{
  559. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  560. TLVUserInfo: wire.TLVUserInfo{
  561. ScreenName: "me",
  562. WarningLevel: 0,
  563. TLVBlock: wire.TLVBlock{
  564. TLVList: wire.TLVList{
  565. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  566. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  567. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagAOL),
  568. },
  569. },
  570. },
  571. },
  572. },
  573. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678:A "},
  574. },
  575. {
  576. name: "send buddy arrival - user class Administrator (userClassString uc[1])",
  577. me: testConn(),
  578. givenMsg: wire.SNACMessage{
  579. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  580. TLVUserInfo: wire.TLVUserInfo{
  581. ScreenName: "me",
  582. WarningLevel: 0,
  583. TLVBlock: wire.TLVBlock{
  584. TLVList: wire.TLVList{
  585. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  586. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  587. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagAdministrator),
  588. },
  589. },
  590. },
  591. },
  592. },
  593. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678: A "},
  594. },
  595. {
  596. name: "send buddy arrival - user class Wireless (userClassString uc[1])",
  597. me: testConn(),
  598. givenMsg: wire.SNACMessage{
  599. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  600. TLVUserInfo: wire.TLVUserInfo{
  601. ScreenName: "me",
  602. WarningLevel: 0,
  603. TLVBlock: wire.TLVBlock{
  604. TLVList: wire.TLVList{
  605. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  606. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  607. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagWireless),
  608. },
  609. },
  610. },
  611. },
  612. },
  613. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678: C "},
  614. },
  615. {
  616. name: "send buddy arrival - user class Unconfirmed (userClassString uc[1])",
  617. me: testConn(),
  618. givenMsg: wire.SNACMessage{
  619. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  620. TLVUserInfo: wire.TLVUserInfo{
  621. ScreenName: "me",
  622. WarningLevel: 0,
  623. TLVBlock: wire.TLVBlock{
  624. TLVList: wire.TLVList{
  625. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  626. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  627. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagUnconfirmed),
  628. },
  629. },
  630. },
  631. },
  632. },
  633. wantCmd: []string{"UPDATE_BUDDY:me:T:0:1234:5678: U "},
  634. },
  635. {
  636. name: "send buddy arrival - TOC2 no caps (userInfoToBuddyCaps returns empty when no OServiceUserInfoOscarCaps TLV)",
  637. me: testConn(withTOC2(false)),
  638. givenMsg: wire.SNACMessage{
  639. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  640. TLVUserInfo: wire.TLVUserInfo{
  641. ScreenName: "buddy",
  642. WarningLevel: 0,
  643. TLVBlock: wire.TLVBlock{
  644. TLVList: wire.TLVList{
  645. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  646. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  647. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  648. },
  649. },
  650. },
  651. },
  652. },
  653. wantCmd: []string{"UPDATE_BUDDY2:buddy:T:0:1234:5678: O :"},
  654. },
  655. {
  656. name: "send buddy arrival - TOC2 caps length not divisible by 16 (userInfoToBuddyCaps returns empty)",
  657. me: testConn(withTOC2(false)),
  658. givenMsg: wire.SNACMessage{
  659. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  660. TLVUserInfo: wire.TLVUserInfo{
  661. ScreenName: "buddy",
  662. WarningLevel: 0,
  663. TLVBlock: wire.TLVBlock{
  664. TLVList: wire.TLVList{
  665. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  666. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  667. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  668. // Invalid: 8 bytes, not divisible by 16
  669. wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, []byte{0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4}),
  670. },
  671. },
  672. },
  673. },
  674. },
  675. wantCmd: []string{"UPDATE_BUDDY2:buddy:T:0:1234:5678: O :"},
  676. },
  677. {
  678. name: "send buddy arrival - TOC2 with one capability (userInfoToBuddyCaps formats caps as UUIDs)",
  679. me: testConn(withTOC2(false)),
  680. givenMsg: wire.SNACMessage{
  681. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  682. TLVUserInfo: wire.TLVUserInfo{
  683. ScreenName: "buddy",
  684. WarningLevel: 0,
  685. TLVBlock: wire.TLVBlock{
  686. TLVList: wire.TLVList{
  687. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  688. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  689. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  690. // One 16-byte cap: UUID 550e8400-e29b-41d4-a716-446655440000
  691. wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, []byte{
  692. 0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4,
  693. 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00,
  694. }),
  695. },
  696. },
  697. },
  698. },
  699. },
  700. wantCmd: []string{"UPDATE_BUDDY2:buddy:T:0:1234:5678: O :", "BUDDY_CAPS2:buddy:550e8400-e29b-41d4-a716-446655440000"},
  701. },
  702. {
  703. name: "send buddy arrival - TOC2 with two capabilities",
  704. me: testConn(withTOC2(false)),
  705. givenMsg: wire.SNACMessage{
  706. Body: wire.SNAC_0x03_0x0B_BuddyArrived{
  707. TLVUserInfo: wire.TLVUserInfo{
  708. ScreenName: "buddy",
  709. WarningLevel: 0,
  710. TLVBlock: wire.TLVBlock{
  711. TLVList: wire.TLVList{
  712. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  713. wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(5678)),
  714. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  715. // Two 16-byte caps
  716. wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, []byte{
  717. 0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00,
  718. 0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00,
  719. }),
  720. },
  721. },
  722. },
  723. },
  724. },
  725. wantCmd: []string{"UPDATE_BUDDY2:buddy:T:0:1234:5678: O :", "BUDDY_CAPS2:buddy:550e8400-e29b-41d4-a716-446655440000,748f2420-6287-11d1-8222-444553540000"},
  726. },
  727. }
  728. for _, tc := range cases {
  729. t.Run(tc.name, func(t *testing.T) {
  730. ctx, cancel := context.WithCancel(context.Background())
  731. svc := testOSCARProxy(t)
  732. ch := make(chan []string)
  733. tc.me.toCh = ch
  734. wg := &sync.WaitGroup{}
  735. wg.Add(1)
  736. go func() {
  737. defer wg.Done()
  738. err := svc.RecvBOS(ctx, tc.me)
  739. assert.NoError(t, err)
  740. }()
  741. tc.me.bos.(*fakeOSCARConn).msgs <- tc.givenMsg
  742. gotCmd := <-ch
  743. assert.Len(t, gotCmd, len(tc.wantCmd))
  744. for i, want := range tc.wantCmd {
  745. assert.Equal(t, want, gotCmd[i])
  746. }
  747. cancel()
  748. wg.Wait()
  749. })
  750. }
  751. }
  752. func TestOSCARProxy_RecvBOS_UpdateBuddyDeparted(t *testing.T) {
  753. cases := []struct {
  754. // name is the unit test name
  755. name string
  756. // me is the TOC user session
  757. me *tocConn
  758. // givenMsg is the incoming SNAC
  759. givenMsg wire.SNACMessage
  760. // wantCmd is the expected TOC response
  761. wantCmd []string
  762. }{
  763. {
  764. name: "send buddy departure TOC1",
  765. me: testConn(),
  766. givenMsg: wire.SNACMessage{
  767. Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
  768. TLVUserInfo: wire.TLVUserInfo{
  769. ScreenName: "me",
  770. },
  771. },
  772. },
  773. wantCmd: []string{"UPDATE_BUDDY:me:F:0:0:0: "},
  774. },
  775. {
  776. name: "send buddy departure TOC2",
  777. me: testConn(withTOC2(true)),
  778. givenMsg: wire.SNACMessage{
  779. Body: wire.SNAC_0x03_0x0C_BuddyDeparted{
  780. TLVUserInfo: wire.TLVUserInfo{
  781. ScreenName: "me",
  782. },
  783. },
  784. },
  785. wantCmd: []string{"UPDATE_BUDDY2:me:F:0:0:0: :"},
  786. },
  787. }
  788. for _, tc := range cases {
  789. t.Run(tc.name, func(t *testing.T) {
  790. ctx, cancel := context.WithCancel(context.Background())
  791. svc := testOSCARProxy(t)
  792. ch := make(chan []string)
  793. tc.me.toCh = ch
  794. wg := &sync.WaitGroup{}
  795. wg.Add(1)
  796. go func() {
  797. defer wg.Done()
  798. err := svc.RecvBOS(ctx, tc.me)
  799. assert.NoError(t, err)
  800. }()
  801. tc.me.bos.(*fakeOSCARConn).msgs <- tc.givenMsg
  802. gotCmd := <-ch
  803. assert.Equal(t, tc.wantCmd[0], gotCmd[0])
  804. cancel()
  805. wg.Wait()
  806. })
  807. }
  808. }
  809. func TestOSCARProxy_RecvBOS_ClientEvent(t *testing.T) {
  810. cases := []struct {
  811. name string
  812. me *tocConn
  813. givenMsg wire.SNACMessage
  814. wantCmd []string
  815. }{
  816. {
  817. name: "TOC2 client receives CLIENT_EVENT2 (typing event)",
  818. me: testConn(withTOC2(false)),
  819. givenMsg: wire.SNACMessage{
  820. Body: wire.SNAC_0x04_0x14_ICBMClientEvent{
  821. ScreenName: "buddy",
  822. Event: 1, // typing
  823. },
  824. },
  825. wantCmd: []string{"CLIENT_EVENT2:buddy:1"},
  826. },
  827. {
  828. name: "TOC2 client receives CLIENT_EVENT2 event 0 (idle)",
  829. me: testConn(withTOC2(false)),
  830. givenMsg: wire.SNACMessage{
  831. Body: wire.SNAC_0x04_0x14_ICBMClientEvent{
  832. ScreenName: "alice",
  833. Event: 0,
  834. },
  835. },
  836. wantCmd: []string{"CLIENT_EVENT2:alice:0"},
  837. },
  838. {
  839. name: "TOC2 client receives CLIENT_EVENT2 event 2 (entered text)",
  840. me: testConn(withTOC2(false)),
  841. givenMsg: wire.SNACMessage{
  842. Body: wire.SNAC_0x04_0x14_ICBMClientEvent{
  843. ScreenName: "bob",
  844. Event: 2,
  845. },
  846. },
  847. wantCmd: []string{"CLIENT_EVENT2:bob:2"},
  848. },
  849. }
  850. for _, tc := range cases {
  851. t.Run(tc.name, func(t *testing.T) {
  852. ctx, cancel := context.WithCancel(context.Background())
  853. svc := testOSCARProxy(t)
  854. ch := make(chan []string)
  855. tc.me.toCh = ch
  856. wg := &sync.WaitGroup{}
  857. wg.Add(1)
  858. go func() {
  859. defer wg.Done()
  860. err := svc.RecvBOS(ctx, tc.me)
  861. assert.NoError(t, err)
  862. }()
  863. tc.me.bos.(*fakeOSCARConn).msgs <- tc.givenMsg
  864. gotCmd := <-ch
  865. assert.Equal(t, tc.wantCmd, gotCmd)
  866. cancel()
  867. wg.Wait()
  868. })
  869. }
  870. }
  871. func TestOSCARProxy_RecvBOS_Inserted2(t *testing.T) {
  872. buddyWithAlias := wire.FeedbagItem{
  873. ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice",
  874. TLVLBlock: wire.TLVLBlock{},
  875. }
  876. buddyWithAlias.Append(wire.NewTLVBE(wire.FeedbagAttributesAlias, []byte("Alice N.")))
  877. cases := []struct {
  878. name string
  879. snac any
  880. wantCmd []string
  881. mockParams mockParams
  882. }{
  883. {
  884. name: "one buddy - group name from feedbag",
  885. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  886. Items: []wire.FeedbagItem{
  887. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  888. },
  889. },
  890. wantCmd: []string{"INSERTED2:b::alice:Work"},
  891. mockParams: mockParams{
  892. feedBagParams: feedBagParams{
  893. feedbagParams: feedbagParams{
  894. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"}}},
  895. },
  896. },
  897. },
  898. },
  899. {
  900. name: "one buddy inserted from another location",
  901. snac: wire.SNAC_0x13_0x08_FeedbagInsertItem{
  902. Items: []wire.FeedbagItem{
  903. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  904. },
  905. },
  906. wantCmd: []string{"INSERTED2:b::alice:Work"},
  907. mockParams: mockParams{
  908. feedBagParams: feedBagParams{
  909. feedbagParams: feedbagParams{
  910. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"}}},
  911. },
  912. },
  913. },
  914. },
  915. {
  916. name: "one buddy with alias",
  917. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  918. Items: []wire.FeedbagItem{buddyWithAlias},
  919. },
  920. wantCmd: []string{"INSERTED2:b:Alice N.:alice:Work"},
  921. mockParams: mockParams{
  922. feedBagParams: feedBagParams{
  923. feedbagParams: feedbagParams{
  924. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"}}},
  925. },
  926. },
  927. },
  928. },
  929. {
  930. name: "one buddy - unknown group ID uses Buddies",
  931. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  932. Items: []wire.FeedbagItem{
  933. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 999, Name: "bob"},
  934. },
  935. },
  936. wantCmd: []string{"INSERTED2:b::bob:Buddies"},
  937. mockParams: mockParams{
  938. feedBagParams: feedBagParams{
  939. feedbagParams: feedbagParams{
  940. {results: []wire.FeedbagItem{}},
  941. },
  942. },
  943. },
  944. },
  945. {
  946. name: "two buddies same group",
  947. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  948. Items: []wire.FeedbagItem{
  949. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  950. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "bob"},
  951. },
  952. },
  953. wantCmd: []string{"INSERTED2:b::alice:Friends", "INSERTED2:b::bob:Friends"},
  954. mockParams: mockParams{
  955. feedBagParams: feedBagParams{
  956. feedbagParams: feedbagParams{
  957. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Friends"}}},
  958. },
  959. },
  960. },
  961. },
  962. {
  963. name: "group added",
  964. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  965. Items: []wire.FeedbagItem{
  966. {ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "NewGroup"},
  967. },
  968. },
  969. wantCmd: []string{"INSERTED2:g:NewGroup"},
  970. mockParams: mockParams{},
  971. },
  972. {
  973. name: "permit and deny added",
  974. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  975. Items: []wire.FeedbagItem{
  976. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice"},
  977. {ItemID: 2, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "bob"},
  978. },
  979. },
  980. wantCmd: []string{"INSERTED2:p:alice", "INSERTED2:d:bob"},
  981. mockParams: mockParams{},
  982. },
  983. {
  984. name: "all four types - group buddy deny permit",
  985. snac: wire.SNAC_0x13_0x09_FeedbagUpdateItem{
  986. Items: []wire.FeedbagItem{
  987. {ClassID: wire.FeedbagClassIdGroup, GroupID: 200, Name: "NewGroup"},
  988. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  989. {ItemID: 3, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "blockedUser"},
  990. {ItemID: 4, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "allowedUser"},
  991. },
  992. },
  993. wantCmd: []string{"INSERTED2:g:NewGroup", "INSERTED2:b::alice:Buddies", "INSERTED2:d:blockedUser", "INSERTED2:p:allowedUser"},
  994. mockParams: mockParams{
  995. feedBagParams: feedBagParams{
  996. feedbagParams: feedbagParams{
  997. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Buddies"}}},
  998. },
  999. },
  1000. },
  1001. },
  1002. }
  1003. for _, tc := range cases {
  1004. t.Run(tc.name, func(t *testing.T) {
  1005. ctx, cancel := context.WithCancel(context.Background())
  1006. svc := testOSCARProxy(t)
  1007. me := testConn(withTOC2(true))
  1008. for _, params := range tc.mockParams.feedbagParams {
  1009. me.feedbag.reset(params.results)
  1010. }
  1011. ch := make(chan []string)
  1012. me.toCh = ch
  1013. wg := &sync.WaitGroup{}
  1014. wg.Add(1)
  1015. go func() {
  1016. defer wg.Done()
  1017. err := svc.RecvBOS(ctx, me)
  1018. assert.NoError(t, err)
  1019. }()
  1020. me.bos.(*fakeOSCARConn).msgs <- wire.SNACMessage{Body: tc.snac}
  1021. assertResponse(t, me, ch, tc.wantCmd)
  1022. cancel()
  1023. wg.Wait()
  1024. })
  1025. }
  1026. }
  1027. func TestOSCARProxy_RecvBOS_Deleted2(t *testing.T) {
  1028. cases := []struct {
  1029. name string
  1030. snac wire.SNAC_0x13_0x0A_FeedbagDeleteItem
  1031. wantCmd []string
  1032. mockParams mockParams
  1033. }{
  1034. {
  1035. name: "one buddy removed",
  1036. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1037. Items: []wire.FeedbagItem{
  1038. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  1039. },
  1040. },
  1041. wantCmd: []string{"DELETED2:b:alice:Buddies"},
  1042. mockParams: mockParams{
  1043. feedBagParams: feedBagParams{
  1044. feedbagParams: feedbagParams{
  1045. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Buddies"}}},
  1046. },
  1047. },
  1048. },
  1049. },
  1050. {
  1051. name: "two buddies removed",
  1052. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1053. Items: []wire.FeedbagItem{
  1054. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  1055. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "bob"},
  1056. },
  1057. },
  1058. wantCmd: []string{"DELETED2:b:alice:Friends", "DELETED2:b:bob:Friends"},
  1059. mockParams: mockParams{
  1060. feedBagParams: feedBagParams{
  1061. feedbagParams: feedbagParams{
  1062. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Friends"}}},
  1063. },
  1064. },
  1065. },
  1066. },
  1067. {
  1068. name: "one buddy - unknown group ID uses Buddies",
  1069. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1070. Items: []wire.FeedbagItem{
  1071. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 999, Name: "alice"},
  1072. },
  1073. },
  1074. wantCmd: []string{"DELETED2:b:alice:Buddies"},
  1075. mockParams: mockParams{
  1076. feedBagParams: feedBagParams{
  1077. feedbagParams: feedbagParams{
  1078. {results: []wire.FeedbagItem{}},
  1079. },
  1080. },
  1081. },
  1082. },
  1083. {
  1084. name: "permit and deny removed",
  1085. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1086. Items: []wire.FeedbagItem{
  1087. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice"},
  1088. {ItemID: 2, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "bob"},
  1089. },
  1090. },
  1091. wantCmd: []string{"DELETED2:p:alice", "DELETED2:d:bob"},
  1092. mockParams: mockParams{},
  1093. },
  1094. {
  1095. name: "group deleted",
  1096. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1097. Items: []wire.FeedbagItem{
  1098. {ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"},
  1099. },
  1100. },
  1101. wantCmd: []string{"DELETED2:g:Work"},
  1102. mockParams: mockParams{},
  1103. },
  1104. {
  1105. name: "mix of buddy permit and deny emitted",
  1106. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1107. Items: []wire.FeedbagItem{
  1108. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "permitUser"},
  1109. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "buddyUser"},
  1110. },
  1111. },
  1112. wantCmd: []string{"DELETED2:p:permitUser", "DELETED2:b:buddyUser:Work"},
  1113. mockParams: mockParams{
  1114. feedBagParams: feedBagParams{
  1115. feedbagParams: feedbagParams{
  1116. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"}}},
  1117. },
  1118. },
  1119. },
  1120. },
  1121. {
  1122. name: "all four types - group buddy deny permit",
  1123. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1124. Items: []wire.FeedbagItem{
  1125. {ClassID: wire.FeedbagClassIdGroup, GroupID: 200, Name: "OldGroup"},
  1126. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice"},
  1127. {ItemID: 3, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "blockedUser"},
  1128. {ItemID: 4, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "allowedUser"},
  1129. },
  1130. },
  1131. wantCmd: []string{"DELETED2:g:OldGroup", "DELETED2:b:alice:Buddies", "DELETED2:d:blockedUser", "DELETED2:p:allowedUser"},
  1132. mockParams: mockParams{
  1133. feedBagParams: feedBagParams{
  1134. feedbagParams: feedbagParams{
  1135. {results: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Buddies"}}},
  1136. },
  1137. },
  1138. },
  1139. },
  1140. {
  1141. name: "empty items",
  1142. snac: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: nil},
  1143. wantCmd: nil,
  1144. mockParams: mockParams{},
  1145. },
  1146. }
  1147. for _, tc := range cases {
  1148. t.Run(tc.name, func(t *testing.T) {
  1149. ctx, cancel := context.WithCancel(context.Background())
  1150. svc := testOSCARProxy(t)
  1151. me := testConn(withTOC2(true))
  1152. for _, params := range tc.mockParams.feedbagParams {
  1153. me.feedbag.reset(params.results)
  1154. }
  1155. ch := make(chan []string)
  1156. me.toCh = ch
  1157. wg := &sync.WaitGroup{}
  1158. wg.Add(1)
  1159. go func() {
  1160. defer wg.Done()
  1161. err := svc.RecvBOS(ctx, me)
  1162. assert.NoError(t, err)
  1163. }()
  1164. me.bos.(*fakeOSCARConn).msgs <- wire.SNACMessage{Body: tc.snac}
  1165. assertResponse(t, me, ch, tc.wantCmd)
  1166. cancel()
  1167. wg.Wait()
  1168. })
  1169. }
  1170. }
  1171. func testOSCARProxy(t *testing.T) OSCARProxy {
  1172. return OSCARProxy{
  1173. Logger: slog.Default(),
  1174. }
  1175. }
  1176. // testConn creates a TOC connection for user "me" whose BOS connection is
  1177. // a fakeOSCARConn.
  1178. func testConn(opts ...func(*tocConn)) *tocConn {
  1179. me, _ := newTestTOCConn(newTestSession("me"))
  1180. for _, opt := range opts {
  1181. opt(me)
  1182. }
  1183. return me
  1184. }
  1185. // withTOC2 makes a TOC connection speak TOC2, with encoded messaging if
  1186. // msgEnc is set.
  1187. func withTOC2(msgEnc bool) func(*tocConn) {
  1188. return func(c *tocConn) {
  1189. c.isTOC2 = true
  1190. c.supportsMsgEnc = msgEnc
  1191. }
  1192. }
  1193. // assertResponse asserts that the SNAC just fed to the BOS connection produced
  1194. // want, or no response at all when want is nil. Since SNACs are handled in
  1195. // order, a nil want is checked by feeding a sentinel SNAC and expecting its
  1196. // response first.
  1197. func assertResponse(t *testing.T, me *tocConn, ch <-chan []string, want []string) {
  1198. if want == nil {
  1199. me.bos.(*fakeOSCARConn).msgs <- wire.SNACMessage{
  1200. Body: wire.SNAC_0x01_0x10_OServiceEvilNotification{NewEvil: 0},
  1201. }
  1202. want = []string{"EVILED:0:"}
  1203. }
  1204. assert.Equal(t, want, <-ch)
  1205. }
  1206. func TestOSCARProxy_RecvBOS_Replies(t *testing.T) {
  1207. roomInfo := func(cookie string, name string) wire.SNAC_0x0D_0x09_ChatNavNavInfo {
  1208. return wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  1209. TLVRestBlock: wire.TLVRestBlock{
  1210. TLVList: wire.TLVList{
  1211. wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  1212. Exchange: 4,
  1213. Cookie: cookie,
  1214. InstanceNumber: 0,
  1215. TLVBlock: wire.TLVBlock{
  1216. TLVList: wire.TLVList{
  1217. wire.NewTLVBE(wire.ChatRoomTLVRoomName, name),
  1218. },
  1219. },
  1220. }),
  1221. },
  1222. },
  1223. }
  1224. }
  1225. adminReply := func(tlvs ...wire.TLV) wire.SNAC_0x07_0x05_AdminChangeReply {
  1226. return wire.SNAC_0x07_0x05_AdminChangeReply{
  1227. TLVBlock: wire.TLVBlock{TLVList: tlvs},
  1228. }
  1229. }
  1230. serviceRequest := func(cookie string) wire.SNAC_0x01_0x04_OServiceServiceRequest {
  1231. return wire.SNAC_0x01_0x04_OServiceServiceRequest{
  1232. FoodGroup: wire.Chat,
  1233. TLVRestBlock: wire.TLVRestBlock{
  1234. TLVList: wire.TLVList{
  1235. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  1236. Cookie: cookie,
  1237. }),
  1238. },
  1239. },
  1240. }
  1241. }
  1242. cases := []struct {
  1243. // name is the unit test name
  1244. name string
  1245. // reqFrame is the cached request the reply answers, as request ID 1
  1246. reqFrame *wire.SNACFrame
  1247. // reqData is the cached request context
  1248. reqData any
  1249. // givenRoom is a chat room the user was invited to, as chat ID 0
  1250. givenRoom *wire.ICBMRoomInfo
  1251. // givenMsg is the incoming SNAC
  1252. givenMsg wire.SNACMessage
  1253. // wantCmd is the expected TOC response, nil for none
  1254. wantCmd []string
  1255. // wantSNACs are the follow-up SNACs sent to the BOS server
  1256. wantSNACs []wire.SNACMessage
  1257. // wantCached are the requests still cached afterward, by request ID
  1258. wantCached map[uint32]any
  1259. }{
  1260. {
  1261. name: "toc_get_status reply",
  1262. reqFrame: &wire.SNACFrame{FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery},
  1263. reqData: getStatusReqCtx{screenName: "them"},
  1264. givenMsg: newSNAC(wire.Locate, wire.LocateUserInfoReply, 1, wire.SNAC_0x02_0x06_LocateUserInfoReply{
  1265. TLVUserInfo: wire.TLVUserInfo{
  1266. ScreenName: "them",
  1267. TLVBlock: wire.TLVBlock{
  1268. TLVList: wire.TLVList{
  1269. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1234)),
  1270. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree),
  1271. },
  1272. },
  1273. },
  1274. }),
  1275. wantCmd: []string{"UPDATE_BUDDY:them:T:0:1234:0: O "},
  1276. },
  1277. {
  1278. name: "toc_get_status for user who is not online",
  1279. reqFrame: &wire.SNACFrame{FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery},
  1280. reqData: getStatusReqCtx{screenName: "them"},
  1281. givenMsg: newSNAC(wire.Locate, wire.LocateErr, 1, wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}),
  1282. wantCmd: []string{"ERROR:901:them"},
  1283. },
  1284. {
  1285. name: "toc_get_status with unexpected error code",
  1286. reqFrame: &wire.SNACFrame{FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery},
  1287. reqData: getStatusReqCtx{screenName: "them"},
  1288. givenMsg: newSNAC(wire.Locate, wire.LocateErr, 1, wire.SNACError{Code: wire.ErrorCodeRequestDenied}),
  1289. wantCmd: []string{cmdInternalSvcErr},
  1290. },
  1291. {
  1292. name: "toc_evil reply",
  1293. reqFrame: &wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: wire.ICBMEvilRequest},
  1294. reqData: evilReqCtx{},
  1295. givenMsg: newSNAC(wire.ICBM, wire.ICBMEvilReply, 1, wire.SNAC_0x04_0x09_ICBMEvilReply{}),
  1296. },
  1297. {
  1298. name: "toc_evil error",
  1299. reqFrame: &wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: wire.ICBMEvilRequest},
  1300. reqData: evilReqCtx{},
  1301. givenMsg: newSNAC(wire.ICBM, wire.ICBMErr, 1, wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}),
  1302. wantCmd: []string{"ERROR:" + wire.TOCErrorGeneralWarningUserNotAvailable},
  1303. },
  1304. {
  1305. name: "instant message error is not reported",
  1306. reqFrame: &wire.SNACFrame{FoodGroup: wire.ICBM, SubGroup: wire.ICBMChannelMsgToHost},
  1307. givenMsg: newSNAC(wire.ICBM, wire.ICBMErr, 1, wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}),
  1308. },
  1309. {
  1310. name: "error for unknown request is ignored",
  1311. givenMsg: newSNAC(wire.ICBM, wire.ICBMErr, 1, wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}),
  1312. },
  1313. {
  1314. name: "error from another food group does not match request",
  1315. reqFrame: &wire.SNACFrame{FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery},
  1316. reqData: getStatusReqCtx{screenName: "them"},
  1317. givenMsg: newSNAC(wire.ICBM, wire.ICBMErr, 1, wire.SNACError{Code: wire.ErrorCodeNotLoggedOn}),
  1318. wantCached: map[uint32]any{1: getStatusReqCtx{screenName: "them"}},
  1319. },
  1320. {
  1321. name: "toc_chat_join room creation error",
  1322. reqFrame: &wire.SNACFrame{FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavCreateRoom},
  1323. reqData: chatJoinReqCtx{},
  1324. givenMsg: newSNAC(wire.ChatNav, wire.ChatNavErr, 1, wire.SNACError{Code: wire.ErrorCodeNoMatch}),
  1325. wantCmd: []string{cmdInternalSvcErr},
  1326. },
  1327. {
  1328. name: "toc_change_passwd success",
  1329. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1330. reqData: changePasswdReqCtx{},
  1331. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply()),
  1332. wantCmd: []string{"ADMIN_PASSWD_STATUS:0"},
  1333. },
  1334. {
  1335. name: "toc_change_passwd invalid password length",
  1336. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1337. reqData: changePasswdReqCtx{},
  1338. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1339. wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidPasswordLength),
  1340. )),
  1341. wantCmd: []string{"ERROR:" + wire.TOCErrorAdminInvalidInput},
  1342. },
  1343. {
  1344. name: "toc_change_passwd incorrect password",
  1345. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1346. reqData: changePasswdReqCtx{},
  1347. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1348. wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorValidatePassword),
  1349. )),
  1350. wantCmd: []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword},
  1351. },
  1352. {
  1353. name: "toc_change_passwd catch-all error",
  1354. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1355. reqData: changePasswdReqCtx{},
  1356. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1357. wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors),
  1358. )),
  1359. wantCmd: []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest},
  1360. },
  1361. {
  1362. name: "toc_format_nickname success",
  1363. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1364. reqData: formatNickReqCtx{},
  1365. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1366. wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, "M E"),
  1367. )),
  1368. wantCmd: []string{"ADMIN_NICK_STATUS:0", "NICK:M E"},
  1369. },
  1370. {
  1371. name: "toc_format_nickname invalid nickname",
  1372. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1373. reqData: formatNickReqCtx{},
  1374. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1375. wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorInvalidNickName),
  1376. )),
  1377. wantCmd: []string{"ERROR:" + wire.TOCErrorAdminInvalidInput},
  1378. },
  1379. {
  1380. name: "toc_format_nickname catch-all error",
  1381. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1382. reqData: formatNickReqCtx{},
  1383. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply(
  1384. wire.NewTLVBE(wire.AdminTLVErrorCode, wire.AdminInfoErrorAllOtherErrors),
  1385. )),
  1386. wantCmd: []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest},
  1387. },
  1388. {
  1389. name: "toc_format_nickname reply missing formatted screen name",
  1390. reqFrame: &wire.SNACFrame{FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
  1391. reqData: formatNickReqCtx{},
  1392. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply()),
  1393. wantCmd: []string{cmdInternalSvcErr},
  1394. },
  1395. {
  1396. name: "admin reply for unknown request is ignored",
  1397. givenMsg: newSNAC(wire.Admin, wire.AdminInfoChangeReply, 1, adminReply()),
  1398. },
  1399. {
  1400. name: "toc_chat_join room info requests a chat connection",
  1401. reqFrame: &wire.SNACFrame{FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavCreateRoom},
  1402. reqData: chatJoinReqCtx{},
  1403. givenMsg: newSNAC(wire.ChatNav, wire.ChatNavNavInfo, 1, roomInfo("4-0-my room", "my room")),
  1404. wantSNACs: []wire.SNACMessage{
  1405. newSNAC(wire.OService, wire.OServiceServiceRequest, 2, serviceRequest("4-0-my room")),
  1406. },
  1407. wantCached: map[uint32]any{2: chatServiceReqCtx{chatID: 0, roomName: "my room"}},
  1408. },
  1409. {
  1410. name: "toc_chat_accept room info requests a chat connection",
  1411. reqFrame: &wire.SNACFrame{FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestRoomInfo},
  1412. reqData: chatAcceptReqCtx{chatID: 0},
  1413. givenRoom: &wire.ICBMRoomInfo{Exchange: 4, Cookie: "4-0-their room"},
  1414. givenMsg: newSNAC(wire.ChatNav, wire.ChatNavNavInfo, 1, roomInfo("4-0-their room", "their room")),
  1415. wantSNACs: []wire.SNACMessage{
  1416. newSNAC(wire.OService, wire.OServiceServiceRequest, 2, serviceRequest("4-0-their room")),
  1417. },
  1418. wantCached: map[uint32]any{2: chatServiceReqCtx{chatID: 0, roomName: "their room"}},
  1419. },
  1420. {
  1421. name: "chat room info missing room TLV",
  1422. reqFrame: &wire.SNACFrame{FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavCreateRoom},
  1423. reqData: chatJoinReqCtx{},
  1424. givenMsg: newSNAC(wire.ChatNav, wire.ChatNavNavInfo, 1, wire.SNAC_0x0D_0x09_ChatNavNavInfo{}),
  1425. wantCmd: []string{cmdInternalSvcErr},
  1426. },
  1427. {
  1428. name: "chat room info missing room name",
  1429. reqFrame: &wire.SNACFrame{FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavCreateRoom},
  1430. reqData: chatJoinReqCtx{},
  1431. givenMsg: newSNAC(wire.ChatNav, wire.ChatNavNavInfo, 1, wire.SNAC_0x0D_0x09_ChatNavNavInfo{
  1432. TLVRestBlock: wire.TLVRestBlock{
  1433. TLVList: wire.TLVList{
  1434. wire.NewTLVBE(wire.ChatNavTLVRoomInfo, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{Exchange: 4, Cookie: "4-0-my room"}),
  1435. },
  1436. },
  1437. }),
  1438. wantCmd: []string{cmdInternalSvcErr},
  1439. },
  1440. {
  1441. name: "failed feedbag change",
  1442. reqFrame: &wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  1443. reqData: feedbagChangeReqCtx{},
  1444. givenMsg: newSNAC(wire.Feedbag, wire.FeedbagErr, 1, wire.SNACError{Code: wire.ErrorCodeInsufficientRights}),
  1445. },
  1446. {
  1447. name: "feedbag change acknowledgement",
  1448. reqFrame: &wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1449. givenMsg: newSNAC(wire.Feedbag, wire.FeedbagStatus, 1, wire.SNAC_0x13_0x0E_FeedbagStatus{Results: []uint16{0}}),
  1450. },
  1451. }
  1452. for _, tc := range cases {
  1453. t.Run(tc.name, func(t *testing.T) {
  1454. ctx, cancel := context.WithCancel(context.Background())
  1455. me := testConn()
  1456. if tc.reqFrame != nil {
  1457. me.cache.add(tc.reqFrame, tc.reqData)
  1458. }
  1459. if tc.givenRoom != nil {
  1460. me.chats.Add(*tc.givenRoom)
  1461. }
  1462. ch := make(chan []string)
  1463. me.toCh = ch
  1464. wg := &sync.WaitGroup{}
  1465. wg.Add(1)
  1466. go func() {
  1467. defer wg.Done()
  1468. assert.NoError(t, testOSCARProxy(t).RecvBOS(ctx, me))
  1469. }()
  1470. bos := me.bos.(*fakeOSCARConn)
  1471. bos.msgs <- tc.givenMsg
  1472. assertResponse(t, me, ch, tc.wantCmd)
  1473. assert.Equal(t, tc.wantSNACs, bos.sentSNACs())
  1474. cached := make(map[uint32]any)
  1475. for id, entry := range me.cache.entries {
  1476. cached[id] = entry.data
  1477. }
  1478. if tc.wantCached == nil {
  1479. tc.wantCached = map[uint32]any{}
  1480. }
  1481. assert.Equal(t, tc.wantCached, cached)
  1482. cancel()
  1483. wg.Wait()
  1484. })
  1485. }
  1486. }
  1487. func TestOSCARProxy_RecvBOS_ChatServiceResponse(t *testing.T) {
  1488. ctx, cancel := context.WithCancel(context.Background())
  1489. defer cancel()
  1490. chat := &fakeService{
  1491. reply: func(frame wire.SNACFrame) []wire.SNACMessage {
  1492. if frame.FoodGroup == wire.OService && frame.SubGroup == wire.OServiceClientOnline {
  1493. return []wire.SNACMessage{newSNAC(wire.Chat, wire.ChatUsersJoined, wire.ReqIDFromServer,
  1494. wire.SNAC_0x0E_0x03_ChatUsersJoined{
  1495. Users: []wire.TLVUserInfo{{ScreenName: "me"}},
  1496. })}
  1497. }
  1498. return nil
  1499. },
  1500. }
  1501. oscarServer := &fakeOSCARServer{handlers: []func(net.Conn){chat.handle}}
  1502. svc := OSCARProxy{
  1503. Logger: slog.Default(),
  1504. OSCARServer: oscarServer,
  1505. }
  1506. me := testConn()
  1507. me.cache.add(&wire.SNACFrame{FoodGroup: wire.OService, SubGroup: wire.OServiceServiceRequest},
  1508. chatServiceReqCtx{chatID: 0, roomName: "My Room"})
  1509. ch := make(chan []string)
  1510. me.toCh = ch
  1511. wg := &sync.WaitGroup{}
  1512. wg.Add(1)
  1513. go func() {
  1514. defer wg.Done()
  1515. assert.NoError(t, svc.RecvBOS(ctx, me))
  1516. }()
  1517. me.bos.(*fakeOSCARConn).msgs <- newSNAC(wire.OService, wire.OServiceServiceResponse, 1,
  1518. wire.SNAC_0x01_0x05_OServiceServiceResponse{
  1519. TLVRestBlock: wire.TLVRestBlock{
  1520. TLVList: wire.TLVList{
  1521. wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("chat-cookie")),
  1522. },
  1523. },
  1524. })
  1525. // the room is announced before anything from the room is relayed
  1526. assert.Equal(t, []string{"CHAT_JOIN:0:My Room"}, <-ch)
  1527. assert.Equal(t, []string{"CHAT_UPDATE_BUDDY:0:T:me"}, <-ch)
  1528. gotCookie, gotSNACs := chat.received()
  1529. assert.Equal(t, []byte("chat-cookie"), gotCookie)
  1530. assert.Equal(t, []receivedSNAC{
  1531. newReceivedSNAC(t, wire.OService, wire.OServiceRateParamsSubAdd, 2, wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
  1532. ClassIDs: []uint16{1, 2, 3, 4, 5},
  1533. }),
  1534. newReceivedSNAC(t, wire.OService, wire.OServiceClientOnline, 3, wire.SNAC_0x01_0x02_OServiceClientOnline{}),
  1535. }, gotSNACs)
  1536. assert.NotNil(t, me.chats.RetrieveConn(0))
  1537. assert.Equal(t, []net.Addr{me.clientAddr}, oscarServer.clientAddrs)
  1538. cancel()
  1539. wg.Wait()
  1540. }
  1541. func TestOSCARProxy_RecvBOS_RateParamsChange(t *testing.T) {
  1542. cases := []struct {
  1543. name string
  1544. code uint16
  1545. wantCmd []string
  1546. }{
  1547. {
  1548. name: "rate class becomes limited",
  1549. code: rateParamsChangeLimited,
  1550. wantCmd: []string{rateLimitExceededErr},
  1551. },
  1552. {
  1553. name: "rate class clears",
  1554. code: 4,
  1555. },
  1556. }
  1557. for _, tc := range cases {
  1558. t.Run(tc.name, func(t *testing.T) {
  1559. ctx, cancel := context.WithCancel(context.Background())
  1560. me := testConn()
  1561. ch := make(chan []string)
  1562. me.toCh = ch
  1563. wg := &sync.WaitGroup{}
  1564. wg.Add(1)
  1565. go func() {
  1566. defer wg.Done()
  1567. assert.NoError(t, testOSCARProxy(t).RecvBOS(ctx, me))
  1568. }()
  1569. me.bos.(*fakeOSCARConn).msgs <- newSNAC(wire.OService, wire.OServiceRateParamChange, wire.ReqIDFromServer,
  1570. wire.SNAC_0x01_0x0A_OServiceRateParamsChange{Code: tc.code})
  1571. assertResponse(t, me, ch, tc.wantCmd)
  1572. cancel()
  1573. wg.Wait()
  1574. })
  1575. }
  1576. }
  1577. func TestOSCARProxy_RecvChat_RateParamsChange(t *testing.T) {
  1578. ctx, cancel := context.WithCancel(context.Background())
  1579. me := testConn()
  1580. ch := make(chan []string)
  1581. me.toCh = ch
  1582. chatConn := newFakeOSCARConn()
  1583. wg := &sync.WaitGroup{}
  1584. wg.Add(1)
  1585. go func() {
  1586. defer wg.Done()
  1587. testOSCARProxy(t).RecvChat(ctx, me, chatConn, 0)
  1588. }()
  1589. chatConn.msgs <- newSNAC(wire.OService, wire.OServiceRateParamChange, wire.ReqIDFromServer,
  1590. wire.SNAC_0x01_0x0A_OServiceRateParamsChange{Code: rateParamsChangeLimited})
  1591. assert.Equal(t, []string{rateLimitExceededErr}, <-ch)
  1592. cancel()
  1593. wg.Wait()
  1594. assert.True(t, chatConn.isClosed())
  1595. }
  1596. func TestOSCARProxy_RecvBOS_Backlog(t *testing.T) {
  1597. ctx, cancel := context.WithCancel(context.Background())
  1598. me := testConn()
  1599. me.backlog = []wire.SNACMessage{
  1600. newSNAC(wire.Buddy, wire.BuddyDeparted, wire.ReqIDFromServer, wire.SNAC_0x03_0x0C_BuddyDeparted{
  1601. TLVUserInfo: wire.TLVUserInfo{ScreenName: "them"},
  1602. }),
  1603. }
  1604. ch := make(chan []string)
  1605. me.toCh = ch
  1606. wg := &sync.WaitGroup{}
  1607. wg.Add(1)
  1608. go func() {
  1609. defer wg.Done()
  1610. assert.NoError(t, testOSCARProxy(t).RecvBOS(ctx, me))
  1611. }()
  1612. // SNACs received during signon come before anything newer
  1613. assert.Equal(t, []string{"UPDATE_BUDDY:them:F:0:0:0: "}, <-ch)
  1614. cancel()
  1615. wg.Wait()
  1616. assert.Nil(t, me.backlog)
  1617. }
  1618. func TestOSCARProxy_RecvBOS_FeedbagMirror(t *testing.T) {
  1619. work := wire.FeedbagItem{ClassID: wire.FeedbagClassIdGroup, GroupID: 100, Name: "Work"}
  1620. alice := wire.FeedbagItem{ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, ItemID: 1, Name: "alice"}
  1621. bob := wire.FeedbagItem{ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, ItemID: 2, Name: "bob"}
  1622. carol := wire.FeedbagItem{ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, ItemID: 3, Name: "carol"}
  1623. dave := wire.FeedbagItem{ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, ItemID: 4, Name: "dave"}
  1624. ctx, cancel := context.WithCancel(context.Background())
  1625. me := testConn(withTOC2(true))
  1626. me.feedbag.reset([]wire.FeedbagItem{work, alice})
  1627. svc := testOSCARProxy(t)
  1628. ch := make(chan []string)
  1629. me.toCh = ch
  1630. wg := &sync.WaitGroup{}
  1631. wg.Add(1)
  1632. go func() {
  1633. defer wg.Done()
  1634. assert.NoError(t, svc.RecvBOS(ctx, me))
  1635. }()
  1636. bos := me.bos.(*fakeOSCARConn)
  1637. // the server confirms the items of a change it accepted
  1638. assert.Nil(t, svc.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, []wire.FeedbagItem{bob, carol}))
  1639. bos.msgs <- newSNAC(wire.Feedbag, wire.FeedbagStatus, 1, wire.SNAC_0x13_0x0E_FeedbagStatus{Results: []uint16{0, 0x0E}})
  1640. assertResponse(t, me, ch, nil)
  1641. assert.False(t, me.feedbag.hasPending())
  1642. assert.Equal(t, []wire.FeedbagItem{work, alice, bob}, me.feedbag.snapshot())
  1643. // changes from other locations update the mirror
  1644. bos.msgs <- newSNAC(wire.Feedbag, wire.FeedbagInsertItem, wire.ReqIDFromServer,
  1645. wire.SNAC_0x13_0x08_FeedbagInsertItem{Items: []wire.FeedbagItem{carol}})
  1646. assert.Equal(t, []string{"INSERTED2:b::carol:Work"}, <-ch)
  1647. // a deleted buddy's group is named from the mirror before the buddy leaves it
  1648. bos.msgs <- newSNAC(wire.Feedbag, wire.FeedbagDeleteItem, wire.ReqIDFromServer,
  1649. wire.SNAC_0x13_0x0A_FeedbagDeleteItem{Items: []wire.FeedbagItem{alice}})
  1650. assert.Equal(t, []string{"DELETED2:b:alice:Work"}, <-ch)
  1651. assert.Equal(t, []wire.FeedbagItem{work, bob, carol}, me.feedbag.snapshot())
  1652. // a change may be dropped while the server is limiting requests, so the
  1653. // mirror is reloaded once the limit clears
  1654. assert.Nil(t, svc.upsertFeedbag(ctx, me, wire.FeedbagInsertItem, []wire.FeedbagItem{dave}))
  1655. bos.msgs <- newSNAC(wire.OService, wire.OServiceRateParamChange, wire.ReqIDFromServer,
  1656. wire.SNAC_0x01_0x0A_OServiceRateParamsChange{Code: rateParamsChangeLimited})
  1657. assert.Equal(t, []string{rateLimitExceededErr}, <-ch)
  1658. assert.Len(t, bos.sentSNACs(), 2)
  1659. bos.msgs <- newSNAC(wire.OService, wire.OServiceRateParamChange, wire.ReqIDFromServer,
  1660. wire.SNAC_0x01_0x0A_OServiceRateParamsChange{Code: rateParamsChangeClear})
  1661. assertResponse(t, me, ch, nil)
  1662. assert.Equal(t, newSNAC(wire.Feedbag, wire.FeedbagQuery, 3, struct{}{}), bos.sentSNACs()[2])
  1663. // the reload drops the change the server dropped
  1664. bos.msgs <- newSNAC(wire.Feedbag, wire.FeedbagReply, 3,
  1665. wire.SNAC_0x13_0x06_FeedbagReply{Items: []wire.FeedbagItem{work, bob, carol}})
  1666. assertResponse(t, me, ch, nil)
  1667. assert.False(t, me.feedbag.hasPending())
  1668. assert.Equal(t, []wire.FeedbagItem{work, bob, carol}, me.feedbag.snapshot())
  1669. // with nothing pending, a cleared limit doesn't reload the mirror
  1670. bos.msgs <- newSNAC(wire.OService, wire.OServiceRateParamChange, wire.ReqIDFromServer,
  1671. wire.SNAC_0x01_0x0A_OServiceRateParamsChange{Code: rateParamsChangeClear})
  1672. assertResponse(t, me, ch, nil)
  1673. assert.Len(t, bos.sentSNACs(), 3)
  1674. cancel()
  1675. wg.Wait()
  1676. assert.True(t, bos.isClosed())
  1677. }