cmd_client_test.go 126 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103
  1. package toc
  2. import (
  3. "context"
  4. "errors"
  5. "io"
  6. "log/slog"
  7. "net"
  8. "strings"
  9. "testing"
  10. "time"
  11. "github.com/google/uuid"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/mk6i/open-oscar-server/state"
  14. "github.com/mk6i/open-oscar-server/wire"
  15. )
  16. // cmdTestCase is a TOC command and the SNACs and replies it should produce.
  17. type cmdTestCase struct {
  18. // name is the unit test name
  19. name string
  20. // givenCmd is the TOC command
  21. givenCmd string
  22. // sendErrs are the errors returned by each SNAC send, in order
  23. sendErrs []error
  24. // wantSNACs are the SNACs the command sends to the BOS server
  25. wantSNACs []wire.SNACMessage
  26. // wantCacheData is the request context cached with the first SNAC
  27. wantCacheData any
  28. // wantMsg is the expected TOC response
  29. wantMsg []string
  30. }
  31. // runCmdTests runs TOC commands that only send SNACs over the BOS connection.
  32. // TOC2 commands run on a TOC2 connection.
  33. func runCmdTests(t *testing.T, cases []cmdTestCase) {
  34. for _, tc := range cases {
  35. t.Run(tc.name, func(t *testing.T) {
  36. me, bos := newTestTOCConn(newTestSession("me"))
  37. me.isTOC2 = strings.HasPrefix(tc.givenCmd, "toc2_")
  38. bos.sendErrs = tc.sendErrs
  39. svc := OSCARProxy{
  40. Logger: slog.Default(),
  41. }
  42. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  43. assert.Equal(t, tc.wantMsg, msg)
  44. assert.Equal(t, tc.wantSNACs, bos.sentSNACs())
  45. if tc.wantCacheData != nil {
  46. req, ok := me.cache.take(wire.SNACFrame{FoodGroup: tc.wantSNACs[0].Frame.FoodGroup, RequestID: 1})
  47. assert.True(t, ok)
  48. assert.Equal(t, tc.wantCacheData, req.data)
  49. }
  50. })
  51. }
  52. }
  53. func TestOSCARProxy_send(t *testing.T) {
  54. t.Run("caches the request until its reply arrives", func(t *testing.T) {
  55. me, bos := newTestTOCConn(newTestSession("me"))
  56. svc := OSCARProxy{Logger: slog.Default()}
  57. msg := svc.send(context.Background(), me, bos, wire.Locate, wire.LocateUserInfoQuery, struct{}{}, getStatusReqCtx{screenName: "them"})
  58. assert.Nil(t, msg)
  59. req, ok := me.cache.take(wire.SNACFrame{FoodGroup: wire.Locate, RequestID: 1})
  60. assert.True(t, ok)
  61. assert.Equal(t, wire.LocateUserInfoQuery, req.frame.SubGroup)
  62. assert.Equal(t, getStatusReqCtx{screenName: "them"}, req.data)
  63. })
  64. t.Run("drops the cached request when the send fails", func(t *testing.T) {
  65. me, bos := newTestTOCConn(newTestSession("me"))
  66. bos.sendErrs = []error{io.EOF}
  67. svc := OSCARProxy{Logger: slog.Default()}
  68. msg := svc.send(context.Background(), me, bos, wire.Locate, wire.LocateUserInfoQuery, struct{}{}, nil)
  69. assert.Equal(t, []string{cmdInternalSvcErr}, msg)
  70. _, ok := me.cache.take(wire.SNACFrame{FoodGroup: wire.Locate, RequestID: 1})
  71. assert.False(t, ok)
  72. })
  73. }
  74. func TestOSCARProxy_RecvClientCmd_AddBuddy(t *testing.T) {
  75. runCmdTests(t, []cmdTestCase{
  76. {
  77. name: "successfully add buddies",
  78. givenCmd: "toc_add_buddy friend1 friend2 friend3",
  79. wantSNACs: []wire.SNACMessage{
  80. newSNAC(wire.Buddy, wire.BuddyAddBuddies, 1, wire.SNAC_0x03_0x04_BuddyAddBuddies{
  81. Buddies: []struct {
  82. ScreenName string `oscar:"len_prefix=uint8"`
  83. }{
  84. {ScreenName: "friend1"},
  85. {ScreenName: "friend2"},
  86. {ScreenName: "friend3"},
  87. },
  88. }),
  89. },
  90. wantMsg: []string{},
  91. },
  92. {
  93. name: "add buddies with empty list",
  94. givenCmd: "toc_add_buddy",
  95. wantSNACs: []wire.SNACMessage{
  96. newSNAC(wire.Buddy, wire.BuddyAddBuddies, 1, wire.SNAC_0x03_0x04_BuddyAddBuddies{}),
  97. },
  98. wantMsg: []string{},
  99. },
  100. {
  101. name: "add buddies, send fails",
  102. givenCmd: "toc_add_buddy friend1",
  103. sendErrs: []error{io.EOF},
  104. wantSNACs: []wire.SNACMessage{
  105. newSNAC(wire.Buddy, wire.BuddyAddBuddies, 1, wire.SNAC_0x03_0x04_BuddyAddBuddies{
  106. Buddies: []struct {
  107. ScreenName string `oscar:"len_prefix=uint8"`
  108. }{
  109. {ScreenName: "friend1"},
  110. },
  111. }),
  112. },
  113. wantMsg: []string{cmdInternalSvcErr},
  114. },
  115. })
  116. }
  117. func TestOSCARProxy_RecvClientCmd_AddPermit(t *testing.T) {
  118. runCmdTests(t, []cmdTestCase{
  119. {
  120. name: "successfully permit buddies",
  121. givenCmd: "toc_add_permit friend1 friend2",
  122. wantSNACs: []wire.SNACMessage{
  123. newSNAC(wire.PermitDeny, wire.PermitDenyAddPermListEntries, 1, wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{
  124. Users: []struct {
  125. ScreenName string `oscar:"len_prefix=uint8"`
  126. }{
  127. {ScreenName: "friend1"},
  128. {ScreenName: "friend2"},
  129. },
  130. }),
  131. },
  132. wantMsg: []string{},
  133. },
  134. {
  135. name: "permit buddies with empty list",
  136. givenCmd: "toc_add_permit",
  137. wantSNACs: []wire.SNACMessage{
  138. newSNAC(wire.PermitDeny, wire.PermitDenyAddPermListEntries, 1, wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{}),
  139. },
  140. wantMsg: []string{},
  141. },
  142. {
  143. name: "permit buddies, send fails",
  144. givenCmd: "toc_add_permit",
  145. sendErrs: []error{io.EOF},
  146. wantSNACs: []wire.SNACMessage{
  147. newSNAC(wire.PermitDeny, wire.PermitDenyAddPermListEntries, 1, wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{}),
  148. },
  149. wantMsg: []string{cmdInternalSvcErr},
  150. },
  151. })
  152. }
  153. func TestOSCARProxy_RecvClientCmd_AddDeny(t *testing.T) {
  154. runCmdTests(t, []cmdTestCase{
  155. {
  156. name: "successfully deny buddies",
  157. givenCmd: "toc_add_deny friend1 friend2",
  158. wantSNACs: []wire.SNACMessage{
  159. newSNAC(wire.PermitDeny, wire.PermitDenyAddDenyListEntries, 1, wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{
  160. Users: []struct {
  161. ScreenName string `oscar:"len_prefix=uint8"`
  162. }{
  163. {ScreenName: "friend1"},
  164. {ScreenName: "friend2"},
  165. },
  166. }),
  167. },
  168. wantMsg: []string{},
  169. },
  170. {
  171. name: "deny buddies with empty list",
  172. givenCmd: "toc_add_deny",
  173. wantSNACs: []wire.SNACMessage{
  174. newSNAC(wire.PermitDeny, wire.PermitDenyAddDenyListEntries, 1, wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{}),
  175. },
  176. wantMsg: []string{},
  177. },
  178. {
  179. name: "deny buddies, send fails",
  180. givenCmd: "toc_add_deny",
  181. sendErrs: []error{io.EOF},
  182. wantSNACs: []wire.SNACMessage{
  183. newSNAC(wire.PermitDeny, wire.PermitDenyAddDenyListEntries, 1, wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{}),
  184. },
  185. wantMsg: []string{cmdInternalSvcErr},
  186. },
  187. })
  188. }
  189. func TestOSCARProxy_RecvClientCmd_RemoveBuddy(t *testing.T) {
  190. runCmdTests(t, []cmdTestCase{
  191. {
  192. name: "successfully remove buddies",
  193. givenCmd: "toc_remove_buddy friend1 friend2",
  194. wantSNACs: []wire.SNACMessage{
  195. newSNAC(wire.Buddy, wire.BuddyDelBuddies, 1, wire.SNAC_0x03_0x05_BuddyDelBuddies{
  196. Buddies: []struct {
  197. ScreenName string `oscar:"len_prefix=uint8"`
  198. }{
  199. {ScreenName: "friend1"},
  200. {ScreenName: "friend2"},
  201. },
  202. }),
  203. },
  204. wantMsg: []string{},
  205. },
  206. {
  207. name: "remove buddies with empty list",
  208. givenCmd: "toc_remove_buddy",
  209. wantSNACs: []wire.SNACMessage{
  210. newSNAC(wire.Buddy, wire.BuddyDelBuddies, 1, wire.SNAC_0x03_0x05_BuddyDelBuddies{}),
  211. },
  212. wantMsg: []string{},
  213. },
  214. {
  215. name: "remove buddies, send fails",
  216. givenCmd: "toc_remove_buddy",
  217. sendErrs: []error{io.EOF},
  218. wantSNACs: []wire.SNACMessage{
  219. newSNAC(wire.Buddy, wire.BuddyDelBuddies, 1, wire.SNAC_0x03_0x05_BuddyDelBuddies{}),
  220. },
  221. wantMsg: []string{cmdInternalSvcErr},
  222. },
  223. })
  224. }
  225. func TestOSCARProxy_RecvClientCmd_ChangePassword(t *testing.T) {
  226. runCmdTests(t, []cmdTestCase{
  227. {
  228. name: "successfully request password change",
  229. givenCmd: `toc_change_passwd "old\$pass" newpass`,
  230. wantSNACs: []wire.SNACMessage{
  231. newSNAC(wire.Admin, wire.AdminInfoChangeRequest, 1, wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  232. TLVRestBlock: wire.TLVRestBlock{
  233. TLVList: wire.TLVList{
  234. wire.NewTLVBE(wire.AdminTLVOldPassword, "old$pass"),
  235. wire.NewTLVBE(wire.AdminTLVNewPassword, "newpass"),
  236. },
  237. },
  238. }),
  239. },
  240. wantCacheData: changePasswdReqCtx{},
  241. wantMsg: []string{},
  242. },
  243. {
  244. name: "request password change, send fails",
  245. givenCmd: `toc_change_passwd oldpass newpass`,
  246. sendErrs: []error{io.EOF},
  247. wantSNACs: []wire.SNACMessage{
  248. newSNAC(wire.Admin, wire.AdminInfoChangeRequest, 1, wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  249. TLVRestBlock: wire.TLVRestBlock{
  250. TLVList: wire.TLVList{
  251. wire.NewTLVBE(wire.AdminTLVOldPassword, "oldpass"),
  252. wire.NewTLVBE(wire.AdminTLVNewPassword, "newpass"),
  253. },
  254. },
  255. }),
  256. },
  257. wantMsg: []string{cmdInternalSvcErr},
  258. },
  259. {
  260. name: "bad command",
  261. givenCmd: `toc_change_passwd`,
  262. wantMsg: []string{cmdInternalSvcErr},
  263. },
  264. })
  265. }
  266. func TestOSCARProxy_RecvClientCmd_FormatNickname(t *testing.T) {
  267. runCmdTests(t, []cmdTestCase{
  268. {
  269. name: "successfully request screen name format change",
  270. givenCmd: `toc_format_nickname "{M E}"`,
  271. wantSNACs: []wire.SNACMessage{
  272. newSNAC(wire.Admin, wire.AdminInfoChangeRequest, 1, wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  273. TLVRestBlock: wire.TLVRestBlock{
  274. TLVList: wire.TLVList{
  275. wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, "M E"),
  276. },
  277. },
  278. }),
  279. },
  280. wantCacheData: formatNickReqCtx{},
  281. wantMsg: []string{},
  282. },
  283. {
  284. name: "bad command",
  285. givenCmd: `toc_format_nickname`,
  286. wantMsg: []string{cmdInternalSvcErr},
  287. },
  288. })
  289. }
  290. func TestOSCARProxy_RecvClientCmd_Evil(t *testing.T) {
  291. runCmdTests(t, []cmdTestCase{
  292. {
  293. name: "successfully warn normally",
  294. givenCmd: `toc_evil them norm`,
  295. wantSNACs: []wire.SNACMessage{
  296. newSNAC(wire.ICBM, wire.ICBMEvilRequest, 1, wire.SNAC_0x04_0x08_ICBMEvilRequest{
  297. SendAs: 0,
  298. ScreenName: "them",
  299. }),
  300. },
  301. wantCacheData: evilReqCtx{},
  302. wantMsg: []string{},
  303. },
  304. {
  305. name: "successfully warn anonymously",
  306. givenCmd: `toc_evil them anon`,
  307. wantSNACs: []wire.SNACMessage{
  308. newSNAC(wire.ICBM, wire.ICBMEvilRequest, 1, wire.SNAC_0x04_0x08_ICBMEvilRequest{
  309. SendAs: 1,
  310. ScreenName: "them",
  311. }),
  312. },
  313. wantCacheData: evilReqCtx{},
  314. wantMsg: []string{},
  315. },
  316. {
  317. name: "warn with incorrect type",
  318. givenCmd: `toc_evil them blah`,
  319. wantMsg: []string{cmdInternalSvcErr},
  320. },
  321. {
  322. name: "bad command",
  323. givenCmd: `toc_evil`,
  324. wantMsg: []string{cmdInternalSvcErr},
  325. },
  326. })
  327. }
  328. func TestOSCARProxy_RecvClientCmd_GetStatus(t *testing.T) {
  329. runCmdTests(t, []cmdTestCase{
  330. {
  331. name: "successfully request status",
  332. givenCmd: `toc_get_status them`,
  333. wantSNACs: []wire.SNACMessage{
  334. newSNAC(wire.Locate, wire.LocateUserInfoQuery, 1, wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  335. ScreenName: "them",
  336. }),
  337. },
  338. wantCacheData: getStatusReqCtx{screenName: "them"},
  339. wantMsg: []string{},
  340. },
  341. {
  342. name: "request status, send fails",
  343. givenCmd: `toc_get_status them`,
  344. sendErrs: []error{io.EOF},
  345. wantSNACs: []wire.SNACMessage{
  346. newSNAC(wire.Locate, wire.LocateUserInfoQuery, 1, wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  347. ScreenName: "them",
  348. }),
  349. },
  350. wantMsg: []string{cmdInternalSvcErr},
  351. },
  352. {
  353. name: "bad command",
  354. givenCmd: `toc_get_status`,
  355. wantMsg: []string{cmdInternalSvcErr},
  356. },
  357. })
  358. }
  359. func TestOSCARProxy_RecvClientCmd_ChatJoin(t *testing.T) {
  360. runCmdTests(t, []cmdTestCase{
  361. {
  362. name: "successfully request room creation",
  363. givenCmd: `toc_chat_join 4 "my \$room"`,
  364. wantSNACs: []wire.SNACMessage{
  365. newSNAC(wire.ChatNav, wire.ChatNavCreateRoom, 1, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  366. Exchange: 4,
  367. Cookie: "create",
  368. TLVBlock: wire.TLVBlock{
  369. TLVList: wire.TLVList{
  370. wire.NewTLVBE(wire.ChatRoomTLVRoomName, "my $room"),
  371. },
  372. },
  373. }),
  374. },
  375. wantCacheData: chatJoinReqCtx{},
  376. wantMsg: []string{},
  377. },
  378. {
  379. name: "request room creation, send fails",
  380. givenCmd: `toc_chat_join 4 room`,
  381. sendErrs: []error{io.EOF},
  382. wantSNACs: []wire.SNACMessage{
  383. newSNAC(wire.ChatNav, wire.ChatNavCreateRoom, 1, wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  384. Exchange: 4,
  385. Cookie: "create",
  386. TLVBlock: wire.TLVBlock{
  387. TLVList: wire.TLVList{
  388. wire.NewTLVBE(wire.ChatRoomTLVRoomName, "room"),
  389. },
  390. },
  391. }),
  392. },
  393. wantMsg: []string{cmdInternalSvcErr},
  394. },
  395. {
  396. name: "bad command",
  397. givenCmd: `toc_chat_join`,
  398. wantMsg: []string{cmdInternalSvcErr},
  399. },
  400. {
  401. name: "bad exchange number",
  402. givenCmd: `toc_chat_join four room`,
  403. wantMsg: []string{cmdInternalSvcErr},
  404. },
  405. })
  406. }
  407. func TestOSCARProxy_RecvClientCmd_ChatAccept(t *testing.T) {
  408. room := wire.ICBMRoomInfo{
  409. Exchange: 4,
  410. Cookie: "4-0-the room",
  411. Instance: 0,
  412. }
  413. cases := []struct {
  414. name string
  415. givenCmd string
  416. wantSNACs []wire.SNACMessage
  417. wantCacheData any
  418. wantMsg []string
  419. }{
  420. {
  421. name: "successfully request room info",
  422. givenCmd: `toc_chat_accept 0`,
  423. wantSNACs: []wire.SNACMessage{
  424. newSNAC(wire.ChatNav, wire.ChatNavRequestRoomInfo, 1, wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
  425. Cookie: "4-0-the room",
  426. Exchange: 4,
  427. InstanceNumber: 0,
  428. }),
  429. },
  430. wantCacheData: chatAcceptReqCtx{chatID: 0},
  431. wantMsg: []string{},
  432. },
  433. {
  434. name: "accept invitation to unknown room",
  435. givenCmd: `toc_chat_accept 1`,
  436. wantMsg: []string{cmdInternalSvcErr},
  437. },
  438. {
  439. name: "bad chat room ID",
  440. givenCmd: `toc_chat_accept zero`,
  441. wantMsg: []string{cmdInternalSvcErr},
  442. },
  443. {
  444. name: "bad command",
  445. givenCmd: `toc_chat_accept`,
  446. wantMsg: []string{cmdInternalSvcErr},
  447. },
  448. }
  449. for _, tc := range cases {
  450. t.Run(tc.name, func(t *testing.T) {
  451. me, bos := newTestTOCConn(newTestSession("me"))
  452. me.chats.Add(room)
  453. svc := OSCARProxy{
  454. Logger: slog.Default(),
  455. }
  456. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  457. assert.Equal(t, tc.wantMsg, msg)
  458. assert.Equal(t, tc.wantSNACs, bos.sentSNACs())
  459. if tc.wantCacheData != nil {
  460. req, ok := me.cache.take(wire.SNACFrame{FoodGroup: wire.ChatNav, RequestID: 1})
  461. assert.True(t, ok)
  462. assert.Equal(t, tc.wantCacheData, req.data)
  463. }
  464. })
  465. }
  466. }
  467. func TestOSCARProxy_RecvClientCmd_ChatInvite(t *testing.T) {
  468. room := wire.ICBMRoomInfo{
  469. Exchange: 4,
  470. Cookie: "4-0-the room",
  471. Instance: 0,
  472. }
  473. invite := func(reqID uint32, guest string) wire.SNACMessage {
  474. return newSNAC(wire.ICBM, wire.ICBMChannelMsgToHost, reqID, wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  475. ChannelID: wire.ICBMChannelRendezvous,
  476. ScreenName: guest,
  477. TLVRestBlock: wire.TLVRestBlock{
  478. TLVList: wire.TLVList{
  479. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  480. Type: wire.ICBMRdvMessagePropose,
  481. Capability: wire.CapChat,
  482. TLVRestBlock: wire.TLVRestBlock{
  483. TLVList: wire.TLVList{
  484. wire.NewTLVBE(wire.ICBMRdvTLVTagsSeqNum, uint16(1)),
  485. wire.NewTLVBE(wire.ICBMRdvTLVTagsInvitation, "join $us"),
  486. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMECharset, "us-ascii"),
  487. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMELang, "en"),
  488. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, room),
  489. },
  490. },
  491. }),
  492. },
  493. },
  494. })
  495. }
  496. cases := []struct {
  497. name string
  498. givenCmd string
  499. sendErrs []error
  500. wantSNACs []wire.SNACMessage
  501. wantMsg []string
  502. }{
  503. {
  504. name: "successfully send chat invitations",
  505. givenCmd: `toc_chat_invite 0 "join \$us" friend1 friend2`,
  506. wantSNACs: []wire.SNACMessage{
  507. invite(1, "friend1"),
  508. invite(2, "friend2"),
  509. },
  510. wantMsg: []string{},
  511. },
  512. {
  513. name: "send chat invitations, first send fails",
  514. givenCmd: `toc_chat_invite 0 "join \$us" friend1 friend2`,
  515. sendErrs: []error{io.EOF},
  516. wantSNACs: []wire.SNACMessage{
  517. invite(1, "friend1"),
  518. },
  519. wantMsg: []string{cmdInternalSvcErr},
  520. },
  521. {
  522. name: "send chat invitation to unknown room",
  523. givenCmd: `toc_chat_invite 1 "join us" friend1`,
  524. wantMsg: []string{cmdInternalSvcErr},
  525. },
  526. {
  527. name: "bad chat room ID",
  528. givenCmd: `toc_chat_invite zero "join us" friend1`,
  529. wantMsg: []string{cmdInternalSvcErr},
  530. },
  531. {
  532. name: "bad command",
  533. givenCmd: `toc_chat_invite`,
  534. wantMsg: []string{cmdInternalSvcErr},
  535. },
  536. }
  537. for _, tc := range cases {
  538. t.Run(tc.name, func(t *testing.T) {
  539. me, bos := newTestTOCConn(newTestSession("me"))
  540. bos.sendErrs = tc.sendErrs
  541. me.chats.Add(room)
  542. svc := OSCARProxy{
  543. Logger: slog.Default(),
  544. }
  545. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  546. assert.Equal(t, tc.wantMsg, msg)
  547. assert.Equal(t, tc.wantSNACs, bos.sentSNACs())
  548. })
  549. }
  550. }
  551. func TestOSCARProxy_RecvClientCmd_ChatLeave(t *testing.T) {
  552. cases := []struct {
  553. name string
  554. givenCmd string
  555. wantMsg []string
  556. wantConnClose bool
  557. }{
  558. {
  559. name: "successfully leave chat",
  560. givenCmd: `toc_chat_leave 0`,
  561. wantMsg: []string{"CHAT_LEFT:0"},
  562. wantConnClose: true,
  563. },
  564. {
  565. name: "chat room ID with invalid format",
  566. givenCmd: `toc_chat_leave zero`,
  567. wantMsg: []string{cmdInternalSvcErr},
  568. },
  569. {
  570. name: "missing chat session",
  571. givenCmd: `toc_chat_leave 1`,
  572. wantMsg: []string{cmdInternalSvcErr},
  573. },
  574. {
  575. name: "bad command",
  576. givenCmd: `toc_chat_leave`,
  577. wantMsg: []string{cmdInternalSvcErr},
  578. },
  579. }
  580. for _, tc := range cases {
  581. t.Run(tc.name, func(t *testing.T) {
  582. me, _ := newTestTOCConn(newTestSession("me"))
  583. chatConn := newFakeOSCARConn()
  584. me.chats.RegisterConn(0, chatConn)
  585. svc := OSCARProxy{
  586. Logger: slog.Default(),
  587. }
  588. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  589. assert.Equal(t, tc.wantMsg, msg)
  590. assert.Equal(t, tc.wantConnClose, chatConn.isClosed())
  591. if tc.wantConnClose {
  592. assert.Nil(t, me.chats.RetrieveConn(0))
  593. }
  594. })
  595. }
  596. }
  597. func TestOSCARProxy_RecvClientCmd_ChatSend(t *testing.T) {
  598. cases := []struct {
  599. name string
  600. givenCmd string
  601. sendErrs []error
  602. wantSNACs []wire.SNACMessage
  603. wantMsg []string
  604. }{
  605. {
  606. name: "successfully send chat message",
  607. givenCmd: `toc_chat_send 0 "Hello \$world"`,
  608. wantSNACs: []wire.SNACMessage{
  609. newSNAC(wire.Chat, wire.ChatChannelMsgToHost, 1, wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  610. Channel: wire.ICBMChannelMIME,
  611. TLVRestBlock: wire.TLVRestBlock{
  612. TLVList: wire.TLVList{
  613. wire.NewTLVBE(wire.ChatTLVEnableReflectionFlag, uint8(1)),
  614. wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
  615. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  616. TLVList: wire.TLVList{
  617. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello $world"),
  618. },
  619. }),
  620. },
  621. },
  622. }),
  623. },
  624. wantMsg: []string{},
  625. },
  626. {
  627. name: "send chat message, send fails",
  628. givenCmd: `toc_chat_send 0 "Hello"`,
  629. sendErrs: []error{io.EOF},
  630. wantSNACs: []wire.SNACMessage{
  631. newSNAC(wire.Chat, wire.ChatChannelMsgToHost, 1, wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  632. Channel: wire.ICBMChannelMIME,
  633. TLVRestBlock: wire.TLVRestBlock{
  634. TLVList: wire.TLVList{
  635. wire.NewTLVBE(wire.ChatTLVEnableReflectionFlag, uint8(1)),
  636. wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}),
  637. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  638. TLVList: wire.TLVList{
  639. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello"),
  640. },
  641. }),
  642. },
  643. },
  644. }),
  645. },
  646. wantMsg: []string{cmdInternalSvcErr},
  647. },
  648. {
  649. name: "chat room ID with invalid format",
  650. givenCmd: `toc_chat_send zero "Hello"`,
  651. wantMsg: []string{cmdInternalSvcErr},
  652. },
  653. {
  654. name: "missing chat session",
  655. givenCmd: `toc_chat_send 1 "Hello"`,
  656. wantMsg: []string{cmdInternalSvcErr},
  657. },
  658. {
  659. name: "bad command",
  660. givenCmd: `toc_chat_send`,
  661. wantMsg: []string{cmdInternalSvcErr},
  662. },
  663. }
  664. for _, tc := range cases {
  665. t.Run(tc.name, func(t *testing.T) {
  666. me, bos := newTestTOCConn(newTestSession("me"))
  667. chatConn := newFakeOSCARConn()
  668. chatConn.sendErrs = tc.sendErrs
  669. me.chats.RegisterConn(0, chatConn)
  670. svc := OSCARProxy{
  671. Logger: slog.Default(),
  672. }
  673. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  674. assert.Equal(t, tc.wantMsg, msg)
  675. assert.Equal(t, tc.wantSNACs, chatConn.sentSNACs())
  676. assert.Empty(t, bos.sentSNACs())
  677. })
  678. }
  679. }
  680. func TestOSCARProxy_RecvClientCmd_ChatWhisper(t *testing.T) {
  681. cases := []struct {
  682. name string
  683. givenCmd string
  684. sendErrs []error
  685. wantSNACs []wire.SNACMessage
  686. wantMsg []string
  687. }{
  688. {
  689. name: "successfully send chat whisper",
  690. givenCmd: `toc_chat_whisper 0 them "Hello \$world"`,
  691. wantSNACs: []wire.SNACMessage{
  692. newSNAC(wire.Chat, wire.ChatChannelMsgToHost, 1, wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  693. Channel: wire.ICBMChannelMIME,
  694. TLVRestBlock: wire.TLVRestBlock{
  695. TLVList: wire.TLVList{
  696. wire.NewTLVBE(wire.ChatTLVWhisperToUser, "them"),
  697. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  698. TLVList: wire.TLVList{
  699. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello $world"),
  700. },
  701. }),
  702. },
  703. },
  704. }),
  705. },
  706. wantMsg: []string{},
  707. },
  708. {
  709. name: "send chat whisper, send fails",
  710. givenCmd: `toc_chat_whisper 0 them "Hello"`,
  711. sendErrs: []error{io.EOF},
  712. wantSNACs: []wire.SNACMessage{
  713. newSNAC(wire.Chat, wire.ChatChannelMsgToHost, 1, wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  714. Channel: wire.ICBMChannelMIME,
  715. TLVRestBlock: wire.TLVRestBlock{
  716. TLVList: wire.TLVList{
  717. wire.NewTLVBE(wire.ChatTLVWhisperToUser, "them"),
  718. wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  719. TLVList: wire.TLVList{
  720. wire.NewTLVBE(wire.ChatTLVMessageInfoText, "Hello"),
  721. },
  722. }),
  723. },
  724. },
  725. }),
  726. },
  727. wantMsg: []string{cmdInternalSvcErr},
  728. },
  729. {
  730. name: "chat room ID with invalid format",
  731. givenCmd: `toc_chat_whisper zero them "Hello"`,
  732. wantMsg: []string{cmdInternalSvcErr},
  733. },
  734. {
  735. name: "missing chat session",
  736. givenCmd: `toc_chat_whisper 1 them "Hello"`,
  737. wantMsg: []string{cmdInternalSvcErr},
  738. },
  739. {
  740. name: "bad command",
  741. givenCmd: `toc_chat_whisper`,
  742. wantMsg: []string{cmdInternalSvcErr},
  743. },
  744. }
  745. for _, tc := range cases {
  746. t.Run(tc.name, func(t *testing.T) {
  747. me, _ := newTestTOCConn(newTestSession("me"))
  748. chatConn := newFakeOSCARConn()
  749. chatConn.sendErrs = tc.sendErrs
  750. me.chats.RegisterConn(0, chatConn)
  751. svc := OSCARProxy{
  752. Logger: slog.Default(),
  753. }
  754. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  755. assert.Equal(t, tc.wantMsg, msg)
  756. assert.Equal(t, tc.wantSNACs, chatConn.sentSNACs())
  757. })
  758. }
  759. }
  760. func TestOSCARProxy_RecvClientCmd_ClientEvent(t *testing.T) {
  761. event := func(status uint16) wire.SNACMessage {
  762. return newSNAC(wire.ICBM, wire.ICBMClientEvent, 1, wire.SNAC_0x04_0x14_ICBMClientEvent{
  763. ChannelID: wire.ICBMChannelIM,
  764. ScreenName: "them",
  765. Event: status,
  766. })
  767. }
  768. runCmdTests(t, []cmdTestCase{
  769. {
  770. name: "send typing status 0 (no activity)",
  771. givenCmd: `toc2_client_event them 0`,
  772. wantSNACs: []wire.SNACMessage{event(0)},
  773. wantMsg: []string{},
  774. },
  775. {
  776. name: "send typing status 1 (typing paused)",
  777. givenCmd: `toc2_client_event them 1`,
  778. wantSNACs: []wire.SNACMessage{event(1)},
  779. wantMsg: []string{},
  780. },
  781. {
  782. name: "send typing status 2 (currently typing)",
  783. givenCmd: `toc2_client_event them 2`,
  784. wantSNACs: []wire.SNACMessage{event(2)},
  785. wantMsg: []string{},
  786. },
  787. {
  788. name: "send typing status, send fails",
  789. givenCmd: `toc2_client_event them 2`,
  790. sendErrs: []error{io.EOF},
  791. wantSNACs: []wire.SNACMessage{event(2)},
  792. wantMsg: []string{cmdInternalSvcErr},
  793. },
  794. {
  795. name: "missing args",
  796. givenCmd: `toc2_client_event`,
  797. wantMsg: []string{cmdInternalSvcErr},
  798. },
  799. {
  800. name: "missing typing status",
  801. givenCmd: `toc2_client_event them`,
  802. wantMsg: []string{cmdInternalSvcErr},
  803. },
  804. {
  805. name: "invalid typing status (must be 0, 1, or 2)",
  806. givenCmd: `toc2_client_event them 3`,
  807. wantMsg: []string{cmdInternalSvcErr},
  808. },
  809. {
  810. name: "invalid typing status non-numeric",
  811. givenCmd: `toc2_client_event them x`,
  812. wantMsg: []string{cmdInternalSvcErr},
  813. },
  814. })
  815. }
  816. func TestOSCARProxy_RecvClientCmd_InitDone(t *testing.T) {
  817. runCmdTests(t, []cmdTestCase{
  818. {
  819. name: "successfully initialize connection",
  820. givenCmd: "toc_init_done",
  821. wantSNACs: []wire.SNACMessage{
  822. newSNAC(wire.OService, wire.OServiceClientOnline, 1, wire.SNAC_0x01_0x02_OServiceClientOnline{}),
  823. },
  824. wantMsg: []string{},
  825. },
  826. {
  827. name: "initialize connection, send fails",
  828. givenCmd: "toc_init_done",
  829. sendErrs: []error{io.EOF},
  830. wantSNACs: []wire.SNACMessage{
  831. newSNAC(wire.OService, wire.OServiceClientOnline, 1, wire.SNAC_0x01_0x02_OServiceClientOnline{}),
  832. },
  833. wantMsg: []string{cmdInternalSvcErr},
  834. },
  835. })
  836. }
  837. func TestOSCARProxy_RecvClientCmd_RvousAccept(t *testing.T) {
  838. runCmdTests(t, []cmdTestCase{
  839. {
  840. name: "successfully send rendezvous accept",
  841. givenCmd: `toc_rvous_accept them aGFoYWhhaGE= 09461343-4C7F-11D1-8222-444553540000`,
  842. wantSNACs: []wire.SNACMessage{
  843. newSNAC(wire.ICBM, wire.ICBMChannelMsgToHost, 1, wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  844. ChannelID: wire.ICBMChannelRendezvous,
  845. ScreenName: "them",
  846. TLVRestBlock: wire.TLVRestBlock{
  847. TLVList: wire.TLVList{
  848. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  849. Type: wire.ICBMRdvMessageAccept,
  850. Cookie: [8]byte{'h', 'a', 'h', 'a', 'h', 'a', 'h', 'a'},
  851. Capability: wire.CapFileTransfer,
  852. }),
  853. },
  854. },
  855. }),
  856. },
  857. wantMsg: []string{},
  858. },
  859. {
  860. name: "bad cookie",
  861. givenCmd: `toc_rvous_accept them @@@ 09461343-4C7F-11D1-8222-444553540000`,
  862. wantMsg: []string{cmdInternalSvcErr},
  863. },
  864. {
  865. name: "bad capability",
  866. givenCmd: `toc_rvous_accept them aGFoYWhhaGE= notauuid`,
  867. wantMsg: []string{cmdInternalSvcErr},
  868. },
  869. {
  870. name: "bad command",
  871. givenCmd: `toc_rvous_accept`,
  872. wantMsg: []string{cmdInternalSvcErr},
  873. },
  874. })
  875. }
  876. func TestOSCARProxy_RecvClientCmd_RvousCancel(t *testing.T) {
  877. runCmdTests(t, []cmdTestCase{
  878. {
  879. name: "successfully send rendezvous cancellation",
  880. givenCmd: `toc_rvous_cancel them aGFoYWhhaGE= 09461343-4C7F-11D1-8222-444553540000`,
  881. wantSNACs: []wire.SNACMessage{
  882. newSNAC(wire.ICBM, wire.ICBMChannelMsgToHost, 1, wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  883. ChannelID: wire.ICBMChannelRendezvous,
  884. ScreenName: "them",
  885. TLVRestBlock: wire.TLVRestBlock{
  886. TLVList: wire.TLVList{
  887. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  888. Type: wire.ICBMRdvMessageCancel,
  889. Cookie: [8]byte{'h', 'a', 'h', 'a', 'h', 'a', 'h', 'a'},
  890. Capability: wire.CapFileTransfer,
  891. TLVRestBlock: wire.TLVRestBlock{
  892. TLVList: wire.TLVList{
  893. wire.NewTLVBE(wire.ICBMRdvTLVTagsCancelReason, wire.ICBMRdvCancelReasonsUserCancel),
  894. },
  895. },
  896. }),
  897. },
  898. },
  899. }),
  900. },
  901. wantMsg: []string{},
  902. },
  903. {
  904. name: "bad cookie",
  905. givenCmd: `toc_rvous_cancel them @@@ 09461343-4C7F-11D1-8222-444553540000`,
  906. wantMsg: []string{cmdInternalSvcErr},
  907. },
  908. {
  909. name: "bad command",
  910. givenCmd: `toc_rvous_cancel`,
  911. wantMsg: []string{cmdInternalSvcErr},
  912. },
  913. })
  914. }
  915. // asciiIM returns the ICBM channel 1 message a TOC IM command sends for text.
  916. func asciiIM(reqID uint32, recip string, text string, autoReply bool) wire.SNACMessage {
  917. payload := append([]byte{0x00, 0x00, 0x00, 0x00}, text...)
  918. body := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  919. ChannelID: wire.ICBMChannelIM,
  920. ScreenName: recip,
  921. TLVRestBlock: wire.TLVRestBlock{
  922. TLVList: wire.TLVList{
  923. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  924. {
  925. ID: 5,
  926. Version: 1,
  927. Payload: []byte{1, 1, 2},
  928. },
  929. {
  930. ID: 1,
  931. Version: 1,
  932. Payload: payload,
  933. },
  934. }),
  935. },
  936. },
  937. }
  938. if autoReply {
  939. body.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  940. }
  941. return newSNAC(wire.ICBM, wire.ICBMChannelMsgToHost, reqID, body)
  942. }
  943. func TestOSCARProxy_RecvClientCmd_SendIM(t *testing.T) {
  944. runCmdTests(t, []cmdTestCase{
  945. {
  946. name: "successfully send instant message",
  947. givenCmd: `toc_send_im chattingChuck "hello \$world"`,
  948. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello $world", false)},
  949. wantMsg: []string{},
  950. },
  951. {
  952. name: "successfully send TOC2 instant message",
  953. givenCmd: `toc2_send_im chattingChuck "hello"`,
  954. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  955. wantMsg: []string{},
  956. },
  957. {
  958. name: "successfully auto-reply send instant message",
  959. givenCmd: `toc_send_im chattingChuck "hello" auto`,
  960. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", true)},
  961. wantMsg: []string{},
  962. },
  963. {
  964. name: "send instant message, send fails",
  965. givenCmd: `toc_send_im chattingChuck "hello"`,
  966. sendErrs: []error{io.EOF},
  967. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  968. wantMsg: []string{cmdInternalSvcErr},
  969. },
  970. {
  971. name: "bad command",
  972. givenCmd: `toc_send_im`,
  973. wantMsg: []string{cmdInternalSvcErr},
  974. },
  975. })
  976. }
  977. func TestOSCARProxy_RecvClientCmd_SendIMEnc(t *testing.T) {
  978. runCmdTests(t, []cmdTestCase{
  979. {
  980. name: "successfully send encoded instant message",
  981. givenCmd: `toc2_send_im_enc chattingChuck "F" utf-8 en "hello"`,
  982. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  983. wantMsg: []string{},
  984. },
  985. {
  986. name: "successfully send ASCII-encoded instant message",
  987. givenCmd: `toc2_send_im_enc chattingChuck "F" A en "hello"`,
  988. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  989. wantMsg: []string{},
  990. },
  991. {
  992. name: "successfully send UTF-8-encoded non-ASCII instant message as unicode",
  993. givenCmd: `toc2_send_im_enc chattingChuck "F" U en "привет"`,
  994. wantSNACs: []wire.SNACMessage{
  995. newSNAC(wire.ICBM, wire.ICBMChannelMsgToHost, 1, wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  996. ChannelID: wire.ICBMChannelIM,
  997. ScreenName: "chattingChuck",
  998. TLVRestBlock: wire.TLVRestBlock{
  999. TLVList: wire.TLVList{
  1000. wire.NewTLVBE(wire.ICBMTLVAOLIMData, []wire.ICBMCh1Fragment{
  1001. {
  1002. ID: 5,
  1003. Version: 1,
  1004. Payload: []byte{1, 6},
  1005. },
  1006. {
  1007. ID: 1,
  1008. Version: 1,
  1009. Payload: []byte{
  1010. 0x00, 0x02,
  1011. 0x00, 0x00,
  1012. 0x04, 0x3F, 0x04, 0x40, 0x04, 0x38, 0x04, 0x32, 0x04, 0x35, 0x04, 0x42,
  1013. },
  1014. },
  1015. }),
  1016. },
  1017. },
  1018. }),
  1019. },
  1020. wantMsg: []string{},
  1021. },
  1022. {
  1023. name: "successfully send UTF-8-encoded ASCII instant message as ASCII",
  1024. givenCmd: `toc2_send_im_enc chattingChuck "F" U en "hello"`,
  1025. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  1026. wantMsg: []string{},
  1027. },
  1028. {
  1029. name: "successfully send encoded instant message with auto",
  1030. givenCmd: `toc2_send_im_enc chattingChuck "F" utf-8 en "hello" auto`,
  1031. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", true)},
  1032. wantMsg: []string{},
  1033. },
  1034. {
  1035. name: "send encoded instant message, send fails",
  1036. givenCmd: `toc2_send_im_enc chattingChuck "F" utf-8 en "hello"`,
  1037. sendErrs: []error{io.EOF},
  1038. wantSNACs: []wire.SNACMessage{asciiIM(1, "chattingChuck", "hello", false)},
  1039. wantMsg: []string{cmdInternalSvcErr},
  1040. },
  1041. {
  1042. name: "invalid args too few",
  1043. givenCmd: `toc2_send_im_enc`,
  1044. wantMsg: []string{cmdInternalSvcErr},
  1045. },
  1046. })
  1047. }
  1048. // setInfoSNAC returns the locate set info SNAC a TOC command sends.
  1049. func setInfoSNAC(reqID uint32, tlv wire.TLV) wire.SNACMessage {
  1050. return newSNAC(wire.Locate, wire.LocateSetInfo, reqID, wire.SNAC_0x02_0x04_LocateSetInfo{
  1051. TLVRestBlock: wire.TLVRestBlock{
  1052. TLVList: wire.TLVList{tlv},
  1053. },
  1054. })
  1055. }
  1056. func TestOSCARProxy_RecvClientCmd_SetAway(t *testing.T) {
  1057. runCmdTests(t, []cmdTestCase{
  1058. {
  1059. name: "successfully set away with message",
  1060. givenCmd: `toc_set_away "brb \$5"`,
  1061. wantSNACs: []wire.SNACMessage{
  1062. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, "brb $5")),
  1063. },
  1064. wantMsg: []string{},
  1065. },
  1066. {
  1067. name: "successfully set away without message",
  1068. givenCmd: `toc_set_away`,
  1069. wantSNACs: []wire.SNACMessage{
  1070. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, "")),
  1071. },
  1072. wantMsg: []string{},
  1073. },
  1074. {
  1075. name: "set away message, send fails",
  1076. givenCmd: `toc_set_away "brb"`,
  1077. sendErrs: []error{io.EOF},
  1078. wantSNACs: []wire.SNACMessage{
  1079. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, "brb")),
  1080. },
  1081. wantMsg: []string{cmdInternalSvcErr},
  1082. },
  1083. })
  1084. }
  1085. func TestOSCARProxy_RecvClientCmd_SetCaps(t *testing.T) {
  1086. runCmdTests(t, []cmdTestCase{
  1087. {
  1088. name: "successfully set capabilities",
  1089. givenCmd: `toc_set_caps 09460000-4C7F-11D1-8222-444553540000 09460001-4C7F-11D1-8222-444553540000`,
  1090. wantSNACs: []wire.SNACMessage{
  1091. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{
  1092. uuid.MustParse("09460000-4C7F-11D1-8222-444553540000"),
  1093. uuid.MustParse("09460001-4C7F-11D1-8222-444553540000"),
  1094. wire.CapChat,
  1095. })),
  1096. },
  1097. wantMsg: []string{},
  1098. },
  1099. {
  1100. name: "set capabilities with empty list",
  1101. givenCmd: `toc_set_caps`,
  1102. wantSNACs: []wire.SNACMessage{
  1103. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{
  1104. wire.CapChat,
  1105. })),
  1106. },
  1107. wantMsg: []string{},
  1108. },
  1109. {
  1110. name: "set capability, send fails",
  1111. givenCmd: `toc_set_caps 09460000-4C7F-11D1-8222-444553540000`,
  1112. sendErrs: []error{io.EOF},
  1113. wantSNACs: []wire.SNACMessage{
  1114. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{
  1115. uuid.MustParse("09460000-4C7F-11D1-8222-444553540000"),
  1116. wire.CapChat,
  1117. })),
  1118. },
  1119. wantMsg: []string{cmdInternalSvcErr},
  1120. },
  1121. {
  1122. name: "set malformed capability UUID is skipped",
  1123. givenCmd: `toc_set_caps 09460000-`,
  1124. wantSNACs: []wire.SNACMessage{
  1125. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{
  1126. wire.CapChat,
  1127. })),
  1128. },
  1129. wantMsg: []string{},
  1130. },
  1131. {
  1132. name: "set capabilities with comma-separated list (TameClone format)",
  1133. givenCmd: `toc_set_caps 748F2420-6287-11D1-8222-444553540000,1348,134B,1341,1343,1FF,1345,1346,1347,`,
  1134. wantSNACs: []wire.SNACMessage{
  1135. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{
  1136. wire.CapChat, // 748F... from client
  1137. wire.CapFileSharing, // 1348
  1138. wire.CapBuddyListTransfer, // 134B
  1139. wire.CapVoiceChat, // 1341
  1140. wire.CapFileTransfer, // 1343
  1141. wire.CapSmartCaps, // 1FF
  1142. wire.CapDirectICBM, // 1345
  1143. wire.CapAvatarService, // 1346
  1144. wire.CapStocksAddins, // 1347
  1145. wire.CapChat, // auto-appended
  1146. })),
  1147. },
  1148. wantMsg: []string{},
  1149. },
  1150. })
  1151. }
  1152. // setDirSNAC returns the locate set dir info SNAC a toc_set_dir command
  1153. // sends for the given fields.
  1154. func setDirSNAC(reqID uint32, first, middle, last, maiden, city, state, country string) wire.SNACMessage {
  1155. return newSNAC(wire.Locate, wire.LocateSetDirInfo, reqID, wire.SNAC_0x02_0x09_LocateSetDirInfo{
  1156. TLVRestBlock: wire.TLVRestBlock{
  1157. TLVList: wire.TLVList{
  1158. wire.NewTLVBE(wire.ODirTLVFirstName, first),
  1159. wire.NewTLVBE(wire.ODirTLVMiddleName, middle),
  1160. wire.NewTLVBE(wire.ODirTLVLastName, last),
  1161. wire.NewTLVBE(wire.ODirTLVMaidenName, maiden),
  1162. wire.NewTLVBE(wire.ODirTLVCountry, country),
  1163. wire.NewTLVBE(wire.ODirTLVState, state),
  1164. wire.NewTLVBE(wire.ODirTLVCity, city),
  1165. },
  1166. },
  1167. })
  1168. }
  1169. func TestOSCARProxy_RecvClientCmd_SetDir(t *testing.T) {
  1170. runCmdTests(t, []cmdTestCase{
  1171. {
  1172. name: "successfully set directory info with quoted fields",
  1173. givenCmd: `toc_set_dir "first name\$":"middle name":"last name":"maiden name":"city":"state":"country":"email":"allow web searches"`,
  1174. wantSNACs: []wire.SNACMessage{
  1175. setDirSNAC(1, "first name$", "middle name", "last name", "maiden name", "city", "state", "country"),
  1176. },
  1177. wantMsg: []string{},
  1178. },
  1179. {
  1180. name: "successfully set directory info with some blank fields",
  1181. givenCmd: `toc_set_dir "first name"::"last name"::"city":"state":"country":"email":"allow web searches"`,
  1182. wantSNACs: []wire.SNACMessage{
  1183. setDirSNAC(1, "first name", "", "last name", "", "city", "state", "country"),
  1184. },
  1185. wantMsg: []string{},
  1186. },
  1187. {
  1188. name: "successfully set directory info with last two fields absent",
  1189. givenCmd: `toc_set_dir "first name"::"last name"::"city":"state":"country"`,
  1190. wantSNACs: []wire.SNACMessage{
  1191. setDirSNAC(1, "first name", "", "last name", "", "city", "state", "country"),
  1192. },
  1193. wantMsg: []string{},
  1194. },
  1195. {
  1196. name: "set directory info, send fails",
  1197. givenCmd: `toc_set_dir "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email":"allow web searches"`,
  1198. sendErrs: []error{io.EOF},
  1199. wantSNACs: []wire.SNACMessage{
  1200. setDirSNAC(1, "first name", "middle name", "last name", "maiden name", "city", "state", "country"),
  1201. },
  1202. wantMsg: []string{cmdInternalSvcErr},
  1203. },
  1204. {
  1205. name: "set directory with too many fields present",
  1206. givenCmd: `toc_set_dir "first name"::"last name"::"city":"state":"country":"email":"allow web searches":"extra":"extra"`,
  1207. wantMsg: []string{cmdInternalSvcErr},
  1208. },
  1209. {
  1210. name: "bad command",
  1211. givenCmd: `toc_set_dir`,
  1212. wantMsg: []string{cmdInternalSvcErr},
  1213. },
  1214. })
  1215. }
  1216. func TestOSCARProxy_RecvClientCmd_SetIdle(t *testing.T) {
  1217. runCmdTests(t, []cmdTestCase{
  1218. {
  1219. name: "successfully set idle status",
  1220. givenCmd: `toc_set_idle 10`,
  1221. wantSNACs: []wire.SNACMessage{
  1222. newSNAC(wire.OService, wire.OServiceIdleNotification, 1, wire.SNAC_0x01_0x11_OServiceIdleNotification{
  1223. IdleTime: 10,
  1224. }),
  1225. },
  1226. wantMsg: []string{},
  1227. },
  1228. {
  1229. name: "set idle status, send fails",
  1230. givenCmd: `toc_set_idle 10`,
  1231. sendErrs: []error{io.EOF},
  1232. wantSNACs: []wire.SNACMessage{
  1233. newSNAC(wire.OService, wire.OServiceIdleNotification, 1, wire.SNAC_0x01_0x11_OServiceIdleNotification{
  1234. IdleTime: 10,
  1235. }),
  1236. },
  1237. wantMsg: []string{cmdInternalSvcErr},
  1238. },
  1239. {
  1240. name: "bad secs param",
  1241. givenCmd: `toc_set_idle ten`,
  1242. wantMsg: []string{cmdInternalSvcErr},
  1243. },
  1244. {
  1245. name: "bad command",
  1246. givenCmd: `toc_set_idle`,
  1247. wantMsg: []string{cmdInternalSvcErr},
  1248. },
  1249. })
  1250. }
  1251. func TestOSCARProxy_RecvClientCmd_SetInfo(t *testing.T) {
  1252. runCmdTests(t, []cmdTestCase{
  1253. {
  1254. name: "successfully set profile",
  1255. givenCmd: `toc_set_info "my profile costs \$5"`,
  1256. wantSNACs: []wire.SNACMessage{
  1257. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, "my profile costs $5")),
  1258. },
  1259. wantMsg: []string{},
  1260. },
  1261. {
  1262. name: "set profile, send fails",
  1263. givenCmd: `toc_set_info "my profile"`,
  1264. sendErrs: []error{io.EOF},
  1265. wantSNACs: []wire.SNACMessage{
  1266. setInfoSNAC(1, wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, "my profile")),
  1267. },
  1268. wantMsg: []string{cmdInternalSvcErr},
  1269. },
  1270. {
  1271. name: "bad command",
  1272. givenCmd: `toc_set_info`,
  1273. wantMsg: []string{cmdInternalSvcErr},
  1274. },
  1275. })
  1276. }
  1277. func TestOSCARProxy_RecvClientCmd_UnknownCmd(t *testing.T) {
  1278. runCmdTests(t, []cmdTestCase{
  1279. {
  1280. name: "unknown command",
  1281. givenCmd: `toc_does_not_exist`,
  1282. wantMsg: []string{cmdInternalSvcErr},
  1283. },
  1284. })
  1285. }
  1286. func TestOSCARProxy_RecvClientCmd_NewBuddies(t *testing.T) {
  1287. cases := []struct {
  1288. name string
  1289. me *state.SessionInstance
  1290. givenCmd []byte
  1291. wantMsg []string
  1292. mockParams mockParams
  1293. }{
  1294. {
  1295. name: "add new users to existing group",
  1296. me: newTestSession("me"),
  1297. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:mike\nb:mk6i\ng:Family\nb:alice\nb:bob\n}"),
  1298. wantMsg: []string{"NEW_BUDDY_REPLY2:mike:added", "NEW_BUDDY_REPLY2:mk6i:added", "NEW_BUDDY_REPLY2:alice:added", "NEW_BUDDY_REPLY2:bob:added"},
  1299. mockParams: mockParams{
  1300. feedBagParams: feedBagParams{
  1301. feedbagParams: feedbagParams{
  1302. {
  1303. results: []wire.FeedbagItem{
  1304. {
  1305. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1306. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 21827, 29709})}},
  1307. },
  1308. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1309. {Name: "Co-Workers", GroupID: 21827, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1310. {Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1311. },
  1312. },
  1313. },
  1314. upsertFeedbagParams: upsertFeedbagParams{
  1315. {
  1316. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1317. items: []wire.FeedbagItem{
  1318. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mike", TLVLBlock: wire.TLVLBlock{}},
  1319. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mk6i", TLVLBlock: wire.TLVLBlock{}},
  1320. },
  1321. err: nil,
  1322. },
  1323. {
  1324. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1325. items: []wire.FeedbagItem{
  1326. {
  1327. Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1328. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2})}},
  1329. },
  1330. },
  1331. err: nil,
  1332. },
  1333. {
  1334. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1335. items: []wire.FeedbagItem{
  1336. {ItemID: 3, ClassID: wire.FeedbagClassIdBuddy, GroupID: 29709, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  1337. {ItemID: 4, ClassID: wire.FeedbagClassIdBuddy, GroupID: 29709, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  1338. },
  1339. err: nil,
  1340. },
  1341. {
  1342. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1343. items: []wire.FeedbagItem{
  1344. {
  1345. Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1346. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{3, 4})}},
  1347. },
  1348. },
  1349. err: nil,
  1350. },
  1351. },
  1352. },
  1353. },
  1354. },
  1355. {
  1356. name: "empty feedbag: add new group and buddies",
  1357. me: newTestSession("me"),
  1358. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:mike\nb:mk6i\n}"),
  1359. wantMsg: []string{"NEW_BUDDY_REPLY2:mike:added", "NEW_BUDDY_REPLY2:mk6i:added"},
  1360. mockParams: mockParams{
  1361. feedBagParams: feedBagParams{
  1362. feedbagParams: feedbagParams{
  1363. {
  1364. results: []wire.FeedbagItem{},
  1365. },
  1366. },
  1367. upsertFeedbagParams: upsertFeedbagParams{
  1368. // First: insert the two buddy items (deterministic randIntn gives GroupID 1, ItemIDs 2 and 3)
  1369. {
  1370. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1371. items: []wire.FeedbagItem{
  1372. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, Name: "mike", TLVLBlock: wire.TLVLBlock{}},
  1373. {ItemID: 3, ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, Name: "mk6i", TLVLBlock: wire.TLVLBlock{}},
  1374. },
  1375. err: nil,
  1376. },
  1377. // Second: insert the root group so its Order lists the new group ID
  1378. {
  1379. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1380. items: []wire.FeedbagItem{
  1381. {
  1382. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1383. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  1384. },
  1385. },
  1386. err: nil,
  1387. },
  1388. // Third: insert the new Buddies group with Order listing the buddy item IDs
  1389. {
  1390. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1391. items: []wire.FeedbagItem{
  1392. {
  1393. Name: "Buddies", GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1394. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2, 3})}},
  1395. },
  1396. },
  1397. err: nil,
  1398. },
  1399. },
  1400. },
  1401. },
  1402. },
  1403. {
  1404. name: "add new group and buddies",
  1405. me: newTestSession("me"),
  1406. givenCmd: []byte("toc2_new_buddies {g:Co-Workers\nb:carol\nb:dan\ng:Family\nb:alice\nb:bob\n}"),
  1407. wantMsg: []string{"NEW_BUDDY_REPLY2:carol:added", "NEW_BUDDY_REPLY2:dan:added", "NEW_BUDDY_REPLY2:alice:added", "NEW_BUDDY_REPLY2:bob:added"},
  1408. mockParams: mockParams{
  1409. feedBagParams: feedBagParams{
  1410. feedbagParams: feedbagParams{
  1411. {
  1412. results: []wire.FeedbagItem{
  1413. {
  1414. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1415. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1416. },
  1417. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1418. },
  1419. },
  1420. },
  1421. upsertFeedbagParams: upsertFeedbagParams{
  1422. {
  1423. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1424. items: []wire.FeedbagItem{
  1425. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, Name: "carol", TLVLBlock: wire.TLVLBlock{}},
  1426. {ItemID: 3, ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, Name: "dan", TLVLBlock: wire.TLVLBlock{}},
  1427. },
  1428. err: nil,
  1429. },
  1430. // Root updated once, before the new groups
  1431. {
  1432. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1433. items: []wire.FeedbagItem{
  1434. {
  1435. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1436. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 1, 4})}},
  1437. },
  1438. },
  1439. err: nil,
  1440. },
  1441. {
  1442. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1443. items: []wire.FeedbagItem{
  1444. {
  1445. Name: "Co-Workers", GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1446. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2, 3})}},
  1447. },
  1448. },
  1449. err: nil,
  1450. },
  1451. {
  1452. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1453. items: []wire.FeedbagItem{
  1454. {ItemID: 5, ClassID: wire.FeedbagClassIdBuddy, GroupID: 4, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  1455. {ItemID: 6, ClassID: wire.FeedbagClassIdBuddy, GroupID: 4, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  1456. },
  1457. err: nil,
  1458. },
  1459. {
  1460. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1461. items: []wire.FeedbagItem{
  1462. {
  1463. Name: "Family", GroupID: 4, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1464. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{5, 6})}},
  1465. },
  1466. },
  1467. err: nil,
  1468. },
  1469. },
  1470. },
  1471. },
  1472. },
  1473. {
  1474. name: "empty config returns error",
  1475. me: newTestSession("me"),
  1476. givenCmd: []byte("toc2_new_buddies "),
  1477. wantMsg: []string{cmdInternalSvcErr},
  1478. mockParams: mockParams{},
  1479. },
  1480. {
  1481. name: "whitespace-only config returns error",
  1482. me: newTestSession("me"),
  1483. givenCmd: []byte("toc2_new_buddies "),
  1484. wantMsg: []string{cmdInternalSvcErr},
  1485. mockParams: mockParams{},
  1486. },
  1487. {
  1488. name: "buddy without group returns parse error",
  1489. me: newTestSession("me"),
  1490. givenCmd: []byte("toc2_new_buddies {b:alice\n}"),
  1491. wantMsg: []string{cmdInternalSvcErr},
  1492. mockParams: mockParams{},
  1493. },
  1494. {
  1495. name: "empty group name returns parse error",
  1496. me: newTestSession("me"),
  1497. givenCmd: []byte("toc2_new_buddies {g:\nb:alice\n}"),
  1498. wantMsg: []string{cmdInternalSvcErr},
  1499. mockParams: mockParams{},
  1500. },
  1501. {
  1502. name: "empty buddy name returns parse error",
  1503. me: newTestSession("me"),
  1504. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:\n}"),
  1505. wantMsg: []string{cmdInternalSvcErr},
  1506. mockParams: mockParams{},
  1507. },
  1508. {
  1509. name: "group with no buddies does not call UpsertItem",
  1510. me: newTestSession("me"),
  1511. givenCmd: []byte("toc2_new_buddies {g:EmptyGroup\n}"),
  1512. wantMsg: nil,
  1513. mockParams: mockParams{
  1514. feedBagParams: feedBagParams{
  1515. feedbagParams: feedbagParams{
  1516. {
  1517. results: []wire.FeedbagItem{
  1518. {
  1519. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1520. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1521. },
  1522. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1523. },
  1524. },
  1525. },
  1526. upsertFeedbagParams: upsertFeedbagParams{},
  1527. },
  1528. },
  1529. },
  1530. {
  1531. name: "feedbag upsert send error on first insert returns internal error",
  1532. me: newTestSession("me"),
  1533. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:alice\n}"),
  1534. wantMsg: []string{cmdInternalSvcErr},
  1535. mockParams: mockParams{
  1536. feedBagParams: feedBagParams{
  1537. feedbagParams: feedbagParams{
  1538. {
  1539. results: []wire.FeedbagItem{
  1540. {
  1541. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1542. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  1543. },
  1544. {
  1545. Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1546. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}},
  1547. },
  1548. },
  1549. },
  1550. },
  1551. upsertFeedbagParams: upsertFeedbagParams{
  1552. {
  1553. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1554. items: []wire.FeedbagItem{
  1555. {
  1556. Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1557. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  1558. },
  1559. },
  1560. err: nil,
  1561. },
  1562. {
  1563. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1564. items: []wire.FeedbagItem{
  1565. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  1566. },
  1567. err: errors.New("EOF"),
  1568. },
  1569. },
  1570. },
  1571. },
  1572. },
  1573. {
  1574. name: "buddy already in group skips and adds only new buddy",
  1575. me: newTestSession("me"),
  1576. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:mike\nb:mk6i\n}"),
  1577. wantMsg: []string{"NEW_BUDDY_REPLY2:mk6i:added"},
  1578. mockParams: mockParams{
  1579. feedBagParams: feedBagParams{
  1580. feedbagParams: feedbagParams{
  1581. {
  1582. results: []wire.FeedbagItem{
  1583. {
  1584. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1585. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1586. },
  1587. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1588. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}}},
  1589. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mike", TLVLBlock: wire.TLVLBlock{}},
  1590. },
  1591. },
  1592. },
  1593. upsertFeedbagParams: upsertFeedbagParams{
  1594. {
  1595. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1596. items: []wire.FeedbagItem{
  1597. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mk6i", TLVLBlock: wire.TLVLBlock{}},
  1598. },
  1599. err: nil,
  1600. },
  1601. {
  1602. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1603. items: []wire.FeedbagItem{
  1604. {
  1605. Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1606. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2})}},
  1607. },
  1608. },
  1609. err: nil,
  1610. },
  1611. },
  1612. },
  1613. },
  1614. },
  1615. {
  1616. name: "all buddies already in group returns no replies and no UpsertItem",
  1617. me: newTestSession("me"),
  1618. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:mike\nb:mk6i\n}"),
  1619. wantMsg: nil,
  1620. mockParams: mockParams{
  1621. feedBagParams: feedBagParams{
  1622. feedbagParams: feedbagParams{
  1623. {
  1624. results: []wire.FeedbagItem{
  1625. {
  1626. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1627. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1628. },
  1629. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1630. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2})}}},
  1631. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mike", TLVLBlock: wire.TLVLBlock{}},
  1632. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 17724, Name: "mk6i", TLVLBlock: wire.TLVLBlock{}},
  1633. },
  1634. },
  1635. },
  1636. upsertFeedbagParams: upsertFeedbagParams{},
  1637. },
  1638. },
  1639. },
  1640. {
  1641. name: "single buddy in new group",
  1642. me: newTestSession("me"),
  1643. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:alice\n}"),
  1644. wantMsg: []string{"NEW_BUDDY_REPLY2:alice:added"},
  1645. mockParams: mockParams{
  1646. feedBagParams: feedBagParams{
  1647. feedbagParams: feedbagParams{
  1648. {
  1649. results: []wire.FeedbagItem{},
  1650. },
  1651. },
  1652. upsertFeedbagParams: upsertFeedbagParams{
  1653. {
  1654. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1655. items: []wire.FeedbagItem{
  1656. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 1, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  1657. },
  1658. err: nil,
  1659. },
  1660. {
  1661. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1662. items: []wire.FeedbagItem{
  1663. {
  1664. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1665. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  1666. },
  1667. },
  1668. err: nil,
  1669. },
  1670. {
  1671. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1672. items: []wire.FeedbagItem{
  1673. {
  1674. Name: "Buddies", GroupID: 1, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1675. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2})}},
  1676. },
  1677. },
  1678. err: nil,
  1679. },
  1680. },
  1681. },
  1682. },
  1683. },
  1684. {
  1685. name: "buddy with alias and note",
  1686. me: newTestSession("me"),
  1687. givenCmd: []byte("toc2_new_buddies {g:Buddies\nb:bob:Bob Smith:::::Friend from work\n}"),
  1688. wantMsg: []string{"NEW_BUDDY_REPLY2:bob:added"},
  1689. mockParams: mockParams{
  1690. feedBagParams: feedBagParams{
  1691. feedbagParams: feedbagParams{
  1692. {
  1693. results: []wire.FeedbagItem{
  1694. {
  1695. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1696. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  1697. },
  1698. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1699. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}}},
  1700. },
  1701. },
  1702. },
  1703. upsertFeedbagParams: upsertFeedbagParams{
  1704. {
  1705. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1706. items: []wire.FeedbagItem{
  1707. {
  1708. ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "bob",
  1709. TLVLBlock: wire.TLVLBlock{
  1710. TLVList: wire.TLVList{
  1711. wire.NewTLVBE(wire.FeedbagAttributesAlias, "Bob Smith"),
  1712. wire.NewTLVBE(wire.FeedbagAttributesNote, "Friend from work"),
  1713. },
  1714. },
  1715. },
  1716. },
  1717. err: nil,
  1718. },
  1719. {
  1720. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  1721. items: []wire.FeedbagItem{
  1722. {
  1723. Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1724. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  1725. },
  1726. },
  1727. err: nil,
  1728. },
  1729. },
  1730. },
  1731. },
  1732. },
  1733. }
  1734. for _, tc := range cases {
  1735. t.Run(tc.name, func(t *testing.T) {
  1736. ctx := context.Background()
  1737. me, bos := newTestTOCConn(tc.me)
  1738. me.isTOC2 = true
  1739. for _, params := range tc.mockParams.feedbagParams {
  1740. me.feedbag.reset(params.results)
  1741. }
  1742. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  1743. bos.sendErrs = sendErrs
  1744. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  1745. var randCall int
  1746. randIntn := func(n int) int {
  1747. randCall++
  1748. return randCall
  1749. }
  1750. svc := OSCARProxy{
  1751. Logger: slog.Default(),
  1752. RandIntn: randIntn,
  1753. }
  1754. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  1755. assert.Equal(t, tc.wantMsg, msg)
  1756. assert.Equal(t, wantSNACs, bos.sentSNACs())
  1757. })
  1758. }
  1759. }
  1760. func TestOSCARProxy_RecvClientCmd_NewGroup(t *testing.T) {
  1761. cases := []struct {
  1762. name string
  1763. me *state.SessionInstance
  1764. givenCmd []byte
  1765. wantMsg []string
  1766. mockParams mockParams
  1767. }{
  1768. {
  1769. name: "add new group when feedbag has root and existing groups",
  1770. me: newTestSession("me"),
  1771. givenCmd: []byte("toc2_new_group Family"),
  1772. wantMsg: []string{},
  1773. mockParams: mockParams{
  1774. feedBagParams: feedBagParams{
  1775. feedbagParams: feedbagParams{
  1776. {
  1777. results: []wire.FeedbagItem{
  1778. {
  1779. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1780. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1781. },
  1782. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1783. },
  1784. },
  1785. },
  1786. upsertFeedbagParams: upsertFeedbagParams{
  1787. {
  1788. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1789. items: []wire.FeedbagItem{
  1790. {
  1791. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1792. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 1})}},
  1793. },
  1794. {ClassID: wire.FeedbagClassIdGroup, GroupID: 1, Name: "Family"},
  1795. },
  1796. err: nil,
  1797. },
  1798. },
  1799. },
  1800. },
  1801. },
  1802. {
  1803. name: "add new group when feedbag is empty creates root and group",
  1804. me: newTestSession("me"),
  1805. givenCmd: []byte("toc2_new_group Buddies"),
  1806. wantMsg: []string{},
  1807. mockParams: mockParams{
  1808. feedBagParams: feedBagParams{
  1809. feedbagParams: feedbagParams{
  1810. {
  1811. results: []wire.FeedbagItem{},
  1812. },
  1813. },
  1814. upsertFeedbagParams: upsertFeedbagParams{
  1815. {
  1816. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1817. items: []wire.FeedbagItem{
  1818. {
  1819. ClassID: wire.FeedbagClassIdGroup,
  1820. GroupID: 0,
  1821. Name: "",
  1822. TLVLBlock: wire.TLVLBlock{
  1823. TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})},
  1824. },
  1825. },
  1826. {ClassID: wire.FeedbagClassIdGroup, GroupID: 1, Name: "Buddies"},
  1827. },
  1828. err: nil,
  1829. },
  1830. },
  1831. },
  1832. },
  1833. },
  1834. {
  1835. name: "group already exists returns success idempotently",
  1836. me: newTestSession("me"),
  1837. givenCmd: []byte("toc2_new_group Buddies"),
  1838. wantMsg: []string{},
  1839. mockParams: mockParams{
  1840. feedBagParams: feedBagParams{
  1841. feedbagParams: feedbagParams{
  1842. {
  1843. results: []wire.FeedbagItem{
  1844. {
  1845. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1846. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1847. },
  1848. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1849. },
  1850. },
  1851. },
  1852. upsertFeedbagParams: upsertFeedbagParams{},
  1853. },
  1854. },
  1855. },
  1856. {
  1857. name: "empty group name returns error",
  1858. me: newTestSession("me"),
  1859. givenCmd: []byte("toc2_new_group"),
  1860. wantMsg: []string{cmdInternalSvcErr},
  1861. mockParams: mockParams{
  1862. feedBagParams: feedBagParams{
  1863. feedbagParams: feedbagParams{},
  1864. upsertFeedbagParams: upsertFeedbagParams{},
  1865. },
  1866. },
  1867. },
  1868. {
  1869. name: "feedbag upsert send error returns error",
  1870. me: newTestSession("me"),
  1871. givenCmd: []byte("toc2_new_group Family"),
  1872. wantMsg: []string{cmdInternalSvcErr},
  1873. mockParams: mockParams{
  1874. feedBagParams: feedBagParams{
  1875. feedbagParams: feedbagParams{
  1876. {
  1877. results: []wire.FeedbagItem{
  1878. {
  1879. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1880. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1881. },
  1882. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  1883. },
  1884. },
  1885. },
  1886. upsertFeedbagParams: upsertFeedbagParams{
  1887. {
  1888. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1889. items: []wire.FeedbagItem{
  1890. {
  1891. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1892. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 1})}},
  1893. },
  1894. {ClassID: wire.FeedbagClassIdGroup, GroupID: 1, Name: "Family"},
  1895. },
  1896. err: assert.AnError,
  1897. },
  1898. },
  1899. },
  1900. },
  1901. },
  1902. }
  1903. for _, tc := range cases {
  1904. t.Run(tc.name, func(t *testing.T) {
  1905. ctx := context.Background()
  1906. me, bos := newTestTOCConn(tc.me)
  1907. me.isTOC2 = true
  1908. for _, params := range tc.mockParams.feedbagParams {
  1909. me.feedbag.reset(params.results)
  1910. }
  1911. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  1912. bos.sendErrs = sendErrs
  1913. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  1914. var randCall int
  1915. randIntn := func(n int) int {
  1916. randCall++
  1917. return randCall
  1918. }
  1919. svc := OSCARProxy{
  1920. Logger: slog.Default(),
  1921. RandIntn: randIntn,
  1922. }
  1923. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  1924. assert.Equal(t, tc.wantMsg, msg)
  1925. assert.Equal(t, wantSNACs, bos.sentSNACs())
  1926. })
  1927. }
  1928. }
  1929. func TestOSCARProxy_RecvClientCmd_DelGroup(t *testing.T) {
  1930. cases := []struct {
  1931. name string
  1932. me *state.SessionInstance
  1933. givenCmd []byte
  1934. wantMsg []string
  1935. mockParams mockParams
  1936. }{
  1937. {
  1938. name: "delete existing group",
  1939. me: newTestSession("me"),
  1940. givenCmd: []byte("toc2_del_group Family"),
  1941. wantMsg: []string{},
  1942. mockParams: mockParams{
  1943. feedBagParams: feedBagParams{
  1944. feedbagParams: feedbagParams{
  1945. {
  1946. results: []wire.FeedbagItem{
  1947. {
  1948. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1949. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 29709})}},
  1950. },
  1951. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1952. TLVLBlock: wire.TLVLBlock{
  1953. TLVList: wire.TLVList{
  1954. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1}),
  1955. },
  1956. },
  1957. },
  1958. {Name: "Jane", GroupID: 17724, ItemID: 1, ClassID: wire.FeedbagClassIdBuddy},
  1959. {Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1960. TLVLBlock: wire.TLVLBlock{
  1961. TLVList: wire.TLVList{
  1962. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2, 3}),
  1963. },
  1964. },
  1965. },
  1966. {Name: "Joe", GroupID: 29709, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy},
  1967. {Name: "Fred", GroupID: 29709, ItemID: 3, ClassID: wire.FeedbagClassIdBuddy},
  1968. },
  1969. },
  1970. },
  1971. deleteFeedbagParams: deleteFeedbagParams{
  1972. {
  1973. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  1974. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1975. Items: []wire.FeedbagItem{
  1976. {Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1977. TLVLBlock: wire.TLVLBlock{
  1978. TLVList: wire.TLVList{
  1979. wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2, 3}),
  1980. },
  1981. },
  1982. },
  1983. {Name: "Joe", GroupID: 29709, ItemID: 2, ClassID: wire.FeedbagClassIdBuddy},
  1984. {Name: "Fred", GroupID: 29709, ItemID: 3, ClassID: wire.FeedbagClassIdBuddy},
  1985. },
  1986. },
  1987. err: nil,
  1988. },
  1989. },
  1990. upsertFeedbagParams: upsertFeedbagParams{
  1991. {
  1992. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  1993. items: []wire.FeedbagItem{
  1994. {
  1995. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  1996. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  1997. },
  1998. },
  1999. err: nil,
  2000. },
  2001. },
  2002. },
  2003. },
  2004. },
  2005. {
  2006. name: "empty group name returns error",
  2007. me: newTestSession("me"),
  2008. givenCmd: []byte("toc2_del_group"),
  2009. wantMsg: []string{cmdInternalSvcErr},
  2010. mockParams: mockParams{
  2011. feedBagParams: feedBagParams{
  2012. feedbagParams: feedbagParams{},
  2013. deleteFeedbagParams: deleteFeedbagParams{},
  2014. },
  2015. },
  2016. },
  2017. {
  2018. name: "group not found returns error",
  2019. me: newTestSession("me"),
  2020. givenCmd: []byte("toc2_del_group Nonexistent"),
  2021. wantMsg: []string{cmdInternalSvcErr},
  2022. mockParams: mockParams{
  2023. feedBagParams: feedBagParams{
  2024. feedbagParams: feedbagParams{
  2025. {
  2026. results: []wire.FeedbagItem{
  2027. {
  2028. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  2029. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724})}},
  2030. },
  2031. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  2032. },
  2033. },
  2034. },
  2035. deleteFeedbagParams: deleteFeedbagParams{},
  2036. },
  2037. },
  2038. },
  2039. {
  2040. name: "feedbag delete send error returns error",
  2041. me: newTestSession("me"),
  2042. givenCmd: []byte("toc2_del_group Family"),
  2043. wantMsg: []string{cmdInternalSvcErr},
  2044. mockParams: mockParams{
  2045. feedBagParams: feedBagParams{
  2046. feedbagParams: feedbagParams{
  2047. {
  2048. results: []wire.FeedbagItem{
  2049. {
  2050. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  2051. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{17724, 29709})}},
  2052. },
  2053. {Name: "Buddies", GroupID: 17724, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  2054. {Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  2055. },
  2056. },
  2057. },
  2058. deleteFeedbagParams: deleteFeedbagParams{
  2059. {
  2060. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2061. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2062. Items: []wire.FeedbagItem{
  2063. {Name: "Family", GroupID: 29709, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  2064. },
  2065. },
  2066. err: assert.AnError,
  2067. },
  2068. },
  2069. },
  2070. },
  2071. },
  2072. }
  2073. for _, tc := range cases {
  2074. t.Run(tc.name, func(t *testing.T) {
  2075. ctx := context.Background()
  2076. me, bos := newTestTOCConn(tc.me)
  2077. me.isTOC2 = true
  2078. for _, params := range tc.mockParams.feedbagParams {
  2079. me.feedbag.reset(params.results)
  2080. }
  2081. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2082. bos.sendErrs = sendErrs
  2083. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2084. var randCall int
  2085. randIntn := func(n int) int {
  2086. randCall++
  2087. return randCall
  2088. }
  2089. svc := OSCARProxy{
  2090. Logger: slog.Default(),
  2091. RandIntn: randIntn,
  2092. }
  2093. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2094. assert.Equal(t, tc.wantMsg, msg)
  2095. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2096. })
  2097. }
  2098. }
  2099. func TestOSCARProxy_RecvClientCmd_SetPDMode(t *testing.T) {
  2100. cases := []struct {
  2101. name string
  2102. me *state.SessionInstance
  2103. givenCmd []byte
  2104. wantMsg []string
  2105. mockParams mockParams
  2106. }{
  2107. {
  2108. name: "empty feedbag creates new Pdinfo with mode 4",
  2109. me: newTestSession("me"),
  2110. givenCmd: []byte("toc2_set_pdmode 4"),
  2111. wantMsg: []string{},
  2112. mockParams: mockParams{
  2113. feedBagParams: feedBagParams{
  2114. feedbagParams: feedbagParams{
  2115. {
  2116. results: []wire.FeedbagItem{},
  2117. },
  2118. },
  2119. upsertFeedbagParams: upsertFeedbagParams{
  2120. {
  2121. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2122. items: []wire.FeedbagItem{
  2123. {
  2124. ClassID: wire.FeedbagClassIdPdinfo,
  2125. GroupID: 0,
  2126. ItemID: 1,
  2127. TLVLBlock: wire.TLVLBlock{
  2128. TLVList: wire.TLVList{
  2129. wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(4)),
  2130. },
  2131. },
  2132. },
  2133. },
  2134. err: nil,
  2135. },
  2136. },
  2137. },
  2138. },
  2139. },
  2140. {
  2141. name: "pdinfo already exists but does not have FeedbagAttributesPdMode",
  2142. me: newTestSession("me"),
  2143. givenCmd: []byte("toc2_set_pdmode 2"),
  2144. wantMsg: []string{},
  2145. mockParams: mockParams{
  2146. feedBagParams: feedBagParams{
  2147. feedbagParams: feedbagParams{
  2148. {
  2149. results: []wire.FeedbagItem{
  2150. {
  2151. ClassID: wire.FeedbagClassIdPdinfo,
  2152. GroupID: 0,
  2153. ItemID: 99,
  2154. TLVLBlock: wire.TLVLBlock{
  2155. TLVList: wire.TLVList{},
  2156. },
  2157. },
  2158. },
  2159. },
  2160. },
  2161. upsertFeedbagParams: upsertFeedbagParams{
  2162. {
  2163. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2164. items: []wire.FeedbagItem{
  2165. {
  2166. ClassID: wire.FeedbagClassIdPdinfo,
  2167. GroupID: 0,
  2168. ItemID: 99,
  2169. TLVLBlock: wire.TLVLBlock{
  2170. TLVList: wire.TLVList{
  2171. wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(2)),
  2172. },
  2173. },
  2174. },
  2175. },
  2176. err: nil,
  2177. },
  2178. },
  2179. },
  2180. },
  2181. },
  2182. {
  2183. name: "pdinfo already exists and already has FeedbagAttributesPdMode with same mode",
  2184. me: newTestSession("me"),
  2185. givenCmd: []byte("toc2_set_pdmode 3"),
  2186. wantMsg: []string{},
  2187. mockParams: mockParams{
  2188. feedBagParams: feedBagParams{
  2189. feedbagParams: feedbagParams{
  2190. {
  2191. results: []wire.FeedbagItem{
  2192. {
  2193. ClassID: wire.FeedbagClassIdPdinfo,
  2194. GroupID: 0,
  2195. ItemID: 99,
  2196. TLVLBlock: wire.TLVLBlock{
  2197. TLVList: wire.TLVList{
  2198. wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(3)),
  2199. },
  2200. },
  2201. },
  2202. },
  2203. },
  2204. },
  2205. upsertFeedbagParams: upsertFeedbagParams{},
  2206. },
  2207. },
  2208. },
  2209. {
  2210. name: "pdinfo already exists and already has FeedbagAttributesPdMode with different mode",
  2211. me: newTestSession("me"),
  2212. givenCmd: []byte("toc2_set_pdmode 3"),
  2213. wantMsg: []string{},
  2214. mockParams: mockParams{
  2215. feedBagParams: feedBagParams{
  2216. feedbagParams: feedbagParams{
  2217. {
  2218. results: []wire.FeedbagItem{
  2219. {
  2220. ClassID: wire.FeedbagClassIdPdinfo,
  2221. GroupID: 0,
  2222. ItemID: 99,
  2223. TLVLBlock: wire.TLVLBlock{
  2224. TLVList: wire.TLVList{
  2225. wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(1)),
  2226. },
  2227. },
  2228. },
  2229. },
  2230. },
  2231. },
  2232. upsertFeedbagParams: upsertFeedbagParams{
  2233. {
  2234. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2235. items: []wire.FeedbagItem{
  2236. {
  2237. ClassID: wire.FeedbagClassIdPdinfo,
  2238. GroupID: 0,
  2239. ItemID: 99,
  2240. TLVLBlock: wire.TLVLBlock{
  2241. TLVList: wire.TLVList{
  2242. wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(3)),
  2243. },
  2244. },
  2245. },
  2246. },
  2247. err: nil,
  2248. },
  2249. },
  2250. },
  2251. },
  2252. },
  2253. }
  2254. for _, tc := range cases {
  2255. t.Run(tc.name, func(t *testing.T) {
  2256. ctx := context.Background()
  2257. me, bos := newTestTOCConn(tc.me)
  2258. me.isTOC2 = true
  2259. for _, params := range tc.mockParams.feedbagParams {
  2260. me.feedbag.reset(params.results)
  2261. }
  2262. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2263. bos.sendErrs = sendErrs
  2264. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2265. var randCall int
  2266. randIntn := func(n int) int {
  2267. randCall++
  2268. return randCall
  2269. }
  2270. svc := OSCARProxy{
  2271. Logger: slog.Default(),
  2272. RandIntn: randIntn,
  2273. }
  2274. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2275. assert.Equal(t, tc.wantMsg, msg)
  2276. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2277. })
  2278. }
  2279. }
  2280. func TestOSCARProxy_RecvClientCmd_AddPermit2(t *testing.T) {
  2281. cases := []struct {
  2282. name string
  2283. me *state.SessionInstance
  2284. givenCmd []byte
  2285. wantMsg []string
  2286. mockParams mockParams
  2287. }{
  2288. {
  2289. name: "no screennames provided returns internal error",
  2290. me: newTestSession("me"),
  2291. givenCmd: []byte("toc2_add_permit"),
  2292. wantMsg: []string{cmdInternalSvcErr},
  2293. mockParams: mockParams{},
  2294. },
  2295. {
  2296. name: "feedbag upsert send error returns internal error",
  2297. me: newTestSession("me"),
  2298. givenCmd: []byte("toc2_add_permit alice"),
  2299. wantMsg: []string{cmdInternalSvcErr},
  2300. mockParams: mockParams{
  2301. feedBagParams: feedBagParams{
  2302. feedbagParams: feedbagParams{
  2303. {
  2304. results: []wire.FeedbagItem{},
  2305. },
  2306. },
  2307. upsertFeedbagParams: upsertFeedbagParams{
  2308. {
  2309. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2310. items: []wire.FeedbagItem{
  2311. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2312. },
  2313. err: errors.New("EOF"),
  2314. },
  2315. },
  2316. },
  2317. },
  2318. },
  2319. {
  2320. name: "add permits to empty feedbag",
  2321. me: newTestSession("me"),
  2322. givenCmd: []byte("toc2_add_permit alice bob"),
  2323. wantMsg: []string{},
  2324. mockParams: mockParams{
  2325. feedBagParams: feedBagParams{
  2326. feedbagParams: feedbagParams{
  2327. {
  2328. results: []wire.FeedbagItem{},
  2329. },
  2330. },
  2331. upsertFeedbagParams: upsertFeedbagParams{
  2332. {
  2333. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2334. items: []wire.FeedbagItem{
  2335. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2336. {ItemID: 2, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2337. },
  2338. err: nil,
  2339. },
  2340. },
  2341. },
  2342. },
  2343. },
  2344. {
  2345. name: "skip existing permit, add only new",
  2346. me: newTestSession("me"),
  2347. givenCmd: []byte("toc2_add_permit alice bob"),
  2348. wantMsg: []string{},
  2349. mockParams: mockParams{
  2350. feedBagParams: feedBagParams{
  2351. feedbagParams: feedbagParams{
  2352. {
  2353. results: []wire.FeedbagItem{
  2354. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2355. },
  2356. },
  2357. },
  2358. upsertFeedbagParams: upsertFeedbagParams{
  2359. {
  2360. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2361. items: []wire.FeedbagItem{
  2362. {ItemID: 2, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2363. },
  2364. err: nil,
  2365. },
  2366. },
  2367. },
  2368. },
  2369. },
  2370. {
  2371. name: "no-op when permit already in feedbag",
  2372. me: newTestSession("me"),
  2373. givenCmd: []byte("toc2_add_permit alice"),
  2374. wantMsg: []string{},
  2375. mockParams: mockParams{
  2376. feedBagParams: feedBagParams{
  2377. feedbagParams: feedbagParams{
  2378. {
  2379. results: []wire.FeedbagItem{
  2380. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2381. },
  2382. },
  2383. },
  2384. upsertFeedbagParams: upsertFeedbagParams{},
  2385. },
  2386. },
  2387. },
  2388. }
  2389. for _, tc := range cases {
  2390. t.Run(tc.name, func(t *testing.T) {
  2391. ctx := context.Background()
  2392. me, bos := newTestTOCConn(tc.me)
  2393. me.isTOC2 = true
  2394. for _, params := range tc.mockParams.feedbagParams {
  2395. me.feedbag.reset(params.results)
  2396. }
  2397. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2398. bos.sendErrs = sendErrs
  2399. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2400. var randCall int
  2401. randIntn := func(n int) int {
  2402. randCall++
  2403. return randCall
  2404. }
  2405. svc := OSCARProxy{
  2406. Logger: slog.Default(),
  2407. RandIntn: randIntn,
  2408. }
  2409. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2410. assert.Equal(t, tc.wantMsg, msg)
  2411. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2412. })
  2413. }
  2414. }
  2415. func TestOSCARProxy_RecvClientCmd_AddDeny2(t *testing.T) {
  2416. cases := []struct {
  2417. name string
  2418. me *state.SessionInstance
  2419. givenCmd []byte
  2420. wantMsg []string
  2421. mockParams mockParams
  2422. }{
  2423. {
  2424. name: "no screennames provided returns internal error",
  2425. me: newTestSession("me"),
  2426. givenCmd: []byte("toc2_add_deny"),
  2427. wantMsg: []string{cmdInternalSvcErr},
  2428. mockParams: mockParams{},
  2429. },
  2430. {
  2431. name: "feedbag upsert send error returns internal error",
  2432. me: newTestSession("me"),
  2433. givenCmd: []byte("toc2_add_deny alice"),
  2434. wantMsg: []string{cmdInternalSvcErr},
  2435. mockParams: mockParams{
  2436. feedBagParams: feedBagParams{
  2437. feedbagParams: feedbagParams{
  2438. {
  2439. results: []wire.FeedbagItem{},
  2440. },
  2441. },
  2442. upsertFeedbagParams: upsertFeedbagParams{
  2443. {
  2444. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2445. items: []wire.FeedbagItem{
  2446. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2447. },
  2448. err: errors.New("EOF"),
  2449. },
  2450. },
  2451. },
  2452. },
  2453. },
  2454. {
  2455. name: "add deny to empty feedbag",
  2456. me: newTestSession("me"),
  2457. givenCmd: []byte("toc2_add_deny alice"),
  2458. wantMsg: []string{},
  2459. mockParams: mockParams{
  2460. feedBagParams: feedBagParams{
  2461. feedbagParams: feedbagParams{
  2462. {
  2463. results: []wire.FeedbagItem{},
  2464. },
  2465. },
  2466. upsertFeedbagParams: upsertFeedbagParams{
  2467. {
  2468. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2469. items: []wire.FeedbagItem{
  2470. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2471. },
  2472. err: nil,
  2473. },
  2474. },
  2475. },
  2476. },
  2477. },
  2478. {
  2479. name: "skip existing deny, add only new",
  2480. me: newTestSession("me"),
  2481. givenCmd: []byte("toc2_add_deny alice bob"),
  2482. wantMsg: []string{},
  2483. mockParams: mockParams{
  2484. feedBagParams: feedBagParams{
  2485. feedbagParams: feedbagParams{
  2486. {
  2487. results: []wire.FeedbagItem{
  2488. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2489. },
  2490. },
  2491. },
  2492. upsertFeedbagParams: upsertFeedbagParams{
  2493. {
  2494. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
  2495. items: []wire.FeedbagItem{
  2496. {ItemID: 2, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2497. },
  2498. err: nil,
  2499. },
  2500. },
  2501. },
  2502. },
  2503. },
  2504. {
  2505. name: "no-op when deny already in feedbag",
  2506. me: newTestSession("me"),
  2507. givenCmd: []byte("toc2_add_deny alice"),
  2508. wantMsg: []string{},
  2509. mockParams: mockParams{
  2510. feedBagParams: feedBagParams{
  2511. feedbagParams: feedbagParams{
  2512. {
  2513. results: []wire.FeedbagItem{
  2514. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2515. },
  2516. },
  2517. },
  2518. upsertFeedbagParams: upsertFeedbagParams{},
  2519. },
  2520. },
  2521. },
  2522. }
  2523. for _, tc := range cases {
  2524. t.Run(tc.name, func(t *testing.T) {
  2525. ctx := context.Background()
  2526. me, bos := newTestTOCConn(tc.me)
  2527. me.isTOC2 = true
  2528. for _, params := range tc.mockParams.feedbagParams {
  2529. me.feedbag.reset(params.results)
  2530. }
  2531. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2532. bos.sendErrs = sendErrs
  2533. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2534. var randCall int
  2535. randIntn := func(n int) int {
  2536. randCall++
  2537. return randCall
  2538. }
  2539. svc := OSCARProxy{
  2540. Logger: slog.Default(),
  2541. RandIntn: randIntn,
  2542. }
  2543. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2544. assert.Equal(t, tc.wantMsg, msg)
  2545. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2546. })
  2547. }
  2548. }
  2549. func TestOSCARProxy_RecvClientCmd_RemoveBuddy2(t *testing.T) {
  2550. cases := []struct {
  2551. name string
  2552. me *state.SessionInstance
  2553. givenCmd []byte
  2554. wantMsg []string
  2555. mockParams mockParams
  2556. }{
  2557. {
  2558. name: "removes buddy from group order TLV and upserts group",
  2559. me: newTestSession("me"),
  2560. givenCmd: []byte("toc2_remove_buddy friend2 Buddies"),
  2561. wantMsg: []string{},
  2562. mockParams: mockParams{
  2563. feedBagParams: feedBagParams{
  2564. feedbagParams: feedbagParams{
  2565. {
  2566. results: []wire.FeedbagItem{
  2567. {
  2568. Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  2569. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2, 3})}},
  2570. },
  2571. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend1", TLVLBlock: wire.TLVLBlock{}},
  2572. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend2", TLVLBlock: wire.TLVLBlock{}},
  2573. {ItemID: 3, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend3", TLVLBlock: wire.TLVLBlock{}},
  2574. },
  2575. },
  2576. },
  2577. deleteFeedbagParams: deleteFeedbagParams{
  2578. {
  2579. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2580. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2581. Items: []wire.FeedbagItem{
  2582. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend2", TLVLBlock: wire.TLVLBlock{}},
  2583. },
  2584. },
  2585. err: nil,
  2586. },
  2587. },
  2588. upsertFeedbagParams: upsertFeedbagParams{
  2589. {
  2590. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
  2591. items: []wire.FeedbagItem{
  2592. {
  2593. Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  2594. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 3})}},
  2595. },
  2596. },
  2597. err: nil,
  2598. },
  2599. },
  2600. },
  2601. },
  2602. },
  2603. {
  2604. name: "parseArgs error returns internal error",
  2605. me: newTestSession("me"),
  2606. givenCmd: []byte("toc2_remove_buddy \"unclosed"),
  2607. wantMsg: []string{cmdInternalSvcErr},
  2608. mockParams: mockParams{},
  2609. },
  2610. {
  2611. name: "missing params returns internal error",
  2612. me: newTestSession("me"),
  2613. givenCmd: []byte("toc2_remove_buddy friend1"),
  2614. wantMsg: []string{cmdInternalSvcErr},
  2615. mockParams: mockParams{},
  2616. },
  2617. {
  2618. name: "group not found returns internal error",
  2619. me: newTestSession("me"),
  2620. givenCmd: []byte("toc2_remove_buddy friend1 NoSuchGroup"),
  2621. wantMsg: []string{cmdInternalSvcErr},
  2622. mockParams: mockParams{
  2623. feedBagParams: feedBagParams{
  2624. feedbagParams: feedbagParams{
  2625. {
  2626. results: []wire.FeedbagItem{
  2627. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  2628. },
  2629. },
  2630. },
  2631. deleteFeedbagParams: deleteFeedbagParams{},
  2632. upsertFeedbagParams: upsertFeedbagParams{},
  2633. },
  2634. },
  2635. },
  2636. {
  2637. name: "no-op when buddy not in group",
  2638. me: newTestSession("me"),
  2639. givenCmd: []byte("toc2_remove_buddy stranger Buddies"),
  2640. wantMsg: []string{},
  2641. mockParams: mockParams{
  2642. feedBagParams: feedBagParams{
  2643. feedbagParams: feedbagParams{
  2644. {
  2645. results: []wire.FeedbagItem{
  2646. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  2647. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}}},
  2648. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend1", TLVLBlock: wire.TLVLBlock{}},
  2649. },
  2650. },
  2651. },
  2652. deleteFeedbagParams: deleteFeedbagParams{},
  2653. upsertFeedbagParams: upsertFeedbagParams{},
  2654. },
  2655. },
  2656. },
  2657. }
  2658. for _, tc := range cases {
  2659. t.Run(tc.name, func(t *testing.T) {
  2660. ctx := context.Background()
  2661. me, bos := newTestTOCConn(tc.me)
  2662. me.isTOC2 = true
  2663. for _, params := range tc.mockParams.feedbagParams {
  2664. me.feedbag.reset(params.results)
  2665. }
  2666. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2667. bos.sendErrs = sendErrs
  2668. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2669. var randCall int
  2670. randIntn := func(n int) int {
  2671. randCall++
  2672. return randCall
  2673. }
  2674. svc := OSCARProxy{
  2675. Logger: slog.Default(),
  2676. RandIntn: randIntn,
  2677. }
  2678. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2679. assert.Equal(t, tc.wantMsg, msg)
  2680. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2681. })
  2682. }
  2683. }
  2684. func TestOSCARProxy_RecvClientCmd_RemovePermit2(t *testing.T) {
  2685. cases := []struct {
  2686. name string
  2687. me *state.SessionInstance
  2688. givenCmd []byte
  2689. wantMsg []string
  2690. mockParams mockParams
  2691. }{
  2692. {
  2693. name: "no screennames provided returns internal error",
  2694. me: newTestSession("me"),
  2695. givenCmd: []byte("toc2_remove_permit"),
  2696. wantMsg: []string{cmdInternalSvcErr},
  2697. mockParams: mockParams{},
  2698. },
  2699. {
  2700. name: "feedbag delete send error returns internal error",
  2701. me: newTestSession("me"),
  2702. givenCmd: []byte("toc2_remove_permit alice"),
  2703. wantMsg: []string{cmdInternalSvcErr},
  2704. mockParams: mockParams{
  2705. feedBagParams: feedBagParams{
  2706. feedbagParams: feedbagParams{
  2707. {
  2708. results: []wire.FeedbagItem{
  2709. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2710. },
  2711. },
  2712. },
  2713. deleteFeedbagParams: deleteFeedbagParams{
  2714. {
  2715. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2716. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2717. Items: []wire.FeedbagItem{
  2718. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2719. },
  2720. },
  2721. err: errors.New("EOF"),
  2722. },
  2723. },
  2724. },
  2725. },
  2726. },
  2727. {
  2728. name: "remove permits from feedbag",
  2729. me: newTestSession("me"),
  2730. givenCmd: []byte("toc2_remove_permit alice bob"),
  2731. wantMsg: []string{},
  2732. mockParams: mockParams{
  2733. feedBagParams: feedBagParams{
  2734. feedbagParams: feedbagParams{
  2735. {
  2736. results: []wire.FeedbagItem{
  2737. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2738. {ItemID: 2, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2739. },
  2740. },
  2741. },
  2742. deleteFeedbagParams: deleteFeedbagParams{
  2743. {
  2744. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2745. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2746. Items: []wire.FeedbagItem{
  2747. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2748. {ItemID: 2, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2749. },
  2750. },
  2751. err: nil,
  2752. },
  2753. },
  2754. },
  2755. },
  2756. },
  2757. {
  2758. name: "remove only existing permits",
  2759. me: newTestSession("me"),
  2760. givenCmd: []byte("toc2_remove_permit alice bob"),
  2761. wantMsg: []string{},
  2762. mockParams: mockParams{
  2763. feedBagParams: feedBagParams{
  2764. feedbagParams: feedbagParams{
  2765. {
  2766. results: []wire.FeedbagItem{
  2767. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2768. },
  2769. },
  2770. },
  2771. deleteFeedbagParams: deleteFeedbagParams{
  2772. {
  2773. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2774. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2775. Items: []wire.FeedbagItem{
  2776. {ItemID: 1, ClassID: wire.FeedbagClassIDPermit, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2777. },
  2778. },
  2779. err: nil,
  2780. },
  2781. },
  2782. },
  2783. },
  2784. },
  2785. {
  2786. name: "no-op when no permits in feedbag",
  2787. me: newTestSession("me"),
  2788. givenCmd: []byte("toc2_remove_permit alice"),
  2789. wantMsg: []string{},
  2790. mockParams: mockParams{
  2791. feedBagParams: feedBagParams{
  2792. feedbagParams: feedbagParams{
  2793. {
  2794. results: []wire.FeedbagItem{},
  2795. },
  2796. },
  2797. deleteFeedbagParams: deleteFeedbagParams{},
  2798. },
  2799. },
  2800. },
  2801. }
  2802. for _, tc := range cases {
  2803. t.Run(tc.name, func(t *testing.T) {
  2804. ctx := context.Background()
  2805. me, bos := newTestTOCConn(tc.me)
  2806. me.isTOC2 = true
  2807. for _, params := range tc.mockParams.feedbagParams {
  2808. me.feedbag.reset(params.results)
  2809. }
  2810. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2811. bos.sendErrs = sendErrs
  2812. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2813. var randCall int
  2814. randIntn := func(n int) int {
  2815. randCall++
  2816. return randCall
  2817. }
  2818. svc := OSCARProxy{
  2819. Logger: slog.Default(),
  2820. RandIntn: randIntn,
  2821. }
  2822. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2823. assert.Equal(t, tc.wantMsg, msg)
  2824. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2825. })
  2826. }
  2827. }
  2828. func TestOSCARProxy_RecvClientCmd_RemoveDeny2(t *testing.T) {
  2829. cases := []struct {
  2830. name string
  2831. me *state.SessionInstance
  2832. givenCmd []byte
  2833. wantMsg []string
  2834. mockParams mockParams
  2835. }{
  2836. {
  2837. name: "no screennames provided returns internal error",
  2838. me: newTestSession("me"),
  2839. givenCmd: []byte("toc2_remove_deny"),
  2840. wantMsg: []string{cmdInternalSvcErr},
  2841. mockParams: mockParams{},
  2842. },
  2843. {
  2844. name: "feedbag delete send error returns internal error",
  2845. me: newTestSession("me"),
  2846. givenCmd: []byte("toc2_remove_deny alice"),
  2847. wantMsg: []string{cmdInternalSvcErr},
  2848. mockParams: mockParams{
  2849. feedBagParams: feedBagParams{
  2850. feedbagParams: feedbagParams{
  2851. {
  2852. results: []wire.FeedbagItem{
  2853. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2854. },
  2855. },
  2856. },
  2857. deleteFeedbagParams: deleteFeedbagParams{
  2858. {
  2859. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2860. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2861. Items: []wire.FeedbagItem{
  2862. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2863. },
  2864. },
  2865. err: errors.New("EOF"),
  2866. },
  2867. },
  2868. },
  2869. },
  2870. },
  2871. {
  2872. name: "remove denies from feedbag",
  2873. me: newTestSession("me"),
  2874. givenCmd: []byte("toc2_remove_deny alice bob"),
  2875. wantMsg: []string{},
  2876. mockParams: mockParams{
  2877. feedBagParams: feedBagParams{
  2878. feedbagParams: feedbagParams{
  2879. {
  2880. results: []wire.FeedbagItem{
  2881. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2882. {ItemID: 2, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2883. },
  2884. },
  2885. },
  2886. deleteFeedbagParams: deleteFeedbagParams{
  2887. {
  2888. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2889. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2890. Items: []wire.FeedbagItem{
  2891. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2892. {ItemID: 2, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "bob", TLVLBlock: wire.TLVLBlock{}},
  2893. },
  2894. },
  2895. err: nil,
  2896. },
  2897. },
  2898. },
  2899. },
  2900. },
  2901. {
  2902. name: "remove only existing denies",
  2903. me: newTestSession("me"),
  2904. givenCmd: []byte("toc2_remove_deny alice bob"),
  2905. wantMsg: []string{},
  2906. mockParams: mockParams{
  2907. feedBagParams: feedBagParams{
  2908. feedbagParams: feedbagParams{
  2909. {
  2910. results: []wire.FeedbagItem{
  2911. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2912. },
  2913. },
  2914. },
  2915. deleteFeedbagParams: deleteFeedbagParams{
  2916. {
  2917. frame: wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
  2918. inBody: wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  2919. Items: []wire.FeedbagItem{
  2920. {ItemID: 1, ClassID: wire.FeedbagClassIDDeny, GroupID: 0, Name: "alice", TLVLBlock: wire.TLVLBlock{}},
  2921. },
  2922. },
  2923. err: nil,
  2924. },
  2925. },
  2926. },
  2927. },
  2928. },
  2929. {
  2930. name: "no-op when no denies in feedbag",
  2931. me: newTestSession("me"),
  2932. givenCmd: []byte("toc2_remove_deny alice"),
  2933. wantMsg: []string{},
  2934. mockParams: mockParams{
  2935. feedBagParams: feedBagParams{
  2936. feedbagParams: feedbagParams{
  2937. {
  2938. results: []wire.FeedbagItem{},
  2939. },
  2940. },
  2941. deleteFeedbagParams: deleteFeedbagParams{},
  2942. },
  2943. },
  2944. },
  2945. }
  2946. for _, tc := range cases {
  2947. t.Run(tc.name, func(t *testing.T) {
  2948. ctx := context.Background()
  2949. me, bos := newTestTOCConn(tc.me)
  2950. me.isTOC2 = true
  2951. for _, params := range tc.mockParams.feedbagParams {
  2952. me.feedbag.reset(params.results)
  2953. }
  2954. wantSNACs, sendErrs := feedbagSNACs(tc.mockParams.feedBagParams)
  2955. bos.sendErrs = sendErrs
  2956. // Deterministic item ID: increment by 1 per call so IDs are unique and predictable per test.
  2957. var randCall int
  2958. randIntn := func(n int) int {
  2959. randCall++
  2960. return randCall
  2961. }
  2962. svc := OSCARProxy{
  2963. Logger: slog.Default(),
  2964. RandIntn: randIntn,
  2965. }
  2966. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  2967. assert.Equal(t, tc.wantMsg, msg)
  2968. assert.Equal(t, wantSNACs, bos.sentSNACs())
  2969. })
  2970. }
  2971. }
  2972. func TestOSCARProxy_RecvClientCmd_GetDirSearchURL(t *testing.T) {
  2973. cases := []struct {
  2974. // name is the unit test name
  2975. name string
  2976. // me is the TOC user session
  2977. me *state.SessionInstance
  2978. // givenCmd is the TOC command
  2979. givenCmd []byte
  2980. // wantMsg is the expected TOC response
  2981. wantMsg []string
  2982. // mockParams is the list of params sent to mocks that satisfy this
  2983. // method's dependencies
  2984. mockParams mockParams
  2985. }{
  2986. {
  2987. name: "successfully request user info",
  2988. me: newTestSession("me"),
  2989. givenCmd: []byte(`toc_dir_search "first \[name\]":"middle name":"last name":"maiden name":"city":"state":"country":"email"`),
  2990. mockParams: mockParams{
  2991. cookieBakerParams: cookieBakerParams{
  2992. issueParams: issueParams{
  2993. {
  2994. data: []byte("me"),
  2995. returnData: []byte("monster"),
  2996. },
  2997. },
  2998. },
  2999. },
  3000. wantMsg: []string{"GOTO_URL:search results:dir_search?city=city&cookie=6d6f6e73746572&country=country&email=email&first_name=first+%5Bname%5D&last_name=last+name&maiden_name=maiden+name&middle_name=middle+name&state=state"},
  3001. },
  3002. {
  3003. name: "successfully request user info by keywords",
  3004. me: newTestSession("me"),
  3005. givenCmd: []byte(`toc_dir_search ::::::::::"searchkw"`),
  3006. mockParams: mockParams{
  3007. cookieBakerParams: cookieBakerParams{
  3008. issueParams: issueParams{
  3009. {
  3010. data: []byte("me"),
  3011. returnData: []byte("monster"),
  3012. },
  3013. },
  3014. },
  3015. },
  3016. wantMsg: []string{"GOTO_URL:search results:dir_search?cookie=6d6f6e73746572&keyword=searchkw"},
  3017. },
  3018. {
  3019. name: "request user info with too many params",
  3020. me: newTestSession("me"),
  3021. givenCmd: []byte(`toc_dir_search ::::::::::::::::::::"searchkw"`),
  3022. wantMsg: []string{cmdInternalSvcErr},
  3023. },
  3024. {
  3025. name: "request user info, get cookie issue error",
  3026. me: newTestSession("me"),
  3027. givenCmd: []byte(`toc_dir_search them`),
  3028. mockParams: mockParams{
  3029. cookieBakerParams: cookieBakerParams{
  3030. issueParams: issueParams{
  3031. {
  3032. data: []byte("me"),
  3033. returnErr: io.EOF,
  3034. },
  3035. },
  3036. },
  3037. },
  3038. wantMsg: []string{cmdInternalSvcErr},
  3039. },
  3040. {
  3041. name: "bad command",
  3042. me: newTestSession("me"),
  3043. givenCmd: []byte(`toc_dir_search`),
  3044. wantMsg: []string{cmdInternalSvcErr},
  3045. },
  3046. }
  3047. for _, tc := range cases {
  3048. t.Run(tc.name, func(t *testing.T) {
  3049. ctx := context.Background()
  3050. cookieBaker := newMockCookieBaker(t)
  3051. for _, params := range tc.mockParams.issueParams {
  3052. cookieBaker.EXPECT().
  3053. Issue(params.data, state.DefaultCookieTTL).
  3054. Return(params.returnData, params.returnErr)
  3055. }
  3056. me, bos := newTestTOCConn(tc.me)
  3057. svc := OSCARProxy{
  3058. Logger: slog.Default(),
  3059. CookieBaker: cookieBaker,
  3060. }
  3061. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  3062. assert.Equal(t, tc.wantMsg, msg)
  3063. assert.Empty(t, bos.sentSNACs())
  3064. })
  3065. }
  3066. }
  3067. func TestOSCARProxy_RecvClientCmd_GetDirURL(t *testing.T) {
  3068. cases := []struct {
  3069. // name is the unit test name
  3070. name string
  3071. // me is the TOC user session
  3072. me *state.SessionInstance
  3073. // givenCmd is the TOC command
  3074. givenCmd []byte
  3075. // wantMsg is the expected TOC response
  3076. wantMsg []string
  3077. // mockParams is the list of params sent to mocks that satisfy this
  3078. // method's dependencies
  3079. mockParams mockParams
  3080. }{
  3081. {
  3082. name: "successfully request user dir info",
  3083. me: newTestSession("me"),
  3084. givenCmd: []byte(`toc_get_dir them`),
  3085. mockParams: mockParams{
  3086. cookieBakerParams: cookieBakerParams{
  3087. issueParams: issueParams{
  3088. {
  3089. data: []byte("me"),
  3090. returnData: []byte("monster"),
  3091. },
  3092. },
  3093. },
  3094. },
  3095. wantMsg: []string{"GOTO_URL:directory info:dir_info?cookie=6d6f6e73746572&user=them"},
  3096. },
  3097. {
  3098. name: "request user info, get cookie issue error",
  3099. me: newTestSession("me"),
  3100. givenCmd: []byte(`toc_get_dir them`),
  3101. mockParams: mockParams{
  3102. cookieBakerParams: cookieBakerParams{
  3103. issueParams: issueParams{
  3104. {
  3105. data: []byte("me"),
  3106. returnErr: io.EOF,
  3107. },
  3108. },
  3109. },
  3110. },
  3111. wantMsg: []string{cmdInternalSvcErr},
  3112. },
  3113. {
  3114. name: "bad command",
  3115. me: newTestSession("me"),
  3116. givenCmd: []byte(`toc_get_dir`),
  3117. wantMsg: []string{cmdInternalSvcErr},
  3118. },
  3119. }
  3120. for _, tc := range cases {
  3121. t.Run(tc.name, func(t *testing.T) {
  3122. ctx := context.Background()
  3123. cookieBaker := newMockCookieBaker(t)
  3124. for _, params := range tc.mockParams.issueParams {
  3125. cookieBaker.EXPECT().
  3126. Issue(params.data, state.DefaultCookieTTL).
  3127. Return(params.returnData, params.returnErr)
  3128. }
  3129. me, bos := newTestTOCConn(tc.me)
  3130. svc := OSCARProxy{
  3131. Logger: slog.Default(),
  3132. CookieBaker: cookieBaker,
  3133. }
  3134. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  3135. assert.Equal(t, tc.wantMsg, msg)
  3136. assert.Empty(t, bos.sentSNACs())
  3137. })
  3138. }
  3139. }
  3140. func TestOSCARProxy_RecvClientCmd_GetInfoURL(t *testing.T) {
  3141. cases := []struct {
  3142. // name is the unit test name
  3143. name string
  3144. // me is the TOC user session
  3145. me *state.SessionInstance
  3146. // givenCmd is the TOC command
  3147. givenCmd []byte
  3148. // wantMsg is the expected TOC response
  3149. wantMsg []string
  3150. // mockParams is the list of params sent to mocks that satisfy this
  3151. // method's dependencies
  3152. mockParams mockParams
  3153. }{
  3154. {
  3155. name: "successfully request user info",
  3156. me: newTestSession("me"),
  3157. givenCmd: []byte(`toc_get_info them`),
  3158. mockParams: mockParams{
  3159. cookieBakerParams: cookieBakerParams{
  3160. issueParams: issueParams{
  3161. {
  3162. data: []byte("me"),
  3163. returnData: []byte("monster"),
  3164. },
  3165. },
  3166. },
  3167. },
  3168. wantMsg: []string{"GOTO_URL:profile:info?cookie=6d6f6e73746572&from=me&user=them"},
  3169. },
  3170. {
  3171. name: "request user info, get cookie issue error",
  3172. me: newTestSession("me"),
  3173. givenCmd: []byte(`toc_get_info them`),
  3174. mockParams: mockParams{
  3175. cookieBakerParams: cookieBakerParams{
  3176. issueParams: issueParams{
  3177. {
  3178. data: []byte("me"),
  3179. returnErr: io.EOF,
  3180. },
  3181. },
  3182. },
  3183. },
  3184. wantMsg: []string{cmdInternalSvcErr},
  3185. },
  3186. {
  3187. name: "bad command",
  3188. me: newTestSession("me"),
  3189. givenCmd: []byte(`toc_get_info`),
  3190. wantMsg: []string{cmdInternalSvcErr},
  3191. },
  3192. }
  3193. for _, tc := range cases {
  3194. t.Run(tc.name, func(t *testing.T) {
  3195. ctx := context.Background()
  3196. cookieBaker := newMockCookieBaker(t)
  3197. for _, params := range tc.mockParams.issueParams {
  3198. cookieBaker.EXPECT().
  3199. Issue(params.data, state.DefaultCookieTTL).
  3200. Return(params.returnData, params.returnErr)
  3201. }
  3202. me, bos := newTestTOCConn(tc.me)
  3203. svc := OSCARProxy{
  3204. Logger: slog.Default(),
  3205. CookieBaker: cookieBaker,
  3206. }
  3207. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  3208. assert.Equal(t, tc.wantMsg, msg)
  3209. assert.Empty(t, bos.sentSNACs())
  3210. })
  3211. }
  3212. }
  3213. func TestOSCARProxy_RecvClientCmd_SetConfig(t *testing.T) {
  3214. cases := []struct {
  3215. // name is the unit test name
  3216. name string
  3217. // me is the TOC user session
  3218. me *state.SessionInstance
  3219. // givenCmd is the TOC command
  3220. givenCmd []byte
  3221. // wantMsg is the expected TOC response
  3222. wantMsg []string
  3223. // mockParams is the list of params sent to mocks that satisfy this
  3224. // method's dependencies
  3225. mockParams mockParams
  3226. }{
  3227. {
  3228. name: "successfully set permit all config (unquoted)",
  3229. me: newTestSession("me"),
  3230. givenCmd: []byte("toc_set_config {m 1\ng Buddies\nb friend1\nb friend2\n}\n"),
  3231. mockParams: mockParams{
  3232. tocConfigParams: tocConfigParams{
  3233. setTOCConfigParams: setTOCConfigParams{
  3234. {
  3235. user: state.NewIdentScreenName("me"),
  3236. config: "{m 1\ng Buddies\nb friend1\nb friend2\n}\n",
  3237. },
  3238. },
  3239. },
  3240. },
  3241. wantMsg: []string{},
  3242. },
  3243. {
  3244. name: "successfully set permit all config (double-quoted)",
  3245. me: newTestSession("me"),
  3246. givenCmd: []byte("toc_set_config \"{m 1\ng Buddies\nb friend1\nb friend2\n}\n\""),
  3247. mockParams: mockParams{
  3248. tocConfigParams: tocConfigParams{
  3249. setTOCConfigParams: setTOCConfigParams{
  3250. {
  3251. user: state.NewIdentScreenName("me"),
  3252. config: "{m 1\ng Buddies\nb friend1\nb friend2\n}\n",
  3253. },
  3254. },
  3255. },
  3256. },
  3257. wantMsg: []string{},
  3258. },
  3259. {
  3260. name: "successfully set permit all config (single-quoted)",
  3261. me: newTestSession("me"),
  3262. givenCmd: []byte("toc_set_config '{m 1\ng Buddies\nb friend1\nb friend2\n}\n'"),
  3263. mockParams: mockParams{
  3264. tocConfigParams: tocConfigParams{
  3265. setTOCConfigParams: setTOCConfigParams{
  3266. {
  3267. user: state.NewIdentScreenName("me"),
  3268. config: "{m 1\ng Buddies\nb friend1\nb friend2\n}\n",
  3269. },
  3270. },
  3271. },
  3272. },
  3273. wantMsg: []string{},
  3274. },
  3275. {
  3276. name: "successfully set permit all config (double-quoted with spaces)",
  3277. me: newTestSession("me"),
  3278. givenCmd: []byte("toc_set_config \" {m 1\ng Buddies\nb friend1\nb friend2\n}\n \""),
  3279. mockParams: mockParams{
  3280. tocConfigParams: tocConfigParams{
  3281. setTOCConfigParams: setTOCConfigParams{
  3282. {
  3283. user: state.NewIdentScreenName("me"),
  3284. config: "{m 1\ng Buddies\nb friend1\nb friend2\n}\n",
  3285. },
  3286. },
  3287. },
  3288. },
  3289. wantMsg: []string{},
  3290. },
  3291. {
  3292. name: "set config, receive error from toc config store",
  3293. me: newTestSession("me"),
  3294. givenCmd: []byte("toc_set_config {m 1\ng Buddies\nb friend1\nb friend2\n}\n"),
  3295. mockParams: mockParams{
  3296. tocConfigParams: tocConfigParams{
  3297. setTOCConfigParams: setTOCConfigParams{
  3298. {
  3299. user: state.NewIdentScreenName("me"),
  3300. config: "{m 1\ng Buddies\nb friend1\nb friend2\n}\n",
  3301. err: io.EOF,
  3302. },
  3303. },
  3304. },
  3305. },
  3306. wantMsg: []string{cmdInternalSvcErr},
  3307. },
  3308. {
  3309. name: "bad command",
  3310. me: newTestSession("me"),
  3311. givenCmd: []byte(`toc_set_config`),
  3312. wantMsg: []string{cmdInternalSvcErr},
  3313. },
  3314. }
  3315. for _, tc := range cases {
  3316. t.Run(tc.name, func(t *testing.T) {
  3317. ctx := context.Background()
  3318. tocConfigSvc := newMockTOCConfigStore(t)
  3319. for _, params := range tc.mockParams.setTOCConfigParams {
  3320. tocConfigSvc.EXPECT().
  3321. SetTOCConfig(matchContext(), params.user, params.config).
  3322. Return(params.err)
  3323. }
  3324. me, bos := newTestTOCConn(tc.me)
  3325. svc := OSCARProxy{
  3326. Logger: slog.Default(),
  3327. TOCConfigStore: tocConfigSvc,
  3328. }
  3329. msg := svc.RecvClientCmd(ctx, me, tc.givenCmd)
  3330. assert.Equal(t, tc.wantMsg, msg)
  3331. assert.Empty(t, bos.sentSNACs())
  3332. })
  3333. }
  3334. }
  3335. func Test_parseArgs(t *testing.T) {
  3336. type testCase struct {
  3337. name string
  3338. givenPayload string
  3339. givenArgs []*string
  3340. wantVarArgs []string
  3341. wantArgs []string
  3342. wantErrMsg string
  3343. }
  3344. tests := []testCase{
  3345. {
  3346. name: "no positional args or varargs",
  3347. givenPayload: ``,
  3348. givenArgs: nil,
  3349. wantVarArgs: []string{},
  3350. },
  3351. {
  3352. name: "positional args with varargs",
  3353. givenPayload: `1234 "Join me!" user1 user2 user3`,
  3354. givenArgs: []*string{new(string), new(string)},
  3355. wantVarArgs: []string{"user1", "user2", "user3"},
  3356. wantArgs: []string{"1234", "Join me!"},
  3357. },
  3358. {
  3359. name: "nil positional argument placeholders should get skipped",
  3360. givenPayload: `1234 "Join me!" user1 user2 user3`,
  3361. givenArgs: []*string{nil, nil}, // still 2 placeholders, both nil
  3362. wantVarArgs: []string{"user1", "user2", "user3"},
  3363. wantArgs: []string{"", ""},
  3364. },
  3365. {
  3366. name: "positional args with no varargs",
  3367. givenPayload: `1234 "Join me!"`,
  3368. givenArgs: []*string{new(string), new(string)}, // roomID + msg
  3369. wantVarArgs: []string{},
  3370. wantArgs: []string{"1234", "Join me!"},
  3371. },
  3372. {
  3373. name: "varargs only",
  3374. givenPayload: `user1 user2 user3`,
  3375. givenArgs: nil,
  3376. wantVarArgs: []string{"user1", "user2", "user3"},
  3377. },
  3378. {
  3379. name: "too many positional arg placeholders",
  3380. givenPayload: `toc_chat_invite`,
  3381. givenArgs: []*string{new(string), new(string)},
  3382. wantVarArgs: []string{},
  3383. wantErrMsg: "command contains fewer arguments than expected",
  3384. },
  3385. {
  3386. name: "CSV parser error",
  3387. givenPayload: ``,
  3388. givenArgs: []*string{nil},
  3389. wantVarArgs: []string{},
  3390. wantErrMsg: "CSV reader error",
  3391. },
  3392. }
  3393. for _, tt := range tests {
  3394. t.Run(tt.name, func(t *testing.T) {
  3395. varArgs, err := parseArgs([]byte(tt.givenPayload), tt.givenArgs...)
  3396. if tt.wantErrMsg != "" {
  3397. assert.ErrorContains(t, err, tt.wantErrMsg)
  3398. return
  3399. }
  3400. assert.NoError(t, err)
  3401. // verify the placeholder pointers got populated
  3402. for i, want := range tt.wantArgs {
  3403. if want == "" {
  3404. assert.Nil(t, tt.givenArgs[i])
  3405. } else {
  3406. got := *tt.givenArgs[i]
  3407. assert.Equal(t, want, got)
  3408. }
  3409. }
  3410. // verify we have the same varargs
  3411. assert.Equal(t, tt.wantVarArgs, varArgs)
  3412. assert.Equal(t, len(tt.wantArgs), len(tt.givenArgs))
  3413. })
  3414. }
  3415. }
  3416. func TestBuildToc2Config(t *testing.T) {
  3417. tests := []struct {
  3418. name string
  3419. fb []wire.FeedbagItem
  3420. want []string // CONFIG2 command string(s), each starts with "CONFIG2:"
  3421. wantErr string
  3422. }{
  3423. {
  3424. name: "root group missing order attribute returns error",
  3425. fb: []wire.FeedbagItem{
  3426. {Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup, TLVLBlock: wire.TLVLBlock{}},
  3427. },
  3428. wantErr: "root group missing order attribute",
  3429. },
  3430. {
  3431. name: "empty buddylist yields only done",
  3432. fb: []wire.FeedbagItem{
  3433. {
  3434. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3435. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}},
  3436. },
  3437. },
  3438. want: []string{"CONFIG2:done:\n"},
  3439. },
  3440. {
  3441. name: "single group with buddies",
  3442. fb: []wire.FeedbagItem{
  3443. {
  3444. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3445. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  3446. },
  3447. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3448. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2})}}},
  3449. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "user1", TLVLBlock: wire.TLVLBlock{}},
  3450. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "user2", TLVLBlock: wire.TLVLBlock{}},
  3451. },
  3452. want: []string{"CONFIG2:g:Buddies\nb:user1\nb:user2\ndone:\n"},
  3453. },
  3454. {
  3455. name: "blocked user (d:) and private user (p:)",
  3456. fb: []wire.FeedbagItem{
  3457. {
  3458. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3459. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}},
  3460. },
  3461. {ClassID: wire.FeedbagClassIDDeny, Name: "blockeduser"},
  3462. {ClassID: wire.FeedbagClassIDPermit, Name: "allowuser"},
  3463. },
  3464. want: []string{"CONFIG2:d:blockeduser\np:allowuser\ndone:\n"},
  3465. },
  3466. {
  3467. name: "privacy level (m:) from pdinfo",
  3468. fb: []wire.FeedbagItem{
  3469. {
  3470. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3471. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}},
  3472. },
  3473. {
  3474. ClassID: wire.FeedbagClassIdPdinfo,
  3475. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesPdMode, uint8(3))}},
  3476. },
  3477. },
  3478. want: []string{"CONFIG2:m:3\ndone:\n"},
  3479. },
  3480. {
  3481. name: "buddy with alias",
  3482. fb: []wire.FeedbagItem{
  3483. {
  3484. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3485. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  3486. },
  3487. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3488. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}}},
  3489. {
  3490. ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "bob",
  3491. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesAlias, "Bob Smith")}},
  3492. },
  3493. },
  3494. want: []string{"CONFIG2:g:Buddies\nb:bob:Bob Smith\ndone:\n"},
  3495. },
  3496. {
  3497. name: "buddy with note (comment) uses five colons before note",
  3498. fb: []wire.FeedbagItem{
  3499. {
  3500. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3501. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  3502. },
  3503. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3504. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}}},
  3505. {
  3506. ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "alice",
  3507. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesNote, "Friend from work")}},
  3508. },
  3509. },
  3510. want: []string{"CONFIG2:g:Buddies\nb:alice:::::Friend from work\ndone:\n"},
  3511. },
  3512. {
  3513. name: "multiple groups in root order",
  3514. fb: []wire.FeedbagItem{
  3515. {
  3516. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3517. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100, 200})}},
  3518. },
  3519. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3520. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}}},
  3521. {Name: "Family", GroupID: 200, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3522. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2})}}},
  3523. {ItemID: 1, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend1", TLVLBlock: wire.TLVLBlock{}},
  3524. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 200, Name: "mom", TLVLBlock: wire.TLVLBlock{}},
  3525. },
  3526. want: []string{"CONFIG2:g:Buddies\nb:friend1\ng:Family\nb:mom\ndone:\n"},
  3527. },
  3528. {
  3529. name: "group order listing a missing buddy skips it",
  3530. fb: []wire.FeedbagItem{
  3531. {
  3532. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3533. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100})}},
  3534. },
  3535. {Name: "Buddies", GroupID: 100, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3536. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1, 2})}}},
  3537. {ItemID: 2, ClassID: wire.FeedbagClassIdBuddy, GroupID: 100, Name: "friend2", TLVLBlock: wire.TLVLBlock{}},
  3538. },
  3539. want: []string{"CONFIG2:g:Buddies\nb:friend2\ndone:\n"},
  3540. },
  3541. {
  3542. name: "root order listing a missing group skips it",
  3543. fb: []wire.FeedbagItem{
  3544. {
  3545. Name: "", GroupID: 0, ItemID: 0, ClassID: wire.FeedbagClassIdGroup,
  3546. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{100, 200})}},
  3547. },
  3548. {Name: "Family", GroupID: 200, ItemID: 0, ClassID: wire.FeedbagClassIdGroup},
  3549. },
  3550. want: []string{"CONFIG2:g:Family\ndone:\n"},
  3551. },
  3552. }
  3553. for _, tt := range tests {
  3554. t.Run(tt.name, func(t *testing.T) {
  3555. got, err := buildToc2Config(tt.fb)
  3556. if tt.wantErr != "" {
  3557. assert.ErrorContains(t, err, tt.wantErr)
  3558. assert.Empty(t, got)
  3559. return
  3560. }
  3561. assert.NoError(t, err)
  3562. assert.Equal(t, tt.want, got)
  3563. })
  3564. }
  3565. }
  3566. // TestUnescape tests the unescape function
  3567. func TestUnescape(t *testing.T) {
  3568. tests := []struct {
  3569. name string
  3570. input string
  3571. expected string
  3572. }{
  3573. {"No Escapes", "Hello World", "Hello World"},
  3574. {"Escaped Brace", "Hello \\{World\\}", "Hello {World}"},
  3575. {"Escaped Parentheses", "Test\\(123\\)", "Test(123)"},
  3576. {"Escaped Brackets", "\\[List\\]", "[List]"},
  3577. {"Escaped Dollar", "Price: \\$100", "Price: $100"},
  3578. {"Escaped Quote", "She said \\\"Hello\\\"", "She said \"Hello\""},
  3579. {"Multiple Escapes", "One\\, Two\\, Three", "One, Two, Three"},
  3580. {"Consecutive Escapes", "\\\\\\$100", "\\$100"},
  3581. {"Only Escape Character", "\\", ""},
  3582. {"Empty Input", "", ""},
  3583. }
  3584. for _, tt := range tests {
  3585. t.Run(tt.name, func(t *testing.T) {
  3586. result := unescape(tt.input)
  3587. if result != tt.expected {
  3588. t.Errorf("unescape(%q) = %q; want %q", tt.input, result, tt.expected)
  3589. }
  3590. })
  3591. }
  3592. }
  3593. func TestOSCARProxy_Signon(t *testing.T) {
  3594. clientAddr := &net.TCPAddr{IP: net.IPv4(10, 0, 0, 5), Port: 4321}
  3595. roastedPass := []byte{0x24, 0x08, 0x10, 0x5c, 0x23, 0x00, 0x11, 0x30}
  3596. cookie := []byte("the-cookie")
  3597. // bosReply answers signon queries the way the BOS server does.
  3598. bosReply := func(feedbag []wire.FeedbagItem) func(frame wire.SNACFrame) []wire.SNACMessage {
  3599. return func(frame wire.SNACFrame) []wire.SNACMessage {
  3600. switch {
  3601. case frame.FoodGroup == wire.OService && frame.SubGroup == wire.OServiceUserInfoQuery:
  3602. return []wire.SNACMessage{newSNAC(wire.OService, wire.OServiceUserInfoUpdate, frame.RequestID,
  3603. wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate{
  3604. UserInfo: []wire.TLVUserInfo{{ScreenName: "M E"}},
  3605. })}
  3606. case frame.FoodGroup == wire.Feedbag && frame.SubGroup == wire.FeedbagQuery:
  3607. return []wire.SNACMessage{newSNAC(wire.Feedbag, wire.FeedbagReply, frame.RequestID,
  3608. wire.SNAC_0x13_0x06_FeedbagReply{Items: feedbag})}
  3609. }
  3610. return nil
  3611. }
  3612. }
  3613. signonSNACs := func(t *testing.T) []receivedSNAC {
  3614. return []receivedSNAC{
  3615. newReceivedSNAC(t, wire.OService, wire.OServiceRateParamsSubAdd, 1, wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
  3616. ClassIDs: []uint16{1, 2, 3, 4, 5},
  3617. }),
  3618. newReceivedSNAC(t, wire.Locate, wire.LocateSetInfo, 2, wire.SNAC_0x02_0x04_LocateSetInfo{
  3619. TLVRestBlock: wire.TLVRestBlock{
  3620. TLVList: wire.TLVList{
  3621. wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, []uuid.UUID{wire.CapChat}),
  3622. },
  3623. },
  3624. }),
  3625. newReceivedSNAC(t, wire.OService, wire.OServiceUserInfoQuery, 3, struct{}{}),
  3626. }
  3627. }
  3628. authOK := []wire.TLV{
  3629. wire.NewTLVBE(wire.LoginTLVTagsScreenName, "me"),
  3630. wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, cookie),
  3631. }
  3632. toc1AuthTLVs := wire.TLVList{
  3633. wire.NewTLVBE(wire.LoginTLVTagsScreenName, "me"),
  3634. wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, roastedPass),
  3635. }
  3636. toc2AuthTLVs := append(append(wire.TLVList{}, toc1AuthTLVs...),
  3637. wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient))
  3638. cases := []struct {
  3639. // name is the unit test name
  3640. name string
  3641. // givenCmd is the signon command
  3642. givenCmd string
  3643. // authReply are the TLVs the auth service replies with, nil when TOC
  3644. // shouldn't connect to it
  3645. authReply []wire.TLV
  3646. // bos is the BOS service, nil when TOC shouldn't connect to it
  3647. bos *fakeService
  3648. // tocConfig is the stored TOC1 config, or tocConfigErr its lookup error
  3649. tocConfig *state.User
  3650. tocConfigErr error
  3651. // wantAuthTLVs are the TLVs TOC sends the auth service
  3652. wantAuthTLVs wire.TLVList
  3653. // wantBOSSNACs returns the SNACs TOC sends the BOS service
  3654. wantBOSSNACs func(t *testing.T) []receivedSNAC
  3655. // wantMsg is the expected TOC response
  3656. wantMsg []string
  3657. // wantTOC2 and wantMsgEnc are the TOC version of a successful signon
  3658. wantTOC2, wantMsgEnc bool
  3659. // wantBacklog is the number of BOS SNACs kept for RecvBOS
  3660. wantBacklog int
  3661. // wantFeedbag is the feedbag mirror after signon
  3662. wantFeedbag []wire.FeedbagItem
  3663. // bucpAuth makes the auth service reply the way it does to an address
  3664. // that recently used BUCP login
  3665. bucpAuth bool
  3666. }{
  3667. {
  3668. name: "successfully login TOC1",
  3669. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3670. authReply: authOK,
  3671. bos: &fakeService{reply: bosReply(nil)},
  3672. tocConfig: &state.User{TOCConfig: "m 1\ng Buddies"},
  3673. wantAuthTLVs: toc1AuthTLVs,
  3674. wantBOSSNACs: signonSNACs,
  3675. wantMsg: []string{"SIGN_ON:TOC1.0", "CONFIG:m 1\ng Buddies", "NICK:M E"},
  3676. },
  3677. {
  3678. name: "successfully login TOC2",
  3679. givenCmd: `toc2_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3680. authReply: authOK,
  3681. bos: &fakeService{reply: bosReply(nil)},
  3682. wantAuthTLVs: toc2AuthTLVs,
  3683. wantBOSSNACs: func(t *testing.T) []receivedSNAC {
  3684. return append(signonSNACs(t),
  3685. newReceivedSNAC(t, wire.Feedbag, wire.FeedbagUse, 4, struct{}{}),
  3686. newReceivedSNAC(t, wire.Feedbag, wire.FeedbagQuery, 5, struct{}{}),
  3687. )
  3688. },
  3689. wantMsg: []string{"SIGN_ON:TOC2.0", "NICK:M E", "CONFIG2:done:\n"},
  3690. wantTOC2: true,
  3691. },
  3692. {
  3693. name: "successfully login toc2_login (TOC2 with encoded messaging)",
  3694. givenCmd: `toc2_login localhost 9898 me 0x2408105c23001130 english "TIC:TOC" 160 US "" "" 3 0 30303 -kentucky -utf8 103547776`,
  3695. authReply: authOK,
  3696. bos: &fakeService{reply: bosReply([]wire.FeedbagItem{
  3697. {
  3698. ClassID: wire.FeedbagClassIdGroup,
  3699. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  3700. },
  3701. {ClassID: wire.FeedbagClassIdGroup, GroupID: 1, Name: "Buddies"},
  3702. })},
  3703. wantAuthTLVs: toc2AuthTLVs,
  3704. wantBOSSNACs: func(t *testing.T) []receivedSNAC {
  3705. return append(signonSNACs(t),
  3706. newReceivedSNAC(t, wire.Feedbag, wire.FeedbagUse, 4, struct{}{}),
  3707. newReceivedSNAC(t, wire.Feedbag, wire.FeedbagQuery, 5, struct{}{}),
  3708. )
  3709. },
  3710. wantMsg: []string{"SIGN_ON:TOC2.0", "NICK:M E", "CONFIG2:g:Buddies\ndone:\n"},
  3711. wantTOC2: true,
  3712. wantMsgEnc: true,
  3713. wantFeedbag: []wire.FeedbagItem{
  3714. {
  3715. ClassID: wire.FeedbagClassIdGroup,
  3716. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{1})}},
  3717. },
  3718. {ClassID: wire.FeedbagClassIdGroup, GroupID: 1, Name: "Buddies"},
  3719. },
  3720. },
  3721. {
  3722. name: "SNACs that arrive during signon are kept for later",
  3723. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3724. authReply: authOK,
  3725. bos: &fakeService{
  3726. greeting: []wire.SNACMessage{
  3727. newSNAC(wire.Buddy, wire.BuddyArrived, wire.ReqIDFromServer, wire.SNAC_0x03_0x0B_BuddyArrived{
  3728. TLVUserInfo: wire.TLVUserInfo{ScreenName: "them"},
  3729. }),
  3730. },
  3731. reply: bosReply(nil),
  3732. },
  3733. tocConfig: &state.User{},
  3734. wantAuthTLVs: toc1AuthTLVs,
  3735. wantBOSSNACs: signonSNACs,
  3736. wantMsg: []string{"SIGN_ON:TOC1.0", "CONFIG:", "NICK:M E"},
  3737. wantBacklog: 1,
  3738. },
  3739. {
  3740. name: "login with bad credentials",
  3741. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3742. authReply: []wire.TLV{wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrInvalidUsernameOrPassword)},
  3743. wantAuthTLVs: toc1AuthTLVs,
  3744. wantMsg: []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword},
  3745. },
  3746. {
  3747. name: "login rate limited",
  3748. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3749. authReply: []wire.TLV{wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrRateLimitExceeded)},
  3750. wantAuthTLVs: toc1AuthTLVs,
  3751. wantMsg: []string{"ERROR:" + wire.TOCErrorAuthRatedFromLogin},
  3752. },
  3753. {
  3754. name: "login rate limited at an address that recently used BUCP login",
  3755. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3756. authReply: []wire.TLV{wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrRateLimitExceeded)},
  3757. bucpAuth: true,
  3758. wantAuthTLVs: toc1AuthTLVs,
  3759. wantMsg: []string{"ERROR:" + wire.TOCErrorAuthRatedFromLogin},
  3760. },
  3761. {
  3762. name: "login reply missing cookie",
  3763. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3764. authReply: []wire.TLV{wire.NewTLVBE(wire.LoginTLVTagsScreenName, "me")},
  3765. wantAuthTLVs: toc1AuthTLVs,
  3766. wantMsg: []string{cmdInternalSvcErr},
  3767. },
  3768. {
  3769. name: "BOS service refuses the session",
  3770. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3771. authReply: authOK,
  3772. bos: &fakeService{refuse: true},
  3773. wantAuthTLVs: toc1AuthTLVs,
  3774. wantBOSSNACs: func(t *testing.T) []receivedSNAC { return nil },
  3775. wantMsg: []string{cmdInternalSvcErr},
  3776. },
  3777. {
  3778. name: "user info query fails",
  3779. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3780. authReply: authOK,
  3781. bos: &fakeService{reply: func(frame wire.SNACFrame) []wire.SNACMessage {
  3782. if frame.SubGroup == wire.OServiceUserInfoQuery {
  3783. return []wire.SNACMessage{newSNAC(wire.OService, wire.OServiceErr, frame.RequestID,
  3784. wire.SNACError{Code: wire.ErrorCodeServiceUnavailable})}
  3785. }
  3786. return nil
  3787. }},
  3788. wantAuthTLVs: toc1AuthTLVs,
  3789. wantBOSSNACs: signonSNACs,
  3790. wantMsg: []string{cmdInternalSvcErr},
  3791. },
  3792. {
  3793. name: "TOC1 config lookup fails",
  3794. givenCmd: `toc_signon localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3795. authReply: authOK,
  3796. bos: &fakeService{reply: bosReply(nil)},
  3797. tocConfigErr: io.EOF,
  3798. wantAuthTLVs: toc1AuthTLVs,
  3799. wantBOSSNACs: signonSNACs,
  3800. wantMsg: []string{cmdInternalSvcErr},
  3801. },
  3802. {
  3803. name: "unknown signon command",
  3804. givenCmd: `toc_bogus localhost 9898 me 0x2408105c23001130 english "TIC:TOC"`,
  3805. wantMsg: []string{cmdInternalSvcErr},
  3806. },
  3807. {
  3808. name: "password too short",
  3809. givenCmd: `toc_signon localhost 9898 me 0x english "TIC:TOC"`,
  3810. wantMsg: []string{cmdInternalSvcErr},
  3811. },
  3812. {
  3813. name: "password not hex",
  3814. givenCmd: `toc_signon localhost 9898 me 0xzz english "TIC:TOC"`,
  3815. wantMsg: []string{cmdInternalSvcErr},
  3816. },
  3817. }
  3818. for _, tc := range cases {
  3819. t.Run(tc.name, func(t *testing.T) {
  3820. ctx := context.Background()
  3821. var gotAuth wire.FLAPSignonFrame
  3822. oscarServer := &fakeOSCARServer{}
  3823. switch {
  3824. case tc.bucpAuth:
  3825. oscarServer.handlers = append(oscarServer.handlers, fakeAuthBUCP(&gotAuth, tc.authReply...))
  3826. case tc.authReply != nil:
  3827. oscarServer.handlers = append(oscarServer.handlers, fakeAuth(&gotAuth, tc.authReply...))
  3828. }
  3829. if tc.bos != nil {
  3830. tc.bos.ended = make(chan struct{})
  3831. oscarServer.handlers = append(oscarServer.handlers, tc.bos.handle)
  3832. }
  3833. tocConfigStore := newMockTOCConfigStore(t)
  3834. if tc.tocConfig != nil || tc.tocConfigErr != nil {
  3835. tocConfigStore.EXPECT().
  3836. User(ctx, state.NewIdentScreenName("me")).
  3837. Return(tc.tocConfig, tc.tocConfigErr)
  3838. }
  3839. svc := OSCARProxy{
  3840. Logger: slog.Default(),
  3841. OSCARServer: oscarServer,
  3842. TOCConfigStore: tocConfigStore,
  3843. }
  3844. me, msg := svc.Signon(ctx, []byte(tc.givenCmd), clientAddr)
  3845. assert.Equal(t, tc.wantMsg, msg)
  3846. if tc.authReply != nil {
  3847. assert.Equal(t, tc.wantAuthTLVs, gotAuth.TLVList)
  3848. }
  3849. if tc.bos != nil {
  3850. gotCookie, gotSNACs := tc.bos.received()
  3851. assert.Equal(t, cookie, gotCookie)
  3852. assert.Equal(t, tc.wantBOSSNACs(t), gotSNACs)
  3853. }
  3854. oscarServer.mu.Lock()
  3855. assert.Empty(t, oscarServer.handlers, "TOC didn't open every expected connection")
  3856. for _, addr := range oscarServer.clientAddrs {
  3857. assert.Equal(t, clientAddr, addr)
  3858. }
  3859. oscarServer.mu.Unlock()
  3860. if me == nil {
  3861. if tc.bos != nil {
  3862. // a failed signon doesn't leave the user signed on
  3863. select {
  3864. case <-tc.bos.ended:
  3865. case <-time.After(time.Second):
  3866. t.Fatal("BOS connection still open after failed signon")
  3867. }
  3868. }
  3869. return
  3870. }
  3871. assert.Equal(t, state.NewIdentScreenName("me"), me.IdentScreenName())
  3872. assert.Equal(t, tc.wantTOC2, me.IsTOC2())
  3873. assert.Equal(t, tc.wantMsgEnc, me.SupportsTOC2MsgEnc())
  3874. assert.Len(t, me.backlog, tc.wantBacklog)
  3875. assert.Equal(t, tc.wantFeedbag, me.feedbag.snapshot())
  3876. me.bos.Close()
  3877. })
  3878. }
  3879. }
  3880. func TestOSCARProxy_RecvClientCmd_LocalRateLimit(t *testing.T) {
  3881. cookieBaker := newMockCookieBaker(t)
  3882. cookieBaker.EXPECT().
  3883. Issue([]byte("me"), state.DefaultCookieTTL).
  3884. Return([]byte("cookie"), nil).
  3885. Times(5)
  3886. me, bos := newTestTOCConn(newTestSession("me"))
  3887. svc := OSCARProxy{
  3888. Logger: slog.Default(),
  3889. CookieBaker: cookieBaker,
  3890. }
  3891. // commands that don't reach the OSCAR server are limited locally
  3892. for range 5 {
  3893. msg := svc.RecvClientCmd(context.Background(), me, []byte("toc_get_info them"))
  3894. assert.Len(t, msg, 1)
  3895. assert.Contains(t, msg[0], "GOTO_URL:profile:")
  3896. }
  3897. msg := svc.RecvClientCmd(context.Background(), me, []byte("toc_get_info them"))
  3898. assert.Equal(t, []string{rateLimitExceededErr}, msg)
  3899. assert.Empty(t, bos.sentSNACs())
  3900. }
  3901. func TestOSCARProxy_RecvClientCmd_FeedbagMirrorFollowsChanges(t *testing.T) {
  3902. me, bos := newTestTOCConn(newTestSession("me"))
  3903. me.isTOC2 = true
  3904. me.feedbag.reset([]wire.FeedbagItem{
  3905. {
  3906. ClassID: wire.FeedbagClassIdGroup,
  3907. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{})}},
  3908. },
  3909. })
  3910. var randCall int
  3911. svc := OSCARProxy{
  3912. Logger: slog.Default(),
  3913. RandIntn: func(n int) int {
  3914. randCall++
  3915. return randCall
  3916. },
  3917. }
  3918. // the second command sees the group the first one added
  3919. assert.Equal(t, []string{}, svc.RecvClientCmd(context.Background(), me, []byte("toc2_new_group Family")))
  3920. assert.Equal(t, []string{"NEW_BUDDY_REPLY2:alice:added"},
  3921. svc.RecvClientCmd(context.Background(), me, []byte("toc2_new_buddies {g:Family\nb:alice\n}")))
  3922. sent := bos.sentSNACs()
  3923. assert.Len(t, sent, 3)
  3924. assert.Equal(t, wire.FeedbagInsertItem, sent[0].Frame.SubGroup)
  3925. assert.Equal(t, wire.FeedbagUpdateItem, sent[1].Frame.SubGroup)
  3926. assert.Equal(t, []wire.FeedbagItem{
  3927. {
  3928. ClassID: wire.FeedbagClassIdGroup,
  3929. GroupID: 1,
  3930. Name: "Family",
  3931. TLVLBlock: wire.TLVLBlock{TLVList: wire.TLVList{wire.NewTLVBE(wire.FeedbagAttributesOrder, []uint16{2})}},
  3932. },
  3933. }, sent[1].Body.(wire.SNAC_0x13_0x09_FeedbagUpdateItem).Items)
  3934. assert.Equal(t, wire.FeedbagInsertItem, sent[2].Frame.SubGroup)
  3935. names := me.feedbag.groupNames()
  3936. assert.Equal(t, "Family", names[1])
  3937. // deleting the group removes it from the mirror
  3938. assert.Equal(t, []string{}, svc.RecvClientCmd(context.Background(), me, []byte("toc2_del_group Family")))
  3939. _, hasFamily := me.feedbag.groupNames()[1]
  3940. assert.False(t, hasFamily)
  3941. }
  3942. func TestOSCARProxy_RecvClientCmd_TOC2CmdFromTOC1(t *testing.T) {
  3943. cases := []struct {
  3944. name string
  3945. givenCmd string
  3946. }{
  3947. {name: "toc2_send_im", givenCmd: `toc2_send_im them "hello"`},
  3948. {name: "toc2_client_event", givenCmd: `toc2_client_event them 1`},
  3949. {name: "toc2_new_buddies", givenCmd: "toc2_new_buddies {g:Friends\nb:alice\n}"},
  3950. }
  3951. for _, tc := range cases {
  3952. t.Run(tc.name, func(t *testing.T) {
  3953. me, bos := newTestTOCConn(newTestSession("me"))
  3954. svc := OSCARProxy{Logger: slog.Default()}
  3955. msg := svc.RecvClientCmd(context.Background(), me, []byte(tc.givenCmd))
  3956. assert.Equal(t, []string{cmdInternalSvcErr}, msg)
  3957. assert.Empty(t, bos.sentSNACs())
  3958. })
  3959. }
  3960. }