notify: a failed Mattermost delivery's error names Mattermost (closes #227)
check / check (push) Successful in 1m8s

Mattermost is sent by the Slack sender, which wrapped every HTTP error
status in ErrSlackFailed, so a Mattermost endpoint answering 503 was
logged as "slack notification failed". The sender now takes the error
to wrap: the Slack endpoint passes ErrSlackFailed and the Mattermost
endpoint passes ErrMattermostFailed, which was defined but unused.

A new delivery test sets both endpoints to a stand-in server answering
503 and checks the error logged for each names its own endpoint.

Model: opus-5-5
This commit is contained in:
2026-10-02 06:38:05 +00:00
parent ceb24c5004
commit e3c632c793
4 changed files with 82 additions and 7 deletions
+69 -3
View File
@@ -6,9 +6,11 @@ import (
"encoding/json"
"errors"
"io"
"maps"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
@@ -413,7 +415,8 @@ func sendSlackInfo(
svc *notify.Service, target *url.URL,
) error {
return svc.SendSlack(
context.Background(), target, "t", "m", prioInfo,
context.Background(), target, notify.ErrSlackFailed,
"t", "m", prioInfo,
)
}
@@ -506,6 +509,7 @@ func TestSendSlackPayloadFields(t *testing.T) {
err := svc.SendSlack(
context.Background(),
webhookURL,
notify.ErrSlackFailed,
"Alert Title",
"Alert body text",
"warning",
@@ -608,7 +612,8 @@ func TestSendSlackAllColors(t *testing.T) {
err := svc.SendSlack(
context.Background(),
webhookURL, "t", "m", tc.priority,
webhookURL, notify.ErrSlackFailed,
"t", "m", tc.priority,
)
if err != nil {
t.Fatalf("SendSlack error: %v", err)
@@ -659,7 +664,8 @@ func TestSendSlackNetworkError(t *testing.T) {
)
err := svc.SendSlack(
context.Background(), webhookURL, "t", "m", "info",
context.Background(), webhookURL, notify.ErrSlackFailed,
"t", "m", "info",
)
if err == nil {
t.Fatal("expected error for network failure")
@@ -1028,6 +1034,66 @@ func TestSendNotificationMattermostError(t *testing.T) {
)
}
// TestSendNotificationErrorNamesEndpoint verifies that, with both
// Slack and Mattermost set, a failed delivery's logged error names
// the endpoint that failed. Both are sent by the Slack sender.
func TestSendNotificationErrorNamesEndpoint(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}),
)
defer srv.Close()
target, _ := url.Parse(srv.URL)
svc, logs := newLoggingService(http.DefaultTransport)
svc.SetSlackWebhookURL(target)
svc.SetMattermostWebhookURL(target)
svc.SetSleepFunc(instantSleep)
svc.SetRetryConfig(notify.RetryConfig{
MaxRetries: 1,
BaseDelay: time.Millisecond,
MaxDelay: time.Millisecond,
})
svc.SendNotification(
context.Background(), "t", "m", prioError,
)
waitForCondition(t, func() bool {
return svc.OutstandingDeliveries() == 0
})
got := map[string]string{}
for line := range strings.Lines(logs.String()) {
var record struct {
Msg string `json:"msg"`
Endpoint string `json:"endpoint"`
Error string `json:"error"`
}
_ = json.Unmarshal([]byte(line), &record)
if record.Msg == "failed to send notification after retries" {
got[record.Endpoint] = record.Error
}
}
want := map[string]string{
"slack": "slack notification failed: status 503",
"mattermost": "mattermost notification failed: status 503",
}
if !maps.Equal(got, want) {
t.Errorf("logged errors = %v, want %v", got, want)
}
}
// ── SlackPayload JSON marshaling ──────────────────────────
func TestSlackPayloadJSON(t *testing.T) {