mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Fixes panic: assignment to entry in nil map in PMG polling tests. **Problem:** Tests were manually creating Monitor structs without initializing internal maps like pollStatusMap, causing nil map panics when recordTaskResult() tried to update task status. **Root Cause:** - TestPollPMGInstancePopulatesState (line 90) - TestPollPMGInstanceRecordsAuthFailures (line 189) Both created Monitor with only partial field initialization, missing: - pollStatusMap - dlqInsightMap - instanceInfoCache - Other internal state maps **Solution:** Changed both tests to use New() constructor which properly initializes all maps and internal state (monitor.go:1541). This ensures tests match production initialization and will automatically pick up any future map additions. **Tests:** ✅ TestPollPMGInstancePopulatesState - now passes ✅ TestPollPMGInstanceRecordsAuthFailures - now passes ✅ All monitoring tests pass (0.125s) Follows best practice: use constructors instead of manual struct creation to maintain initialization invariants.
230 lines
7.1 KiB
Go
230 lines
7.1 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
|
|
)
|
|
|
|
func TestPollPMGInstancePopulatesState(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api2/json/access/ticket":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"ticket":"ticket123","CSRFPreventionToken":"csrf123"}}`)
|
|
|
|
case "/api2/json/version":
|
|
if !strings.Contains(r.Header.Get("Cookie"), "PMGAuthCookie=ticket123") {
|
|
t.Fatalf("expected auth cookie, got %s", r.Header.Get("Cookie"))
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"version":"8.3.1","release":"1"}}`)
|
|
|
|
case "/api2/json/config/cluster/status":
|
|
if r.URL.Query().Get("list_single_node") != "1" {
|
|
t.Fatalf("expected list_single_node query, got %s", r.URL.RawQuery)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":[{"cid":1,"name":"mail-gateway","type":"master","ip":"10.0.0.1"}]}`)
|
|
|
|
case "/api2/json/statistics/mail":
|
|
if r.URL.Query().Get("timeframe") != "day" {
|
|
t.Fatalf("expected timeframe=day, got %s", r.URL.RawQuery)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"count":100,"count_in":60,"count_out":40,"spamcount_in":5,"spamcount_out":2,"viruscount_in":1,"viruscount_out":0,"bounces_in":3,"bounces_out":1,"bytes_in":12345,"bytes_out":54321,"glcount":7,"junk_in":4,"avptime":0.5,"rbl_rejects":2,"pregreet_rejects":1}}`)
|
|
|
|
case "/api2/json/statistics/mailcount":
|
|
if r.URL.Query().Get("timespan") != "24" {
|
|
t.Fatalf("expected timespan=24, got %s", r.URL.RawQuery)
|
|
}
|
|
now := time.Now().Unix()
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprintf(w, `{"data":[{"index":0,"time":%d,"count":100,"count_in":60,"count_out":40,"spamcount_in":5,"spamcount_out":2,"viruscount_in":1,"viruscount_out":0,"bounces_in":3,"bounces_out":1,"rbl_rejects":2,"pregreet_rejects":1,"glcount":7}]}`, now)
|
|
|
|
case "/api2/json/statistics/spamscores":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":[{"level":"low","count":10,"ratio":0.1}]}`)
|
|
|
|
case "/api2/json/quarantine/spamstatus":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"count":5,"avgbytes":0,"mbytes":0}}`)
|
|
|
|
case "/api2/json/quarantine/virusstatus":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"count":2,"avgbytes":0,"mbytes":0}}`)
|
|
|
|
case "/api2/json/nodes/mail-gateway/postfix/queue":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprint(w, `{"data":{"active":5,"deferred":2,"hold":0,"maildrop":0}}`)
|
|
|
|
case "/api2/json/nodes/mail-gateway/backup":
|
|
w.Header().Set("Content-Type", "application/json")
|
|
timestamp := time.Now().Add(-8 * 24 * time.Hour).Unix()
|
|
fmt.Fprintf(w, `{"data":[{"filename":"pmg-backup_2024-01-01.tgz","size":123456,"timestamp":%d}]}`, timestamp)
|
|
|
|
default:
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client, err := pmg.NewClient(pmg.ClientConfig{
|
|
Host: server.URL,
|
|
User: "api@pmg",
|
|
Password: "secret",
|
|
VerifySSL: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected client error: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
PMGInstances: []config.PMGInstance{
|
|
{
|
|
Name: "primary",
|
|
Host: server.URL,
|
|
User: "api@pmg",
|
|
Password: "secret",
|
|
MonitorMailStats: true,
|
|
MonitorQueues: true,
|
|
MonitorQuarantine: true,
|
|
MonitorDomainStats: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
mon, err := New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("failed to create monitor: %v", err)
|
|
}
|
|
|
|
mon.pollPMGInstance(context.Background(), "primary", client)
|
|
|
|
snapshot := mon.state.GetSnapshot()
|
|
|
|
if len(snapshot.PMGInstances) != 1 {
|
|
t.Fatalf("expected 1 PMG instance in state, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
instance := snapshot.PMGInstances[0]
|
|
|
|
if instance.Status != "online" {
|
|
t.Fatalf("expected PMG status online, got %s", instance.Status)
|
|
}
|
|
|
|
if instance.ConnectionHealth != "healthy" {
|
|
t.Fatalf("expected connection health healthy, got %s", instance.ConnectionHealth)
|
|
}
|
|
|
|
if instance.MailStats == nil || instance.MailStats.CountTotal != 100 {
|
|
t.Fatalf("expected mail stats totals, got %+v", instance.MailStats)
|
|
}
|
|
|
|
if len(instance.MailCount) != 1 {
|
|
t.Fatalf("expected 1 mail count point, got %d", len(instance.MailCount))
|
|
}
|
|
|
|
if len(instance.SpamDistribution) != 1 {
|
|
t.Fatalf("expected 1 spam distribution bucket, got %d", len(instance.SpamDistribution))
|
|
}
|
|
|
|
if instance.Quarantine == nil || instance.Quarantine.Spam != 5 || instance.Quarantine.Virus != 2 {
|
|
t.Fatalf("expected quarantine counts, got %+v", instance.Quarantine)
|
|
}
|
|
|
|
if health := snapshot.ConnectionHealth["pmg-primary"]; !health {
|
|
t.Fatalf("expected connection health tracked as healthy, got %v", health)
|
|
}
|
|
|
|
if failures := mon.authFailures["pmg-primary"]; failures != 0 {
|
|
t.Fatalf("expected no auth failures tracked, got %d", failures)
|
|
}
|
|
|
|
if len(snapshot.PMGBackups) != 1 {
|
|
t.Fatalf("expected 1 PMG backup in state, got %d", len(snapshot.PMGBackups))
|
|
}
|
|
|
|
pmgBackup := snapshot.PMGBackups[0]
|
|
if pmgBackup.Node != "mail-gateway" {
|
|
t.Fatalf("expected PMG backup node mail-gateway, got %s", pmgBackup.Node)
|
|
}
|
|
}
|
|
|
|
func TestPollPMGInstanceRecordsAuthFailures(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api2/json/version":
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
fmt.Fprint(w, "unauthorized")
|
|
|
|
case "/api2/json/access/ticket":
|
|
t.Fatalf("token client should not request auth ticket")
|
|
|
|
default:
|
|
t.Fatalf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client, err := pmg.NewClient(pmg.ClientConfig{
|
|
Host: server.URL,
|
|
User: "apitest@pmg",
|
|
TokenName: "apitoken",
|
|
TokenValue: "secret",
|
|
VerifySSL: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error creating token client: %v", err)
|
|
}
|
|
|
|
cfg := &config.Config{
|
|
PMGInstances: []config.PMGInstance{
|
|
{
|
|
Name: "failing",
|
|
Host: server.URL,
|
|
User: "apitest@pmg",
|
|
TokenName: "apitoken",
|
|
TokenValue: "secret",
|
|
},
|
|
},
|
|
}
|
|
|
|
mon, err := New(cfg)
|
|
if err != nil {
|
|
t.Fatalf("failed to create monitor: %v", err)
|
|
}
|
|
|
|
mon.pollPMGInstance(context.Background(), "failing", client)
|
|
|
|
snapshot := mon.state.GetSnapshot()
|
|
|
|
if health := snapshot.ConnectionHealth["pmg-failing"]; health {
|
|
t.Fatalf("expected unhealthy connection, got %v", health)
|
|
}
|
|
|
|
if len(snapshot.PMGInstances) != 1 {
|
|
t.Fatalf("expected failed PMG instance in state, got %d", len(snapshot.PMGInstances))
|
|
}
|
|
|
|
instance := snapshot.PMGInstances[0]
|
|
if instance.Status != "offline" {
|
|
t.Fatalf("expected offline status, got %s", instance.Status)
|
|
}
|
|
|
|
if instance.ConnectionHealth != "unhealthy" {
|
|
t.Fatalf("expected unhealthy connection status, got %s", instance.ConnectionHealth)
|
|
}
|
|
|
|
if failures := mon.authFailures["pmg-failing"]; failures != 1 {
|
|
t.Fatalf("expected one auth failure tracked, got %d", failures)
|
|
}
|
|
}
|