package apprise import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" ) // TestSendTagsUsesKeyTagAndBearer proves the request shape the house Apprise // API expects: POST /notify/{key} with the tag list and the bearer token. func TestSendTagsUsesKeyTagAndBearer(t *testing.T) { var ( path, auth string body notifyBody ) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path = r.URL.Path auth = r.Header.Get("Authorization") if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Errorf("decode body: %v", err) } w.WriteHeader(http.StatusOK) })) defer srv.Close() c := New(srv.URL, "abc123", "secret-token", 2*time.Second) res, err := c.SendTags(context.Background(), []string{"harry_email", "harry_whatsapp"}, "Harry — school brief", "OVERDUE\n- Essay", TypeInfo) if err != nil { t.Fatalf("send: %v", err) } if !res.OK() || res.Status != http.StatusOK { t.Errorf("result = %+v, want 200", res) } if path != "/notify/abc123" { t.Errorf("path = %q, want /notify/abc123", path) } if auth != "Bearer secret-token" { t.Errorf("auth = %q, want the bearer token", auth) } if len(body.Tag) != 2 || body.Tag[0] != "harry_email" || body.Tag[1] != "harry_whatsapp" { t.Errorf("tag = %#v, want both tags", body.Tag) } if body.Title != "Harry — school brief" || body.Body != "OVERDUE\n- Essay" || body.Type != TypeInfo { t.Errorf("body = %+v", body) } if body.URLs != nil { t.Errorf("urls = %#v, want none in keyed mode", body.URLs) } } // TestSendTagsFailsLoudlyAndRedacts proves a 424 is an error and that the // echoed address and phone number never reach the caller (AGENTS.md §10 rule 3). func TestSendTagsFailsLoudlyAndRedacts(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusFailedDependency) _, _ = w.Write([]byte(`{"error":"One or more notifications not sent. Failed to send Email to person@example.com and WhatsApp to 61400000000"}`)) })) defer srv.Close() c := New(srv.URL, "abc123", "", 2*time.Second) res, err := c.SendTags(context.Background(), []string{"harry_email"}, "t", "b", TypeInfo) if err == nil { t.Fatal("want an error on 424") } if res.Status != http.StatusFailedDependency { t.Errorf("status = %d, want 424", res.Status) } msg := err.Error() for _, leak := range []string{"person", "example.com", "61400000000"} { if strings.Contains(msg, leak) { t.Errorf("error leaks %q: %s", leak, msg) } } if !strings.Contains(msg, "424") { t.Errorf("error should carry the status: %s", msg) } } // TestSendTagsRefusesEmptyTag proves a send without a tag is refused: sending // the whole stored configuration would hit destinations that are configured for // other purposes, so the result would be ambiguous. func TestSendTagsRefusesEmptyTag(t *testing.T) { c := New("http://127.0.0.1:1", "abc123", "", time.Second) if _, err := c.SendTags(context.Background(), []string{"", " "}, "t", "b", TypeInfo); err == nil { t.Fatal("want an error for an empty tag list") } } // TestSendTagsNeedsKey proves stateless mode is explicit: without a key the // caller must use SendURLs, so a missing key never silently sends nowhere. func TestSendTagsNeedsKey(t *testing.T) { c := New("http://127.0.0.1:1", "", "", time.Second) if _, err := c.SendTags(context.Background(), []string{"harry_email"}, "t", "b", TypeInfo); err == nil { t.Fatal("want an error without a key") } } // TestSendURLsStateless covers the fallback shape. func TestSendURLsStateless(t *testing.T) { var body notifyBody srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Errorf("decode: %v", err) } w.WriteHeader(http.StatusOK) })) defer srv.Close() c := New(srv.URL, "", "", 2*time.Second) if _, err := c.SendURLs(context.Background(), []string{"mailto://user:pass@example.com"}, "t", "b", TypeWarning); err != nil { t.Fatalf("send: %v", err) } if len(body.URLs) != 1 || body.Type != TypeWarning { t.Errorf("body = %+v", body) } } func TestRedact(t *testing.T) { cases := []struct { in, want string }{ {"plain text", "plain text"}, {"Failed to send Email to sam@example.com", "Failed to send Email to "}, {"phone 61412345678", "phone "}, {"short 12 stays", "short 12 stays"}, {"two: a@b.co and c@d.co", "two: and "}, } for _, c := range cases { if got := Redact(c.in); got != c.want { t.Errorf("Redact(%q) = %q, want %q", c.in, got, c.want) } } } func TestTruncate(t *testing.T) { if got := Truncate("short", 10); got != "short" { t.Errorf("Truncate short = %q", got) } long := strings.Repeat("word ", 40) got := Truncate(long, 40) if len(got) > 60 || !strings.HasSuffix(got, "(truncated)") { t.Errorf("Truncate long = %q", got) } }