The mesh noticed 48 core failures in six days and told nobody (ADR 0227). messenger holds the operator-channel seat: it consumes the controller's condition events and sends them to Telegram and the desktop notifier, deduplicated by key, reminded once, edited on clear, capped at 20 an hour with the rest folded, and refusing anything carrying an address, a path or a secret. mesh-watcher, on a machine other than the control node, sends to Telegram directly when the self-check heartbeat or the bus goes silent.
92 lines
3.0 KiB
Go
92 lines
3.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const goodToken = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawQ"
|
|
|
|
func tokenFile(t *testing.T, content string) string {
|
|
t.Helper()
|
|
p := filepath.Join(t.TempDir(), "telegram-token")
|
|
if content != "" {
|
|
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestTelegramSaysWhatItLacks(t *testing.T) {
|
|
for _, c := range []struct{ token, chat, says string }{
|
|
{"", "42", "not on this machine yet"},
|
|
{"r4nd0mlyMadeByTheMeshBecauseNobodyAcceptedOne", "42", "not a bot token"},
|
|
{goodToken, "", "telegram-chat-id is not given"},
|
|
{goodToken, "not a chat", "is not a chat id"},
|
|
} {
|
|
tg := NewTelegram(TelegramConfig{TokenFile: tokenFile(t, c.token), ChatID: func() string { return c.chat }})
|
|
err := tg.Ready()
|
|
if err == nil || !strings.Contains(err.Error(), c.says) {
|
|
t.Errorf("%+v: %v", c, err)
|
|
}
|
|
if err != nil && strings.Contains(err.Error(), goodToken) {
|
|
t.Errorf("the token is in the words")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTelegramSendsEditsAndNeverSaysItsToken(t *testing.T) {
|
|
var asked []string
|
|
var bodies []map[string]any
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
asked = append(asked, r.URL.Path)
|
|
raw, _ := io.ReadAll(r.Body)
|
|
var b map[string]any
|
|
_ = json.Unmarshal(raw, &b)
|
|
bodies = append(bodies, b)
|
|
switch {
|
|
case strings.HasSuffix(r.URL.Path, "/sendMessage"):
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":{"message_id":77}}`))
|
|
case strings.HasSuffix(r.URL.Path, "/editMessageText"):
|
|
_, _ = w.Write([]byte(`{"ok":false,"error_code":400,"description":"Bad Request: message to edit not found"}`))
|
|
default:
|
|
_, _ = w.Write([]byte(`{"ok":true,"result":{"username":"mesh_bot"}}`))
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
old := TelegramAPI
|
|
TelegramAPI = srv.URL
|
|
defer func() { TelegramAPI = old }()
|
|
tg := NewTelegram(TelegramConfig{TokenFile: tokenFile(t, goodToken+"\n"), ChatID: func() string { return "-1001" }})
|
|
id, err := tg.Send(Message{Title: "URGENT: x", Body: "key: a.b.c"})
|
|
if err != nil || id != "77" {
|
|
t.Fatalf("%q %v", id, err)
|
|
}
|
|
if asked[0] != "/bot"+goodToken+"/sendMessage" || bodies[0]["chat_id"] != "-1001" || bodies[0]["text"] != "URGENT: x\nkey: a.b.c" {
|
|
t.Fatalf("%v %v", asked, bodies[0])
|
|
}
|
|
err = tg.Edit("77", Message{Title: "CLEARED"})
|
|
if err == nil || !strings.Contains(err.Error(), "message to edit not found") || strings.Contains(err.Error(), goodToken) {
|
|
t.Fatalf("edit: %v", err)
|
|
}
|
|
if bodies[1]["message_id"] != float64(77) {
|
|
t.Fatalf("message id: %v", bodies[1]["message_id"])
|
|
}
|
|
if who, err := tg.Who(); err != nil || who != "mesh_bot" {
|
|
t.Fatalf("%q %v", who, err)
|
|
}
|
|
// Nothing answers: the error is in words, without the URL that carries the token.
|
|
srv.Close()
|
|
_, err = tg.Send(Message{Title: "x"})
|
|
if err == nil || strings.Contains(err.Error(), goodToken) || strings.Contains(err.Error(), "/bot") {
|
|
t.Fatalf("unreachable: %v", err)
|
|
}
|
|
}
|