| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277 |
- package state
- import (
- "context"
- "math"
- "net/netip"
- "sync"
- "testing"
- "time"
- "github.com/mk6i/open-oscar-server/wire"
- "github.com/stretchr/testify/assert"
- )
- func TestSession_IncrementAndGetWarning(t *testing.T) {
- s := NewSession().AddInstance()
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- s.Session().ScaleWarningAndRateLimit(1, 1)
- s.Session().ScaleWarningAndRateLimit(2, 1)
- s.Session().ScaleWarningAndRateLimit(3, 1)
- }()
- assert.Equal(t, uint16(1), <-s.WarningCh())
- assert.Equal(t, uint16(3), <-s.WarningCh())
- assert.Equal(t, uint16(6), <-s.WarningCh())
- wg.Wait()
- }
- func TestSession_SetAndGetInvisible(t *testing.T) {
- s := NewSession().AddInstance()
- assert.False(t, s.Invisible())
- s.SetUserStatusBitmask(wire.OServiceUserStatusInvisible)
- assert.True(t, s.Invisible())
- }
- func TestSession_SetAndGetScreenName(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Empty(t, s.IdentScreenName())
- sn := NewIdentScreenName("user-screen-name")
- s.Session().SetIdentScreenName(sn)
- assert.Equal(t, sn, s.IdentScreenName())
- }
- func TestSession_SetAndGetChatRoomCookie(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Empty(t, s.ChatRoomCookie())
- sn := "the-chat-cookie"
- s.Session().SetChatRoomCookie(sn)
- assert.Equal(t, sn, s.ChatRoomCookie())
- }
- func TestSession_SetAndGetUIN(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Empty(t, s.UIN())
- uin := uint32(100003)
- s.Session().SetUIN(uin)
- assert.Equal(t, uin, s.UIN())
- }
- func TestSession_SetAndGetClientID(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Empty(t, s.ClientID())
- clientID := "AIM Client ID"
- s.SetClientID(clientID)
- assert.Equal(t, clientID, s.ClientID())
- }
- func TestSession_SetAndGetKerberosAuth(t *testing.T) {
- s := NewSession().AddInstance()
- assert.False(t, s.KerberosAuth())
- s.SetKerberosAuth(true)
- assert.True(t, s.KerberosAuth())
- s.SetKerberosAuth(false)
- assert.False(t, s.KerberosAuth())
- }
- func TestSession_SetAndGetRemoteAddr(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Empty(t, s.RemoteAddr())
- remoteAddr, _ := netip.ParseAddrPort("1.2.3.4:1234")
- s.SetRemoteAddr(&remoteAddr)
- assert.Equal(t, &remoteAddr, s.RemoteAddr())
- }
- func TestSession_TLVUserInfo(t *testing.T) {
- tests := []struct {
- name string
- givenSessionFn func() *SessionInstance
- want wire.TLVUserInfo
- }{
- {
- name: "user is active and visible",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.Session().SetIdentScreenName(NewIdentScreenName("xXAIMUSERXx"))
- s.Session().SetDisplayScreenName("xXAIMUSERXx")
- s.Session().ScaleWarningAndRateLimit(10, 1)
- s.SetUserInfoFlag(wire.OServiceUserFlagOSCARFree)
- return s
- },
- want: wire.TLVUserInfo{
- ScreenName: "xXAIMUSERXx",
- WarningLevel: 10,
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user is on ICQ",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.Session().SetIdentScreenName(NewIdentScreenName("1000003"))
- s.Session().SetDisplayScreenName("1000003")
- s.SetUserInfoFlag(wire.OServiceUserFlagICQ)
- return s
- },
- want: wire.TLVUserInfo{
- ScreenName: "1000003",
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, wire.OServiceUserFlagOSCARFree|wire.OServiceUserFlagICQ),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoICQDC, wire.ICQDCInfo{}),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user has away message set - all instances away",
- givenSessionFn: func() *SessionInstance {
- sg := NewSession()
- s := sg.AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // Add a second instance that is also away
- s2 := sg.AddInstance()
- s2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x30)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user has one instance away, one not away - away flag not set",
- givenSessionFn: func() *SessionInstance {
- sg := NewSession()
- // Create the NOT away instance first so it's used as the base
- s2 := sg.AddInstance()
- s2.Session().SetSignonTime(time.Unix(1, 0))
- // s2 is NOT away - it has default flags only (OServiceUserFlagOSCARFree)
- // Now create the away instance
- s := sg.AddInstance()
- s.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // Since s2 is the first instance and is not away, and allAway() returns false,
- // the unavailable flag should not be set
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x10)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user has two instances away, second goes off away - away flag not set",
- givenSessionFn: func() *SessionInstance {
- sg := NewSession()
- sg.SetSignonTime(time.Unix(1, 0))
- // Set the first instance as away
- s1 := sg.AddInstance()
- s1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // Set the second instance as away
- s2 := sg.AddInstance()
- s2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // Make the second instance as not away
- s2.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable)
- return s1
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x10)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user is invisible",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.SetUserStatusBitmask(wire.OServiceUserStatusInvisible)
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0100)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user is idle",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- // sign on at t=0m
- timeBegin := time.Unix(0, 0)
- s.Session().SetSignonTime(timeBegin)
- // set idle for 1m at t=+5m (ergo user idled @ t=+4m)
- timeIdle := timeBegin.Add(5 * time.Minute)
- s.Session().SetNowFn(func() time.Time { return timeIdle })
- s.SetIdle(1 * time.Minute)
- // now it's t=+10m, ergo idle time should be t10-t4=6m
- timeNow := timeBegin.Add(10 * time.Minute)
- s.Session().SetNowFn(func() time.Time { return timeNow })
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(0)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoIdleTime, uint16(6)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user goes idle then returns",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.SetIdle(1 * time.Second)
- s.UnsetIdle()
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user has capabilities",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- s.SetCaps([][16]byte{
- {
- // chat: "748F2420-6287-11D1-8222-444553540000"
- 0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1,
- 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00,
- },
- {
- // chat2: "748F2420-6287-11D1-8222-444553540000"
- 0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1,
- 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01,
- },
- })
- return s
- },
- want: wire.TLVUserInfo{
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoOscarCaps, []byte{
- // chat: "748F2420-6287-11D1-8222-444553540000"
- 0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1,
- 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00,
- // chat: "748F2420-6287-11D1-8222-444553540000"
- 0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1,
- 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01,
- }),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- {
- name: "user has buddy icon",
- givenSessionFn: func() *SessionInstance {
- s := NewSession().AddInstance()
- s.Session().SetSignonTime(time.Unix(1, 0))
- return s
- },
- want: wire.TLVUserInfo{
- WarningLevel: 0,
- TLVBlock: wire.TLVBlock{
- TLVList: wire.TLVList{
- wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(1)),
- wire.NewTLVBE(wire.OServiceUserInfoUserFlags, uint16(0x0010)),
- wire.NewTLVBE(wire.OServiceUserInfoStatus, uint32(0x0000)),
- wire.NewTLVBE(wire.OServiceUserInfoMySubscriptions, uint32(0)),
- },
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- s := tt.givenSessionFn()
- assert.Equal(t, tt.want, s.Session().TLVUserInfo())
- })
- }
- }
- func TestSession_SendAndRecvMessage_ExpectSessSendOK(t *testing.T) {
- s := NewSession().AddInstance()
- s.SetSignonComplete()
- msg := wire.SNACMessage{
- Frame: wire.SNACFrame{
- FoodGroup: wire.ICBM,
- },
- }
- var wg sync.WaitGroup
- wg.Add(1)
- go func() {
- defer wg.Done()
- defer s.CloseInstance()
- status := s.RelayMessageToInstance(msg)
- assert.Equal(t, SessSendOK, status)
- }()
- loop:
- for {
- select {
- case m := <-s.ReceiveMessage():
- assert.Equal(t, msg, m)
- case <-s.Closed():
- break loop
- }
- }
- wg.Wait()
- }
- func TestSession_SendMessage_SessSendClosed(t *testing.T) {
- s := NewSession().AddInstance()
- s.CloseInstance()
- if res := s.RelayMessageToInstance(wire.SNACMessage{}); res != SessSendClosed {
- t.Fatalf("expected SessSendClosed, got %+v", res)
- }
- }
- func TestSession_SendMessage_SessQueueFull(t *testing.T) {
- s := NewSession().AddInstance()
- s.SetSignonComplete()
- // Fill up the message channel (default buffer size is 1000)
- for i := 0; i < 1000; i++ {
- assert.Equal(t, SessSendOK, s.RelayMessageToInstance(wire.SNACMessage{}))
- }
- assert.Equal(t, SessQueueFull, s.RelayMessageToInstance(wire.SNACMessage{}))
- }
- func TestSession_Close_Twice(t *testing.T) {
- s := NewSession().AddInstance()
- s.CloseInstance()
- s.CloseInstance() // make sure close is idempotent
- // Check that the session is closed by trying to relay a message
- if res := s.RelayMessageToInstance(wire.SNACMessage{}); res != SessSendClosed {
- t.Fatalf("expected SessSendClosed, got %+v", res)
- }
- select {
- case <-s.Closed():
- case <-time.After(1 * time.Second):
- t.Fatalf("channel is not closed")
- }
- }
- func TestSession_Closed(t *testing.T) {
- s := NewSession().AddInstance()
- select {
- case <-s.Closed():
- assert.Fail(t, "channel is closed")
- default:
- // channel is open by default
- }
- s.Session().CloseSession()
- <-s.Closed()
- }
- func TestSession_EvaluateRateLimit_ObserveRateChanges(t *testing.T) {
- classParams := [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,
- },
- }
- rateClasses := wire.NewRateLimitClasses(classParams)
- t.Run("we can action every 5 seconds indefinitely without getting rate limited", func(t *testing.T) {
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- rateClass := rateClasses.Get(3)
- instance.Session().SubscribeRateLimits([]wire.RateLimitClassID{rateClass.ID})
- for i := 0; i < 100; i++ {
- now = now.Add(5 * time.Second)
- have := instance.Session().EvaluateRateLimit(now, rateClass.ID)
- assert.Equal(t, wire.RateLimitStatusClear, have)
- }
- })
- t.Run("reach disconnect threshold", func(t *testing.T) {
- now := time.Now()
- sess := NewSession()
- sess.SetRateClasses(now, rateClasses)
- sess.AddInstance()
- sess.AddInstance()
- sess.AddInstance()
- rateClass := rateClasses.Get(3)
- sess.SubscribeRateLimits([]wire.RateLimitClassID{rateClass.ID})
- // record some event in the rate limiter
- want := []wire.RateLimitStatus{
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusLimited,
- wire.RateLimitStatusDisconnect,
- }
- for i := 0; i < len(want); i++ {
- now = now.Add(1 * time.Second)
- have := sess.EvaluateRateLimit(now, rateClass.ID)
- assert.Equal(t, want[i], have)
- }
- for _, instance := range sess.Instances() {
- select {
- case <-instance.Closed():
- default:
- t.Error("expected session to be closed")
- }
- }
- })
- t.Run("reach rate limit threshold, wait for clear threshold", func(t *testing.T) {
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- rateClass := rateClasses.Get(3)
- instance.Session().SubscribeRateLimits([]wire.RateLimitClassID{rateClass.ID})
- // first reach the rate limit threshold
- want := []wire.RateLimitStatus{
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusClear,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusAlert,
- wire.RateLimitStatusLimited,
- }
- for i := 0; i < len(want); i++ {
- now = now.Add(1 * time.Second)
- have := instance.Session().EvaluateRateLimit(now, rateClass.ID)
- assert.Equal(t, want[i], have)
- if i > 0 && want[i-1] != want[i] {
- classChanges, rateChanges := instance.Session().ObserveRateChanges(now)
- assert.Empty(t, classChanges)
- if assert.NotEmpty(t, rateChanges) {
- rateDelta := rateChanges[0]
- assert.Equal(t, rateClass, rateDelta.RateClass)
- assert.Equal(t, want[i], rateDelta.CurrentStatus)
- assert.True(t, rateDelta.Subscribed)
- if want[i] == wire.RateLimitStatusLimited {
- assert.True(t, rateDelta.LimitedNow)
- }
- }
- }
- }
- // this is a rearranged moving average formula that determines how many
- // milliseconds it will take to reach the clear threshold
- rateLimitStates := instance.RateLimitStates()
- timeToRecover := int(math.Ceil((time.Duration(rateClass.ClearLevel*rateClass.WindowSize-rateLimitStates[rateClass.ID-1].CurrentLevel*(rateClass.WindowSize-1)) * time.Millisecond).Seconds()))
- assert.True(t, timeToRecover > 0)
- // indicate the time rate limiting kicked in
- timeLimited := now
- for i := 0; i < timeToRecover; i++ {
- now = now.Add(1 * time.Second)
- classDelta, stateDelta := instance.Session().ObserveRateChanges(now)
- assert.Empty(t, classDelta)
- if i == timeToRecover-1 {
- // assert that the clear threshold has been met.
- assert.ElementsMatch(t, stateDelta, []RateClassState{
- {
- RateClass: rateClass,
- CurrentLevel: 5140,
- CurrentStatus: wire.RateLimitStatusClear,
- LastTime: timeLimited,
- Subscribed: true,
- LimitedNow: false,
- }})
- } else {
- // assert that no changed have been observed, it's still rate-limited
- assert.Nil(t, stateDelta)
- }
- }
- })
- t.Run("observe a rate class change", func(t *testing.T) {
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- rateClass := rateClasses.Get(3)
- instance.Session().SubscribeRateLimits([]wire.RateLimitClassID{rateClass.ID})
- now = now.Add(1 * time.Second)
- classDelta, stateDelta := instance.Session().ObserveRateChanges(now)
- assert.Empty(t, classDelta)
- assert.Empty(t, stateDelta)
- paramsCopy := classParams
- paramsCopy[rateClass.ID-1].LimitLevel++
- newRateClasses := wire.NewRateLimitClasses(paramsCopy)
- now = now.Add(1 * time.Second)
- instance.Session().SetRateClasses(now, newRateClasses)
- now = now.Add(1 * time.Second)
- classDelta, stateDelta = instance.Session().ObserveRateChanges(now)
- assert.Equal(t, classDelta[0].RateClass, newRateClasses.Get(rateClass.ID))
- assert.Empty(t, stateDelta)
- })
- t.Run("as a bot, I can action every second indefinitely without getting rate limited", func(t *testing.T) {
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.SetUserInfoFlag(wire.OServiceUserFlagBot)
- instance.Session().SetRateClasses(now, rateClasses)
- for i := 0; i < 100; i++ {
- now = now.Add(1 * time.Second)
- have := instance.Session().EvaluateRateLimit(now, wire.RateLimitClassID(1))
- assert.Equal(t, wire.RateLimitStatusClear, have)
- }
- })
- }
- func TestSession_SetAndGetFoodGroupVersions(t *testing.T) {
- versions := [wire.MDir + 1]uint16{}
- versions[wire.Feedbag] = 1
- versions[wire.OService] = 2
- s := NewSession().AddInstance()
- s.SetFoodGroupVersions(versions)
- assert.Equal(t, versions, s.FoodGroupVersions())
- }
- func TestSession_SetAndGetTypingEventsEnabled(t *testing.T) {
- s := NewSession().AddInstance()
- assert.False(t, s.TypingEventsEnabled())
- s.Session().SetTypingEventsEnabled(true)
- assert.True(t, s.TypingEventsEnabled())
- s.Session().SetTypingEventsEnabled(false)
- assert.False(t, s.TypingEventsEnabled())
- }
- func TestSession_SetAndGetMultiConnFlag(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Zero(t, s.MultiConnFlag())
- s.SetMultiConnFlag(wire.MultiConnFlagsOldClient)
- assert.Equal(t, wire.MultiConnFlagsOldClient, s.MultiConnFlag())
- s.SetMultiConnFlag(wire.MultiConnFlagsRecentClient)
- assert.Equal(t, wire.MultiConnFlagsRecentClient, s.MultiConnFlag())
- s.SetMultiConnFlag(wire.MultiConnFlagsSingleClient)
- assert.Equal(t, wire.MultiConnFlagsSingleClient, s.MultiConnFlag())
- }
- func TestSession_SetAndGetLastWarnLevel(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Zero(t, s.Warning())
- level := uint16(500)
- s.Session().SetWarning(level)
- assert.Equal(t, level, s.Warning())
- }
- func TestInstance_Active(t *testing.T) {
- tests := []struct {
- name string
- setupInstance func() *SessionInstance
- expectedActive bool
- }{
- {
- name: "active instance - not closed, not idle, no away message",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: false,
- awayMsg: "",
- signonComplete: true,
- }
- return instance
- },
- expectedActive: true,
- },
- {
- name: "inactive instance - closed",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: true,
- idle: false,
- awayMsg: "",
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - idle",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: true,
- awayMsg: "",
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - has away message",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: false,
- awayMsg: "I'm away",
- userInfoBitmask: wire.OServiceUserFlagUnavailable,
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - closed and idle",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: true,
- idle: true,
- awayMsg: "",
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - closed and has away message",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: true,
- idle: false,
- awayMsg: "I'm away",
- userInfoBitmask: wire.OServiceUserFlagUnavailable,
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - idle and has away message",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: true,
- awayMsg: "I'm away",
- userInfoBitmask: wire.OServiceUserFlagUnavailable,
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - closed, idle, and has away message",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: true,
- idle: true,
- awayMsg: "I'm away",
- userInfoBitmask: wire.OServiceUserFlagUnavailable,
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - signon not complete",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: false,
- awayMsg: "",
- signonComplete: false,
- }
- return instance
- },
- expectedActive: false,
- },
- {
- name: "inactive instance - signon not complete and idle",
- setupInstance: func() *SessionInstance {
- sg := NewSession()
- instance := &SessionInstance{
- session: sg,
- closed: false,
- idle: true,
- awayMsg: "",
- signonComplete: false,
- }
- return instance
- },
- expectedActive: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- instance := tt.setupInstance()
- assert.Equal(t, tt.expectedActive, instance.active())
- })
- }
- }
- func TestSessionGroup_AllInactive(t *testing.T) {
- tests := []struct {
- name string
- setupSessionGroup func() *Session
- expectedResult bool
- }{
- {
- name: "no instances - should return true",
- setupSessionGroup: func() *Session {
- return NewSession()
- },
- expectedResult: true,
- },
- {
- name: "one active instance - should return false",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.closed = false
- instance.idle = false
- instance.awayMsg = ""
- instance.signonComplete = true
- return sg
- },
- expectedResult: false,
- },
- {
- name: "one closed instance - should return true",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.closed = true
- instance.idle = false
- instance.awayMsg = ""
- return sg
- },
- expectedResult: true,
- },
- {
- name: "one idle instance - should return true",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.closed = false
- instance.idle = true
- instance.awayMsg = ""
- return sg
- },
- expectedResult: true,
- },
- {
- name: "one instance with away message - should return true",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.closed = false
- instance.idle = false
- instance.awayMsg = "I'm away"
- return sg
- },
- expectedResult: true,
- },
- {
- name: "multiple instances - all inactive - should return true",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- // Add closed instance
- instance1 := sg.AddInstance()
- instance1.closed = true
- instance1.idle = false
- instance1.awayMsg = ""
- // Add idle instance
- instance2 := sg.AddInstance()
- instance2.closed = false
- instance2.idle = true
- instance2.awayMsg = ""
- // Add instance with away message
- instance3 := sg.AddInstance()
- instance3.closed = false
- instance3.idle = false
- instance3.awayMsg = "I'm away"
- return sg
- },
- expectedResult: true,
- },
- {
- name: "multiple instances - one active - should return false",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- // Add closed instance
- instance1 := sg.AddInstance()
- instance1.closed = true
- instance1.idle = false
- instance1.awayMsg = ""
- // Add active instance
- instance2 := sg.AddInstance()
- instance2.closed = false
- instance2.idle = false
- instance2.awayMsg = ""
- instance2.signonComplete = true
- // Add idle instance
- instance3 := sg.AddInstance()
- instance3.closed = false
- instance3.idle = true
- instance3.awayMsg = ""
- return sg
- },
- expectedResult: false,
- },
- {
- name: "multiple instances - all active - should return false",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- // Add first active instance
- instance1 := sg.AddInstance()
- instance1.closed = false
- instance1.idle = false
- instance1.awayMsg = ""
- instance1.signonComplete = true
- // Add second active instance
- instance2 := sg.AddInstance()
- instance2.closed = false
- instance2.idle = false
- instance2.awayMsg = ""
- instance2.signonComplete = true
- return sg
- },
- expectedResult: false,
- },
- {
- name: "mixed scenarios - some closed, some idle, some away, one active - should return false",
- setupSessionGroup: func() *Session {
- sg := NewSession()
- // Add closed instance
- instance1 := sg.AddInstance()
- instance1.closed = true
- instance1.idle = false
- instance1.awayMsg = ""
- // Add idle instance
- instance2 := sg.AddInstance()
- instance2.closed = false
- instance2.idle = true
- instance2.awayMsg = ""
- // Add instance with away message
- instance3 := sg.AddInstance()
- instance3.closed = false
- instance3.idle = false
- instance3.awayMsg = "I'm away"
- // Add active instance
- instance4 := sg.AddInstance()
- instance4.closed = false
- instance4.idle = false
- instance4.awayMsg = ""
- instance4.signonComplete = true
- return sg
- },
- expectedResult: false,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sg := tt.setupSessionGroup()
- assert.Equal(t, tt.expectedResult, sg.Inactive())
- })
- }
- }
- func TestSessionGroup_InstanceCount(t *testing.T) {
- tests := []struct {
- name string
- setupGroup func() *Session
- expectedCount int
- }{
- {
- name: "empty session group should return 0",
- setupGroup: func() *Session {
- return NewSession()
- },
- expectedCount: 0,
- },
- {
- name: "one instance should return 1",
- setupGroup: func() *Session {
- sg := NewSession()
- sg.AddInstance()
- return sg
- },
- expectedCount: 1,
- },
- {
- name: "multiple instances should return correct count",
- setupGroup: func() *Session {
- sg := NewSession()
- for i := 0; i < 3; i++ {
- sg.AddInstance()
- }
- return sg
- },
- expectedCount: 3,
- },
- {
- name: "instance count decreases after removal",
- setupGroup: func() *Session {
- sg := NewSession()
- sg.AddInstance()
- instance2 := sg.AddInstance()
- sg.AddInstance()
- // Remove one instance
- sg.RemoveInstance(instance2)
- return sg
- },
- expectedCount: 2,
- },
- {
- name: "instance count is correct after multiple add/remove operations",
- setupGroup: func() *Session {
- sg := NewSession()
- instance1 := sg.AddInstance()
- sg.AddInstance()
- sg.RemoveInstance(instance1)
- sg.AddInstance()
- return sg
- },
- expectedCount: 2,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sg := tt.setupGroup()
- assert.Equal(t, tt.expectedCount, sg.InstanceCount())
- })
- }
- }
- func TestSessionGroup_Instances(t *testing.T) {
- tests := []struct {
- name string
- setupGroup func() *Session
- expectedCount int
- expectedAll bool // whether all instances should be returned (including non-signed-in)
- }{
- {
- name: "empty session group should return empty slice",
- setupGroup: func() *Session {
- return NewSession()
- },
- expectedCount: 0,
- expectedAll: true,
- },
- {
- name: "returns all instances including non-signed-in",
- setupGroup: func() *Session {
- sg := NewSession()
- instance1 := sg.AddInstance()
- instance1.SetSignonComplete()
- _ = sg.AddInstance()
- // instance2 has not completed signon
- return sg
- },
- expectedCount: 2,
- expectedAll: true,
- },
- {
- name: "returns all instances with mixed signon states",
- setupGroup: func() *Session {
- sg := NewSession()
- instance1 := sg.AddInstance()
- instance1.SetSignonComplete()
- _ = sg.AddInstance()
- // instance2 has not completed signon
- instance3 := sg.AddInstance()
- instance3.SetSignonComplete()
- return sg
- },
- expectedCount: 3,
- expectedAll: true,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sg := tt.setupGroup()
- instances := sg.Instances()
- assert.Equal(t, tt.expectedCount, len(instances), "should return all instances")
- if tt.expectedAll {
- // Verify that instances() returns all instances regardless of their state
- // This is the key change: it should return all instances, not just live ones
- assert.Equal(t, sg.InstanceCount(), len(instances), "Instances() should return all instances")
- }
- })
- }
- }
- func TestSession_SetAndGetProfile(t *testing.T) {
- s := NewSession().AddInstance()
- profile := s.Session().Profile()
- assert.Empty(t, profile.ProfileText)
- assert.Empty(t, profile.MIMEType)
- assert.True(t, profile.UpdateTime.IsZero())
- profileTime := time.Unix(1234567890, 0)
- newProfile := UserProfile{
- ProfileText: "My profile text",
- MIMEType: "text/plain",
- UpdateTime: profileTime,
- }
- s.SetProfile(newProfile)
- retrievedProfile := s.Session().Profile()
- assert.Equal(t, newProfile, retrievedProfile)
- assert.Equal(t, "My profile text", retrievedProfile.ProfileText)
- assert.Equal(t, "text/plain", retrievedProfile.MIMEType)
- assert.Equal(t, profileTime, retrievedProfile.UpdateTime)
- }
- func TestSession_Profile(t *testing.T) {
- tests := []struct {
- name string
- setupSession func() *Session
- expectedProfile UserProfile
- }{
- {
- name: "no instances - returns empty profile",
- setupSession: func() *Session {
- return NewSession()
- },
- expectedProfile: UserProfile{},
- },
- {
- name: "one instance with empty profile - returns empty profile",
- setupSession: func() *Session {
- s := NewSession()
- s.AddInstance()
- return s
- },
- expectedProfile: UserProfile{},
- },
- {
- name: "one instance with non-empty profile - returns that profile",
- setupSession: func() *Session {
- s := NewSession()
- instance := s.AddInstance()
- profileTime := time.Unix(1234567890, 0)
- instance.SetProfile(UserProfile{
- ProfileText: "My profile",
- MIMEType: "text/plain",
- UpdateTime: profileTime,
- })
- return s
- },
- expectedProfile: UserProfile{
- ProfileText: "My profile",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0),
- },
- },
- {
- name: "multiple instances, all empty - returns empty profile",
- setupSession: func() *Session {
- s := NewSession()
- s.AddInstance()
- s.AddInstance()
- s.AddInstance()
- return s
- },
- expectedProfile: UserProfile{},
- },
- {
- name: "multiple instances, one non-empty - returns that one",
- setupSession: func() *Session {
- s := NewSession()
- s.AddInstance() // empty instance
- instance2 := s.AddInstance()
- instance2.SetProfile(UserProfile{
- ProfileText: "Profile 2",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0),
- })
- s.AddInstance() // empty instance
- return s
- },
- expectedProfile: UserProfile{
- ProfileText: "Profile 2",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0),
- },
- },
- {
- name: "multiple instances, multiple non-empty - returns most recent UpdateTime",
- setupSession: func() *Session {
- s := NewSession()
- instance1 := s.AddInstance()
- instance1.SetProfile(UserProfile{
- ProfileText: "Profile 1",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567900, 0), // later time - should be returned
- })
- instance2 := s.AddInstance()
- instance2.SetProfile(UserProfile{
- ProfileText: "Profile 2",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0), // earlier time
- })
- instance3 := s.AddInstance()
- instance3.SetProfile(UserProfile{
- ProfileText: "Profile 3",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567895, 0), // middle time
- })
- return s
- },
- expectedProfile: UserProfile{
- ProfileText: "Profile 1",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567900, 0),
- },
- },
- {
- name: "first instance empty, later instances have profiles - returns most recent non-empty",
- setupSession: func() *Session {
- s := NewSession()
- s.AddInstance() // empty instance
- instance2 := s.AddInstance()
- instance2.SetProfile(UserProfile{
- ProfileText: "Profile 2",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0), // earlier
- })
- instance3 := s.AddInstance()
- instance3.SetProfile(UserProfile{
- ProfileText: "Profile 3",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567900, 0), // later time - should be returned
- })
- return s
- },
- expectedProfile: UserProfile{
- ProfileText: "Profile 3",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567900, 0),
- },
- },
- {
- name: "profile with empty ProfileText is considered empty",
- setupSession: func() *Session {
- s := NewSession()
- instance := s.AddInstance()
- instance.SetProfile(UserProfile{
- ProfileText: "",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0),
- })
- return s
- },
- expectedProfile: UserProfile{},
- },
- {
- name: "profile with null byte ProfileText is considered empty",
- setupSession: func() *Session {
- s := NewSession()
- instance := s.AddInstance()
- instance.SetProfile(UserProfile{
- ProfileText: "\x00",
- MIMEType: "text/plain",
- UpdateTime: time.Unix(1234567890, 0),
- })
- return s
- },
- expectedProfile: UserProfile{},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- s := tt.setupSession()
- profile := s.Profile()
- assert.Equal(t, tt.expectedProfile, profile)
- })
- }
- }
- func TestSession_SetAndGetMemberSince(t *testing.T) {
- s := NewSession().AddInstance()
- assert.True(t, s.Session().MemberSince().IsZero())
- memberTime := time.Unix(1234567890, 0)
- s.Session().SetMemberSince(memberTime)
- assert.Equal(t, memberTime, s.Session().MemberSince())
- }
- func TestSession_SetAndGetOfflineMsgCount(t *testing.T) {
- s := NewSession().AddInstance()
- assert.Zero(t, s.OfflineMsgCount())
- count := 5
- s.Session().SetOfflineMsgCount(count)
- assert.Equal(t, count, s.OfflineMsgCount())
- count = 10
- s.Session().SetOfflineMsgCount(count)
- assert.Equal(t, count, s.OfflineMsgCount())
- }
- func TestSession_ScaleWarningAndRateLimit(t *testing.T) {
- t.Run("scale up", func(t *testing.T) {
- classParams := [5]wire.RateClass{
- {},
- {},
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {},
- {},
- }
- rateClasses := wire.NewRateLimitClasses(classParams)
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- var wg sync.WaitGroup
- wg.Add(1)
- ctx, cancel := context.WithCancel(t.Context())
- go func() {
- defer wg.Done()
- for {
- select {
- case <-ctx.Done():
- return
- case <-instance.WarningCh():
- }
- }
- }()
- rateLimitStates := instance.RateLimitStates()
- assert.Equal(t, int32(5000), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5100), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4000), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5085), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5175), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4185), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5170), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5250), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4370), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5255), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5325), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4555), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5340), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5400), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4740), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5425), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5475), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4925), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5510), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5550), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5110), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5595), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5625), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5295), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5680), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5700), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5480), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5765), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5775), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5665), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5850), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5850), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].LimitLevel)
- cancel()
- wg.Wait()
- })
- t.Run("scale down", func(t *testing.T) {
- currentClassParams := [5]wire.RateClass{
- {},
- {},
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {},
- {},
- }
- rateClasses := wire.NewRateLimitClasses(currentClassParams)
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- var wg sync.WaitGroup
- wg.Add(1)
- ctx, cancel := context.WithCancel(t.Context())
- go func() {
- defer wg.Done()
- for {
- select {
- case <-ctx.Done():
- return
- case <-instance.WarningCh():
- }
- }
- }()
- for i := 0; i < 10; i++ {
- instance.Session().ScaleWarningAndRateLimit(100, 3)
- }
- rateLimitStates := instance.RateLimitStates()
- assert.Equal(t, int32(5850), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5765), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5775), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5665), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5680), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5700), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5480), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5595), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5625), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5295), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5510), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5550), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5110), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5425), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5475), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4925), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5340), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5400), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4740), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5255), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5325), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4555), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5170), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5250), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4370), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5085), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5175), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4185), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5000), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5100), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4000), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(-100, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5000), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5100), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4000), rateLimitStates[2].LimitLevel)
- cancel()
- wg.Wait()
- })
- t.Run("increment 100%", func(t *testing.T) {
- classParams := [5]wire.RateClass{
- {},
- {},
- {
- ID: 3,
- WindowSize: 20,
- ClearLevel: 5100,
- AlertLevel: 5000,
- LimitLevel: 4000,
- DisconnectLevel: 3000,
- MaxLevel: 6000,
- },
- {},
- {},
- }
- rateClasses := wire.NewRateLimitClasses(classParams)
- now := time.Now()
- instance := NewSession().AddInstance()
- instance.Session().SetRateClasses(now, rateClasses)
- var wg sync.WaitGroup
- wg.Add(1)
- ctx, cancel := context.WithCancel(t.Context())
- go func() {
- defer wg.Done()
- for {
- select {
- case <-ctx.Done():
- return
- case <-instance.WarningCh():
- }
- }
- }()
- rateLimitStates := instance.RateLimitStates()
- assert.Equal(t, int32(5000), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5100), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(4000), rateLimitStates[2].LimitLevel)
- instance.Session().ScaleWarningAndRateLimit(1000, 3)
- rateLimitStates = instance.RateLimitStates()
- assert.Equal(t, int32(5850), rateLimitStates[2].AlertLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].ClearLevel)
- assert.Equal(t, int32(5850), rateLimitStates[2].LimitLevel)
- cancel()
- wg.Wait()
- })
- }
- func TestSession_RunOnce(t *testing.T) {
- t.Run("runs function on first call", func(t *testing.T) {
- s := NewSession()
- callCount := 0
- err := s.RunOnce(func() error {
- callCount++
- return nil
- })
- assert.NoError(t, err)
- assert.Equal(t, 1, callCount)
- })
- t.Run("does not run function on subsequent calls", func(t *testing.T) {
- s := NewSession()
- callCount := 0
- // First call
- err1 := s.RunOnce(func() error {
- callCount++
- return nil
- })
- // Second call
- err2 := s.RunOnce(func() error {
- callCount++
- return nil
- })
- // Third call
- err3 := s.RunOnce(func() error {
- callCount++
- return nil
- })
- assert.NoError(t, err1)
- assert.NoError(t, err2)
- assert.NoError(t, err3)
- assert.Equal(t, 1, callCount, "function should only be called once")
- })
- t.Run("returns error from function", func(t *testing.T) {
- s := NewSession()
- expectedErr := assert.AnError
- err := s.RunOnce(func() error {
- return expectedErr
- })
- assert.Error(t, err)
- assert.Equal(t, expectedErr, err)
- })
- }
- func TestSession_CloseInstance(t *testing.T) {
- s := NewSession()
- sessionCloseCount := 0
- s.OnSessionClose(func() {
- sessionCloseCount++
- })
- instance1CloseCount := 0
- instance2CloseCount := 0
- instance3CloseCount := 0
- instance1 := s.AddInstance()
- instance2 := s.AddInstance()
- instance3 := s.AddInstance()
- instance1.OnClose(func() {
- // ensure instance is removed from the session before calling this func
- assert.Equal(t, 2, s.InstanceCount())
- instance1CloseCount++
- })
- instance2.OnClose(func() {
- assert.Equal(t, 1, s.InstanceCount())
- instance2CloseCount++
- })
- instance3.OnClose(func() {
- instance3CloseCount++
- })
- // Close instance1 (instances 2 and 3 remain)
- instance1.CloseInstance()
- instance2.CloseInstance()
- instance3.CloseInstance()
- assert.Equal(t, 1, instance1CloseCount, "instance1 onInstanceCloseFn should only be called once")
- assert.Equal(t, 1, instance2CloseCount, "instance2 onInstanceCloseFn should only be called once")
- assert.Equal(t, 0, instance3CloseCount, "instance3 onInstanceCloseFn should not be called because it's the last instance")
- assert.Equal(t, 1, sessionCloseCount, "session onSessCloseFn should not be called")
- }
- func TestSession_CloseSession(t *testing.T) {
- s := NewSession()
- sessionCloseCount := 0
- s.OnSessionClose(func() {
- sessionCloseCount++
- })
- instance1CloseCount := 0
- instance2CloseCount := 0
- instance3CloseCount := 0
- instance1 := s.AddInstance()
- instance2 := s.AddInstance()
- instance3 := s.AddInstance()
- instance1.OnClose(func() {
- instance1CloseCount++
- })
- instance2.OnClose(func() {
- instance2CloseCount++
- })
- instance3.OnClose(func() {
- instance3CloseCount++
- })
- s.CloseSession()
- assert.Equal(t, 0, instance1CloseCount, "instance1 onInstanceCloseFn should not be called")
- assert.Equal(t, 0, instance2CloseCount, "instance2 onInstanceCloseFn should not be called")
- assert.Equal(t, 0, instance3CloseCount, "instance3 onInstanceCloseFn should not be called")
- assert.Equal(t, 1, sessionCloseCount, "session onSessCloseFn should only be called once")
- }
- func TestSession_AwayMessage(t *testing.T) {
- tests := []struct {
- name string
- setupSession func() *Session
- expectedResult string
- }{
- {
- name: "no instances - should return empty string",
- setupSession: func() *Session {
- return NewSession()
- },
- expectedResult: "",
- },
- {
- name: "one instance not away - should return empty string",
- setupSession: func() *Session {
- sg := NewSession()
- _ = sg.AddInstance()
- // instance has no away message and is not set as away
- return sg
- },
- expectedResult: "",
- },
- {
- name: "one instance away via SetUserInfoFlag - should return away message",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance.SetAwayMessage("I'm away")
- return sg
- },
- expectedResult: "I'm away",
- },
- {
- name: "one instance away via SetUserStatusBitmask - should return away message",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.SetUserStatusBitmask(wire.OServiceUserStatusAway)
- instance.SetAwayMessage("I'm away")
- return sg
- },
- expectedResult: "I'm away",
- },
- {
- name: "multiple instances - not all away - should return away message from away instance",
- setupSession: func() *Session {
- sg := NewSession()
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("I'm away")
- _ = sg.AddInstance()
- // instance2 has no away message and is not set as away
- return sg
- },
- expectedResult: "I'm away",
- },
- {
- name: "multiple instances - all away - should return latest away message",
- setupSession: func() *Session {
- sg := NewSession()
- baseTime := time.Now()
- callCount := 0
- sg.nowFn = func() time.Time {
- callCount++
- return baseTime.Add(time.Duration(callCount) * time.Second)
- }
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("First away message")
- instance2 := sg.AddInstance()
- instance2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance2.SetAwayMessage("Second away message")
- return sg
- },
- expectedResult: "Second away message",
- },
- {
- name: "multiple instances - all away after multiple updates - should return latest away message",
- setupSession: func() *Session {
- sg := NewSession()
- baseTime := time.Now()
- callCount := 0
- sg.nowFn = func() time.Time {
- callCount++
- return baseTime.Add(time.Duration(callCount) * time.Second)
- }
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("First away message")
- instance2 := sg.AddInstance()
- instance2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance2.SetAwayMessage("Second away message")
- // Update instance1's away status again (this will update awayTime)
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("Third away message")
- return sg
- },
- expectedResult: "Third away message",
- },
- {
- name: "multiple instances - different away methods - should return latest away message",
- setupSession: func() *Session {
- sg := NewSession()
- baseTime := time.Now()
- callCount := 0
- sg.nowFn = func() time.Time {
- callCount++
- return baseTime.Add(time.Duration(callCount) * time.Second)
- }
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("First away message")
- instance2 := sg.AddInstance()
- instance2.SetUserStatusBitmask(wire.OServiceUserStatusAway)
- instance2.SetAwayMessage("Second away message")
- return sg
- },
- expectedResult: "Second away message",
- },
- {
- name: "instance sets away message then clears message - should return empty string",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance.SetAwayMessage("I'm away")
- instance.SetAwayMessage("") // clear away message (but still away)
- return sg
- },
- expectedResult: "",
- },
- {
- name: "instance sets away message then clears away status - should return empty string",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance.SetAwayMessage("I'm away")
- instance.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable) // clear away status
- return sg
- },
- expectedResult: "",
- },
- {
- name: "multiple instances - one away with message, one away without message - should return message from most recent",
- setupSession: func() *Session {
- sg := NewSession()
- baseTime := time.Now()
- callCount := 0
- sg.nowFn = func() time.Time {
- callCount++
- return baseTime.Add(time.Duration(callCount) * time.Second)
- }
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("I'm away")
- instance2 := sg.AddInstance()
- instance2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // instance2 is away but has no message, and was set away after instance1
- return sg
- },
- expectedResult: "", // instance2 has more recent awayTime but no message
- },
- {
- name: "multiple instances - one away with message set later - should return that message",
- setupSession: func() *Session {
- sg := NewSession()
- baseTime := time.Now()
- callCount := 0
- sg.nowFn = func() time.Time {
- callCount++
- return baseTime.Add(time.Duration(callCount) * time.Second)
- }
- instance1 := sg.AddInstance()
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- instance1.SetAwayMessage("I'm away")
- instance2 := sg.AddInstance()
- instance2.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- // instance2 is away but has no message
- // Now update instance1's away status to make it more recent
- instance1.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
- return sg
- },
- expectedResult: "I'm away", // instance1 has more recent awayTime and has a message
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sg := tt.setupSession()
- result := sg.AwayMessage()
- assert.Equal(t, tt.expectedResult, result)
- })
- }
- }
- func TestSession_Caps(t *testing.T) {
- // Helper function to compare capability slices (order-independent)
- capsEqual := func(a, b [][16]byte) bool {
- if len(a) != len(b) {
- return false
- }
- capMap := make(map[[16]byte]bool)
- for _, cap := range a {
- capMap[cap] = true
- }
- for _, cap := range b {
- if !capMap[cap] {
- return false
- }
- }
- return true
- }
- tests := []struct {
- name string
- setupSession func() *Session
- expectedCaps [][16]byte
- expectedCount int
- }{
- {
- name: "empty session with no instances - should return empty slice",
- setupSession: func() *Session {
- return NewSession()
- },
- expectedCaps: [][16]byte{},
- expectedCount: 0,
- },
- {
- name: "single instance with no capabilities - should return empty slice",
- setupSession: func() *Session {
- sg := NewSession()
- _ = sg.AddInstance()
- return sg
- },
- expectedCaps: [][16]byte{},
- expectedCount: 0,
- },
- {
- name: "single instance with one cap - should return that cap",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- instance.SetCaps([][16]byte{cap1})
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- },
- expectedCount: 1,
- },
- {
- name: "single instance with multiple capabilities - should return all capabilities",
- setupSession: func() *Session {
- sg := NewSession()
- instance := sg.AddInstance()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- cap2 := [16]byte{0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01}
- cap3 := [16]byte{0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02}
- instance.SetCaps([][16]byte{cap1, cap2, cap3})
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- {0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01},
- {0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02},
- },
- expectedCount: 3,
- },
- {
- name: "multiple instances with no overlapping capabilities - should return union of all capabilities",
- setupSession: func() *Session {
- sg := NewSession()
- instance1 := sg.AddInstance()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- instance1.SetCaps([][16]byte{cap1})
- instance2 := sg.AddInstance()
- cap2 := [16]byte{0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01}
- instance2.SetCaps([][16]byte{cap2})
- instance3 := sg.AddInstance()
- cap3 := [16]byte{0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02}
- instance3.SetCaps([][16]byte{cap3})
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- {0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01},
- {0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02},
- },
- expectedCount: 3,
- },
- {
- name: "multiple instances with overlapping capabilities - should deduplicate",
- setupSession: func() *Session {
- sg := NewSession()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- cap2 := [16]byte{0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01}
- cap3 := [16]byte{0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02}
- instance1 := sg.AddInstance()
- instance1.SetCaps([][16]byte{cap1, cap2})
- instance2 := sg.AddInstance()
- instance2.SetCaps([][16]byte{cap2, cap3}) // cap2 overlaps
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- {0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01},
- {0x76, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x02},
- },
- expectedCount: 3,
- },
- {
- name: "multiple instances with all same capabilities - should return unique capabilities",
- setupSession: func() *Session {
- sg := NewSession()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- cap2 := [16]byte{0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01}
- instance1 := sg.AddInstance()
- instance1.SetCaps([][16]byte{cap1, cap2})
- instance2 := sg.AddInstance()
- instance2.SetCaps([][16]byte{cap1, cap2}) // same caps
- instance3 := sg.AddInstance()
- instance3.SetCaps([][16]byte{cap1, cap2}) // same caps
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- {0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01},
- },
- expectedCount: 2,
- },
- {
- name: "multiple instances with some having capabilities and some not - should return union",
- setupSession: func() *Session {
- sg := NewSession()
- _ = sg.AddInstance()
- // instance1 has no caps
- instance2 := sg.AddInstance()
- cap1 := [16]byte{0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00}
- instance2.SetCaps([][16]byte{cap1})
- _ = sg.AddInstance()
- // instance3 has no caps
- instance4 := sg.AddInstance()
- cap2 := [16]byte{0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01}
- instance4.SetCaps([][16]byte{cap2})
- return sg
- },
- expectedCaps: [][16]byte{
- {0x74, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00},
- {0x75, 0x8f, 0x24, 0x20, 0x62, 0x87, 0x11, 0xd1, 0x82, 0x22, 0x44, 0x45, 0x53, 0x54, 0x00, 0x01},
- },
- expectedCount: 2,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- sg := tt.setupSession()
- result := sg.Caps()
- assert.Equal(t, tt.expectedCount, len(result), "cap count should match")
- assert.True(t, capsEqual(tt.expectedCaps, result), "capabilities should match (order-independent)")
- })
- }
- }
- func TestSession_InstanceNumberAssignment(t *testing.T) {
- t.Run("first instance gets number 1", func(t *testing.T) {
- s := NewSession()
- instance := s.AddInstance()
- assert.Equal(t, uint8(1), instance.Num())
- })
- t.Run("multiple instances get sequential numbers", func(t *testing.T) {
- s := NewSession()
- instance1 := s.AddInstance()
- instance2 := s.AddInstance()
- instance3 := s.AddInstance()
- assert.Equal(t, uint8(1), instance1.Num())
- assert.Equal(t, uint8(2), instance2.Num())
- assert.Equal(t, uint8(3), instance3.Num())
- })
- t.Run("removed instance numbers are reused", func(t *testing.T) {
- s := NewSession()
- instance1 := s.AddInstance()
- instance2 := s.AddInstance()
- instance3 := s.AddInstance()
- assert.Equal(t, uint8(1), instance1.Num())
- assert.Equal(t, uint8(2), instance2.Num())
- assert.Equal(t, uint8(3), instance3.Num())
- // Remove instance 2
- s.RemoveInstance(instance2)
- // New instance should reuse number 2
- instance4 := s.AddInstance()
- assert.Equal(t, uint8(2), instance4.Num())
- // Verify all instance numbers are unique
- instances := s.Instances()
- instanceNums := make(map[uint8]bool)
- for _, inst := range instances {
- assert.False(t, instanceNums[inst.Num()], "instance number %d should be unique", inst.Num())
- instanceNums[inst.Num()] = true
- }
- })
- t.Run("finds lowest available number", func(t *testing.T) {
- s := NewSession()
- // Create instances 1, 2, 3
- instance1 := s.AddInstance()
- instance2 := s.AddInstance()
- instance3 := s.AddInstance()
- assert.Equal(t, uint8(1), instance1.Num())
- assert.Equal(t, uint8(2), instance2.Num())
- assert.Equal(t, uint8(3), instance3.Num())
- // Remove instance 1
- s.RemoveInstance(instance1)
- // New instance should get number 1 (lowest available)
- instance4 := s.AddInstance()
- assert.Equal(t, uint8(1), instance4.Num())
- // Remove instance 2
- s.RemoveInstance(instance2)
- // New instance should get number 2 (lowest available)
- instance5 := s.AddInstance()
- assert.Equal(t, uint8(2), instance5.Num())
- // Verify instance 3 still has its number
- assert.Equal(t, uint8(3), instance3.Num())
- })
- t.Run("panics when all instance numbers are taken", func(t *testing.T) {
- s := NewSession()
- // Fill up all 255 instance numbers
- instances := make([]*SessionInstance, 255)
- for i := 0; i < 255; i++ {
- instances[i] = s.AddInstance()
- }
- // Verify we have 255 instances
- assert.Equal(t, 255, s.InstanceCount())
- // Try to create one more - should panic
- assert.PanicsWithValue(t, "all instance numbers are taken (max 255 instances per session)", func() {
- s.AddInstance()
- })
- })
- }
|