Files
Pulse/internal/monitoring/monitor_pmg_test.go
rcourtman 656ae0d254 fix: use proper Monitor constructor in PMG tests to initialize all maps
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.
2025-10-20 15:22:23 +00:00

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)
}
}