| Server IP : 157.230.181.24 / Your IP : 216.73.216.56 Web Server : Apache/2.4.58 (Ubuntu) System : Linux conductive 6.8.0-117-generic #117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026 x86_64 User : ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/vhosts/ceagon/open-oscar-server/state/ |
Upload File : |
package state
import (
"context"
"fmt"
"math"
"net/netip"
"sync"
"testing"
"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)
})
}
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()
})
})
}