ScuttleBot

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

Keyboard Shortcuts

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