168 lines
5.3 KiB
Go
168 lines
5.3 KiB
Go
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")
|
||
}
|
||
}
|