| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521 |
- package state
- import (
- "context"
- "fmt"
- "math"
- "net/netip"
- "sync"
- "testing"
- "testing/synctest"
- "time"
- "github.com/mk6i/open-oscar-server/wire"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func TestSession_UsesFeedbag(t *testing.T) {
- s := NewSession()
- if s.UsesFeedbag() {
- t.Fatalf("UsesFeedbag() = true; want false")
- }
- s.SetUsesFeedbag()
- if !s.UsesFeedbag() {
- t.Fatalf("UsesFeedbag() = false; want true")
- }
- // idempotent
- s.SetUsesFeedbag()
- if !s.UsesFeedbag() {
- t.Fatalf("UsesFeedbag() = false after second SetUsesFeedbag; want true")
- }
- }
- func TestSessionInstance_NotifyTxn(t *testing.T) {
- alice := NewIdentScreenName("Alice")
- bob := NewIdentScreenName("Bob")
- t.Run("lifecycle", func(t *testing.T) {
- inst := NewSession().AddInstance()
- inst.BeginNotifyTxn()
- assert.True(t, inst.InNotifyTxn())
- require.NoError(t, inst.NotifyTxn(alice, bob))
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.True(t, shouldNotify)
- assert.ElementsMatch(t, []IdentScreenName{alice, bob}, screenNames)
- assert.False(t, inst.InNotifyTxn())
- shouldNotify, screenNames = inst.EndNotifyTxn()
- assert.False(t, shouldNotify)
- assert.Nil(t, screenNames)
- })
- t.Run("clear on begin", func(t *testing.T) {
- inst := NewSession().AddInstance()
- inst.BeginNotifyTxn()
- require.NoError(t, inst.NotifyTxn(alice))
- inst.BeginNotifyTxn()
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.False(t, shouldNotify)
- assert.Empty(t, screenNames)
- })
- t.Run("notify without names", func(t *testing.T) {
- inst := NewSession().AddInstance()
- inst.BeginNotifyTxn()
- require.NoError(t, inst.NotifyTxn())
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.True(t, shouldNotify)
- assert.Empty(t, screenNames)
- })
- t.Run("inactive notify", func(t *testing.T) {
- inst := NewSession().AddInstance()
- assert.ErrorIs(t, inst.NotifyTxn(alice), errNotifyTxnNotActive)
- inst.BeginNotifyTxn()
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.False(t, shouldNotify)
- assert.Empty(t, screenNames)
- })
- t.Run("dedup", func(t *testing.T) {
- inst := NewSession().AddInstance()
- inst.BeginNotifyTxn()
- require.NoError(t, inst.NotifyTxn(alice, alice))
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.True(t, shouldNotify)
- assert.Equal(t, []IdentScreenName{alice}, screenNames)
- })
- t.Run("exceeds max names", func(t *testing.T) {
- inst := NewSession().AddInstance()
- inst.BeginNotifyTxn()
- names := make([]IdentScreenName, maxNotifyTxnNames)
- for i := range names {
- names[i] = NewIdentScreenName(fmt.Sprintf("user%d", i))
- }
- require.NoError(t, inst.NotifyTxn(names...))
- shouldNotify, screenNames := inst.EndNotifyTxn()
- assert.True(t, shouldNotify)
- assert.Len(t, screenNames, maxNotifyTxnNames)
- inst.BeginNotifyTxn()
- require.NoError(t, inst.NotifyTxn(names...))
- assert.ErrorIs(t, inst.NotifyTxn(NewIdentScreenName("overflow")), errNotifyTxnTooManyNames)
- })
- }
- 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 is on ICQ with direct connect info from client",
- 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)
- s.SetICQDCInfo(wire.ICQDCInfo{
- DCType: 4,
- ProtoVersion: 10,
- })
- 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{
- DCType: 4,
- ProtoVersion: 10,
- }),
- 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 TestSessionInstance_ContactsInit(t *testing.T) {
- instance := NewSession().AddInstance()
- assert.False(t, instance.ContactsInit())
- instance.SetContactsInit()
- assert.True(t, instance.ContactsInit())
- instance.SetContactsInit()
- assert.True(t, instance.ContactsInit())
- }
- 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)
- })
- // Init is one-shot, so a session that failed to initialize must not linger for
- // later instances to attach to.
- t.Run("closes the session when the function fails", func(t *testing.T) {
- s := NewSession()
- instance := s.AddInstance()
- err := s.RunOnce(func() error {
- return assert.AnError
- })
- assert.Error(t, err)
- assert.True(t, s.IsClosed(), "session should be closed after a failed init")
- assert.True(t, instance.IsClosed(), "instances should be torn down with the session")
- })
- t.Run("leaves the session open when the function succeeds", func(t *testing.T) {
- s := NewSession()
- s.AddInstance()
- assert.NoError(t, s.RunOnce(func() error { return nil }))
- assert.False(t, s.IsClosed())
- })
- // A failed init tears the session down, so a caller that did not run fn must
- // still learn it failed rather than sign a client on to a session whose
- // instances have just been closed underneath it.
- t.Run("reports the init error to every caller", func(t *testing.T) {
- s := NewSession()
- s.AddInstance()
- first := s.RunOnce(func() error { return assert.AnError })
- second := s.RunOnce(func() error { return nil })
- assert.ErrorIs(t, first, assert.AnError)
- assert.ErrorIs(t, second, assert.AnError, "the caller that did not run fn must not read failure as success")
- })
- // Two instances can be added to a fresh session and reach RunOnce
- // concurrently. Whichever one loses the race still has a client to sign on,
- // and must be told the session is not usable.
- t.Run("reports the init error to a concurrent caller", func(t *testing.T) {
- synctest.Test(t, func(t *testing.T) {
- s := NewSession()
- s.AddInstance()
- s.AddInstance()
- var wg sync.WaitGroup
- errs := make([]error, 2)
- for i := range errs {
- wg.Add(1)
- go func() {
- defer wg.Done()
- errs[i] = s.RunOnce(func() error { return assert.AnError })
- }()
- }
- wg.Wait()
- for i, err := range errs {
- assert.ErrorIs(t, err, assert.AnError, "caller %d", i)
- }
- })
- })
- }
- // An instance added to a closed session is live but orphaned: Closed() has
- // already fired, so the per-account goroutines that select on it — the rate limit
- // monitor above all — have exited, and RunOnce is spent so nothing restarts them.
- // AddInstance must refuse, so AddSession can fall through to the tombstone wait it
- // already implements.
- func TestSession_AddInstance_RefusesClosedSession(t *testing.T) {
- s := NewSession()
- first := s.AddInstance()
- // Stand in for OServiceService.MonitorRateLimits: a per-account goroutine
- // started from RunOnce that runs until the session closes.
- require.NoError(t, s.RunOnce(func() error { return nil }))
- monitorDone := make(chan struct{})
- go func() {
- defer close(monitorDone)
- <-s.Closed()
- }()
- // The account's last instance departs, closing the session and stopping the
- // monitor. This is the moment AddSession's IsClosed check races.
- first.CloseInstance()
- select {
- case <-monitorDone:
- case <-time.After(2 * time.Second):
- t.Fatal("monitor did not exit when the session closed")
- }
- assert.Nil(t, s.AddInstance(), "AddInstance must refuse a closed session")
- // RunOnce is spent, so an instance that did attach could never start a
- // replacement monitor.
- ran := false
- assert.NoError(t, s.RunOnce(func() error { ran = true; return nil }))
- assert.False(t, ran)
- }
- 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()
- })
- })
- }
|