test(monitor): add comprehensive test suite for engine and checkers

55 tests covering state machine transitions, heartbeat handling, push
deadline checks, group aggregation, history recording, probe aggregation,
log management, state management, and concurrency safety.

Checker tests cover HTTP (via httptest), port (via net.Listen),
isCodeAccepted ranges, and siteTimeout defaults. Ping and DNS
checkers skipped (need ICMP privileges and DNS server).

Coverage: 64.2% overall, 100% on handleStatusChange, triggerAlert,
checkPush, recordCheck, and AggregateStatus.
This commit is contained in:
2026-05-23 21:06:28 -04:00
parent 4b5495fb49
commit 94296e8286
2 changed files with 1247 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
package monitor
import (
"crypto/tls"
"go-upkeep/internal/models"
"net"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"
)
func TestRunCheck_HTTP_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status)
}
if result.StatusCode != 200 {
t.Errorf("expected 200, got %d", result.StatusCode)
}
if result.LatencyNs <= 0 {
t.Error("expected positive latency")
}
}
func TestRunCheck_HTTP_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "DOWN" {
t.Errorf("expected DOWN, got %s", result.Status)
}
if result.StatusCode != 500 {
t.Errorf("expected 500, got %d", result.StatusCode)
}
}
func TestRunCheck_HTTP_CustomAcceptedCodes(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(302)
}))
defer srv.Close()
client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}}
site := models.Site{ID: 1, Type: "http", URL: srv.URL, AcceptedCodes: "200-399"}
result := RunCheck(site, client, client, false)
if result.Status != "UP" {
t.Errorf("expected UP with accepted 200-399, got %s", result.Status)
}
}
func TestRunCheck_HTTP_MethodRespected(t *testing.T) {
var receivedMethod string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedMethod = r.Method
w.WriteHeader(200)
}))
defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL, Method: "HEAD"}
RunCheck(site, http.DefaultClient, http.DefaultClient, false)
if receivedMethod != "HEAD" {
t.Errorf("expected HEAD, got %s", receivedMethod)
}
}
func TestRunCheck_HTTP_Timeout(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
w.WriteHeader(200)
}))
defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL, Timeout: 1}
result := RunCheck(site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "DOWN" {
t.Errorf("expected DOWN on timeout, got %s", result.Status)
}
}
func TestRunCheck_HTTP_SSLFields(t *testing.T) {
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}))
defer srv.Close()
insecureClient := &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
}
site := models.Site{ID: 1, Type: "http", URL: srv.URL, CheckSSL: true, IgnoreTLS: true}
result := RunCheck(site, http.DefaultClient, insecureClient, false)
if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status)
}
if !result.HasSSL {
t.Error("expected HasSSL=true")
}
if result.CertExpiry.IsZero() {
t.Error("expected CertExpiry populated")
}
}
func TestRunCheck_Port_Open(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
_, portStr, _ := net.SplitHostPort(ln.Addr().String())
port, _ := strconv.Atoi(portStr)
site := models.Site{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2}
result := RunCheck(site, nil, nil, false)
if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status)
}
if result.LatencyNs <= 0 {
t.Error("expected positive latency")
}
}
func TestRunCheck_Port_Closed(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
_, portStr, _ := net.SplitHostPort(ln.Addr().String())
port, _ := strconv.Atoi(portStr)
ln.Close()
site := models.Site{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 1}
result := RunCheck(site, nil, nil, false)
if result.Status != "DOWN" {
t.Errorf("expected DOWN, got %s", result.Status)
}
}
func TestRunCheck_UnknownType(t *testing.T) {
site := models.Site{ID: 1, Type: "invalid"}
result := RunCheck(site, nil, nil, false)
if result.Status != "DOWN" {
t.Errorf("expected DOWN for unknown type, got %s", result.Status)
}
}
func TestIsCodeAccepted(t *testing.T) {
tests := []struct {
code int
accepted string
want bool
}{
{200, "", true},
{299, "", true},
{300, "", false},
{302, "200-399", true},
{400, "200-399", false},
{301, "200,301,404", true},
{500, "200,301,404", false},
{404, "200-299,400-499", true},
{500, "200-299,400-499", false},
}
for _, tt := range tests {
got := isCodeAccepted(tt.code, tt.accepted)
if got != tt.want {
t.Errorf("isCodeAccepted(%d, %q) = %v, want %v", tt.code, tt.accepted, got, tt.want)
}
}
}
func TestSiteTimeout(t *testing.T) {
if got := siteTimeout(models.Site{Timeout: 0}); got != 5*time.Second {
t.Errorf("expected 5s default, got %v", got)
}
if got := siteTimeout(models.Site{Timeout: 10}); got != 10*time.Second {
t.Errorf("expected 10s, got %v", got)
}
}
File diff suppressed because it is too large Load Diff