60b30935b3
- Redact PostgreSQL DSN password from stdout/logs - Harden .dockerignore to exclude .ssh/, .claude/, *.db, *.local files - SSRF protection: block private/loopback/link-local IPs by default (UPTOP_ALLOW_PRIVATE_TARGETS=true to override for homelab use) - Fix email header injection via CRLF in monitor names - AES-256-GCM encryption for alert credentials at rest (UPTOP_ENCRYPTION_KEY env var, migrate-secrets subcommand) - TLS support for HTTP server (UPTOP_TLS_CERT/UPTOP_TLS_KEY) with HSTS header when TLS enabled
233 lines
7.1 KiB
Go
233 lines
7.1 KiB
Go
package alert
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"gitea.lerkolabs.com/lerko/uptop/internal/models"
|
|
)
|
|
|
|
func TestHTTPProviderDiscord(t *testing.T) {
|
|
var received map[string]string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
|
if err := p.Send(context.Background(), "Test Title", "Test Body"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
|
|
if received["content"] != "**Test Title**\nTest Body" {
|
|
t.Errorf("unexpected payload: %s", received["content"])
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderSlack(t *testing.T) {
|
|
var received map[string]string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := GetProvider(models.AlertConfig{Type: "slack", Settings: map[string]string{"url": srv.URL}})
|
|
if err := p.Send(context.Background(), "Alert", "Message"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
|
|
if received["text"] != "*Alert*\nMessage" {
|
|
t.Errorf("unexpected payload: %s", received["text"])
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderWebhook(t *testing.T) {
|
|
var received map[string]string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := GetProvider(models.AlertConfig{Type: "webhook", Settings: map[string]string{"url": srv.URL}})
|
|
if err := p.Send(context.Background(), "Title", "Body"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
|
|
if received["title"] != "Title" || received["message"] != "Body" || received["status"] != "alert" {
|
|
t.Errorf("unexpected webhook payload: %v", received)
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderErrorOnHTTP4xx(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(403)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
|
if err := p.Send(context.Background(), "Test", "Test"); err == nil {
|
|
t.Fatal("expected error on 403 response")
|
|
}
|
|
}
|
|
|
|
func TestNtfyProvider(t *testing.T) {
|
|
var title, body string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
title = r.Header.Get("Title")
|
|
buf := make([]byte, 1024)
|
|
n, _ := r.Body.Read(buf)
|
|
body = string(buf[:n])
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := GetProvider(models.AlertConfig{Type: "ntfy", Settings: map[string]string{
|
|
"url": srv.URL,
|
|
"topic": "test",
|
|
}})
|
|
if err := p.Send(context.Background(), "Alert Title", "Alert Body"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
|
|
if title != "Alert Title" {
|
|
t.Errorf("expected title 'Alert Title', got '%s'", title)
|
|
}
|
|
if body != "Alert Body" {
|
|
t.Errorf("expected body 'Alert Body', got '%s'", body)
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderTelegram(t *testing.T) {
|
|
var received map[string]string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &HTTPProvider{URL: srv.URL, Payload: telegramPayload("12345")}
|
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
if received["chat_id"] != "12345" {
|
|
t.Errorf("expected chat_id '12345', got '%s'", received["chat_id"])
|
|
}
|
|
if received["text"] != "*Alert*\nDown" {
|
|
t.Errorf("unexpected text: %s", received["text"])
|
|
}
|
|
if received["parse_mode"] != "Markdown" {
|
|
t.Errorf("expected parse_mode 'Markdown', got '%s'", received["parse_mode"])
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderPagerDuty(t *testing.T) {
|
|
var received map[string]any
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &HTTPProvider{URL: srv.URL, Payload: pagerdutyPayload("test-key", "critical")}
|
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
if received["routing_key"] != "test-key" {
|
|
t.Errorf("expected routing_key 'test-key', got '%v'", received["routing_key"])
|
|
}
|
|
if received["event_action"] != "trigger" {
|
|
t.Errorf("expected event_action 'trigger', got '%v'", received["event_action"])
|
|
}
|
|
payload := received["payload"].(map[string]any)
|
|
if payload["summary"] != "Alert: Down" {
|
|
t.Errorf("unexpected summary: %v", payload["summary"])
|
|
}
|
|
if payload["severity"] != "critical" {
|
|
t.Errorf("expected severity 'critical', got '%v'", payload["severity"])
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderPushover(t *testing.T) {
|
|
var received map[string]string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &HTTPProvider{URL: srv.URL, Payload: pushoverPayload("app-tok", "user-key")}
|
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
if received["token"] != "app-tok" {
|
|
t.Errorf("expected token 'app-tok', got '%s'", received["token"])
|
|
}
|
|
if received["user"] != "user-key" {
|
|
t.Errorf("expected user 'user-key', got '%s'", received["user"])
|
|
}
|
|
if received["title"] != "Alert" || received["message"] != "Down" {
|
|
t.Errorf("unexpected payload: %v", received)
|
|
}
|
|
}
|
|
|
|
func TestHTTPProviderGotify(t *testing.T) {
|
|
var received map[string]any
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
json.NewDecoder(r.Body).Decode(&received)
|
|
w.WriteHeader(200)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := &HTTPProvider{URL: srv.URL, Payload: gotifyPayload("8")}
|
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
|
t.Fatalf("Send: %v", err)
|
|
}
|
|
if received["title"] != "Alert" || received["message"] != "Down" {
|
|
t.Errorf("unexpected payload: %v", received)
|
|
}
|
|
if pri, ok := received["priority"].(float64); !ok || pri != 8 {
|
|
t.Errorf("expected priority 8, got %v", received["priority"])
|
|
}
|
|
}
|
|
|
|
func TestGetProviderNewTypes(t *testing.T) {
|
|
for _, typ := range []string{"telegram", "pagerduty", "pushover", "gotify"} {
|
|
p := GetProvider(models.AlertConfig{Type: typ, Settings: map[string]string{
|
|
"token": "x", "chat_id": "1", "routing_key": "k", "user": "u", "url": "http://localhost",
|
|
}})
|
|
if p == nil {
|
|
t.Errorf("GetProvider(%q) returned nil", typ)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetProviderUnknown(t *testing.T) {
|
|
p := GetProvider(models.AlertConfig{Type: "unknown"})
|
|
if p != nil {
|
|
t.Error("expected nil for unknown provider type")
|
|
}
|
|
}
|
|
|
|
func TestSanitizeHeader(t *testing.T) {
|
|
tests := []struct {
|
|
input, want string
|
|
}{
|
|
{"normal subject", "normal subject"},
|
|
{"inject\r\nBcc: evil@bad.com", "injectBcc: evil@bad.com"},
|
|
{"has\nnewline", "hasnewline"},
|
|
{"has\rcarriage", "hascarriage"},
|
|
}
|
|
for _, tt := range tests {
|
|
got := sanitizeHeader(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("sanitizeHeader(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|