script/fmt now runs golines -m 77 after gofmt, repeating golines until it changes nothing, since one pass can leave a line the next splits. script/fmt-check fails when either would change a file. script/bootstrap installs golines v0.13.0 with go install, pinned to its commit, and checks the installed version with go version -m. The tree is reformatted. The longer lines this makes are fixed by hand: nolint directives moved onto their own line above the statement, one string split, and 20 functions over funlen's 80 lines shortened or split, RunWAF and limitPercentages among them. Model: opus-5-5
This commit is contained in:
1 parent
63e58e326b
commit
7f8a59de04
118 files changed
+9447
-2670
No files matched your search
+57
-18
@@ -291,7 +291,15 @@ func cooldownKeyOf(alert *Alert) cooldownKey {
|
||||
asn, _ := alert.Detail["asn"].(string)
|
||||
name, _ := alert.Detail["name"].(string)
|
||||
|
||||
return cooldownKey{alert.Event, alert.Netblock, file, source, scope, asn, name}
|
||||
return cooldownKey{
|
||||
alert.Event,
|
||||
alert.Netblock,
|
||||
file,
|
||||
source,
|
||||
scope,
|
||||
asn,
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a Queue with no alert yet.
|
||||
@@ -333,7 +341,8 @@ func New(params Params) *Queue {
|
||||
// queue of each destination, from which Run sends it, and with queueSize
|
||||
// alerts waiting for a destination, the oldest is dropped.
|
||||
func (q *Queue) Raise(alert Alert) {
|
||||
if len(q.destinations) == 0 || !slices.Contains(q.params.Events, alert.Event) {
|
||||
if len(q.destinations) == 0 ||
|
||||
!slices.Contains(q.params.Events, alert.Event) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -380,7 +389,8 @@ func (q *Queue) WouldSend(event string, netblock netip.Prefix) bool {
|
||||
now := q.params.Now()
|
||||
|
||||
last, found := q.cooldowns[cooldownKey{event: event, netblock: netblock}]
|
||||
if q.params.Cooldown > 0 && found && now.Sub(last.Sent) < q.params.Cooldown {
|
||||
if q.params.Cooldown > 0 && found &&
|
||||
now.Sub(last.Sent) < q.params.Cooldown {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -474,10 +484,15 @@ func (q *Queue) Snapshot() State {
|
||||
}
|
||||
|
||||
slices.SortFunc(state.Cooldowns, func(a, b Cooldown) int {
|
||||
return cmp.Or(a.Netblock.Compare(b.Netblock), cmp.Compare(a.Event, b.Event),
|
||||
cmp.Compare(a.File, b.File), cmp.Compare(a.Source, b.Source),
|
||||
cmp.Compare(a.Scope, b.Scope), cmp.Compare(a.ASN, b.ASN),
|
||||
cmp.Compare(a.Name, b.Name))
|
||||
return cmp.Or(
|
||||
a.Netblock.Compare(b.Netblock),
|
||||
cmp.Compare(a.Event, b.Event),
|
||||
cmp.Compare(a.File, b.File),
|
||||
cmp.Compare(a.Source, b.Source),
|
||||
cmp.Compare(a.Scope, b.Scope),
|
||||
cmp.Compare(a.ASN, b.ASN),
|
||||
cmp.Compare(a.Name, b.Name),
|
||||
)
|
||||
})
|
||||
|
||||
for _, d := range q.destinations {
|
||||
@@ -609,9 +624,12 @@ func (q *Queue) endHour(now time.Time) {
|
||||
var reasons []string
|
||||
|
||||
if heldBack > 0 {
|
||||
reasons = append(reasons, fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
||||
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour))
|
||||
reasons = append(
|
||||
reasons,
|
||||
fmt.Sprintf("%d alerts held back in the hour from %s, "+
|
||||
"past the %d an hour SWW_ALERT_MAX_PER_HOUR allows", heldBack,
|
||||
q.hour.Start.Format(time.RFC3339), q.params.MaxPerHour),
|
||||
)
|
||||
}
|
||||
|
||||
if repeats > 0 {
|
||||
@@ -690,12 +708,19 @@ func (d *destination) run(ctx context.Context) {
|
||||
case ctx.Err() == nil: // not cut off as sww stops
|
||||
d.sendFailures.Add(1)
|
||||
|
||||
retryDelay = min(max(retryDelayFactor*retryDelay, firstRetryDelay),
|
||||
maxRetryDelay)
|
||||
retryDelay = min(
|
||||
max(retryDelayFactor*retryDelay, firstRetryDelay),
|
||||
maxRetryDelay,
|
||||
)
|
||||
retryAt = time.Now().Add(retryDelay)
|
||||
|
||||
d.processLog.Warn("sending an alert to "+d.setting+" failed",
|
||||
"error", err.Error(), "sending_again_in", retryDelay.String())
|
||||
d.processLog.Warn(
|
||||
"sending an alert to "+d.setting+" failed",
|
||||
"error",
|
||||
err.Error(),
|
||||
"sending_again_in",
|
||||
retryDelay.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -783,8 +808,12 @@ func (d *destination) send(ctx context.Context, alert *Alert) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, sendTimeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, d.url.String(),
|
||||
bytes.NewReader(body))
|
||||
req, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
d.url.String(),
|
||||
bytes.NewReader(body),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("make the request: %w", err)
|
||||
}
|
||||
@@ -838,7 +867,14 @@ func (q *Queue) webhookMessage(alert *Alert) ([]byte, http.Header, error) {
|
||||
// in JSON: its title in bold, then its text, with &, < and > escaped, as
|
||||
// Slack asks, so that nothing in them is read as a link or a mention.
|
||||
func slackMessage(alert *Alert) ([]byte, http.Header, error) {
|
||||
escape := strings.NewReplacer("&", "&", "<", "<", ">", ">").Replace
|
||||
escape := strings.NewReplacer(
|
||||
"&",
|
||||
"&",
|
||||
"<",
|
||||
"<",
|
||||
">",
|
||||
">",
|
||||
).Replace
|
||||
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"text": "*" + escape(title(alert)) + "*\n" + escape(text(alert)),
|
||||
@@ -936,7 +972,10 @@ func text(alert *Alert) string {
|
||||
}
|
||||
|
||||
if alert.SuppressedRepeats > 0 {
|
||||
lines = append(lines, fmt.Sprintf("suppressed repeats: %d", alert.SuppressedRepeats))
|
||||
lines = append(
|
||||
lines,
|
||||
fmt.Sprintf("suppressed repeats: %d", alert.SuppressedRepeats),
|
||||
)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
+314
-90
@@ -65,7 +65,10 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
||||
Netblock: netip.MustParsePrefix("203.0.113.0/24"),
|
||||
Country: "DE",
|
||||
Reason: "requests per minute over the limit of 1000",
|
||||
Detail: map[string]any{"cause": "limit", "ban_expires": anHourOn},
|
||||
Detail: map[string]any{
|
||||
"cause": "limit",
|
||||
"ban_expires": anHourOn,
|
||||
},
|
||||
})
|
||||
synctest.Wait()
|
||||
|
||||
@@ -75,7 +78,12 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
if got[0].method != http.MethodPost || got[0].url != webhookURL {
|
||||
t.Errorf("request %s %s, want POST %s", got[0].method, got[0].url, webhookURL)
|
||||
t.Errorf(
|
||||
"request %s %s, want POST %s",
|
||||
got[0].method,
|
||||
got[0].url,
|
||||
webhookURL,
|
||||
)
|
||||
}
|
||||
|
||||
for name, want := range map[string]string{
|
||||
@@ -84,21 +92,29 @@ func TestAlertIsPostedAsJSONWithItsFieldsAndTheHeaders(t *testing.T) {
|
||||
"X-Team": "ops",
|
||||
} {
|
||||
if got[0].header.Get(name) != want {
|
||||
t.Errorf("header %s is %q, want %q", name, got[0].header.Get(name), want)
|
||||
t.Errorf(
|
||||
"header %s is %q, want %q",
|
||||
name,
|
||||
got[0].header.Get(name),
|
||||
want,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
wantAlert(t, got[0].alert, map[string]any{
|
||||
"instance": instance,
|
||||
"time": started,
|
||||
"event": "ban",
|
||||
"client": "203.0.113.9",
|
||||
"netblock": "203.0.113.0/24",
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"country": "DE",
|
||||
"reason": "requests per minute over the limit of 1000",
|
||||
"detail": map[string]any{"cause": "limit", "ban_expires": anHourOn},
|
||||
"instance": instance,
|
||||
"time": started,
|
||||
"event": "ban",
|
||||
"client": "203.0.113.9",
|
||||
"netblock": "203.0.113.0/24",
|
||||
"asn": "",
|
||||
"as_name": "",
|
||||
"country": "DE",
|
||||
"reason": "requests per minute over the limit of 1000",
|
||||
"detail": map[string]any{
|
||||
"cause": "limit",
|
||||
"ban_expires": anHourOn,
|
||||
},
|
||||
"suppressed_repeats": float64(0),
|
||||
})
|
||||
wantCounts(t, q, 1, 0, 0, 0)
|
||||
@@ -110,12 +126,25 @@ func TestOnlyTheChosenEventsAreSent(t *testing.T) {
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
params := newParams()
|
||||
params.Events = []string{alerts.EventSourceFailure, alerts.EventFileError}
|
||||
params.Events = []string{
|
||||
alerts.EventSourceFailure,
|
||||
alerts.EventFileError,
|
||||
}
|
||||
webhook, q := start(t, params)
|
||||
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
q.Raise(alerts.Alert{Event: alerts.EventFileError, Reason: "a file error"})
|
||||
q.Raise(alerts.Alert{Event: alerts.EventPermanentBan, Netblock: netblock(1)})
|
||||
q.Raise(
|
||||
alerts.Alert{
|
||||
Event: alerts.EventFileError,
|
||||
Reason: "a file error",
|
||||
},
|
||||
)
|
||||
q.Raise(
|
||||
alerts.Alert{
|
||||
Event: alerts.EventPermanentBan,
|
||||
Netblock: netblock(1),
|
||||
},
|
||||
)
|
||||
synctest.Wait()
|
||||
|
||||
wantEvents(t, webhook, alerts.EventFileError)
|
||||
@@ -127,12 +156,17 @@ func TestWouldSendOnlyForTheChosenEvents(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params := newParams()
|
||||
params.Events = []string{alerts.EventSourceFailure, alerts.EventFileError}
|
||||
params.Events = []string{
|
||||
alerts.EventSourceFailure,
|
||||
alerts.EventFileError,
|
||||
}
|
||||
q := alerts.New(params)
|
||||
|
||||
if q.WouldSend(alerts.EventBan, netblock(1)) ||
|
||||
q.WouldSend(alerts.EventPermanentBan, netblock(1)) {
|
||||
t.Error("a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out")
|
||||
t.Error(
|
||||
"a ban alert would be sent, though SWW_ALERT_EVENTS leaves it out",
|
||||
)
|
||||
}
|
||||
|
||||
if !q.WouldSend(alerts.EventFileError, netip.Prefix{}) {
|
||||
@@ -188,7 +222,10 @@ func TestWouldSendWithOnlySlackOrOnlyNtfySet(t *testing.T) {
|
||||
"SWW_ALERT_NTFY_URL": onlyNtfy,
|
||||
} {
|
||||
if !alerts.New(params).WouldSend(alerts.EventBan, netblock(1)) {
|
||||
t.Errorf("a ban alert would not be sent with only %s set", setting)
|
||||
t.Errorf(
|
||||
"a ban alert would not be sent with only %s set",
|
||||
setting,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,8 +262,14 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
||||
synctest.Wait()
|
||||
|
||||
got := webhook.received()
|
||||
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventPermanentBan,
|
||||
alerts.EventBan)
|
||||
wantEvents(
|
||||
t,
|
||||
webhook,
|
||||
alerts.EventBan,
|
||||
alerts.EventBan,
|
||||
alerts.EventPermanentBan,
|
||||
alerts.EventBan,
|
||||
)
|
||||
|
||||
for i, want := range []struct {
|
||||
netblock int
|
||||
@@ -235,9 +278,14 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
||||
alert := got[i].alert
|
||||
if alert["netblock"] != netblock(want.netblock).String() ||
|
||||
alert["suppressed_repeats"] != want.repeats {
|
||||
t.Errorf("alert %d is for %v with %v repeats, want %s with %v", i,
|
||||
alert["netblock"], alert["suppressed_repeats"], netblock(want.netblock),
|
||||
want.repeats)
|
||||
t.Errorf(
|
||||
"alert %d is for %v with %v repeats, want %s with %v",
|
||||
i,
|
||||
alert["netblock"],
|
||||
alert["suppressed_repeats"],
|
||||
netblock(want.netblock),
|
||||
want.repeats,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +293,9 @@ func TestRepeatWithinTheCooldownIsHeldBackAndCountedInTheNext(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(t *testing.T) {
|
||||
func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
@@ -253,8 +303,11 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(t *testing.T)
|
||||
webhook, q := start(t, params)
|
||||
fileError := func(file string) alerts.Alert {
|
||||
return alerts.Alert{
|
||||
Event: alerts.EventFileError,
|
||||
Detail: map[string]any{"file": file, "error": "line 2: an error"},
|
||||
Event: alerts.EventFileError,
|
||||
Detail: map[string]any{
|
||||
"file": file,
|
||||
"error": "line 2: an error",
|
||||
},
|
||||
}
|
||||
}
|
||||
sourceFailure := func(source string) alerts.Alert {
|
||||
@@ -285,7 +338,11 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(t *testing.T)
|
||||
"/rules.d/50-a.rules", "/rules.d/50-b.rules", "geojs", "abuseipdb",
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("the webhook was sent alerts for %v, want %v", got, want)
|
||||
t.Errorf(
|
||||
"the webhook was sent alerts for %v, want %v",
|
||||
got,
|
||||
want,
|
||||
)
|
||||
}
|
||||
|
||||
wantCounts(t, q, 4, 0, 2, 0)
|
||||
@@ -298,8 +355,12 @@ func TestFileErrorAndSourceFailureRepeatOnlyForTheSameFileOrSource(t *testing.T)
|
||||
after.Raise(fileError("/rules.d/50-c.rules"))
|
||||
|
||||
waiting := after.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||
if len(waiting) != 1 || waiting[0].Detail["file"] != "/rules.d/50-c.rules" {
|
||||
t.Errorf("after loading, alerts wait %+v, want the one for 50-c.rules", waiting)
|
||||
if len(waiting) != 1 ||
|
||||
waiting[0].Detail["file"] != "/rules.d/50-c.rules" {
|
||||
t.Errorf(
|
||||
"after loading, alerts wait %+v, want the one for 50-c.rules",
|
||||
waiting,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -336,7 +397,12 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(2)})
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(3)})
|
||||
q.Raise(alerts.Alert{Event: alerts.EventPermanentBan, Netblock: netblock(4)})
|
||||
q.Raise(
|
||||
alerts.Alert{
|
||||
Event: alerts.EventPermanentBan,
|
||||
Netblock: netblock(4),
|
||||
},
|
||||
)
|
||||
q.Raise(alerts.Alert{Event: alerts.EventFileError})
|
||||
|
||||
// The summary is sent as the hour ends, and not before.
|
||||
@@ -346,7 +412,13 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
||||
|
||||
time.Sleep(time.Nanosecond)
|
||||
synctest.Wait()
|
||||
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventSummary)
|
||||
wantEvents(
|
||||
t,
|
||||
webhook,
|
||||
alerts.EventBan,
|
||||
alerts.EventBan,
|
||||
alerts.EventSummary,
|
||||
)
|
||||
|
||||
summary := webhook.received()[2].alert
|
||||
wantAlert(t, summary, map[string]any{
|
||||
@@ -364,7 +436,9 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
||||
"hour": started,
|
||||
"count": float64(3),
|
||||
"events": map[string]any{
|
||||
"ban": float64(1), "permanent_ban": float64(1), "file_error": float64(1),
|
||||
"ban": float64(
|
||||
1,
|
||||
), "permanent_ban": float64(1), "file_error": float64(1),
|
||||
},
|
||||
},
|
||||
"suppressed_repeats": float64(0),
|
||||
@@ -375,13 +449,21 @@ func TestAlertsPastTheHourlyLimitAreRolledIntoOneSummary(t *testing.T) {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(5)})
|
||||
time.Sleep(time.Hour)
|
||||
synctest.Wait()
|
||||
wantEvents(t, webhook, alerts.EventBan, alerts.EventBan, alerts.EventSummary,
|
||||
alerts.EventBan)
|
||||
wantEvents(
|
||||
t,
|
||||
webhook,
|
||||
alerts.EventBan,
|
||||
alerts.EventBan,
|
||||
alerts.EventSummary,
|
||||
alerts.EventBan,
|
||||
)
|
||||
wantCounts(t, q, 4, 0, 3, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T) {
|
||||
func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
@@ -389,7 +471,9 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T
|
||||
params.MaxPerHour = 1
|
||||
webhook, q := start(t, params)
|
||||
raise := func() {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
q.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
||||
)
|
||||
}
|
||||
|
||||
// The hour's one alert, and two repeats the cooldown holds back.
|
||||
@@ -408,7 +492,13 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T
|
||||
raise()
|
||||
synctest.Wait()
|
||||
|
||||
wantEvents(t, webhook, alerts.EventBan, alerts.EventSummary, alerts.EventBan)
|
||||
wantEvents(
|
||||
t,
|
||||
webhook,
|
||||
alerts.EventBan,
|
||||
alerts.EventSummary,
|
||||
alerts.EventBan,
|
||||
)
|
||||
|
||||
got := webhook.received()
|
||||
if len(got) == 3 {
|
||||
@@ -416,11 +506,16 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T
|
||||
summaryRepeats := got[1].alert["suppressed_repeats"]
|
||||
lastRepeats := got[2].alert["suppressed_repeats"]
|
||||
|
||||
if detail["count"] != float64(1) || summaryRepeats != float64(2) ||
|
||||
if detail["count"] != float64(1) ||
|
||||
summaryRepeats != float64(2) ||
|
||||
lastRepeats != float64(0) {
|
||||
t.Errorf("the summary counts %v alerts and %v repeats, and the last "+
|
||||
"alert gives %v repeats, want 1, 2 and 0", detail["count"],
|
||||
summaryRepeats, lastRepeats)
|
||||
t.Errorf(
|
||||
"the summary counts %v alerts and %v repeats, and the last "+
|
||||
"alert gives %v repeats, want 1, 2 and 0",
|
||||
detail["count"],
|
||||
summaryRepeats,
|
||||
lastRepeats,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +523,9 @@ func TestRepeatsBeforeAnAlertPastTheHourlyLimitAreGivenByTheSummary(t *testing.T
|
||||
})
|
||||
}
|
||||
|
||||
func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(t *testing.T) {
|
||||
func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(
|
||||
t *testing.T,
|
||||
) {
|
||||
t.Parallel()
|
||||
|
||||
for name, maxPerHour := range map[string]int{"limit off": 0, "limit set": 60} {
|
||||
@@ -465,8 +562,12 @@ func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(t *testing.T)
|
||||
|
||||
kept := len(q.Snapshot().Cooldowns)
|
||||
if kept > int(cooldown/time.Minute) {
|
||||
t.Errorf("after %d netblocks, %d cooldowns are kept, want at most %d",
|
||||
netblocks, kept, int(cooldown/time.Minute))
|
||||
t.Errorf(
|
||||
"after %d netblocks, %d cooldowns are kept, want at most %d",
|
||||
netblocks,
|
||||
kept,
|
||||
int(cooldown/time.Minute),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,8 +585,13 @@ func TestCooldownsThatHaveRunOutAreDroppedAndTheirRepeatsSummedUp(t *testing.T)
|
||||
|
||||
kept := len(q.Snapshot().Cooldowns)
|
||||
if kept != 0 || repeats != float64(netblocks) {
|
||||
t.Errorf("%d cooldowns are kept and the webhook was given %v repeats, "+
|
||||
"want 0 and %d", kept, repeats, netblocks)
|
||||
t.Errorf(
|
||||
"%d cooldowns are kept and the webhook was given %v repeats, "+
|
||||
"want 0 and %d",
|
||||
kept,
|
||||
repeats,
|
||||
netblocks,
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -620,8 +726,14 @@ func TestFailedRequestIsLoggedWithoutTheURL(t *testing.T) {
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged,
|
||||
`"msg":"sending an alert to SWW_ALERT_WEBHOOK_URL failed"`) ||
|
||||
strings.Contains(logged, "alerts.example") || strings.Contains(logged, "team=ops") {
|
||||
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||
strings.Contains(
|
||||
logged,
|
||||
"alerts.example",
|
||||
) || strings.Contains(logged, "team=ops") {
|
||||
t.Errorf(
|
||||
"process log %q names no failure, or names the URL",
|
||||
logged,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -639,7 +751,9 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
||||
// than the queue holds is raised: none waits, and the oldest, the
|
||||
// one the webhook was sent, is dropped.
|
||||
for n := range alerts.QueueSize + 1 {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||
q.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
||||
)
|
||||
|
||||
if n == 0 {
|
||||
synctest.Wait()
|
||||
@@ -653,9 +767,17 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
||||
wantCounts(t, q, 0, 0, 0, 1)
|
||||
|
||||
waiting := q.Snapshot().Waiting[alerts.DestinationWebhook]
|
||||
if len(waiting) != alerts.QueueSize || waiting[0].Netblock != netblock(1) {
|
||||
t.Fatalf("%d alerts wait, the first for %s, want %d, the first for %s",
|
||||
len(waiting), waiting[0].Netblock, alerts.QueueSize, netblock(1))
|
||||
if len(waiting) != alerts.QueueSize ||
|
||||
waiting[0].Netblock != netblock(1) {
|
||||
t.Fatalf(
|
||||
"%d alerts wait, the first for %s, want %d, the first for %s",
|
||||
len(
|
||||
waiting,
|
||||
),
|
||||
waiting[0].Netblock,
|
||||
alerts.QueueSize,
|
||||
netblock(1),
|
||||
)
|
||||
}
|
||||
|
||||
// The request is abandoned after 10 seconds, and the webhook, which
|
||||
@@ -667,14 +789,22 @@ func TestFullQueueDropsTheOldestAndRaiseNeverWaits(t *testing.T) {
|
||||
got := webhook.received()
|
||||
if len(got) != alerts.QueueSize+1 ||
|
||||
got[0].alert["netblock"] != netblock(0).String() {
|
||||
t.Fatalf("the webhook had %d requests, want %d, the first for %s",
|
||||
len(got), alerts.QueueSize+1, netblock(0))
|
||||
t.Fatalf(
|
||||
"the webhook had %d requests, want %d, the first for %s",
|
||||
len(got),
|
||||
alerts.QueueSize+1,
|
||||
netblock(0),
|
||||
)
|
||||
}
|
||||
|
||||
for i, request := range got[1:] {
|
||||
if request.alert["netblock"] != netblock(i+1).String() {
|
||||
t.Fatalf("request %d is for %v, want %s", i+1, request.alert["netblock"],
|
||||
netblock(i+1))
|
||||
t.Fatalf(
|
||||
"request %d is for %v, want %s",
|
||||
i+1,
|
||||
request.alert["netblock"],
|
||||
netblock(i+1),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,8 +822,12 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
||||
|
||||
// Not sent: Run is not running. The repeat is held back by the
|
||||
// cooldown, and the file error past the hourly limit.
|
||||
before.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
before.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
before.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
||||
)
|
||||
before.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
||||
)
|
||||
before.Raise(alerts.Alert{Event: alerts.EventFileError})
|
||||
|
||||
time.Sleep(time.Minute)
|
||||
@@ -704,7 +838,9 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
||||
// The new queue sends the alert waiting, holds back the repeat as
|
||||
// the cooldown still runs, and sends the summary of the hour, which
|
||||
// gives both repeats, as the cooldown has run out.
|
||||
after.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
after.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)},
|
||||
)
|
||||
synctest.Wait()
|
||||
wantEvents(t, webhook, alerts.EventBan)
|
||||
|
||||
@@ -715,9 +851,13 @@ func TestStateLoadedIntoANewQueueCarriesOn(t *testing.T) {
|
||||
summary := webhook.received()[1].alert
|
||||
detail, _ := summary["detail"].(map[string]any)
|
||||
|
||||
if detail["count"] != float64(1) || summary["suppressed_repeats"] != float64(2) {
|
||||
t.Errorf("the summary counts %v alerts and %v repeats, want 1 and 2",
|
||||
detail["count"], summary["suppressed_repeats"])
|
||||
if detail["count"] != float64(1) ||
|
||||
summary["suppressed_repeats"] != float64(2) {
|
||||
t.Errorf(
|
||||
"the summary counts %v alerts and %v repeats, want 1 and 2",
|
||||
detail["count"],
|
||||
summary["suppressed_repeats"],
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -747,7 +887,13 @@ func TestSlackAndNtfyAreSentAMessageForEachEvent(t *testing.T) {
|
||||
|
||||
for i, each := range want {
|
||||
wantSlackMessage(t, slack[i], "*"+each.title+"*\n"+each.text)
|
||||
wantNtfyMessage(t, ntfy[i], each.title, each.priorityAndTag, each.text)
|
||||
wantNtfyMessage(
|
||||
t,
|
||||
ntfy[i],
|
||||
each.title,
|
||||
each.priorityAndTag,
|
||||
each.text,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -760,14 +906,23 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
||||
params.WebhookURL = nil
|
||||
params.MaxPerHour = 1
|
||||
standIns, q := startAll(t, params)
|
||||
ban := alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1), Reason: "a ban"}
|
||||
ban := alerts.Alert{
|
||||
Event: alerts.EventBan,
|
||||
Netblock: netblock(1),
|
||||
Reason: "a ban",
|
||||
}
|
||||
|
||||
// The hour's one alert, a repeat of it the cooldown holds back, and
|
||||
// an alert past the limit; once the hour has ended, its summary,
|
||||
// which gives the repeat, and the next alert, which gives none.
|
||||
q.Raise(ban)
|
||||
q.Raise(ban)
|
||||
q.Raise(alerts.Alert{Event: alerts.EventFileError, Reason: "a file error"})
|
||||
q.Raise(
|
||||
alerts.Alert{
|
||||
Event: alerts.EventFileError,
|
||||
Reason: "a file error",
|
||||
},
|
||||
)
|
||||
time.Sleep(time.Hour)
|
||||
synctest.Wait()
|
||||
q.Raise(ban)
|
||||
@@ -786,8 +941,18 @@ func TestSlackAndNtfyAreSentTheSummaryAndTheRepeatsHeldBack(t *testing.T) {
|
||||
"by SWW_ALERT_COOLDOWN that no later alert gives\nsuppressed repeats: 1"
|
||||
|
||||
wantSlackMessage(t, slack[1], "*"+instance+": summary*\n"+summary)
|
||||
wantNtfyMessage(t, ntfy[1], instance+": summary", "default bar_chart", summary)
|
||||
wantSlackMessage(t, slack[2], "*"+instance+": ban*\na ban\nnetblock: 203.0.113.1/32")
|
||||
wantNtfyMessage(
|
||||
t,
|
||||
ntfy[1],
|
||||
instance+": summary",
|
||||
"default bar_chart",
|
||||
summary,
|
||||
)
|
||||
wantSlackMessage(
|
||||
t,
|
||||
slack[2],
|
||||
"*"+instance+": ban*\na ban\nnetblock: 203.0.113.1/32",
|
||||
)
|
||||
wantNtfyMessage(t, ntfy[2], instance+": ban", "default no_entry",
|
||||
"a ban\nnetblock: 203.0.113.1/32")
|
||||
})
|
||||
@@ -813,7 +978,11 @@ func TestSlackMessageEscapesAmpersandsAndAngleBrackets(t *testing.T) {
|
||||
|
||||
wantSlackMessage(t, got[0], "*app<1>: file_error*\n"+
|
||||
"<!channel> & <https://x.example|y>")
|
||||
wantBodies(t, standIns[alerts.DestinationNtfy], "<!channel> & <https://x.example|y>")
|
||||
wantBodies(
|
||||
t,
|
||||
standIns[alerts.DestinationNtfy],
|
||||
"<!channel> & <https://x.example|y>",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -835,7 +1004,12 @@ func TestNtfyTokenIsSentToNtfyAlone(t *testing.T) {
|
||||
params.NtfyToken = tc.token
|
||||
standIns, q := startAll(t, params)
|
||||
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(1)})
|
||||
q.Raise(
|
||||
alerts.Alert{
|
||||
Event: alerts.EventBan,
|
||||
Netblock: netblock(1),
|
||||
},
|
||||
)
|
||||
synctest.Wait()
|
||||
|
||||
for destination, want := range map[string][]string{
|
||||
@@ -845,13 +1019,21 @@ func TestNtfyTokenIsSentToNtfyAlone(t *testing.T) {
|
||||
} {
|
||||
got := standIns[destination].received()
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("%s was sent %d requests, want 1", destination, len(got))
|
||||
t.Fatalf(
|
||||
"%s was sent %d requests, want 1",
|
||||
destination,
|
||||
len(got),
|
||||
)
|
||||
}
|
||||
|
||||
authorization := got[0].header.Values("Authorization")
|
||||
if !slices.Equal(authorization, want) {
|
||||
t.Errorf("%s was sent Authorization %q, want %q", destination,
|
||||
authorization, want)
|
||||
t.Errorf(
|
||||
"%s was sent Authorization %q, want %q",
|
||||
destination,
|
||||
authorization,
|
||||
want,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -870,7 +1052,9 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
||||
standIns[alerts.DestinationSlack].set(hanging)
|
||||
|
||||
for n := range 3 {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||
q.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
||||
)
|
||||
}
|
||||
|
||||
synctest.Wait()
|
||||
@@ -885,7 +1069,9 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
||||
wantDestinationCounts(t, q, destination, want)
|
||||
}
|
||||
|
||||
if got := standIns[alerts.DestinationSlack].received(); len(got) != 1 {
|
||||
if got := standIns[alerts.DestinationSlack].received(); len(
|
||||
got,
|
||||
) != 1 {
|
||||
t.Errorf("Slack was sent %d requests, want 1", len(got))
|
||||
}
|
||||
|
||||
@@ -900,11 +1086,16 @@ func TestDestinationThatDoesNotAnswerHoldsUpNeitherOther(t *testing.T) {
|
||||
alerts.Counts{Sent: 3, SendFailures: 1})
|
||||
|
||||
logged := log.String()
|
||||
if !strings.Contains(logged,
|
||||
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`) ||
|
||||
if !strings.Contains(
|
||||
logged,
|
||||
`"msg":"sending an alert to SWW_ALERT_SLACK_WEBHOOK_URL failed"`,
|
||||
) ||
|
||||
strings.Contains(logged, "hooks.slack.example") ||
|
||||
strings.Contains(logged, "T0123") {
|
||||
t.Errorf("process log %q names no failure, or names the URL", logged)
|
||||
t.Errorf(
|
||||
"process log %q names no failure, or names the URL",
|
||||
logged,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -922,7 +1113,9 @@ func TestDropsAreCountedForEachDestination(t *testing.T) {
|
||||
// Each alert is raised once each destination has done all it can
|
||||
// with those before it.
|
||||
for n := range alerts.QueueSize + 1 {
|
||||
q.Raise(alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)})
|
||||
q.Raise(
|
||||
alerts.Alert{Event: alerts.EventBan, Netblock: netblock(n)},
|
||||
)
|
||||
synctest.Wait()
|
||||
}
|
||||
|
||||
@@ -1136,7 +1329,10 @@ func start(t *testing.T, params alerts.Params) (*standIn, *alerts.Queue) {
|
||||
// startAll returns, by destination, a stand-in that answers for each
|
||||
// destination params sets, and a Queue that sends to them, run until the
|
||||
// test ends.
|
||||
func startAll(t *testing.T, params alerts.Params) (map[string]*standIn, *alerts.Queue) {
|
||||
func startAll(
|
||||
t *testing.T,
|
||||
params alerts.Params,
|
||||
) (map[string]*standIn, *alerts.Queue) {
|
||||
t.Helper()
|
||||
|
||||
q := alerts.New(params)
|
||||
@@ -1170,7 +1366,9 @@ func midnight() time.Time {
|
||||
|
||||
// netblock returns the n-th netblock of a test, counted from 0.
|
||||
func netblock(n int) netip.Prefix {
|
||||
return netip.MustParsePrefix(fmt.Sprintf("203.0.%d.%d/32", 113+n/256, n%256))
|
||||
return netip.MustParsePrefix(
|
||||
fmt.Sprintf("203.0.%d.%d/32", 113+n/256, n%256),
|
||||
)
|
||||
}
|
||||
|
||||
// roundTrip returns state once written as JSON and read back, as
|
||||
@@ -1223,12 +1421,22 @@ func wantEvents(t *testing.T, webhook *standIn, want ...string) {
|
||||
// requests to it that it counts as failed, the alerts it counts as held
|
||||
// back, and those it counts as dropped for the webhook.
|
||||
func wantCounts(
|
||||
t *testing.T, q *alerts.Queue, sent, sendFailures, suppressed, dropped int64,
|
||||
t *testing.T,
|
||||
q *alerts.Queue,
|
||||
sent, sendFailures, suppressed, dropped int64,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
wantDestinationCounts(t, q, alerts.DestinationWebhook,
|
||||
alerts.Counts{Sent: sent, SendFailures: sendFailures, Dropped: dropped})
|
||||
wantDestinationCounts(
|
||||
t,
|
||||
q,
|
||||
alerts.DestinationWebhook,
|
||||
alerts.Counts{
|
||||
Sent: sent,
|
||||
SendFailures: sendFailures,
|
||||
Dropped: dropped,
|
||||
},
|
||||
)
|
||||
|
||||
if q.Suppressed() != suppressed {
|
||||
t.Errorf("%d alerts held back, want %d", q.Suppressed(), suppressed)
|
||||
@@ -1261,7 +1469,9 @@ func anAlertForEachEvent() []eventMessage {
|
||||
{
|
||||
alerts.Alert{
|
||||
Event: alerts.EventBan, Client: netip.MustParseAddr("203.0.113.9"),
|
||||
Netblock: netip.MustParsePrefix("203.0.113.0/24"), Country: "DE",
|
||||
Netblock: netip.MustParsePrefix(
|
||||
"203.0.113.0/24",
|
||||
), Country: "DE",
|
||||
Reason: "requests per hour over the limit of 10000",
|
||||
Detail: map[string]any{"mode": "observe"},
|
||||
},
|
||||
@@ -1272,7 +1482,9 @@ func anAlertForEachEvent() []eventMessage {
|
||||
{
|
||||
alerts.Alert{
|
||||
Event: alerts.EventPermanentBan, Client: netip.MustParseAddr("198.51.100.7"),
|
||||
Netblock: netip.MustParsePrefix("198.51.100.7/32"), Country: "FR",
|
||||
Netblock: netip.MustParsePrefix(
|
||||
"198.51.100.7/32",
|
||||
), Country: "FR",
|
||||
Reason: "matched the rule env-file",
|
||||
},
|
||||
"fsn1app1/blog: permanent_ban", "high no_entry",
|
||||
@@ -1347,18 +1559,30 @@ func wantSlackMessage(t *testing.T, request post, text string) {
|
||||
// wantNtfyMessage checks that request, one ntfy was sent, posted the
|
||||
// message text with the title, and with the priority and the tag
|
||||
// priorityAndTag gives, with a space between them.
|
||||
func wantNtfyMessage(t *testing.T, request post, title, priorityAndTag, text string) {
|
||||
func wantNtfyMessage(
|
||||
t *testing.T,
|
||||
request post,
|
||||
title, priorityAndTag, text string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
got := []string{
|
||||
request.method, request.url, request.header.Get("Title"),
|
||||
request.header.Get("Priority") + " " + request.header.Get("Tags"), request.body,
|
||||
request.header.Get(
|
||||
"Priority",
|
||||
) + " " + request.header.Get(
|
||||
"Tags",
|
||||
), request.body,
|
||||
}
|
||||
|
||||
want := []string{http.MethodPost, ntfyURL, title, priorityAndTag, text}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("ntfy was sent the method, URL, title, priority and tag, and text "+
|
||||
"%q, want %q", got, want)
|
||||
t.Errorf(
|
||||
"ntfy was sent the method, URL, title, priority and tag, and text "+
|
||||
"%q, want %q",
|
||||
got,
|
||||
want,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user