Add notification channels and a provider abstraction
Users get a notification_channels table (replacing users.ntfy_topic); pages fan
out to every enabled channel through a notify.Provider registry, with ntfy as
the first provider. Fallbacks are typed {type, address}. Schema edited in place:
existing databases must be recreated.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package notify_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.ryuvia.com/niklas/terdut-server/internal/notify"
|
||||
)
|
||||
|
||||
func TestRegistry_NilAndEmptyAreEmpty(t *testing.T) {
|
||||
var nilReg *notify.Registry
|
||||
for name, r := range map[string]*notify.Registry{"nil": nilReg, "empty": notify.NewRegistry()} {
|
||||
if !r.Empty() || r.Has("ntfy") || len(r.List()) != 0 {
|
||||
t.Errorf("%s registry should be empty", name)
|
||||
}
|
||||
if _, ok := r.Get("ntfy"); ok {
|
||||
t.Errorf("%s registry should have no ntfy", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_ListIsOrderedByType(t *testing.T) {
|
||||
r := notify.NewRegistry(notify.NewNtfy("http://x", ""), fakeProvider{"email"})
|
||||
list := r.List()
|
||||
if len(list) != 2 || list[0].Type() != "email" || list[1].Type() != "ntfy" {
|
||||
t.Errorf("expected providers ordered by type, got %v", list)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeProvider struct{ typ string }
|
||||
|
||||
func (f fakeProvider) Type() string { return f.typ }
|
||||
func (f fakeProvider) Label() string { return f.typ }
|
||||
func (f fakeProvider) AddressLabel() string { return "" }
|
||||
func (f fakeProvider) AddressHint() string { return "" }
|
||||
func (f fakeProvider) Capabilities() notify.Capabilities { return notify.Capabilities{} }
|
||||
func (f fakeProvider) Validate(a string) (string, error) { return a, nil }
|
||||
func (f fakeProvider) Send(context.Context, notify.Message) error { return nil }
|
||||
|
||||
func TestNtfy_ValidateTopic(t *testing.T) {
|
||||
n := notify.NewNtfy("http://x", "")
|
||||
for _, bad := range []string{"", " ", "a b", "a/b", "a?b", "a#b"} {
|
||||
if _, err := n.Validate(bad); err == nil {
|
||||
t.Errorf("topic %q should be rejected", bad)
|
||||
}
|
||||
}
|
||||
if got, err := n.Validate(" my-topic "); err != nil || got != "my-topic" {
|
||||
t.Errorf("a good topic should be trimmed and accepted, got %q, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNtfy_SendEncodesTheMessage(t *testing.T) {
|
||||
var got map[string]any
|
||||
var auth string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth = r.Header.Get("Authorization")
|
||||
_ = json.NewDecoder(r.Body).Decode(&got)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
n := notify.NewNtfy(srv.URL+"/", "tok")
|
||||
err := n.Send(context.Background(), notify.Message{
|
||||
Address: "t", Kind: notify.KindTriggered, Title: "T", Body: "B", Level: 4,
|
||||
ClickURL: "https://x/incidents/1", AckURL: "https://x/ack",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if auth != "Bearer tok" {
|
||||
t.Errorf("expected the bearer token, got %q", auth)
|
||||
}
|
||||
if got["topic"] != "t" || got["title"] != "T" || got["message"] != "B" || got["priority"] != float64(5) {
|
||||
t.Errorf("unexpected body %v", got)
|
||||
}
|
||||
actions, _ := got["actions"].([]any)
|
||||
if len(actions) != 1 || actions[0].(map[string]any)["url"] != "https://x/ack" {
|
||||
t.Errorf("expected the Acknowledge action, got %v", got["actions"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNtfy_ResolvedIsQuietAndTestHasNoPriority(t *testing.T) {
|
||||
var got map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got = nil
|
||||
_ = json.NewDecoder(r.Body).Decode(&got)
|
||||
}))
|
||||
defer srv.Close()
|
||||
n := notify.NewNtfy(srv.URL, "")
|
||||
|
||||
_ = n.Send(context.Background(), notify.Message{Address: "t", Kind: notify.KindResolved, Level: 4})
|
||||
if got["priority"] != float64(2) {
|
||||
t.Errorf("a resolved page should be low priority whatever the severity, got %v", got["priority"])
|
||||
}
|
||||
_ = n.Send(context.Background(), notify.Message{Address: "t", Kind: notify.KindTest})
|
||||
if _, ok := got["priority"]; ok {
|
||||
t.Errorf("a test should carry no priority, got %v", got["priority"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNtfy_SendReportsARejection(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
if err := notify.NewNtfy(srv.URL, "").Send(context.Background(), notify.Message{Address: "t"}); err == nil {
|
||||
t.Error("a 403 from ntfy must be an error so the notifier retries")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user