| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565 |
- package foodgroup
- import (
- "bytes"
- "context"
- "io"
- "log/slog"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/mock"
- "github.com/stretchr/testify/require"
- "github.com/mk6i/open-oscar-server/config"
- "github.com/mk6i/open-oscar-server/state"
- "github.com/mk6i/open-oscar-server/wire"
- )
- func TestOServiceService_ServiceRequest(t *testing.T) {
- chatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName(""), state.PrivateExchange)
- cases := []struct {
- // name is the unit test name
- name string
- // service is the OSCAR service type
- service uint16
- // listener is the connection listener
- listener config.Listener
- // instance is the session of the user requesting the chat service
- // info
- instance *state.SessionInstance
- // inputSNAC is the SNAC sent by the sender client
- inputSNAC wire.SNACMessage
- // expectSNACFrame is the SNAC frame sent from the server to the recipient
- // client
- expectOutput wire.SNACMessage
- // mockParams is the list of params sent to mocks that satisfy this
- // method's dependencies
- mockParams mockParams
- // expectErr is the expected error returned by the router
- expectErr error
- }{
- {
- name: "request info for connecting to admin svc, return admin svc connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Admin,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Admin),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x07, // admin service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to alert svc, return alert svc connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Alert,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Alert),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x18, // alert service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to BART service, return BART connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.BART,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.BART),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x10, // chatnav service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to chat nav, return chat nav connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ChatNav,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.ChatNav),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0d, // chatnav service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to chat room, return chat service and chat room metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Chat,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
- Exchange: chatRoom.Exchange(),
- Cookie: chatRoom.Cookie(),
- InstanceNumber: chatRoom.InstanceNumber(),
- }),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Chat),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-auth-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: func() mockParams {
- return mockParams{
- chatRoomRegistryParams: chatRoomRegistryParams{
- chatRoomByCookieParams: chatRoomByCookieParams{
- {
- cookie: chatRoom.Cookie(),
- room: chatRoom,
- },
- },
- },
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0e, // chat service,
- 0x02, 'm', 'e', // screen name
- 0x00, // no client ID
- 0x11, '4', '-', '0', '-', 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'r', 'o', 'o', 'm',
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-auth-cookie"),
- },
- },
- },
- }
- }(),
- },
- {
- name: "request info for connecting to BART service, return BART connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ODir,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.ODir),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1234"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0F, // chatnav service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to non-existent chat room, return ErrChatRoomNotFound",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234"},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Chat,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
- Exchange: 8,
- Cookie: "the-chat-cookie",
- InstanceNumber: 16,
- }),
- },
- },
- },
- },
- mockParams: mockParams{
- chatRoomRegistryParams: chatRoomRegistryParams{
- chatRoomByCookieParams: chatRoomByCookieParams{
- {
- cookie: "the-chat-cookie",
- err: state.ErrChatRoomNotFound,
- },
- },
- },
- },
- expectErr: state.ErrChatRoomNotFound,
- },
- {
- name: "request info from a non-BOS service",
- service: wire.Chat,
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ICBM,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceErr,
- RequestID: 1234,
- },
- Body: wire.SNACError{
- Code: wire.ErrorCodeNotSupportedByHost,
- },
- },
- },
- {
- name: "request info for ICBM service, return invalid SNAC err",
- service: wire.BOS,
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ICBM,
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceErr,
- RequestID: 1234,
- },
- Body: wire.SNACError{
- Code: wire.ErrorCodeServiceUnavailable,
- },
- },
- },
- {
- name: "request info for connecting to admin svc with SSL, return admin svc SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Admin,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Admin),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x07, // admin service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to alert svc with SSL, return alert svc SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Alert,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Alert),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x18, // alert service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to BART service with SSL, return BART SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.BART,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.BART),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x10, // BART service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to chat nav with SSL, return chat nav SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ChatNav,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.ChatNav),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0d, // chatnav service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request info for connecting to chat room with SSL, return chat service SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Chat,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
- Exchange: chatRoom.Exchange(),
- Cookie: chatRoom.Cookie(),
- InstanceNumber: chatRoom.InstanceNumber(),
- }),
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.Chat),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-auth-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: func() mockParams {
- return mockParams{
- chatRoomRegistryParams: chatRoomRegistryParams{
- chatRoomByCookieParams: chatRoomByCookieParams{
- {
- cookie: chatRoom.Cookie(),
- room: chatRoom,
- },
- },
- },
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0e, // chat service,
- 0x02, 'm', 'e', // screen name
- 0x00, // no client ID
- 0x11, '4', '-', '0', '-', 't', 'h', 'e', '-', 'c', 'h', 'a', 't', '-', 'r', 'o', 'o', 'm',
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-auth-cookie"),
- },
- },
- },
- }
- }(),
- },
- {
- name: "request info for connecting to ODir service with SSL, return ODir SSL connection metadata",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", BOSAdvertisedHostSSL: "127.0.0.1:1235", HasSSL: true},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.ODir,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.ODir),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:1235"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x02)),
- },
- },
- },
- },
- mockParams: mockParams{
- cookieBakerParams: cookieBakerParams{
- cookieIssueParams: cookieIssueParams{
- {
- dataIn: []byte{
- 0x00, 0x0F, // ODir service
- 0x02, 'm', 'e',
- 0x0, // no client ID
- 0x0, // no chat cookie
- 0x0, // multi conn flag
- 0x0, // kerberos flag
- 0x01, // session num
- },
- cookieOut: []byte("the-cookie"),
- },
- },
- },
- },
- },
- {
- name: "request SSL service but listener doesn't support SSL, return error",
- service: wire.BOS,
- listener: config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:1234", HasSSL: false},
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.Admin,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OserviceTLVTagsSSLUseSSL, []byte{}),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceErr,
- RequestID: 1234,
- },
- Body: wire.SNACError{
- Code: wire.ErrorCodeGeneralFailure,
- },
- },
- mockParams: mockParams{},
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- //
- // initialize dependencies
- //
- chatRoomManager := newMockChatRoomRegistry(t)
- for _, params := range tc.mockParams.chatRoomByCookieParams {
- chatRoomManager.EXPECT().
- ChatRoomByCookie(context.Background(), params.cookie).
- Return(params.room, params.err)
- }
- cookieIssuer := newMockCookieBaker(t)
- for _, params := range tc.mockParams.cookieIssueParams {
- cookieIssuer.EXPECT().
- Issue(params.dataIn).
- Return(params.cookieOut, params.err)
- }
- chatMessageRelayer := newMockChatMessageRelayer(t)
- //
- // send input SNAC
- //
- svc := NewOServiceService(config.Config{}, nil, slog.Default(), cookieIssuer, chatRoomManager, nil, nil, nil, wire.DefaultSNACRateLimits(), chatMessageRelayer, nil, nil, nil)
- outputSNAC, err := svc.ServiceRequest(context.Background(), tc.service, tc.instance, tc.inputSNAC.Frame,
- tc.inputSNAC.Body.(wire.SNAC_0x01_0x04_OServiceServiceRequest), tc.listener)
- assert.ErrorIs(t, err, tc.expectErr)
- if tc.expectErr != nil {
- return
- }
- //
- // verify output
- //
- assert.Equal(t, tc.expectOutput, outputSNAC)
- })
- }
- }
- func TestOServiceService_ServiceRequest_LinkedAccountSignon(t *testing.T) {
- primaryUser := state.NewIdentScreenName("PrimaryUser")
- linkedUser := state.DisplayScreenName("LinkedUser")
- linkedUserIdent := state.NewIdentScreenName(string(linkedUser))
- // cookieDataFor returns the serialized ServerCookie bytes that fnIssueCookie
- // will pass to cookieIssuer.Issue, with the given MultiConnFlag.
- cookieDataFor := func(sn state.DisplayScreenName, flag wire.MultiConnFlag) []byte {
- buf := &bytes.Buffer{}
- assert.NoError(t, wire.MarshalBE(state.ServerCookie{
- Service: wire.BOS,
- ScreenName: sn,
- MultiConnFlag: uint8(flag),
- }, buf))
- return buf.Bytes()
- }
- makeBody := func(includeSUID bool, screenName string) wire.SNAC_0x01_0x04_OServiceServiceRequest {
- body := wire.SNAC_0x01_0x04_OServiceServiceRequest{
- FoodGroup: wire.OService,
- }
- if includeSUID {
- body.TLVList = append(body.TLVList,
- wire.NewTLVBE(uint16(0x0028), []byte("some-uuid-bytes")))
- }
- if screenName != "" {
- body.TLVList = append(body.TLVList,
- wire.NewTLVBE(uint16(0x01), []byte(screenName)))
- }
- return body
- }
- cases := []struct {
- name string
- instance *state.SessionInstance
- inputBody wire.SNAC_0x01_0x04_OServiceServiceRequest
- feedbagItems []wire.FeedbagItem
- feedbagErr error
- // setupCookie is whether to expect cookie issuance
- setupCookie bool
- expectOutput wire.SNACMessage
- wantErrContains string
- wantErr error
- }{
- {
- name: "linked account signon OK, returns BOS cookie with no MultiConnFlag",
- inputBody: makeBody(true, string(linkedUser)),
- feedbagItems: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdAlInfo, Name: linkedUserIdent.String()}},
- setupCookie: true,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.OService),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:5190"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- },
- {
- name: "primary has MultiConnFlagsRecentClient, linked account cookie inherits flag",
- instance: newTestInstance(state.DisplayScreenName(primaryUser.String()), sessOptMultiConnFlag(wire.MultiConnFlagsRecentClient)),
- inputBody: makeBody(true, string(linkedUser)),
- feedbagItems: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdAlInfo, Name: linkedUserIdent.String()}},
- setupCookie: true,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.OService),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:5190"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- },
- {
- name: "primary has MultiConnFlagsSingleClient, linked account cookie inherits flag",
- instance: newTestInstance(state.DisplayScreenName(primaryUser.String()), sessOptMultiConnFlag(wire.MultiConnFlagsSingleClient)),
- inputBody: makeBody(true, string(linkedUser)),
- feedbagItems: []wire.FeedbagItem{{ClassID: wire.FeedbagClassIdAlInfo, Name: linkedUserIdent.String()}},
- setupCookie: true,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceServiceResponse,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x05_OServiceServiceResponse{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsGroupID, wire.OService),
- wire.NewTLVBE(wire.OServiceTLVTagsReconnectHere, "127.0.0.1:5190"),
- wire.NewTLVBE(wire.OServiceTLVTagsLoginCookie, []byte("the-cookie")),
- wire.NewTLVBE(wire.OServiceTLVTagsSSLState, uint8(0x00)),
- },
- },
- },
- },
- },
- {
- name: "missing SUID TLV 0x0028, returns error",
- inputBody: makeBody(false, string(linkedUser)),
- wantErrContains: "unknown OService request",
- },
- {
- name: "missing screenname TLV 0x01, returns error",
- inputBody: makeBody(true, ""),
- wantErrContains: "new session request missing linked screenname TLV 0x01",
- },
- {
- name: "accounts not linked, returns error",
- inputBody: makeBody(true, string(linkedUser)),
- wantErrContains: "linked account session requested but accounts are not linked",
- },
- {
- name: "feedbag lookup error, error propagated",
- inputBody: makeBody(true, string(linkedUser)),
- feedbagErr: io.EOF,
- wantErr: io.EOF,
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- instance := tc.instance
- if instance == nil {
- instance = newTestInstance(state.DisplayScreenName(primaryUser.String()))
- }
- cookieIssuer := newMockCookieBaker(t)
- if tc.setupCookie {
- cookieIssuer.EXPECT().
- Issue(cookieDataFor(linkedUser, instance.MultiConnFlag())).
- Return([]byte("the-cookie"), nil)
- }
- feedbagManager := newMockFeedbagManager(t)
- if tc.inputBody.HasTag(0x0028) {
- if snBytes, ok := tc.inputBody.Bytes(0x01); ok && len(snBytes) > 0 {
- feedbagManager.EXPECT().
- Feedbag(matchContext(), primaryUser).
- Return(tc.feedbagItems, tc.feedbagErr)
- }
- }
- svc := NewOServiceService(config.Config{}, nil, slog.Default(), cookieIssuer, nil, nil, nil, nil,
- wire.DefaultSNACRateLimits(), nil, nil, nil, feedbagManager)
- listener := config.Listener{BOSAdvertisedHostPlain: "127.0.0.1:5190"}
- outputSNAC, err := svc.ServiceRequest(context.Background(), wire.BOS, instance,
- wire.SNACFrame{RequestID: 1234}, tc.inputBody, listener)
- if tc.wantErrContains != "" {
- assert.ErrorContains(t, err, tc.wantErrContains)
- return
- }
- if tc.wantErr != nil {
- assert.ErrorIs(t, err, tc.wantErr)
- return
- }
- assert.NoError(t, err)
- assert.Equal(t, tc.expectOutput, outputSNAC)
- })
- }
- }
- func TestOServiceService_SetUserInfoFields(t *testing.T) {
- cases := []struct {
- // name is the unit test name
- name string
- // instance is the session of the user whose info is being set
- instance *state.SessionInstance
- // inputSNAC is the SNAC sent from the client to the server
- inputSNAC wire.SNACMessage
- // expectOutput is the SNAC reply sent from the server back to the
- // client
- expectOutput wire.SNACMessage
- // broadcastMessage is the arrival/departure message sent to buddies
- broadcastMessage []struct {
- recipients []string
- msg wire.SNACMessage
- }
- // interestedUserLookups contains all the users who have this user on
- // their buddy list
- interestedUserLookups map[string][]string
- // expectErr is the expected error returned
- expectErr error
- // mockParams is the list of params sent to mocks that satisfy this
- // method's dependencies
- mockParams mockParams
- // checkSession validates the state of the session
- checkSession func(*testing.T, *state.Session)
- }{
- {
- name: "set user status to visible aim < 6",
- instance: newTestInstance("me", sessOptInvisible),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: func(val any) bool {
- snac, ok := val.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
- if !ok {
- return false
- }
- if len(snac.UserInfo) == 0 {
- return false
- }
- status, hasStatus := snac.UserInfo[0].Uint32BE(wire.OServiceUserInfoStatus)
- return hasStatus && status == uint32(0x0000)
- },
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyArrivedParams: broadcastBuddyArrivedParams{
- {
- screenName: state.DisplayScreenName("me"),
- },
- },
- },
- },
- checkSession: func(t *testing.T, session *state.Session) {
- assert.False(t, session.Invisible())
- },
- },
- {
- name: "set user status to invisible aim < 6",
- instance: newTestInstance("me"),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0100)),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: func(val any) bool {
- snac, ok := val.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
- if !ok {
- return false
- }
- if len(snac.UserInfo) == 0 {
- return false
- }
- status, hasStatus := snac.UserInfo[0].Uint32BE(wire.OServiceUserInfoStatus)
- return hasStatus && status == uint32(0x0100)
- },
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyDepartedParams: broadcastBuddyDepartedParams{
- {
- screenName: state.NewIdentScreenName("me"),
- },
- },
- },
- },
- checkSession: func(t *testing.T, session *state.Session) {
- assert.True(t, session.Invisible())
- },
- },
- {
- name: "set user status to visible aim >= 6",
- instance: newTestInstance("me", sessOptInvisible, sessOptSetFoodGroupVersion(wire.OService, 4)),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: func(val any) bool {
- snac, ok := val.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
- if !ok {
- return false
- }
- if len(snac.UserInfo) == 0 {
- return false
- }
- status, hasStatus := snac.UserInfo[0].Uint32BE(wire.OServiceUserInfoStatus)
- return hasStatus && status == uint32(0x0000)
- },
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyArrivedParams: broadcastBuddyArrivedParams{
- {
- screenName: state.DisplayScreenName("me"),
- },
- },
- },
- },
- checkSession: func(t *testing.T, session *state.Session) {
- assert.False(t, session.Invisible())
- },
- },
- {
- name: "set user status to invisible aim >= 6",
- instance: newTestInstance("me", sessOptSetFoodGroupVersion(wire.OService, 4)),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0100)),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: func(val any) bool {
- snac, ok := val.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
- if !ok {
- return false
- }
- if len(snac.UserInfo) == 0 {
- return false
- }
- status, hasStatus := snac.UserInfo[0].Uint32BE(wire.OServiceUserInfoStatus)
- return hasStatus && status == uint32(0x0100)
- },
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyDepartedParams: broadcastBuddyDepartedParams{
- {
- screenName: state.NewIdentScreenName("me"),
- },
- },
- },
- },
- checkSession: func(t *testing.T, session *state.Session) {
- assert.True(t, session.Invisible())
- },
- },
- {
- name: "set ICQ direct connect info",
- instance: newTestInstance("1000003", sessOptUserInfoFlag(wire.OServiceUserFlagICQ)),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields{
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoICQDC, wire.ICQDCInfo{
- DCType: 4,
- ProtoVersion: 10,
- }),
- },
- },
- },
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: func(val any) bool {
- snac, ok := val.(wire.SNAC_0x01_0x0F_OServiceUserInfoUpdate)
- if !ok || len(snac.UserInfo) == 0 {
- return false
- }
- dc, hasDC := snac.UserInfo[0].Bytes(wire.OServiceUserInfoICQDC)
- if !hasDC {
- return false
- }
- var got wire.ICQDCInfo
- if err := wire.UnmarshalBE(&got, bytes.NewReader(dc)); err != nil {
- return false
- }
- return got.DCType == 4 && got.ProtoVersion == 10
- },
- },
- checkSession: func(t *testing.T, session *state.Session) {
- info := session.Instances()[0].ICQDCInfo()
- assert.Equal(t, uint8(4), info.DCType)
- assert.Equal(t, uint16(10), info.ProtoVersion)
- },
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- buddyUpdateBroadcaster := newMockbuddyBroadcaster(t)
- for _, params := range tc.mockParams.broadcastBuddyArrivedParams {
- buddyUpdateBroadcaster.EXPECT().
- BroadcastBuddyArrived(mock.Anything, state.NewIdentScreenName(params.screenName.String()), mock.MatchedBy(func(userInfo wire.TLVUserInfo) bool {
- return userInfo.ScreenName == params.screenName.String()
- })).
- Return(params.err)
- }
- for _, params := range tc.mockParams.broadcastBuddyDepartedParams {
- buddyUpdateBroadcaster.EXPECT().
- BroadcastBuddyDeparted(mock.Anything, params.screenName).
- Return(params.err)
- }
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- buddyBroadcaster: buddyUpdateBroadcaster,
- }
- outputSNAC, err := svc.SetUserInfoFields(context.TODO(), tc.instance, tc.inputSNAC.Frame,
- tc.inputSNAC.Body.(wire.SNAC_0x01_0x1E_OServiceSetUserInfoFields))
- assert.ErrorIs(t, err, tc.expectErr)
- if tc.expectErr != nil {
- return
- }
- assert.Equal(t, tc.expectOutput.Frame, outputSNAC.Frame)
- if matcherFn, ok := tc.expectOutput.Body.(func(val any) bool); ok {
- assert.True(t, matcherFn(outputSNAC.Body), "Body matcher function failed")
- } else {
- assert.Equal(t, tc.expectOutput.Body, outputSNAC.Body)
- }
- tc.checkSession(t, tc.instance.Session())
- })
- }
- }
- func TestOServiceService_RateParamsQuery(t *testing.T) {
- rateClasses := wire.NewRateLimitClasses([5]wire.RateClass{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- })
- expectRateGroups := []struct {
- ID uint16
- Pairs []struct {
- FoodGroup uint16
- SubGroup uint16
- } `oscar:"count_prefix=uint16"`
- }{
- {
- ID: 1,
- Pairs: []struct {
- FoodGroup uint16
- SubGroup uint16
- }{
- {FoodGroup: wire.OService, SubGroup: wire.OServiceErr},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceClientOnline},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceHostOnline},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceServiceRequest},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceServiceResponse},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceRateParamsQuery},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceRateParamsReply},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceRateParamsSubAdd},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceRateDelParamSub},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceRateParamChange},
- {FoodGroup: wire.OService, SubGroup: wire.OServicePauseReq},
- {FoodGroup: wire.OService, SubGroup: wire.OServicePauseAck},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceResume},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceUserInfoQuery},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceUserInfoUpdate},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceEvilNotification},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceIdleNotification},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceMigrateGroups},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceMotd},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceSetPrivacyFlags},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceWellKnownUrls},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceNoop},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceClientVersions},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceHostVersions},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceMaxConfigQuery},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceMaxConfigReply},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceStoreConfig},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceConfigQuery},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceConfigReply},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceSetUserInfoFields},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceProbeReq},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceProbeAck},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceBartReply},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceBartQuery2},
- {FoodGroup: wire.OService, SubGroup: wire.OServiceBartReply2},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateErr},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateRightsQuery},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateRightsReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateSetInfo},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateWatcherSubRequest},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateWatcherNotification},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateSetDirReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGetDirReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGroupCapabilityQuery},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGroupCapabilityReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateSetKeywordInfo},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateSetKeywordReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGetKeywordInfo},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGetKeywordReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateFindListByEmail},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateFindListReply},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery2},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyErr},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyRightsQuery},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyRightsReply},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyWatcherListQuery},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyWatcherListResponse},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyWatcherSubRequest},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyWatcherNotification},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyRejectNotification},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyArrived},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyDeparted},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyAddTempBuddies},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyDelTempBuddies},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMErr},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMAddParameters},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMDelParameters},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMParameterQuery},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMParameterReply},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMChannelMsgToClient},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMEvilRequest},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMEvilReply},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMMissedCalls},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMClientErr},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMHostAck},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMSinStored},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMSinListQuery},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMSinListReply},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMOfflineRetrieve},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMSinDelete},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMNotifyRequest},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMNotifyReply},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMClientEvent},
- {FoodGroup: wire.Advert, SubGroup: wire.AdvertErr},
- {FoodGroup: wire.Advert, SubGroup: wire.AdvertAdsQuery},
- {FoodGroup: wire.Advert, SubGroup: wire.AdvertAdsReply},
- {FoodGroup: wire.Invite, SubGroup: wire.InviteErr},
- {FoodGroup: wire.Invite, SubGroup: wire.InviteRequestQuery},
- {FoodGroup: wire.Invite, SubGroup: wire.InviteRequestReply},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminErr},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminInfoQuery},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminInfoReply},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeRequest},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminInfoChangeReply},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminAcctConfirmRequest},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminAcctConfirmReply},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminAcctDeleteRequest},
- {FoodGroup: wire.Admin, SubGroup: wire.AdminAcctDeleteReply},
- {FoodGroup: wire.Popup, SubGroup: wire.PopupErr},
- {FoodGroup: wire.Popup, SubGroup: wire.PopupDisplay},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyErr},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyRightsQuery},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyRightsReply},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenySetGroupPermitMask},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyBosErr},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyAddTempPermitListEntries},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyDelTempPermitListEntries},
- {FoodGroup: wire.UserLookup, SubGroup: wire.UserLookupErr},
- {FoodGroup: wire.UserLookup, SubGroup: wire.UserLookupFindByEmail},
- {FoodGroup: wire.UserLookup, SubGroup: wire.UserLookupFindReply},
- {FoodGroup: wire.Stats, SubGroup: wire.StatsErr},
- {FoodGroup: wire.Stats, SubGroup: wire.StatsSetMinReportInterval},
- {FoodGroup: wire.Stats, SubGroup: wire.StatsReportEvents},
- {FoodGroup: wire.Stats, SubGroup: wire.StatsReportAck},
- {FoodGroup: wire.Translate, SubGroup: wire.TranslateErr},
- {FoodGroup: wire.Translate, SubGroup: wire.TranslateRequest},
- {FoodGroup: wire.Translate, SubGroup: wire.TranslateReply},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavErr},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestChatRights},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestExchangeInfo},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestRoomInfo},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestMoreRoomInfo},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavRequestOccupantList},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavSearchForRoom},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavCreateRoom},
- {FoodGroup: wire.ChatNav, SubGroup: wire.ChatNavNavInfo},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatErr},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatRoomInfoUpdate},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatUsersJoined},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatUsersLeft},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatChannelMsgToClient},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatEvilRequest},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatEvilReply},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatClientErr},
- {FoodGroup: wire.ODir, SubGroup: wire.ODirErr},
- {FoodGroup: wire.ODir, SubGroup: wire.ODirInfoQuery},
- {FoodGroup: wire.ODir, SubGroup: wire.ODirInfoReply},
- {FoodGroup: wire.ODir, SubGroup: wire.ODirKeywordListQuery},
- {FoodGroup: wire.BART, SubGroup: wire.BARTErr},
- {FoodGroup: wire.BART, SubGroup: wire.BARTUploadQuery},
- {FoodGroup: wire.BART, SubGroup: wire.BARTDownloadQuery},
- {FoodGroup: wire.BART, SubGroup: wire.BARTDownload2Query},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagErr},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRightsQuery},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRightsReply},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQuery},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagQueryIfModified},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagReply},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUse},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertClass},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateClass},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteClass},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagStatus},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagReplyNotModified},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteUser},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagStartCluster},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagEndCluster},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagAuthorizeBuddy},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagPreAuthorizeBuddy},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagPreAuthorizedBuddy},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRemoveMe},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRemoveMe2},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRequestAuthorizeToHost},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRequestAuthorizeToClient},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRespondAuthorizeToHost},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRespondAuthorizeToClient},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagBuddyAdded},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRequestAuthorizeToBadog},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRespondAuthorizeToBadog},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagBuddyAddedToBadog},
- {FoodGroup: wire.Feedbag, SubGroup: 0x0020},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagTestSnac},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagForwardMsg},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagIsAuthRequiredQuery},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagIsAuthRequiredReply},
- {FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagRecentBuddyUpdate},
- {FoodGroup: wire.Feedbag, SubGroup: 0x0026},
- {FoodGroup: wire.Feedbag, SubGroup: 0x0027},
- {FoodGroup: wire.Feedbag, SubGroup: 0x0028},
- {FoodGroup: wire.ICQ, SubGroup: wire.ICQErr},
- {FoodGroup: wire.ICQ, SubGroup: wire.ICQDBQuery},
- {FoodGroup: wire.ICQ, SubGroup: wire.ICQDBReply},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPErr},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPLoginRequest},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPRegisterRequest},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPChallengeRequest},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPAsasnRequest},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPSecuridRequest},
- {FoodGroup: wire.BUCP, SubGroup: wire.BUCPRegistrationImageRequest},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertErr},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertSetAlertRequest},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertGetSubsRequest},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertNotifyCapabilities},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertNotify},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertGetRuleRequest},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertGetFeedRequest},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertRefreshFeed},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertEvent},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertQogSnac},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertRefreshFeedStock},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertNotifyTransport},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertSetAlertRequestV2},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertNotifyAck},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertNotifyDisplayCapabilities},
- {FoodGroup: wire.Alert, SubGroup: wire.AlertUserOnline},
- },
- },
- {
- ID: 2,
- Pairs: []struct {
- FoodGroup uint16
- SubGroup uint16
- }{
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyAddBuddies},
- {FoodGroup: wire.Buddy, SubGroup: wire.BuddyDelBuddies},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyAddPermListEntries},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyDelPermListEntries},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyAddDenyListEntries},
- {FoodGroup: wire.PermitDeny, SubGroup: wire.PermitDenyDelDenyListEntries},
- {FoodGroup: wire.Chat, SubGroup: wire.ChatChannelMsgToHost},
- },
- },
- {
- ID: 3,
- Pairs: []struct {
- FoodGroup uint16
- SubGroup uint16
- }{
- {FoodGroup: wire.Locate, SubGroup: wire.LocateUserInfoQuery},
- {FoodGroup: wire.ICBM, SubGroup: wire.ICBMChannelMsgToHost},
- },
- },
- {
- ID: 4,
- Pairs: []struct {
- FoodGroup uint16
- SubGroup uint16
- }{
- {FoodGroup: wire.Locate, SubGroup: wire.LocateSetDirInfo},
- {FoodGroup: wire.Locate, SubGroup: wire.LocateGetDirInfo},
- },
- },
- {
- ID: 5,
- Pairs: []struct {
- FoodGroup uint16
- SubGroup uint16
- }{},
- },
- }
- cases := []struct {
- // name is the unit test name
- name string
- // instance is the session of the user requesting the chat service
- // info
- instance *state.SessionInstance
- // inputSNAC is the SNAC sent by the sender client
- inputSNAC wire.SNACMessage
- // expectSNACFrame is the SNAC frame sent from the server to the recipient
- // client
- expectOutput wire.SNACMessage
- // expectErr is the expected error returned by the router
- expectErr error
- // timeNow returns the current time
- timeNow func() time.Time
- }{
- {
- name: "get rate limits for AIM > 1.x clients",
- instance: newTestInstance("me", sessOptSetFoodGroupVersion(wire.OService, 3), sessOptSetRateClasses(rateClasses)),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{RequestID: 1234},
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamsReply,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x07_OServiceRateParamsReply{
- RateClasses: []wire.RateParamsSNAC{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- CurrentLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- LastTime: 999,
- DroppingSNACs: 0x00,
- },
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- CurrentLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- LastTime: 999,
- DroppingSNACs: 0x00,
- },
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- CurrentLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- LastTime: 999,
- DroppingSNACs: 0x00,
- },
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- CurrentLevel: 8000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- LastTime: 999,
- DroppingSNACs: 0x00,
- },
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- CurrentLevel: 8000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- LastTime: 999,
- DroppingSNACs: 0x00,
- },
- },
- },
- RateGroups: expectRateGroups,
- },
- },
- timeNow: func() time.Time {
- return time.Unix(1000, 0)
- },
- },
- {
- name: "get rate limits for AIM 1.x client",
- instance: newTestInstance("me", sessClientID("AOL Instant Messenger (TM), version 1."), sessOptSetRateClasses(rateClasses)),
- inputSNAC: wire.SNACMessage{
- Frame: wire.SNACFrame{RequestID: 1234},
- },
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamsReply,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x07_OServiceRateParamsReply{
- RateClasses: []wire.RateParamsSNAC{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- },
- RateGroups: expectRateGroups,
- },
- },
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- snacRateLimits: wire.DefaultSNACRateLimits(),
- timeNow: tc.timeNow,
- }
- have := svc.RateParamsQuery(context.Background(), tc.instance, tc.inputSNAC.Frame)
- assert.ElementsMatch(t, tc.expectOutput.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[0].Pairs,
- have.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[0].Pairs)
- assert.ElementsMatch(t, tc.expectOutput.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[1].Pairs,
- have.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[1].Pairs)
- assert.ElementsMatch(t, tc.expectOutput.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[2].Pairs,
- have.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[2].Pairs)
- assert.ElementsMatch(t, tc.expectOutput.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[3].Pairs,
- have.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[3].Pairs)
- assert.ElementsMatch(t, tc.expectOutput.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[4].Pairs,
- have.Body.(wire.SNAC_0x01_0x07_OServiceRateParamsReply).RateGroups[4].Pairs)
- })
- }
- }
- func TestOServiceService_HostOnline(t *testing.T) {
- cases := []struct {
- // name is the unit test name
- name string
- // service is the OSCAR service type
- service uint16
- // expectSNACFrame is the SNAC frame sent from the server to the recipient
- // client
- expectOutput wire.SNACMessage
- }{
- {
- name: "Admin service",
- service: wire.Admin,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.OService,
- wire.Admin,
- },
- },
- },
- },
- {
- name: "Alert service",
- service: wire.Alert,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.Alert,
- wire.OService,
- },
- },
- },
- },
- {
- name: "BART service",
- service: wire.BART,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.BART,
- wire.OService,
- },
- },
- },
- },
- {
- name: "BOS service",
- service: wire.BOS,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.Alert,
- wire.BART,
- wire.Buddy,
- wire.Feedbag,
- wire.ICBM,
- wire.ICQ,
- wire.Locate,
- wire.OService,
- wire.PermitDeny,
- wire.UserLookup,
- wire.Invite,
- wire.Popup,
- wire.Stats,
- },
- },
- },
- },
- {
- name: "Chat service",
- service: wire.Chat,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.OService,
- wire.Chat,
- },
- },
- },
- },
- {
- name: "ChatNav service",
- service: wire.ChatNav,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.ChatNav,
- wire.OService,
- },
- },
- },
- },
- {
- name: "ODir service",
- service: wire.ODir,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostOnline,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x03_OServiceHostOnline{
- FoodGroups: []uint16{
- wire.ODir,
- wire.OService,
- },
- },
- },
- },
- {
- name: "Oops, unsupported service",
- service: wire.Kerberos,
- expectOutput: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceErr,
- },
- },
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- svc := NewOServiceService(config.Config{}, nil, slog.Default(), nil, nil, nil, nil, nil, wire.DefaultSNACRateLimits(), nil, nil, nil, nil)
- have := svc.HostOnline(tc.service)
- assert.Equal(t, tc.expectOutput, have)
- })
- }
- }
- func TestOServiceService_ClientVersions(t *testing.T) {
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- }
- want := []wire.SNACMessage{
- {
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceHostVersions,
- RequestID: 1234,
- },
- Body: wire.SNAC_0x01_0x18_OServiceHostVersions{
- Versions: []uint16{5, 6, 7, 8},
- },
- },
- {
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceMotd,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x13_OServiceMOTD{
- MessageType: 0x0004,
- TLVRestBlock: wire.TLVRestBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceTLVTagsMOTDMessage, "Welcome to Open OSCAR Server"),
- },
- },
- },
- },
- }
- instance := newTestInstance("me")
- have := svc.ClientVersions(context.Background(), instance, wire.SNACFrame{
- RequestID: 1234,
- }, wire.SNAC_0x01_0x17_OServiceClientVersions{
- Versions: []uint16{5, 6, 7, 8},
- })
- assert.Equal(t, want, have)
- }
- func TestNewOServiceUserInfoUpdate(t *testing.T) {
- memberSince := time.Unix(1_700_000_000, 0)
- profileUpdated := time.Unix(1_700_100_000, 0)
- signonTime := time.Now().Add(-3 * time.Second)
- t.Run("OService version < 4 without profile", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptMemberSince(memberSince),
- sessOptSignonTime(signonTime))
- signon := session.SignonTime()
- onlineLowerBound := uint32(time.Since(signon).Seconds())
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 1)
- memberVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoMemberSince)
- require.True(t, ok)
- require.Equal(t, uint32(memberSince.Unix()), memberVal)
- signonVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoSignonTOD)
- require.True(t, ok)
- require.Equal(t, uint32(signon.Unix()), signonVal)
- onlineVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoOnlineTime)
- require.True(t, ok)
- require.GreaterOrEqual(t, onlineVal, onlineLowerBound)
- require.LessOrEqual(t, onlineVal-onlineLowerBound, uint32(2))
- hasSigTime := got.UserInfo[0].HasTag(wire.OServiceUserInfoSigTime)
- require.False(t, hasSigTime)
- require.False(t, got.UserInfo[0].HasTag(wire.OServiceUserInfoPrimaryInstance))
- })
- t.Run("includes profile update time when set", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptMemberSince(memberSince),
- sessOptSignonTime(signonTime),
- sessOptProfile(state.UserProfile{UpdateTime: profileUpdated}),
- sessOptSetFoodGroupVersion(wire.OService, 4))
- signon := session.SignonTime()
- onlineLowerBound := uint32(time.Since(signon).Seconds())
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 2)
- memberVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoMemberSince)
- require.True(t, ok)
- require.Equal(t, uint32(memberSince.Unix()), memberVal)
- signonVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoSignonTOD)
- require.True(t, ok)
- require.Equal(t, uint32(signon.Unix()), signonVal)
- onlineVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoOnlineTime)
- require.True(t, ok)
- require.GreaterOrEqual(t, onlineVal, onlineLowerBound)
- require.LessOrEqual(t, onlineVal-onlineLowerBound, uint32(2))
- hasSigTime := got.UserInfo[0].HasTag(wire.OServiceUserInfoSigTime)
- require.False(t, hasSigTime)
- // Signature time is in the instance block, not UserInfo[0]
- hasSigTimeInstance := got.UserInfo[1].HasTag(wire.OServiceUserInfoSigTime)
- require.True(t, hasSigTimeInstance)
- sigVal, ok := got.UserInfo[1].Uint32BE(wire.OServiceUserInfoSigTime)
- require.True(t, ok)
- require.Equal(t, uint32(profileUpdated.Unix()), sigVal)
- require.False(t, got.UserInfo[0].HasTag(wire.OServiceUserInfoPrimaryInstance))
- })
- t.Run("appends additional instance info when food group version >= 4", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptMemberSince(memberSince),
- sessOptSignonTime(signonTime),
- sessOptSetFoodGroupVersion(wire.OService, 4))
- signon := session.SignonTime()
- onlineLowerBound := uint32(time.Since(signon).Seconds())
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 2)
- memberVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoMemberSince)
- require.True(t, ok)
- require.Equal(t, uint32(memberSince.Unix()), memberVal)
- signonVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoSignonTOD)
- require.True(t, ok)
- require.Equal(t, uint32(signon.Unix()), signonVal)
- onlineVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoOnlineTime)
- require.True(t, ok)
- require.GreaterOrEqual(t, onlineVal, onlineLowerBound)
- require.LessOrEqual(t, onlineVal-onlineLowerBound, uint32(2))
- hasSigTime := got.UserInfo[0].HasTag(wire.OServiceUserInfoSigTime)
- require.False(t, hasSigTime)
- instanceBytes, ok := got.UserInfo[0].Bytes(wire.OServiceUserInfoMyInstanceNum)
- require.True(t, ok)
- require.Equal(t, []byte{0x01}, instanceBytes)
- primaryBytes, ok := got.UserInfo[1].Bytes(wire.OServiceUserInfoPrimaryInstance)
- require.True(t, ok)
- require.Equal(t, []byte{0x01}, primaryBytes)
- require.Equal(t, got.UserInfo[0].ScreenName, got.UserInfo[1].ScreenName)
- })
- t.Run("includes info for all instances when session has 2 instances", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptMemberSince(memberSince),
- sessOptSignonTime(signonTime),
- sessOptSetFoodGroupVersion(wire.OService, 4))
- // Add a second instance to the session
- session.Session().AddInstance()
- signon := session.SignonTime()
- onlineLowerBound := uint32(time.Since(signon).Seconds())
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 3)
- memberVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoMemberSince)
- require.True(t, ok)
- require.Equal(t, uint32(memberSince.Unix()), memberVal)
- signonVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoSignonTOD)
- require.True(t, ok)
- require.Equal(t, uint32(signon.Unix()), signonVal)
- onlineVal, ok := got.UserInfo[0].Uint32BE(wire.OServiceUserInfoOnlineTime)
- require.True(t, ok)
- require.GreaterOrEqual(t, onlineVal, onlineLowerBound)
- require.LessOrEqual(t, onlineVal-onlineLowerBound, uint32(2))
- hasSigTime := got.UserInfo[0].HasTag(wire.OServiceUserInfoSigTime)
- require.False(t, hasSigTime)
- instanceBytes, ok := got.UserInfo[0].Bytes(wire.OServiceUserInfoMyInstanceNum)
- require.True(t, ok)
- require.Equal(t, []byte{0x01}, instanceBytes)
- // First instance block
- primary1Bytes, ok := got.UserInfo[1].Bytes(wire.OServiceUserInfoPrimaryInstance)
- require.True(t, ok)
- require.Equal(t, []byte{0x01}, primary1Bytes)
- require.Equal(t, got.UserInfo[0].ScreenName, got.UserInfo[1].ScreenName)
- // Second instance block
- primary2Bytes, ok := got.UserInfo[2].Bytes(wire.OServiceUserInfoPrimaryInstance)
- require.True(t, ok)
- require.Equal(t, []byte{0x02}, primary2Bytes)
- require.Equal(t, got.UserInfo[0].ScreenName, got.UserInfo[2].ScreenName)
- })
- t.Run("marks instance user flags unavailable when session is away", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptUserInfoFlag(wire.OServiceUserFlagUnavailable),
- sessOptSetFoodGroupVersion(wire.OService, 4))
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 2)
- flags, ok := got.UserInfo[1].Uint16BE(wire.OServiceUserInfoUserFlags)
- require.True(t, ok)
- require.Equal(t, wire.OServiceUserFlagOSCARFree|wire.OServiceUserFlagUnavailable, flags)
- })
- t.Run("marks instance status invisible when current instance is invisible", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptInvisible,
- sessOptSetFoodGroupVersion(wire.OService, 4))
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 2)
- status, ok := got.UserInfo[1].Uint32BE(wire.OServiceUserInfoStatus)
- require.True(t, ok)
- require.Equal(t, wire.OServiceUserStatusInvisible, status)
- })
- t.Run("adds buddy icon and profile sig time only for current instance", func(t *testing.T) {
- icon := wire.BARTID{
- Type: 1,
- BARTInfo: wire.BARTInfo{
- Flags: 1,
- Hash: []byte{0xAA, 0xBB, 0xCC},
- },
- }
- session := newTestInstance("me",
- sessOptSetFoodGroupVersion(wire.OService, 4),
- sessOptBuddyIcon(icon),
- sessOptProfile(state.UserProfile{UpdateTime: profileUpdated}))
- session.Session().AddInstance()
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 3)
- require.True(t, got.UserInfo[1].HasTag(wire.OServiceUserInfoBARTInfo))
- require.True(t, got.UserInfo[1].HasTag(wire.OServiceUserInfoSigTime))
- require.False(t, got.UserInfo[2].HasTag(wire.OServiceUserInfoBARTInfo))
- require.False(t, got.UserInfo[2].HasTag(wire.OServiceUserInfoSigTime))
- })
- t.Run("does not add buddy icon when icon type is zero", func(t *testing.T) {
- session := newTestInstance("me",
- sessOptSetFoodGroupVersion(wire.OService, 4),
- sessOptBuddyIcon(wire.BARTID{
- Type: 0,
- BARTInfo: wire.BARTInfo{
- Flags: 1,
- Hash: []byte{0x10, 0x20, 0x30},
- },
- }))
- got := newOServiceUserInfoUpdate(session)
- require.Len(t, got.UserInfo, 2)
- require.False(t, got.UserInfo[1].HasTag(wire.OServiceUserInfoBARTInfo))
- })
- }
- func TestOServiceService_UserInfoQuery(t *testing.T) {
- tests := []struct {
- name string
- instance *state.SessionInstance
- given wire.SNACMessage
- want wire.SNACMessage
- wantErr error
- }{
- {
- name: "happy path windows aim < 6",
- instance: newTestInstance("me"),
- given: wire.SNACMessage{
- Frame: wire.SNACFrame{RequestID: 1234},
- },
- want: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: newOServiceUserInfoUpdate(newTestInstance("me")),
- },
- },
- {
- name: "happy path windows aim >= 6",
- instance: newTestInstance("me", sessOptSetFoodGroupVersion(wire.OService, 4)),
- given: wire.SNACMessage{
- Frame: wire.SNACFrame{RequestID: 1234},
- },
- want: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- RequestID: 1234,
- },
- Body: newOServiceUserInfoUpdate(newTestInstance("me", sessOptSetFoodGroupVersion(wire.OService, 4))),
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- }
- have := svc.UserInfoQuery(context.Background(), tt.instance, tt.given.Frame)
- assert.Equal(t, tt.want, have)
- })
- }
- }
- func TestOServiceService_IdleNotification(t *testing.T) {
- tests := []struct {
- name string
- instance *state.SessionInstance
- bodyIn wire.SNAC_0x01_0x11_OServiceIdleNotification
- // mockParams is the list of params sent to mocks that satisfy this
- // method's dependencies
- mockParams mockParams
- wantErr error
- }{
- {
- name: "set idle from active",
- instance: newTestInstance("me"),
- bodyIn: wire.SNAC_0x01_0x11_OServiceIdleNotification{
- IdleTime: 90,
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyArrivedParams: broadcastBuddyArrivedParams{
- {
- screenName: state.DisplayScreenName("me"),
- },
- },
- },
- },
- },
- {
- name: "set active from idle",
- instance: newTestInstance("me", sessOptIdle(90*time.Second)),
- bodyIn: wire.SNAC_0x01_0x11_OServiceIdleNotification{
- IdleTime: 0,
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastBuddyArrivedParams: broadcastBuddyArrivedParams{
- {
- screenName: state.DisplayScreenName("me"),
- },
- },
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- buddyUpdateBroadcaster := newMockbuddyBroadcaster(t)
- for _, params := range tt.mockParams.broadcastBuddyArrivedParams {
- buddyUpdateBroadcaster.EXPECT().
- BroadcastBuddyArrived(mock.Anything, state.NewIdentScreenName(params.screenName.String()), mock.MatchedBy(func(userInfo wire.TLVUserInfo) bool {
- return userInfo.ScreenName == params.screenName.String()
- })).
- Return(params.err)
- }
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- buddyBroadcaster: buddyUpdateBroadcaster,
- }
- haveErr := svc.IdleNotification(context.TODO(), tt.instance, tt.bodyIn)
- assert.ErrorIs(t, tt.wantErr, haveErr)
- })
- }
- }
- func TestOServiceService_ClientOnline(t *testing.T) {
- chatRoom := state.NewChatRoom("the-chat-room", state.NewIdentScreenName("creator"), state.PrivateExchange)
- chatter1 := newTestInstance("chatter-1", sessOptChatRoomCookie(chatRoom.Cookie()))
- chatter2 := newTestInstance("chatter-2", sessOptChatRoomCookie(chatRoom.Cookie()))
- tests := []struct {
- // name is the name of the test
- name string
- // joiningChatter is the session of the arriving user
- instance *state.SessionInstance
- // bodyIn is the SNAC body sent from the arriving user's client to the
- // server
- bodyIn wire.SNAC_0x01_0x02_OServiceClientOnline
- // service is the OSCAR service type
- service uint16
- // wantErr is the expected error from the handler
- wantErr error
- // mockParams is the list of params sent to mocks that satisfy this
- // method's dependencies
- mockParams mockParams
- // validateSess verifies the session state after the handler has run
- validateSess func(t *testing.T, instance *state.SessionInstance)
- // cfg is the config to use for this test. Zero value uses default config.Config{}
- cfg config.Config
- }{
- {
- name: "notify that BOS user is online",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- },
- },
- {
- name: "ICQ Lite order: ClientOnline before feedbag use",
- instance: newTestInstance("me", sessOptCannedSignonTime),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.False(t, instance.ContactsInit())
- },
- },
- {
- name: "notify that BOS user is online via Kerberos auth, does not have stored profile",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptKerberosAuth, sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- profileManagerParams: profileManagerParams{
- retrieveProfileParams: retrieveProfileParams{
- {
- screenName: state.NewIdentScreenName("me"),
- result: state.UserProfile{},
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.True(t, instance.Session().Profile().IsZero())
- },
- },
- {
- name: "notify that BOS user is online via Kerberos auth, has stored profile",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptKerberosAuth, sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- relayToSelfParams: relayToSelfParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceUserInfoUpdate,
- },
- Body: newOServiceUserInfoUpdate(newTestInstance("me", sessOptCannedSignonTime, sessOptProfile(
- state.UserProfile{
- ProfileText: "profile-result",
- MIMEType: `text/aolrtf; charset="us-ascii"`,
- UpdateTime: time.Unix(100000, 0),
- },
- ))),
- },
- },
- },
- },
- profileManagerParams: profileManagerParams{
- retrieveProfileParams: retrieveProfileParams{
- {
- screenName: state.NewIdentScreenName("me"),
- result: state.UserProfile{
- ProfileText: "profile-result",
- MIMEType: `text/aolrtf; charset="us-ascii"`,
- UpdateTime: time.Unix(100000, 0),
- },
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.False(t, instance.Session().Profile().IsZero(), "profile update time is non-zero")
- assert.Equal(t, "profile-result", instance.Session().Profile().ProfileText, "profile text matches")
- assert.Equal(t, `text/aolrtf; charset="us-ascii"`, instance.Session().Profile().MIMEType, "profile mimetype matches")
- },
- },
- {
- name: "notify that BOS user is online with 0 offline messages, no notification sent",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptOfflineMsgCount(0), sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.Equal(t, 0, instance.OfflineMsgCount())
- },
- },
- {
- name: "notify that BOS user is online with offline messages, send notification and reset count",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptOfflineMsgCount(3), sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- {
- screenName: state.NewIdentScreenName("me"),
- message: func() wire.SNACMessage {
- msg, err := systemMessage("You just received 3 IM(s) while you were offline. If you " +
- "do not wish to receive offline messages, please go to " +
- "<a href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ&start_radio=1&pp=ygUJcmljayByb2xsoAcB\">IM Settings</a>.")
- require.NoError(t, err)
- return msg
- }(),
- },
- },
- },
- offlineMessageManagerParams: offlineMessageManagerParams{
- setOfflineMsgCountParams: setOfflineMsgCountParams{
- {
- screenName: state.NewIdentScreenName("me"),
- count: 0,
- err: nil,
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.Equal(t, 0, instance.OfflineMsgCount())
- },
- },
- {
- name: "ICQ user with offline messages, no offline message notification sent",
- instance: newTestInstance("100001", sessOptCannedSignonTime, sessOptOfflineMsgCount(3), sessOptContactsInit, sessOptUIN(100001)),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("100001"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("100001"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.Equal(t, uint32(100001), instance.UIN())
- assert.Equal(t, 3, instance.OfflineMsgCount())
- },
- },
- {
- name: "notify that BOS user is logged in to multiple locations",
- instance: func() *state.SessionInstance {
- instance1 := newTestInstance("me")
- instance1.SetSignonComplete()
- instance2 := instance1.Session().AddInstance()
- instance2.SetSignonComplete()
- instance3 := instance1.Session().AddInstance()
- instance3.SetContactsInit()
- // instance 3 is not yet signed on
- return instance3
- }(),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- relayToOtherInstancesParams: relayToOtherInstancesParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: func() wire.SNACMessage {
- msg, err := systemMessage("Your screen name (me) is now signed into Open OSCAR Server in 3 locations. Click " +
- "<a href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDdQw4w9WgXcQ&start_radio=1&pp=ygUJcmljayByb2xsoAcB\">here</a> " +
- "for more information.")
- require.NoError(t, err)
- return msg
- }(),
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.Session().Instance(3).SignonComplete())
- },
- },
- {
- name: "BOS user is logged in to multiple locations, DisableMultiLoginNotif is true, no notification sent",
- instance: func() *state.SessionInstance {
- instance1 := newTestInstance("me")
- instance1.SetSignonComplete()
- instance2 := instance1.Session().AddInstance()
- instance2.SetSignonComplete()
- instance3 := instance1.Session().AddInstance()
- instance3.SetContactsInit()
- // instance 3 is not yet signed on
- return instance3
- }(),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- cfg: config.Config{
- DisableMultiLoginNotif: true,
- },
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- // relayToOtherInstancesParams is intentionally omitted - notification should not be sent
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.Session().Instance(3).SignonComplete())
- },
- },
- {
- name: "notify that BOS user is online with offline messages, SetOfflineMsgCount fails",
- instance: newTestInstance("me", sessOptCannedSignonTime, sessOptOfflineMsgCount(2), sessOptContactsInit),
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.BOS,
- wantErr: assert.AnError,
- mockParams: mockParams{
- buddyBroadcasterParams: buddyBroadcasterParams{
- broadcastVisibilityParams: broadcastVisibilityParams{
- {
- from: state.NewIdentScreenName("me"),
- filter: nil,
- doSendDepartures: false,
- },
- },
- },
- messageRelayerParams: messageRelayerParams{
- relayToScreenNameParams: relayToScreenNameParams{
- {
- screenName: state.NewIdentScreenName("me"),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Stats,
- SubGroup: wire.StatsSetMinReportInterval,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x0B_0x02_StatsSetMinReportInterval{
- MinReportInterval: 1,
- },
- },
- },
- },
- },
- offlineMessageManagerParams: offlineMessageManagerParams{
- setOfflineMsgCountParams: setOfflineMsgCountParams{
- {
- screenName: state.NewIdentScreenName("me"),
- count: 0,
- err: assert.AnError,
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- assert.Equal(t, 2, instance.OfflineMsgCount())
- },
- },
- {
- name: "upon joining, send chat room metadata and participant list to joining user; alert arrival to existing participants",
- instance: chatter1,
- bodyIn: wire.SNAC_0x01_0x02_OServiceClientOnline{},
- service: wire.Chat,
- mockParams: mockParams{
- chatMessageRelayerParams: chatMessageRelayerParams{
- chatRelayToAllExceptParams: chatRelayToAllExceptParams{
- {
- screenName: state.NewIdentScreenName("chatter-1"),
- cookie: chatRoom.Cookie(),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Chat,
- SubGroup: wire.ChatUsersJoined,
- },
- Body: wire.SNAC_0x0E_0x03_ChatUsersJoined{
- Users: []wire.TLVUserInfo{
- chatter1.Session().TLVUserInfo(),
- },
- },
- },
- },
- },
- chatAllSessionsParams: chatAllSessionsParams{
- {
- cookie: chatRoom.Cookie(),
- sessions: []*state.Session{
- chatter1.Session(),
- chatter2.Session(),
- },
- },
- },
- chatRelayToScreenNameParams: chatRelayToScreenNameParams{
- {
- cookie: chatRoom.Cookie(),
- screenName: chatter1.IdentScreenName(),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Chat,
- SubGroup: wire.ChatRoomInfoUpdate,
- },
- Body: wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
- Exchange: chatRoom.Exchange(),
- Cookie: chatRoom.Cookie(),
- InstanceNumber: chatRoom.InstanceNumber(),
- DetailLevel: chatRoom.DetailLevel(),
- TLVBlock: wire.TLVBlock{
- TLVList: chatRoom.TLVList(),
- },
- },
- },
- },
- {
- cookie: chatRoom.Cookie(),
- screenName: chatter1.IdentScreenName(),
- message: wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.Chat,
- SubGroup: wire.ChatUsersJoined,
- },
- Body: wire.SNAC_0x0E_0x03_ChatUsersJoined{
- Users: []wire.TLVUserInfo{
- chatter1.Session().TLVUserInfo(),
- chatter2.Session().TLVUserInfo(),
- },
- },
- },
- },
- },
- },
- chatRoomRegistryParams: chatRoomRegistryParams{
- chatRoomByCookieParams: chatRoomByCookieParams{
- {
- cookie: chatRoom.Cookie(),
- room: chatRoom,
- },
- },
- },
- },
- validateSess: func(t *testing.T, instance *state.SessionInstance) {
- assert.True(t, instance.SignonComplete())
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- buddyUpdateBroadcaster := newMockbuddyBroadcaster(t)
- for _, params := range tt.mockParams.broadcastVisibilityParams {
- buddyUpdateBroadcaster.EXPECT().
- BroadcastVisibility(matchContext(), matchSession(params.from), params.filter, params.doSendDepartures).
- Return(params.err)
- }
- messageRelayer := newMockMessageRelayer(t)
- for _, params := range tt.mockParams.relayToScreenNameParams {
- messageRelayer.EXPECT().
- RelayToScreenName(matchContext(), params.screenName, params.message)
- }
- for _, params := range tt.mockParams.relayToSelfParams {
- messageRelayer.EXPECT().
- RelayToSelf(matchContext(), mock.Anything, mock.MatchedBy(func(msg wire.SNACMessage) bool {
- return msg.Frame.FoodGroup == params.message.Frame.FoodGroup &&
- msg.Frame.SubGroup == params.message.Frame.SubGroup
- }))
- }
- for _, params := range tt.mockParams.relayToOtherInstancesParams {
- messageRelayer.EXPECT().
- RelayToOtherInstances(matchContext(), matchSession(params.screenName), params.message)
- }
- chatRoomManager := newMockChatRoomRegistry(t)
- for _, params := range tt.mockParams.chatRoomByCookieParams {
- chatRoomManager.EXPECT().
- ChatRoomByCookie(context.Background(), params.cookie).
- Return(params.room, params.err)
- }
- chatMessageRelayer := newMockChatMessageRelayer(t)
- for _, params := range tt.mockParams.chatRelayToAllExceptParams {
- chatMessageRelayer.EXPECT().
- RelayToAllExcept(matchContext(), params.cookie, params.screenName, params.message)
- }
- for _, params := range tt.mockParams.chatAllSessionsParams {
- chatMessageRelayer.EXPECT().
- AllSessions(params.cookie).
- Return(params.sessions)
- }
- for _, params := range tt.mockParams.chatRelayToScreenNameParams {
- chatMessageRelayer.EXPECT().
- RelayToScreenName(matchContext(), params.cookie, params.screenName, params.message)
- }
- profileManager := newMockProfileManager(t)
- for _, params := range tt.mockParams.retrieveProfileParams {
- profileManager.EXPECT().
- Profile(matchContext(), params.screenName).
- Return(params.result, params.err)
- }
- offlineMessageManager := newMockOfflineMessageManager(t)
- for _, params := range tt.mockParams.setOfflineMsgCountParams {
- offlineMessageManager.EXPECT().
- SetOfflineMsgCount(matchContext(), params.screenName, params.count).
- Return(params.err)
- }
- svc := NewOServiceService(tt.cfg, messageRelayer, slog.Default(), nil, chatRoomManager, nil, nil, nil, wire.DefaultSNACRateLimits(), chatMessageRelayer, profileManager, offlineMessageManager, nil)
- svc.buddyBroadcaster = buddyUpdateBroadcaster
- haveErr := svc.ClientOnline(context.Background(), tt.service, tt.bodyIn, tt.instance)
- assert.ErrorIs(t, haveErr, tt.wantErr)
- tt.validateSess(t, tt.instance)
- })
- }
- }
- func TestOServiceService_SetPrivacyFlags(t *testing.T) {
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- }
- body := wire.SNAC_0x01_0x14_OServiceSetPrivacyFlags{
- PrivacyFlags: wire.OServicePrivacyFlagMember | wire.OServicePrivacyFlagIdle,
- }
- svc.SetPrivacyFlags(context.Background(), body)
- }
- func TestOServiceService_RateLimitUpdates(t *testing.T) {
- rateClasses := [5]wire.RateClass{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- }
- svc := OServiceService{
- cfg: config.Config{},
- logger: slog.Default(),
- }
- t.Run("(win aim 1.x) transition state from clear > alert > limited > clear, then change rate limit param", func(t *testing.T) {
- now := time.Now()
- instance := newTestInstance("me")
- instance.Session().SetRateClasses(now, wire.NewRateLimitClasses(rateClasses))
- classId := wire.RateLimitClassID(3)
- instance.Session().SubscribeRateLimits([]wire.RateLimitClassID{classId})
- // get into an alert state
- maxTries := 4
- for i := 1; i <= maxTries; i++ {
- now = now.Add(time.Millisecond)
- if s := instance.Session().EvaluateRateLimit(now, classId); s == wire.RateLimitStatusAlert {
- break
- }
- if i == maxTries {
- t.Fail()
- return
- }
- }
- outputSNACs := svc.RateLimitUpdates(context.Background(), instance, now)
- expect := wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 2,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 4886,
- MaxLevel: 6000,
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // get into a rate-limited state
- maxTries = 4
- for i := 1; i <= maxTries; i++ {
- now = now.Add(time.Millisecond)
- if s := instance.Session().EvaluateRateLimit(now, classId); s == wire.RateLimitStatusLimited {
- break
- }
- if i == maxTries {
- t.Fail()
- return
- }
- }
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 3,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 3978,
- MaxLevel: 6000,
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // simulate waiting a minute for the clear threshold
- now = now.Add(time.Minute)
- // verify that the clear threshold has been reached
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 4,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 6000,
- MaxLevel: 6000,
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // verify rate class param changes are detected
- classesCopy := rateClasses
- classesCopy[2].DisconnectLevel--
- instance.Session().SetRateClasses(now, wire.NewRateLimitClasses(classesCopy))
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 1,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 2999,
- CurrentLevel: 6000,
- MaxLevel: 6000,
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- })
- t.Run("(win aim > 1.x) transition state from clear > alert > limited > clear", func(t *testing.T) {
- now := time.Now()
- instance := newTestInstance("me")
- instance.Session().SetRateClasses(now, wire.NewRateLimitClasses(rateClasses))
- var versions [wire.MDir + 1]uint16
- versions[wire.OService] = 3
- instance.SetFoodGroupVersions(versions)
- classId := wire.RateLimitClassID(3)
- instance.Session().SubscribeRateLimits([]wire.RateLimitClassID{classId})
- // get into an alert state
- maxTries := 4
- for i := 1; i <= maxTries; i++ {
- now = now.Add(time.Millisecond)
- if s := instance.Session().EvaluateRateLimit(now, classId); s == wire.RateLimitStatusAlert {
- break
- }
- if i == maxTries {
- t.Fail()
- return
- }
- }
- outputSNACs := svc.RateLimitUpdates(context.Background(), instance, now)
- expect := wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 2,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 4886,
- MaxLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- DroppingSNACs: 0,
- },
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // get into a rate-limited state
- maxTries = 4
- for i := 1; i <= maxTries; i++ {
- now = now.Add(time.Millisecond)
- if s := instance.Session().EvaluateRateLimit(now, classId); s == wire.RateLimitStatusLimited {
- break
- }
- if i == maxTries {
- t.Fail()
- return
- }
- }
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 3,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 3978,
- MaxLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- DroppingSNACs: 1,
- },
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // simulate waiting a minute for the clear threshold
- now = now.Add(time.Minute)
- // verify that the clear threshold has been reached
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 4,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- CurrentLevel: 6000,
- MaxLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- DroppingSNACs: 0,
- LastTime: 60,
- },
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- // verify rate class param changes are detected
- classesCopy := rateClasses
- classesCopy[2].DisconnectLevel--
- instance.Session().SetRateClasses(now, wire.NewRateLimitClasses(classesCopy))
- outputSNACs = svc.RateLimitUpdates(context.Background(), instance, now)
- expect = wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.OService,
- SubGroup: wire.OServiceRateParamChange,
- RequestID: wire.ReqIDFromServer,
- },
- Body: wire.SNAC_0x01_0x0A_OServiceRateParamsChange{
- Code: 1,
- Rate: wire.RateParamsSNAC{
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 2999,
- CurrentLevel: 6000,
- MaxLevel: 6000,
- V2Params: &struct {
- LastTime uint32
- DroppingSNACs uint8
- }{
- DroppingSNACs: 0,
- LastTime: 0,
- },
- },
- },
- }
- assert.Equal(t, expect, outputSNACs[0])
- })
- }
- func TestOServiceService_RateParamsSubAdd(t *testing.T) {
- svc := OServiceService{
- logger: slog.New(slog.NewTextHandler(io.Discard, nil)), // silence logs
- }
- classes := [5]wire.RateClass{
- {
- ID: 1,
- WindowSize: 80,
- ClearLevel: 2500,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 800,
- MaxLevel: 6000,
- },
- {
- ID: 2,
- WindowSize: 80,
- ClearLevel: 3000,
- AlertLevel: 2000,
- LimitLevel: 1500,
- DisconnectLevel: 1000,
- MaxLevel: 6000,
- },
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {
- ID: 4,
- WindowSize: 20,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- {
- ID: 5,
- WindowSize: 10,
- ClearLevel: 5500,
- AlertLevel: 5300,
- LimitLevel: 4200,
- DisconnectLevel: 3000,
- MaxLevel: 8000,
- },
- }
- t.Run("happy path", func(t *testing.T) {
- classes := classes
- instance := newTestInstance("me")
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- deltas, _ := instance.Session().ObserveRateChanges(time.Now())
- assert.Len(t, deltas, 0)
- // expect 3 rate limit class changes
- classes[0].MaxLevel = 8888
- classes[1].MaxLevel = 8888
- classes[2].MaxLevel = 8888
- classes[3].MaxLevel = 8888
- classes[4].MaxLevel = 8888
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- snac := wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
- ClassIDs: []uint16{2, 5},
- }
- svc.RateParamsSubAdd(context.Background(), instance, snac)
- deltas, _ = instance.Session().ObserveRateChanges(time.Now())
- assert.Len(t, deltas, 2)
- // expect 5 rate limit class changes
- classes[0].MaxLevel = 9999
- classes[1].MaxLevel = 9999
- classes[2].MaxLevel = 9999
- classes[3].MaxLevel = 9999
- classes[4].MaxLevel = 9999
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- snac = wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
- ClassIDs: []uint16{1, 3, 4},
- }
- svc.RateParamsSubAdd(context.Background(), instance, snac)
- deltas, _ = instance.Session().ObserveRateChanges(time.Now())
- assert.Len(t, deltas, 5)
- })
- t.Run("empty subscribe list", func(t *testing.T) {
- classes := classes
- instance := newTestInstance("me")
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- deltas, _ := instance.Session().ObserveRateChanges(time.Now())
- assert.Len(t, deltas, 0)
- // expect 3 rate limit class changes
- classes[0].MaxLevel = 8888
- classes[1].MaxLevel = 8888
- classes[2].MaxLevel = 8888
- classes[3].MaxLevel = 8888
- classes[4].MaxLevel = 8888
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- snac := wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
- ClassIDs: []uint16{},
- }
- svc.RateParamsSubAdd(context.Background(), instance, snac)
- deltas, _ = instance.Session().ObserveRateChanges(time.Now())
- assert.Empty(t, deltas)
- })
- t.Run("class IDs out of range", func(t *testing.T) {
- classes := classes
- instance := newTestInstance("me")
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- deltas, _ := instance.Session().ObserveRateChanges(time.Now())
- assert.Len(t, deltas, 0)
- // expect 3 rate limit class changes
- classes[0].MaxLevel = 8888
- classes[1].MaxLevel = 8888
- classes[2].MaxLevel = 8888
- classes[3].MaxLevel = 8888
- classes[4].MaxLevel = 8888
- instance.Session().SetRateClasses(time.Now(), wire.NewRateLimitClasses(classes))
- snac := wire.SNAC_0x01_0x08_OServiceRateParamsSubAdd{
- ClassIDs: []uint16{0, 6},
- }
- svc.RateParamsSubAdd(context.Background(), instance, snac)
- deltas, _ = instance.Session().ObserveRateChanges(time.Now())
- assert.Empty(t, deltas)
- })
- }
|