Files
l-ui/internal/web/service/probenotify/channels_test.go
T

168 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package probenotify
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
func TestWebhookPostRendersAndClassifies(t *testing.T) {
newFakeEnv(t) // post() reads the site name through the settings seam
var mu sync.Mutex
var gotBody []byte
var gotHeader string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
gotBody, _ = io.ReadAll(r.Body)
gotHeader = r.Header.Get("X-Api-Key")
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
ch := webhookChannel{
url: srv.URL,
headers: "X-Api-Key: s3cret",
body: `{"text":"{{title}}: {{message}}","node":"{{node}}"}`,
}
note := Note{Event: "offline", Node: "tokyo", Title: "🔴 tokyo 离线", Message: "最后上报 \"x\"", Time: t0.Unix()}
if f := ch.post(note); f != nil {
t.Fatalf("post failed: %+v", f)
}
mu.Lock()
defer mu.Unlock()
if gotHeader != "s3cret" {
t.Fatalf("custom header not sent: %q", gotHeader)
}
var parsed map[string]string
if err := json.Unmarshal(gotBody, &parsed); err != nil {
t.Fatalf("body not JSON: %v (%s)", err, gotBody)
}
if parsed["node"] != "tokyo" || !strings.Contains(parsed["text"], "🔴 tokyo 离线") {
t.Fatalf("placeholders not substituted: %v", parsed)
}
// Failure classification: 5xx retries, 4xx does not, a redirect
// is reported rather than followed.
codes := []struct {
code int
retry bool
}{
{500, true}, {429, true}, {400, false}, {401, false}, {404, false},
}
for _, c := range codes {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(c.code)
}))
f := webhookChannel{url: s.URL, body: defaultBody}.post(note)
s.Close()
if f == nil || f.retry != c.retry {
t.Errorf("HTTP %d: failure = %+v, want retry=%v", c.code, f, c.retry)
}
}
redir := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/elsewhere")
w.WriteHeader(http.StatusFound)
}))
defer redir.Close()
f := webhookChannel{url: redir.URL, body: defaultBody}.post(note)
if f == nil || !strings.Contains(f.shown, "跳转") {
t.Fatalf("redirect must be reported, not followed: %+v", f)
}
if strings.Contains(f.reason, redir.URL) {
t.Fatalf("the webhook URL (a credential) must not leak into the reason: %q", f.reason)
}
}
func TestTelegramPostShapeAndSecrecy(t *testing.T) {
newFakeEnv(t)
var mu sync.Mutex
var paths []string
var bodies []map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
paths = append(paths, r.URL.Path)
var body map[string]any
data, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(data, &body)
bodies = append(bodies, body)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
ch := telegramChannel{
token: "123:SECRET",
chats: []int64{42, -100},
text: "{{title}}\n{{message}}",
apiBase: srv.URL,
}
note := Note{Event: "login", Title: "🔑 面板登录", Message: "密码登录 · 来自 203.0.113.9", Time: t0.Unix()}
if f := ch.post(note); f != nil {
t.Fatalf("post failed: %+v", f)
}
mu.Lock()
if len(paths) != 2 || paths[0] != "/bot123:SECRET/sendMessage" {
t.Fatalf("want one sendMessage per chat, got %v", paths)
}
if bodies[0]["chat_id"] != float64(42) || bodies[1]["chat_id"] != float64(-100) {
t.Fatalf("chat ids wrong: %v", bodies)
}
if bodies[0]["text"] != "🔑 面板登录\n密码登录 · 来自 203.0.113.9" {
t.Fatalf("text not rendered: %v", bodies[0]["text"])
}
mu.Unlock()
// A 401 answers "Bot Token 不对" and the reason never carries
// the token.
bad := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer bad.Close()
ch.apiBase = bad.URL
f := ch.post(note)
if f == nil || !strings.Contains(f.shown, "Bot Token 不对") {
t.Fatalf("401 must read as a bad token: %+v", f)
}
if strings.Contains(f.reason, "SECRET") || strings.Contains(f.shown, "SECRET") {
t.Fatalf("the token must not leak: reason %q shown %q", f.reason, f.shown)
}
}
func TestTestSendChannels(t *testing.T) {
e := newFakeEnv(t)
if _, err := Test(); err == nil || err.Error() != "还没有配置通知渠道" {
t.Fatalf("no channels must be its own error, got %v", err)
}
ok := &fakeChannel{label: "Webhook"}
e.channels = []channel{ok}
sent, err := Test()
if err != nil || len(sent) != 1 || sent[0] != "Webhook" || len(ok.notes) != 1 || ok.notes[0].Event != "test" {
t.Fatalf("test send = %v, %v, notes %v", sent, err, ok.notes)
}
e.channels = []channel{&fakeChannel{label: "Telegram", fail: &failure{shown: "Bot Token 不对(HTTP 401)", reason: "HTTP 401"}}}
if _, err := Test(); err == nil || !strings.Contains(err.Error(), "Telegram:Bot Token 不对") {
t.Fatalf("a failing channel must surface its own error, got %v", err)
}
}
func TestTelegramConfiguredParsing(t *testing.T) {
e := newFakeEnv(t)
if TelegramConfigured() {
t.Fatal("nothing set must be unconfigured")
}
e.settings["tgBotToken"] = "123:abc"
e.settings["tgBotChatId"] = "not-a-number"
if TelegramConfigured() {
t.Fatal("a non-numeric chat list is not a channel")
}
e.settings["tgBotChatId"] = "42, -100123 , junk"
if !TelegramConfigured() {
t.Fatal("numeric ids among the list configure the channel")
}
}