mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 17:19:57 +00:00
315 lines
8.6 KiB
Go
315 lines
8.6 KiB
Go
package notifications
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSendAppriseViaHTTPEmptyServerURL(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{TimeoutSeconds: 1}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), "server URL is not configured") {
|
|
t.Fatalf("expected missing server URL error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPInvalidScheme(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{ServerURL: "ftp://example.com", TimeoutSeconds: 1}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), "http or https") {
|
|
t.Fatalf("expected scheme validation error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPRejectsUserinfoServerURL(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
called := make(chan struct{}, 1)
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case called <- struct{}{}:
|
|
default:
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
serverURL, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("parse server URL: %v", err)
|
|
}
|
|
serverURL.User = url.UserPassword("user", "pass")
|
|
|
|
err = nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: serverURL.String(),
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), "userinfo") {
|
|
t.Fatalf("expected userinfo validation error, got %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-called:
|
|
t.Fatal("expected userinfo-bearing Apprise URL to be rejected before outbound delivery")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPRejectsQueryOrFragmentInServerURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serverURL func(string) string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "query",
|
|
serverURL: func(base string) string {
|
|
return base + "/apprise/api?token=secret"
|
|
},
|
|
wantErr: "base URL must not include query or fragment",
|
|
},
|
|
{
|
|
name: "fragment",
|
|
serverURL: func(base string) string {
|
|
return base + "/apprise/api#notify"
|
|
},
|
|
wantErr: "base URL must not include query or fragment",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
called := make(chan struct{}, 1)
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case called <- struct{}{}:
|
|
default:
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: tt.serverURL(server.URL),
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("expected %q validation error, got %v", tt.wantErr, err)
|
|
}
|
|
|
|
select {
|
|
case <-called:
|
|
t.Fatal("expected query/fragment-bearing Apprise base URL to be rejected before outbound delivery")
|
|
default:
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSendTestWebhookRejectsUserinfoURLBeforeOutboundDelivery(t *testing.T) {
|
|
nm := NewNotificationManager("https://pulse.local")
|
|
defer nm.Stop()
|
|
|
|
called := make(chan struct{}, 1)
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case called <- struct{}{}:
|
|
default:
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
serverURL, err := url.Parse(server.URL)
|
|
if err != nil {
|
|
t.Fatalf("parse server URL: %v", err)
|
|
}
|
|
serverURL.User = url.UserPassword("user", "pass")
|
|
|
|
err = nm.SendTestWebhook(WebhookConfig{
|
|
Name: "Userinfo Webhook",
|
|
URL: serverURL.String(),
|
|
Enabled: true,
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "userinfo") {
|
|
t.Fatalf("expected userinfo validation error, got %v", err)
|
|
}
|
|
|
|
select {
|
|
case <-called:
|
|
t.Fatal("expected userinfo-bearing webhook URL to be rejected before outbound delivery")
|
|
default:
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPBlocksRedirectToLinkLocal(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "http://169.254.169.254/latest/meta-data", http.StatusFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: server.URL,
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err == nil {
|
|
t.Fatalf("expected redirect validation error")
|
|
}
|
|
if !strings.Contains(err.Error(), "link-local addresses are not allowed") {
|
|
t.Fatalf("expected link-local redirect to be blocked, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPSkipTLSVerifyAndDefaultHeader(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
server := newIPv4TLSServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-API-KEY"); got != "secret" {
|
|
t.Fatalf("expected default API key header, got %q", got)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: server.URL,
|
|
SkipTLSVerify: true,
|
|
APIKey: "secret",
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err != nil {
|
|
t.Fatalf("expected HTTPS request to succeed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPIncludesTargetsAndCustomHeader(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
type payload struct {
|
|
Body string `json:"body"`
|
|
Title string `json:"title"`
|
|
Type string `json:"type"`
|
|
URLs []string `json:"urls"`
|
|
}
|
|
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("X-Test-Key"); got != "secret" {
|
|
t.Fatalf("expected custom API key header, got %q", got)
|
|
}
|
|
if got := r.URL.EscapedPath(); got != "/apprise/api/notify/config%20key" {
|
|
t.Fatalf("expected base path to be preserved, got %q", got)
|
|
}
|
|
var p payload
|
|
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
|
|
t.Fatalf("decode payload: %v", err)
|
|
}
|
|
if len(p.URLs) != 1 || p.URLs[0] != "discord://token" {
|
|
t.Fatalf("expected urls in payload, got %#v", p.URLs)
|
|
}
|
|
if p.Type != "warning" {
|
|
t.Fatalf("expected notify type warning, got %q", p.Type)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{"ok":true}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: server.URL + "/apprise/api/",
|
|
ConfigKey: "config key",
|
|
APIKey: "secret",
|
|
APIKeyHeader: "X-Test-Key",
|
|
Targets: []string{"discord://token"},
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "warning")
|
|
if err != nil {
|
|
t.Fatalf("expected HTTP request to succeed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPStatusErrorWithBody(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("boom"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: server.URL,
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), "HTTP 500") {
|
|
t.Fatalf("expected status error with body, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendAppriseViaHTTPStatusErrorWithoutBody(t *testing.T) {
|
|
nm := NewNotificationManager("")
|
|
defer nm.Stop()
|
|
|
|
server := newIPv4HTTPServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := nm.UpdateAllowedPrivateCIDRs("127.0.0.1/32"); err != nil {
|
|
t.Fatalf("allowlist: %v", err)
|
|
}
|
|
|
|
err := nm.sendAppriseViaHTTP(AppriseConfig{
|
|
ServerURL: server.URL,
|
|
TimeoutSeconds: 2,
|
|
}, "title", "body", "info")
|
|
if err == nil || !strings.Contains(err.Error(), "HTTP 500") {
|
|
t.Fatalf("expected status error, got %v", err)
|
|
}
|
|
}
|