ScuttleBot

scuttlebot / internal / bots / snitch / nickwindow_test.go
Blame History Raw 190 lines
1
// Internal tests for the nickWindow sliding-window logic.
2
// In package snitch (not snitch_test) to access unexported types.
3
package snitch
4
5
import (
6
"testing"
7
"time"
8
)
9
10
func TestNickWindowTrimRemovesOldMsgs(t *testing.T) {
11
now := time.Now()
12
nw := &nickWindow{
13
msgs: []time.Time{
14
now.Add(-10 * time.Second), // old — should be trimmed
15
now.Add(-1 * time.Second), // recent — should stay
16
},
17
}
18
nw.trim(now, 5*time.Second, 30*time.Second)
19
if len(nw.msgs) != 1 {
20
t.Errorf("expected 1 msg after trim, got %d", len(nw.msgs))
21
}
22
}
23
24
func TestNickWindowTrimKeepsAllRecent(t *testing.T) {
25
now := time.Now()
26
nw := &nickWindow{
27
msgs: []time.Time{
28
now.Add(-1 * time.Second),
29
now.Add(-2 * time.Second),
30
now.Add(-3 * time.Second),
31
},
32
}
33
nw.trim(now, 10*time.Second, 30*time.Second)
34
if len(nw.msgs) != 3 {
35
t.Errorf("expected 3 msgs after trim, got %d", len(nw.msgs))
36
}
37
}
38
39
func TestNickWindowTrimRemovesOldJoinParts(t *testing.T) {
40
now := time.Now()
41
nw := &nickWindow{
42
joinPart: []time.Time{
43
now.Add(-60 * time.Second), // too old
44
now.Add(-5 * time.Second), // recent
45
},
46
}
47
nw.trim(now, 5*time.Second, 30*time.Second)
48
if len(nw.joinPart) != 1 {
49
t.Errorf("expected 1 join/part after trim, got %d", len(nw.joinPart))
50
}
51
}
52
53
func TestNickWindowTrimEmptyNoop(t *testing.T) {
54
nw := &nickWindow{}
55
// Should not panic on empty slices.
56
nw.trim(time.Now(), 5*time.Second, 30*time.Second)
57
if len(nw.msgs) != 0 || len(nw.joinPart) != 0 {
58
t.Error("expected empty after trimming empty window")
59
}
60
}
61
62
func TestNickWindowTrimAllOld(t *testing.T) {
63
now := time.Now()
64
nw := &nickWindow{
65
msgs: []time.Time{
66
now.Add(-100 * time.Second),
67
now.Add(-200 * time.Second),
68
},
69
joinPart: []time.Time{
70
now.Add(-90 * time.Second),
71
},
72
}
73
nw.trim(now, 5*time.Second, 30*time.Second)
74
if len(nw.msgs) != 0 {
75
t.Errorf("expected 0 msgs after trimming all-old, got %d", len(nw.msgs))
76
}
77
if len(nw.joinPart) != 0 {
78
t.Errorf("expected 0 join/parts after trimming all-old, got %d", len(nw.joinPart))
79
}
80
}
81
82
// Test the flood detection path at the Bot level. We reach into the Bot's
83
// internal window map by calling recordMsg directly, which is the same path
84
// a real PRIVMSG would trigger. This validates the counting logic without
85
// requiring an IRC connection.
86
87
func TestFloodDetectionCounting(t *testing.T) {
88
cfg := Config{
89
IRCAddr: "127.0.0.1:6667",
90
Nick: "snitch",
91
FloodMessages: 3,
92
FloodWindow: 10 * time.Second,
93
}
94
cfg.setDefaults()
95
96
b := &Bot{
97
cfg: cfg,
98
windows: make(map[string]map[string]*nickWindow),
99
alerted: make(map[string]time.Time),
100
}
101
102
// Record 2 messages — below threshold.
103
b.recordMsg("#fleet", "spammer")
104
b.recordMsg("#fleet", "spammer")
105
w := b.window("#fleet", "spammer")
106
if len(w.msgs) != 2 {
107
t.Errorf("expected 2 msgs in window, got %d", len(w.msgs))
108
}
109
110
// Record a third — at threshold.
111
b.recordMsg("#fleet", "spammer")
112
w = b.window("#fleet", "spammer")
113
if len(w.msgs) != 3 {
114
t.Errorf("expected 3 msgs in window, got %d", len(w.msgs))
115
}
116
}
117
118
func TestJoinPartCounting(t *testing.T) {
119
cfg := Config{
120
IRCAddr: "127.0.0.1:6667",
121
Nick: "snitch",
122
JoinPartThreshold: 3,
123
JoinPartWindow: 30 * time.Second,
124
}
125
cfg.setDefaults()
126
127
b := &Bot{
128
cfg: cfg,
129
windows: make(map[string]map[string]*nickWindow),
130
alerted: make(map[string]time.Time),
131
}
132
133
// 2 join/part events — below threshold.
134
b.recordJoinPart("#fleet", "cycler")
135
b.recordJoinPart("#fleet", "cycler")
136
w := b.window("#fleet", "cycler")
137
if len(w.joinPart) != 2 {
138
t.Errorf("expected 2 join/parts before threshold, got %d", len(w.joinPart))
139
}
140
141
// 3rd event hits threshold — window is reset to nil after alert fires.
142
b.recordJoinPart("#fleet", "cycler")
143
w = b.window("#fleet", "cycler")
144
if len(w.joinPart) != 0 {
145
t.Errorf("expected joinPart reset to 0 after threshold hit, got %d", len(w.joinPart))
146
}
147
}
148
149
func TestWindowIsolatedPerNick(t *testing.T) {
150
cfg := Config{IRCAddr: "127.0.0.1:6667", FloodMessages: 5, FloodWindow: 10 * time.Second}
151
cfg.setDefaults()
152
b := &Bot{
153
cfg: cfg,
154
windows: make(map[string]map[string]*nickWindow),
155
alerted: make(map[string]time.Time),
156
}
157
158
b.recordMsg("#fleet", "alice")
159
b.recordMsg("#fleet", "alice")
160
b.recordMsg("#fleet", "bob")
161
162
wa := b.window("#fleet", "alice")
163
wb := b.window("#fleet", "bob")
164
if len(wa.msgs) != 2 {
165
t.Errorf("alice: expected 2, got %d", len(wa.msgs))
166
}
167
if len(wb.msgs) != 1 {
168
t.Errorf("bob: expected 1, got %d", len(wb.msgs))
169
}
170
}
171
172
func TestWindowIsolatedPerChannel(t *testing.T) {
173
cfg := Config{IRCAddr: "127.0.0.1:6667"}
174
cfg.setDefaults()
175
b := &Bot{
176
cfg: cfg,
177
windows: make(map[string]map[string]*nickWindow),
178
alerted: make(map[string]time.Time),
179
}
180
181
b.recordMsg("#fleet", "alice")
182
b.recordMsg("#ops", "alice")
183
184
wf := b.window("#fleet", "alice")
185
wo := b.window("#ops", "alice")
186
if len(wf.msgs) != 1 || len(wo.msgs) != 1 {
187
t.Errorf("expected 1 msg per channel, fleet=%d ops=%d", len(wf.msgs), len(wo.msgs))
188
}
189
}
190

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button