Format Go with gofmt, then golines -m 77 (closes #215)
check / check (push) Waiting to run

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:
sneak committed 2026-11-11 15:14:58 +00:00
1 parent 63e58e326b
commit 7f8a59de04
118 files changed
+9447 -2670

No files matched your search

+57 -18
View File
@@ -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("&", "&amp;", "<", "&lt;", ">", "&gt;").Replace
escape := strings.NewReplacer(
"&",
"&amp;",
"<",
"&lt;",
">",
"&gt;",
).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
View File
@@ -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&lt;1&gt;: file_error*\n"+
"&lt;!channel&gt; &amp; &lt;https://x.example|y&gt;")
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,
)
}
}