send (title/priority/tags), read (poll mode, greppable one-liners), topics (known homelab buses from config). Config with homelab defaults on first run. Unit tests against httptest; smoked against the real ntfy on topic agent-tools-test. Spec: doc/tool-parity.md 3.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
136 lines
3.8 KiB
Go
136 lines
3.8 KiB
Go
package notify
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSendSetsHeadersAndBody(t *testing.T) {
|
|
var got *http.Request
|
|
var body string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
got = r
|
|
b := make([]byte, 1024)
|
|
n, _ := r.Body.Read(b)
|
|
body = string(b[:n])
|
|
fmt.Fprint(w, `{"id":"x"}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
c := New(srv.URL, "tok123")
|
|
err := c.Send("ci-fel", "Build failed", "arm64 test: FAIL", "high", []string{"warning", "ci"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.URL.Path != "/ci-fel" {
|
|
t.Errorf("path = %q", got.URL.Path)
|
|
}
|
|
if body != "arm64 test: FAIL" {
|
|
t.Errorf("body = %q", body)
|
|
}
|
|
for hdr, want := range map[string]string{
|
|
"Title": "Build failed",
|
|
"Priority": "high",
|
|
"Tags": "warning,ci",
|
|
"Authorization": "Bearer tok123",
|
|
} {
|
|
if v := got.Header.Get(hdr); v != want {
|
|
t.Errorf("%s = %q, want %q", hdr, v, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSendValidation(t *testing.T) {
|
|
c := New("http://example.invalid", "")
|
|
if err := c.Send("", "", "hello", "", nil); err == nil {
|
|
t.Error("empty topic accepted")
|
|
}
|
|
if err := c.Send("t", "", "", "", nil); err == nil {
|
|
t.Error("empty message accepted")
|
|
}
|
|
if err := c.Send("t", "", "hello", "banana", nil); err == nil {
|
|
t.Error("bogus priority accepted")
|
|
}
|
|
}
|
|
|
|
func TestReadParsesPollOutput(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("poll") != "1" {
|
|
t.Errorf("poll param missing: %s", r.URL.RawQuery)
|
|
}
|
|
if r.URL.Query().Get("since") != "2h" {
|
|
t.Errorf("since = %q", r.URL.Query().Get("since"))
|
|
}
|
|
fmt.Fprintln(w, `{"id":"a","time":1754500000,"event":"message","topic":"t","message":"first","priority":3}`)
|
|
fmt.Fprintln(w, `{"id":"b","time":1754500060,"event":"keepalive","topic":"t"}`)
|
|
fmt.Fprintln(w, `not json at all`)
|
|
fmt.Fprintln(w, `{"id":"c","time":1754500120,"event":"message","topic":"t","title":"T","message":"second","priority":5,"tags":["x"]}`)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
msgs, err := New(srv.URL, "").Read("t", "2h", 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("got %d messages, want 2 (keepalive + junk filtered)", len(msgs))
|
|
}
|
|
if msgs[0].Message != "first" || msgs[1].Title != "T" {
|
|
t.Errorf("unexpected messages: %+v", msgs)
|
|
}
|
|
}
|
|
|
|
func TestReadLimitKeepsNewest(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
for i := 1; i <= 5; i++ {
|
|
fmt.Fprintf(w, "{\"id\":\"%d\",\"time\":%d,\"event\":\"message\",\"topic\":\"t\",\"message\":\"m%d\"}\n", i, 1754500000+i, i)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
msgs, err := New(srv.URL, "").Read("t", "all", 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 2 || msgs[0].Message != "m4" || msgs[1].Message != "m5" {
|
|
t.Errorf("limit should keep the newest: %+v", msgs)
|
|
}
|
|
}
|
|
|
|
func TestFormatIsOneGreppableLine(t *testing.T) {
|
|
line := Format(Message{Time: 1754500000, Title: "Backup", Message: "done\nall good", Priority: 4, Tags: []string{"ok"}})
|
|
if strings.Contains(line, "\n") {
|
|
t.Error("format must be a single line")
|
|
}
|
|
for _, want := range []string{"[high]", "(Backup)", "done", "#ok"} {
|
|
if !strings.Contains(line, want) {
|
|
t.Errorf("line %q missing %q", line, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigRoundtrip(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "config.json")
|
|
cfg, err := LoadConfig(path) // first run creates defaults
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Server == "" || cfg.DefaultTopic == "" {
|
|
t.Error("defaults incomplete")
|
|
}
|
|
cfg.DefaultTopic = "elsewhere"
|
|
if err := SaveConfig(path, cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := LoadConfig(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again.DefaultTopic != "elsewhere" {
|
|
t.Error("saved change did not persist")
|
|
}
|
|
}
|