| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- package wire
- import (
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func TestNewRateLimitClasses(t *testing.T) {
- input := [5]RateClass{
- {ID: 1, WindowSize: 10},
- {ID: 2, WindowSize: 20},
- {ID: 3, WindowSize: 30},
- {ID: 4, WindowSize: 40},
- {ID: 5, WindowSize: 50},
- }
- classes := NewRateLimitClasses(input)
- assert.Equal(t, input, classes.All())
- }
- func TestCheckRateLimit(t *testing.T) {
- baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
- testCases := []struct {
- name string
- lastTime time.Time
- currentTime time.Time
- rateClass RateClass
- currentAvg int32
- limitedNow bool
- wantStatus RateLimitStatus
- wantNewAvg int32
- }{
- {
- name: "Already limited, but newAvg >= ClearLevel => Clear",
- lastTime: baseTime,
- currentTime: baseTime.Add(50 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 10,
- DisconnectLevel: 2,
- LimitLevel: 5,
- AlertLevel: 8,
- },
- currentAvg: 9, // currentAvg close to ClearLevel
- limitedNow: true, // already limited
- // newAvg = (9*(4-1) + 50) / 4 = (27 + 50) / 4 = 77 / 4 = 19
- // 19 >= ClearLevel(10) => RateLimitStatusClear
- wantStatus: RateLimitStatusClear,
- wantNewAvg: 19,
- },
- {
- name: "Already limited, but newAvg < ClearLevel => Remain Limited",
- lastTime: baseTime,
- currentTime: baseTime.Add(30 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 50,
- DisconnectLevel: 10,
- LimitLevel: 20,
- AlertLevel: 30,
- },
- currentAvg: 10,
- limitedNow: true,
- // newAvg = (10*(4-1) + 30) / 4 = (30 + 30) / 4 = 60 / 4 = 15
- // 15 < ClearLevel(50) => remain limited
- wantStatus: RateLimitStatusLimited,
- wantNewAvg: 15,
- },
- {
- name: "Not Limited Now, New average < DisconnectLevel => Disconnect",
- lastTime: baseTime,
- currentTime: baseTime.Add(10 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 100,
- DisconnectLevel: 5,
- LimitLevel: 20,
- AlertLevel: 40,
- },
- currentAvg: 1,
- limitedNow: false,
- // newAvg = (1*(4-1) + 10) / 4 = (3 + 10) / 4 = 13 / 4 = 3
- // 3 < DisconnectLevel(5) => RateLimitStatusDisconnect
- wantStatus: RateLimitStatusDisconnect,
- wantNewAvg: 3,
- },
- {
- name: "Limited Now, New average < DisconnectLevel => Disconnect",
- lastTime: baseTime,
- currentTime: baseTime.Add(10 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 100,
- DisconnectLevel: 5,
- LimitLevel: 20,
- AlertLevel: 40,
- },
- currentAvg: 1,
- limitedNow: true,
- // newAvg = (1*(4-1) + 10) / 4 = (3 + 10) / 4 = 13 / 4 = 3
- // 3 < DisconnectLevel(5) => RateLimitStatusDisconnect
- wantStatus: RateLimitStatusDisconnect,
- wantNewAvg: 3,
- },
- {
- name: "New average < LimitLevel => Limited",
- lastTime: baseTime,
- currentTime: baseTime.Add(20 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 100,
- DisconnectLevel: 5,
- LimitLevel: 40,
- AlertLevel: 60,
- },
- currentAvg: 10,
- limitedNow: false,
- // newAvg = (10*(4-1) + 20) / 4 = (30 + 20) / 4 = 50 / 4 = 12
- // 12 < LimitLevel(40) => RateLimitStatusLimited
- wantStatus: RateLimitStatusLimited,
- wantNewAvg: 12,
- },
- {
- name: "New average < AlertLevel => Alert",
- lastTime: baseTime,
- currentTime: baseTime.Add(30 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 100,
- DisconnectLevel: 5,
- LimitLevel: 20,
- AlertLevel: 40,
- },
- currentAvg: 20,
- limitedNow: false,
- // newAvg = (20*(4-1) + 30) / 4 = (60 + 30) / 4 = 90 / 4 = 22
- // 22 >= 20 => not "Limited"; 22 < 40 => "Alert"
- wantStatus: RateLimitStatusAlert,
- wantNewAvg: 22,
- },
- {
- name: "New average >= AlertLevel => Clear",
- lastTime: baseTime,
- currentTime: baseTime.Add(50 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 1000,
- ClearLevel: 100,
- DisconnectLevel: 5,
- LimitLevel: 20,
- AlertLevel: 40,
- },
- // Choose 39 so the resulting newAvg is 41, which is >= AlertLevel.
- currentAvg: 39,
- limitedNow: false,
- // newAvg = (39*(4-1) + 50) / 4 = (117 + 50) / 4 = 167 / 4 = 41
- // 41 >= AlertLevel(40) => RateLimitStatusClear
- wantStatus: RateLimitStatusClear,
- wantNewAvg: 41,
- },
- {
- name: "Clamp newAvg to MaxLevel if exceeded",
- lastTime: baseTime,
- currentTime: baseTime.Add(9999 * time.Millisecond),
- rateClass: RateClass{
- WindowSize: 4,
- MaxLevel: 100,
- ClearLevel: 80,
- DisconnectLevel: 20,
- LimitLevel: 40,
- AlertLevel: 60,
- },
- currentAvg: 95,
- limitedNow: false,
- // Without clamping, newAvg would be huge:
- // newAvg = (95*(4-1) + 9999) / 4 = (285 + 9999)/4 = 10284/4 = 2571
- // Clamped to 100 => 100 >= AlertLevel(60) => RateLimitStatusClear
- wantStatus: RateLimitStatusClear,
- wantNewAvg: 100,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- gotStatus, gotNewAvg := CheckRateLimit(
- tc.lastTime,
- tc.currentTime,
- tc.rateClass,
- tc.currentAvg,
- tc.limitedNow,
- )
- assert.Equal(t, tc.wantStatus, gotStatus)
- assert.Equal(t, tc.wantNewAvg, gotNewAvg)
- })
- }
- }
- func TestRateLimitClasses_Get(t *testing.T) {
- classes := DefaultRateLimitClasses()
- // Test Get() returns correct class for each ID
- for i := 1; i <= 5; i++ {
- id := RateLimitClassID(i)
- class := classes.Get(id)
- assert.Equal(t, id, class.ID)
- assert.Equal(t, classes.All()[i-1], class)
- }
- }
- func TestRateLimitClasses_All(t *testing.T) {
- classes := DefaultRateLimitClasses()
- // Test All() returns exactly 5 classes with correct IDs
- all := classes.All()
- assert.Len(t, all, 5)
- for i, class := range all {
- expectedID := RateLimitClassID(i + 1)
- assert.Equal(t, expectedID, class.ID, "class ID mismatch at index %d", i)
- }
- }
- func TestSNACRateLimits_RateClassLookup(t *testing.T) {
- limits := DefaultSNACRateLimits()
- testCases := []struct {
- foodGroup uint16
- subGroup uint16
- expected RateLimitClassID
- found bool
- }{
- {Chat, ChatUsersJoined, 1, true},
- {Chat, ChatChannelMsgToHost, 2, true},
- {0xFFFF, 0x0001, 0, false},
- {Chat, 0xFFFF, 0, false},
- }
- for _, tc := range testCases {
- classID, ok := limits.RateClassLookup(tc.foodGroup, tc.subGroup)
- assert.Equal(t, tc.found, ok)
- assert.Equal(t, tc.expected, classID)
- }
- }
- func TestSNACRateLimits_All(t *testing.T) {
- limits := DefaultSNACRateLimits()
- seen := map[uint16]map[uint16]RateLimitClassID{}
- for entry := range limits.All() {
- if _, ok := seen[entry.FoodGroup]; !ok {
- seen[entry.FoodGroup] = map[uint16]RateLimitClassID{}
- }
- seen[entry.FoodGroup][entry.SubGroup] = entry.RateLimitClass
- }
- // Spot-check a few values
- require.Contains(t, seen, ICBM)
- assert.Equal(t, RateLimitClassID(3), seen[ICBM][ICBMChannelMsgToHost])
- assert.Equal(t, RateLimitClassID(1), seen[ICBM][ICBMChannelMsgToClient])
- require.Contains(t, seen, Locate)
- assert.Equal(t, RateLimitClassID(4), seen[Locate][LocateSetDirInfo])
- assert.Equal(t, RateLimitClassID(3), seen[Locate][LocateUserInfoQuery])
- }
- func TestSNACRateLimits_All_YieldStopsEarly(t *testing.T) {
- limits := DefaultSNACRateLimits()
- count := 0
- limits.All()(func(entry struct {
- FoodGroup uint16
- SubGroup uint16
- RateLimitClass RateLimitClassID
- }) bool {
- count++
- // stop iteration after first item to trigger `if !yield(...) { return }`
- return false
- })
- // Should only yield one entry
- assert.Equal(t, 1, count)
- }
|