fix(audit): each ntfy alert source sends its own title (#163)
Every alert was titled "brain-capture audit degraded", including the symlink tripwire's and governance-edit alerts. NtfyNotifier.WithTitle returns a titled copy (nil stays nil); buildNotifier(getenv, title) is called per source: capture audit (unchanged title), "brain symlink tripwire", "brain governance edit". Tests assert the Title header. Fixes #163. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,9 +13,14 @@ import (
|
||||
type NtfyNotifier struct {
|
||||
topicURL string
|
||||
token string
|
||||
title string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// defaultNtfyTitle is the title of a notifier built without WithTitle: the
|
||||
// capture audit's degraded-state alert, its first user.
|
||||
const defaultNtfyTitle = "brain-capture audit degraded"
|
||||
|
||||
// NewNtfyNotifier constructs a notifier for the given ntfy topic URL
|
||||
// (e.g. https://ntfy.sh/my-topic). token is an optional bearer for
|
||||
// protected ntfy instances; it is held here and only sent in the
|
||||
@@ -27,17 +32,31 @@ func NewNtfyNotifier(topicURL, token string) *NtfyNotifier {
|
||||
return &NtfyNotifier{
|
||||
topicURL: strings.TrimRight(topicURL, "/"),
|
||||
token: token,
|
||||
title: defaultNtfyTitle,
|
||||
http: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// WithTitle returns a copy that sends title as the ntfy Title header, so each
|
||||
// alert source (the capture audit, the symlink tripwire, governance edits)
|
||||
// is recognisable on the phone (hyperguild#163). A nil notifier (ntfy not
|
||||
// configured) stays nil.
|
||||
func (n *NtfyNotifier) WithTitle(title string) *NtfyNotifier {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
c := *n
|
||||
c.title = title
|
||||
return &c
|
||||
}
|
||||
|
||||
// Notify posts a message to the ntfy topic.
|
||||
func (n *NtfyNotifier) Notify(ctx context.Context, msg string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, n.topicURL, strings.NewReader(msg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Title", "brain-capture audit degraded")
|
||||
req.Header.Set("Title", n.title)
|
||||
req.Header.Set("Priority", "high")
|
||||
req.Header.Set("Tags", "warning,brain")
|
||||
if n.token != "" {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// hyperguild#163: every alert went out titled "brain-capture audit
|
||||
// degraded", including the symlink tripwire's and governance alerts.
|
||||
func TestNtfyNotifierSendsItsOwnTitle(t *testing.T) {
|
||||
var got []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got = append(got, r.Header.Get("Title"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
base := NewNtfyNotifier(srv.URL, "")
|
||||
require.NoError(t, base.Notify(context.Background(), "m"))
|
||||
require.NoError(t, base.WithTitle("brain symlink tripwire").Notify(context.Background(), "m"))
|
||||
require.NoError(t, base.Notify(context.Background(), "m"))
|
||||
assert.Equal(t, []string{"brain-capture audit degraded", "brain symlink tripwire", "brain-capture audit degraded"}, got,
|
||||
"WithTitle returns a copy; the base keeps its default title")
|
||||
var nilN *NtfyNotifier
|
||||
assert.Nil(t, nilN.WithTitle("x"), "nil (ntfy not configured) stays nil")
|
||||
}
|
||||
Reference in New Issue
Block a user