refactor(monitor): encapsulate engine state, add graceful shutdown and tests
Replace all monitor package-level mutable state with Engine struct. All state (liveState, logStore, histories, tokenIndex, HTTP clients) is now encapsulated in Engine, created via NewEngine(store). Key changes: - Engine struct holds all monitor state with proper mutex protection - Engine.Start(ctx) and monitorRoutine respect context cancellation for graceful shutdown — no more leaked goroutines - cluster.runFollowerLoop also respects context for clean exit - Token index (map[string]int) for O(1) push heartbeat lookup, replacing O(n) linear scan through LiveState - UpdateSiteConfig preserves 8 runtime fields instead of copying 17 config fields individually - triggerAlert goroutines get 30s timeout context - All consumers (TUI, server, cluster, main) receive *Engine via constructor/parameter — no package-level state access - main.go creates context.WithCancel, passes to engine and cluster First test suite: 12 tests across store and alert packages - Store: CRUD for sites/alerts/users, push token generation, import/export round-trip, check history persistence - Alert: Discord/Slack/Webhook payload format, HTTP 4xx error propagation, Ntfy headers, unknown provider returns nil
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package alert
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go-upkeep/internal/models"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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("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("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("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("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("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 TestGetProviderUnknown(t *testing.T) {
|
||||
p := GetProvider(models.AlertConfig{Type: "unknown"})
|
||||
if p != nil {
|
||||
t.Error("expected nil for unknown provider type")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user