refactor(models): split Site into SiteConfig + SiteState
CI / test (pull_request) Successful in 1m58s
CI / lint (pull_request) Successful in 1m21s
CI / vulncheck (pull_request) Successful in 1m2s

Site now embeds SiteConfig (22 persistent fields) and SiteState
(11 ephemeral runtime fields). Field access unchanged via promotion
— site.Name and site.Status still work.

Store layer deals exclusively in SiteConfig — the DB never sees
runtime state. Engine's liveState keeps full Site composites.
UpdateSiteConfig reduced from 11-line field-by-field copy to
`existing.SiteConfig = cfg`.

RunCheck takes SiteConfig (only needs config fields). Checker is
now statically prevented from reading/writing runtime state.

Backup.Sites changed to []SiteConfig — exports no longer carry
zero-valued runtime fields. Import backward-compatible (json
ignores unknown fields).
This commit was merged in pull request #109.
This commit is contained in:
2026-06-11 17:13:09 -04:00
parent ba4465daa2
commit 52ccd7ad91
23 changed files with 356 additions and 230 deletions
+1 -1
View File
@@ -474,7 +474,7 @@ func seedDemoData(s store.Store) {
alertID = alerts[0].ID alertID = alerts[0].ID
} }
demoSites := []models.Site{ demoSites := []models.SiteConfig{
{Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2}, {Name: "Google", URL: "https://www.google.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 14, MaxRetries: 2},
{Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3}, {Name: "GitHub", URL: "https://github.com", Type: "http", Interval: 30, AlertID: alertID, CheckSSL: true, ExpiryThreshold: 7, MaxRetries: 3},
{Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1}, {Name: "Cloudflare DNS", URL: "https://1.1.1.1", Type: "http", Interval: 60, AlertID: alertID, ExpiryThreshold: 7, MaxRetries: 1},
+4 -4
View File
@@ -203,7 +203,7 @@ func TestProbeRegister_Failure(t *testing.T) {
func TestProbeFetchAssignments_Success(t *testing.T) { func TestProbeFetchAssignments_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string][]models.Site{ json.NewEncoder(w).Encode(map[string][]models.Site{
"sites": {{ID: 1, Name: "s1", Type: "http", URL: "http://example.com"}}, "sites": {{SiteConfig: models.SiteConfig{ID: 1, Name: "s1", Type: "http", URL: "http://example.com"}}},
}) })
})) }))
defer srv.Close() defer srv.Close()
@@ -240,8 +240,8 @@ func TestProbeExecuteChecks(t *testing.T) {
defer srv.Close() defer srv.Close()
sites := []models.Site{ sites := []models.Site{
{ID: 1, Type: "http", URL: srv.URL}, {SiteConfig: models.SiteConfig{ID: 1, Type: "http", URL: srv.URL}},
{ID: 2, Type: "http", URL: srv.URL}, {SiteConfig: models.SiteConfig{ID: 2, Type: "http", URL: srv.URL}},
} }
strict := &http.Client{} strict := &http.Client{}
@@ -277,7 +277,7 @@ func TestProbeExecuteChecks_Concurrency(t *testing.T) {
var sites []models.Site var sites []models.Site
for i := 0; i < 20; i++ { for i := 0; i < 20; i++ {
sites = append(sites, models.Site{ID: i + 1, Type: "http", URL: srv.URL}) sites = append(sites, models.Site{SiteConfig: models.SiteConfig{ID: i + 1, Type: "http", URL: srv.URL}})
} }
results := probeExecuteChecks(context.Background(), sites, &http.Client{}, &http.Client{}, true) results := probeExecuteChecks(context.Background(), sites, &http.Client{}, &http.Client{}, true)
+1 -1
View File
@@ -152,7 +152,7 @@ loop:
defer wg.Done() defer wg.Done()
defer func() { <-sem }() defer func() { <-sem }()
cr := monitor.RunCheck(ctx, s, strict, insecure, false, allowPrivate) cr := monitor.RunCheck(ctx, s.SiteConfig, strict, insecure, false, allowPrivate)
mu.Lock() mu.Lock()
results = append(results, probeResultItem{ results = append(results, probeResultItem{
SiteID: s.ID, SiteID: s.ID,
+6 -6
View File
@@ -42,7 +42,7 @@ func Apply(ctx context.Context, s store.Store, f *File, opts ApplyOpts) ([]Chang
existingAlertsByName[a.Name] = a existingAlertsByName[a.Name] = a
} }
existingSitesByName := make(map[string]models.Site, len(existingSites)) existingSitesByName := make(map[string]models.SiteConfig, len(existingSites))
for _, s := range existingSites { for _, s := range existingSites {
existingSitesByName[s.Name] = s existingSitesByName[s.Name] = s
} }
@@ -181,7 +181,7 @@ func Apply(ctx context.Context, s store.Store, f *File, opts ApplyOpts) ([]Chang
return changes, nil return changes, nil
} }
func applyMonitor(ctx context.Context, s store.Store, m Monitor, alertMap map[string]int, existing map[string]models.Site, parentID int, dryRun bool) ([]Change, error) { func applyMonitor(ctx context.Context, s store.Store, m Monitor, alertMap map[string]int, existing map[string]models.SiteConfig, parentID int, dryRun bool) ([]Change, error) {
alertID, err := resolveAlertID(alertMap, m.Alert) alertID, err := resolveAlertID(alertMap, m.Alert)
if err != nil { if err != nil {
return nil, fmt.Errorf("monitor %q: %w", m.Name, err) return nil, fmt.Errorf("monitor %q: %w", m.Name, err)
@@ -222,8 +222,8 @@ func resolveAlertID(alertMap map[string]int, name string) (int, error) {
return id, nil return id, nil
} }
func monitorToSite(m Monitor, alertID, parentID int) models.Site { func monitorToSite(m Monitor, alertID, parentID int) models.SiteConfig {
s := models.Site{ s := models.SiteConfig{
Name: m.Name, Name: m.Name,
Type: m.Type, Type: m.Type,
URL: m.URL, URL: m.URL,
@@ -269,7 +269,7 @@ func collectMonitorNames(monitors []Monitor, names map[string]bool) {
} }
} }
func normalizeSite(s models.Site) models.Site { func normalizeSite(s models.SiteConfig) models.SiteConfig {
if s.Method == "" { if s.Method == "" {
s.Method = "GET" s.Method = "GET"
} }
@@ -293,7 +293,7 @@ func diffAlert(existing models.AlertConfig, desired Alert) string {
return strings.Join(diffs, ", ") return strings.Join(diffs, ", ")
} }
func diffSite(existing, desired models.Site) string { func diffSite(existing, desired models.SiteConfig) string {
var diffs []string var diffs []string
if existing.URL != desired.URL { if existing.URL != desired.URL {
diffs = append(diffs, fmt.Sprintf("url: %s -> %s", existing.URL, desired.URL)) diffs = append(diffs, fmt.Sprintf("url: %s -> %s", existing.URL, desired.URL))
+3 -3
View File
@@ -114,8 +114,8 @@ func TestApplyUpdate(t *testing.T) {
func TestApplyPrune(t *testing.T) { func TestApplyPrune(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
s.AddSite(context.Background(), models.Site{Name: "Keep", URL: "https://keep.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s.AddSite(context.Background(), models.SiteConfig{Name: "Keep", URL: "https://keep.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
s.AddSite(context.Background(), models.Site{Name: "Remove", URL: "https://remove.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s.AddSite(context.Background(), models.SiteConfig{Name: "Remove", URL: "https://remove.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
f := &File{ f := &File{
Monitors: []Monitor{ Monitors: []Monitor{
@@ -191,7 +191,7 @@ func TestApplyGroupHierarchy(t *testing.T) {
} }
sites, _ := s.GetSites(context.Background()) sites, _ := s.GetSites(context.Background())
var group models.Site var group models.SiteConfig
for _, s := range sites { for _, s := range sites {
if s.Type == "group" { if s.Type == "group" {
group = s group = s
+4 -4
View File
@@ -34,9 +34,9 @@ func Export(ctx context.Context, s store.Store) (*File, error) {
}) })
} }
groups := make(map[int]models.Site) groups := make(map[int]models.SiteConfig)
children := make(map[int][]models.Site) children := make(map[int][]models.SiteConfig)
var topLevel []models.Site var topLevel []models.SiteConfig
for _, s := range dbSites { for _, s := range dbSites {
switch { switch {
@@ -76,7 +76,7 @@ func Export(ctx context.Context, s store.Store) (*File, error) {
return &File{Alerts: yamlAlerts, Monitors: yamlMonitors}, nil return &File{Alerts: yamlAlerts, Monitors: yamlMonitors}, nil
} }
func siteToMonitor(s models.Site, alertIDToName map[int]string) Monitor { func siteToMonitor(s models.SiteConfig, alertIDToName map[int]string) Monitor {
m := Monitor{ m := Monitor{
Name: s.Name, Name: s.Name,
Type: s.Type, Type: s.Type,
+7 -7
View File
@@ -22,7 +22,7 @@ func TestExportAlertNames(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
s.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com"}) s.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com"})
alerts, _ := s.GetAllAlerts(context.Background()) alerts, _ := s.GetAllAlerts(context.Background())
s.AddSite(context.Background(), models.Site{Name: "Web", URL: "https://example.com", Type: "http", Interval: 30, AlertID: alerts[0].ID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s.AddSite(context.Background(), models.SiteConfig{Name: "Web", URL: "https://example.com", Type: "http", Interval: 30, AlertID: alerts[0].ID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
f, err := Export(context.Background(), s) f, err := Export(context.Background(), s)
if err != nil { if err != nil {
@@ -39,9 +39,9 @@ func TestExportAlertNames(t *testing.T) {
func TestExportGroupHierarchy(t *testing.T) { func TestExportGroupHierarchy(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
groupID, _ := s.AddSiteReturningID(context.Background(), models.Site{Name: "Prod", Type: "group", ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) groupID, _ := s.AddSiteReturningID(context.Background(), models.SiteConfig{Name: "Prod", Type: "group", ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
s.AddSite(context.Background(), models.Site{Name: "Prod Web", URL: "https://prod.example.com", Type: "http", Interval: 15, ParentID: groupID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s.AddSite(context.Background(), models.SiteConfig{Name: "Prod Web", URL: "https://prod.example.com", Type: "http", Interval: 15, ParentID: groupID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
s.AddSite(context.Background(), models.Site{Name: "Top Level", URL: "https://example.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s.AddSite(context.Background(), models.SiteConfig{Name: "Top Level", URL: "https://example.com", Type: "http", Interval: 30, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
f, err := Export(context.Background(), s) f, err := Export(context.Background(), s)
if err != nil { if err != nil {
@@ -72,7 +72,7 @@ func TestExportGroupHierarchy(t *testing.T) {
func TestExportOmitsDefaults(t *testing.T) { func TestExportOmitsDefaults(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
s.AddSite(context.Background(), models.Site{ s.AddSite(context.Background(), models.SiteConfig{
Name: "Web", URL: "https://example.com", Type: "http", Interval: 30, Name: "Web", URL: "https://example.com", Type: "http", Interval: 30,
Method: "GET", AcceptedCodes: "200-299", ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299", ExpiryThreshold: 7,
}) })
@@ -98,8 +98,8 @@ func TestExportRoundTrip(t *testing.T) {
s1 := newTestStore(t) s1 := newTestStore(t)
s1.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com"}) s1.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com"})
alerts, _ := s1.GetAllAlerts(context.Background()) alerts, _ := s1.GetAllAlerts(context.Background())
s1.AddSite(context.Background(), models.Site{Name: "Web", URL: "https://example.com", Type: "http", Interval: 30, AlertID: alerts[0].ID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s1.AddSite(context.Background(), models.SiteConfig{Name: "Web", URL: "https://example.com", Type: "http", Interval: 30, AlertID: alerts[0].ID, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
s1.AddSite(context.Background(), models.Site{Name: "Ping", Type: "ping", Hostname: "10.0.0.1", Interval: 60, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"}) s1.AddSite(context.Background(), models.SiteConfig{Name: "Ping", Type: "ping", Hostname: "10.0.0.1", Interval: 60, ExpiryThreshold: 7, Method: "GET", AcceptedCodes: "200-299"})
exported, err := Export(context.Background(), s1) exported, err := Export(context.Background(), s1)
if err != nil { if err != nil {
+5 -4
View File
@@ -3,9 +3,10 @@ package importer
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
"os" "os"
"strings" "strings"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
) )
type KumaBackup struct { type KumaBackup struct {
@@ -80,7 +81,7 @@ func ConvertKuma(kb *KumaBackup) models.Backup {
} }
} }
var sites []models.Site var sites []models.SiteConfig
for _, m := range kb.MonitorList { for _, m := range kb.MonitorList {
site := convertKumaMonitor(m, kumaToUpkeepAlert) site := convertKumaMonitor(m, kumaToUpkeepAlert)
sites = append(sites, site) sites = append(sites, site)
@@ -132,8 +133,8 @@ func convertKumaNotifications(entries []KumaNotifEntry) map[int]models.AlertConf
return result return result
} }
func convertKumaMonitor(m KumaMonitor, alertMap map[int]int) models.Site { func convertKumaMonitor(m KumaMonitor, alertMap map[int]int) models.SiteConfig {
site := models.Site{ site := models.SiteConfig{
ID: m.ID, ID: m.ID,
Name: m.Name, Name: m.Name,
Description: m.Description, Description: m.Description,
+3 -3
View File
@@ -15,16 +15,16 @@ import (
type mockStore struct { type mockStore struct {
storetest.BaseMock storetest.BaseMock
sites []models.Site sites []models.SiteConfig
} }
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { func (m *mockStore) GetSites(_ context.Context) ([]models.SiteConfig, error) {
return m.sites, nil return m.sites, nil
} }
func TestMetricsHandler(t *testing.T) { func TestMetricsHandler(t *testing.T) {
ms := &mockStore{ ms := &mockStore{
sites: []models.Site{ sites: []models.SiteConfig{
{ID: 1, Name: "Example", URL: "https://example.com", Type: "http", Interval: 30}, {ID: 1, Name: "Example", URL: "https://example.com", Type: "http", Interval: 30},
{ID: 2, Name: "DNS Check", Type: "dns", Interval: 60}, {ID: 2, Name: "DNS Check", Type: "dns", Interval: 60},
}, },
+9 -2
View File
@@ -2,7 +2,7 @@ package models
import "time" import "time"
type Site struct { type SiteConfig struct {
ID int ID int
Name string Name string
URL string URL string
@@ -26,7 +26,9 @@ type Site struct {
IgnoreTLS bool IgnoreTLS bool
Paused bool Paused bool
Regions string Regions string
}
type SiteState struct {
FailureCount int FailureCount int
Status Status Status Status
StatusCode int StatusCode int
@@ -40,6 +42,11 @@ type Site struct {
LastSuccessAt time.Time LastSuccessAt time.Time
} }
type Site struct {
SiteConfig
SiteState
}
type StateChange struct { type StateChange struct {
ID int ID int
SiteID int SiteID int
@@ -103,7 +110,7 @@ type MaintenanceWindow struct {
} }
type Backup struct { type Backup struct {
Sites []Site `json:"sites"` Sites []SiteConfig `json:"sites"`
Alerts []AlertConfig `json:"alerts"` Alerts []AlertConfig `json:"alerts"`
Users []User `json:"users"` Users []User `json:"users"`
MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"` MaintenanceWindows []MaintenanceWindow `json:"maintenance_windows,omitempty"`
+6 -6
View File
@@ -35,7 +35,7 @@ type CheckResult struct {
ErrorReason string ErrorReason string
} }
func RunCheck(ctx context.Context, site models.Site, strict, insecure *http.Client, globalInsecure bool, allowPrivate ...bool) CheckResult { func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure bool, allowPrivate ...bool) CheckResult {
private := len(allowPrivate) > 0 && allowPrivate[0] private := len(allowPrivate) > 0 && allowPrivate[0]
if site.Type != "http" && site.Type != "dns" && !private { if site.Type != "http" && site.Type != "dns" && !private {
@@ -68,7 +68,7 @@ func RunCheck(ctx context.Context, site models.Site, strict, insecure *http.Clie
} }
} }
func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.Client, globalInsecure bool) CheckResult { func runHTTPCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure bool) CheckResult {
method := site.Method method := site.Method
if method == "" { if method == "" {
method = "GET" method = "GET"
@@ -128,7 +128,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
return result return result
} }
func runPingCheck(_ context.Context, site models.Site) CheckResult { func runPingCheck(_ context.Context, site models.SiteConfig) CheckResult {
host := site.Hostname host := site.Hostname
if host == "" { if host == "" {
host = site.URL host = site.URL
@@ -157,7 +157,7 @@ func runPingCheck(_ context.Context, site models.Site) CheckResult {
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: stats.AvgRtt.Nanoseconds()} return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: stats.AvgRtt.Nanoseconds()}
} }
func runPortCheck(_ context.Context, site models.Site) CheckResult { func runPortCheck(_ context.Context, site models.SiteConfig) CheckResult {
host := site.Hostname host := site.Hostname
if host == "" { if host == "" {
host = site.URL host = site.URL
@@ -176,7 +176,7 @@ func runPortCheck(_ context.Context, site models.Site) CheckResult {
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()} return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()}
} }
func runDNSCheck(_ context.Context, site models.Site) CheckResult { func runDNSCheck(_ context.Context, site models.SiteConfig) CheckResult {
host := site.Hostname host := site.Hostname
if host == "" { if host == "" {
host = site.URL host = site.URL
@@ -229,7 +229,7 @@ func runDNSCheck(_ context.Context, site models.Site) CheckResult {
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()} return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()}
} }
func siteTimeout(site models.Site) time.Duration { func siteTimeout(site models.SiteConfig) time.Duration {
if site.Timeout > 0 { if site.Timeout > 0 {
return time.Duration(site.Timeout) * time.Second return time.Duration(site.Timeout) * time.Second
} }
+12 -12
View File
@@ -19,7 +19,7 @@ func TestRunCheck_HTTP_Success(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "UP" { if result.Status != "UP" {
@@ -39,7 +39,7 @@ func TestRunCheck_HTTP_ServerError(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
@@ -60,7 +60,7 @@ func TestRunCheck_HTTP_CustomAcceptedCodes(t *testing.T) {
return http.ErrUseLastResponse return http.ErrUseLastResponse
}} }}
site := models.Site{ID: 1, Type: "http", URL: srv.URL, AcceptedCodes: "200-399"} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, AcceptedCodes: "200-399"}
result := RunCheck(context.Background(), site, client, client, false) result := RunCheck(context.Background(), site, client, client, false)
if result.Status != "UP" { if result.Status != "UP" {
@@ -76,7 +76,7 @@ func TestRunCheck_HTTP_MethodRespected(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL, Method: "HEAD"} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Method: "HEAD"}
RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false)
if receivedMethod != "HEAD" { if receivedMethod != "HEAD" {
@@ -91,7 +91,7 @@ func TestRunCheck_HTTP_Timeout(t *testing.T) {
})) }))
defer srv.Close() defer srv.Close()
site := models.Site{ID: 1, Type: "http", URL: srv.URL, Timeout: 1} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Timeout: 1}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
@@ -109,7 +109,7 @@ func TestRunCheck_HTTP_SSLFields(t *testing.T) {
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}},
} }
site := models.Site{ID: 1, Type: "http", URL: srv.URL, CheckSSL: true, IgnoreTLS: true} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, CheckSSL: true, IgnoreTLS: true}
result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false)
if result.Status != "UP" { if result.Status != "UP" {
@@ -133,7 +133,7 @@ func TestRunCheck_Port_Open(t *testing.T) {
_, portStr, _ := net.SplitHostPort(ln.Addr().String()) _, portStr, _ := net.SplitHostPort(ln.Addr().String())
port, _ := strconv.Atoi(portStr) port, _ := strconv.Atoi(portStr)
site := models.Site{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2} site := models.SiteConfig{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2}
result := RunCheck(context.Background(), site, nil, nil, false, true) result := RunCheck(context.Background(), site, nil, nil, false, true)
if result.Status != "UP" { if result.Status != "UP" {
@@ -153,7 +153,7 @@ func TestRunCheck_Port_Closed(t *testing.T) {
port, _ := strconv.Atoi(portStr) port, _ := strconv.Atoi(portStr)
ln.Close() ln.Close()
site := models.Site{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 1} site := models.SiteConfig{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 1}
result := RunCheck(context.Background(), site, nil, nil, false, true) result := RunCheck(context.Background(), site, nil, nil, false, true)
if result.Status != "DOWN" { if result.Status != "DOWN" {
@@ -171,7 +171,7 @@ func TestRunCheck_Port_BlocksPrivateByDefault(t *testing.T) {
_, portStr, _ := net.SplitHostPort(ln.Addr().String()) _, portStr, _ := net.SplitHostPort(ln.Addr().String())
port, _ := strconv.Atoi(portStr) port, _ := strconv.Atoi(portStr)
site := models.Site{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2} site := models.SiteConfig{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2}
result := RunCheck(context.Background(), site, nil, nil, false) result := RunCheck(context.Background(), site, nil, nil, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
@@ -180,7 +180,7 @@ func TestRunCheck_Port_BlocksPrivateByDefault(t *testing.T) {
} }
func TestRunCheck_UnknownType(t *testing.T) { func TestRunCheck_UnknownType(t *testing.T) {
site := models.Site{ID: 1, Type: "invalid"} site := models.SiteConfig{ID: 1, Type: "invalid"}
result := RunCheck(context.Background(), site, nil, nil, false) result := RunCheck(context.Background(), site, nil, nil, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
@@ -214,10 +214,10 @@ func TestIsCodeAccepted(t *testing.T) {
} }
func TestSiteTimeout(t *testing.T) { func TestSiteTimeout(t *testing.T) {
if got := siteTimeout(models.Site{Timeout: 0}); got != 5*time.Second { if got := siteTimeout(models.SiteConfig{Timeout: 0}); got != 5*time.Second {
t.Errorf("expected 5s default, got %v", got) t.Errorf("expected 5s default, got %v", got)
} }
if got := siteTimeout(models.Site{Timeout: 10}); got != 10*time.Second { if got := siteTimeout(models.SiteConfig{Timeout: 10}); got != 10*time.Second {
t.Errorf("expected 10s, got %v", got) t.Errorf("expected 10s, got %v", got)
} }
} }
+19 -29
View File
@@ -418,7 +418,7 @@ func (e *Engine) Start(ctx context.Context) {
e.refreshMaintenanceCache(ctx) e.refreshMaintenanceCache(ctx)
sites, err := e.db.GetSites(ctx) configs, err := e.db.GetSites(ctx)
if err != nil { if err != nil {
e.AddLog(fmt.Sprintf("Failed to load sites: %v", err)) e.AddLog(fmt.Sprintf("Failed to load sites: %v", err))
select { select {
@@ -428,31 +428,31 @@ func (e *Engine) Start(ctx context.Context) {
} }
continue continue
} }
for _, s := range sites { for _, cfg := range configs {
e.mu.RLock() e.mu.RLock()
_, exists := e.liveState[s.ID] _, exists := e.liveState[cfg.ID]
e.mu.RUnlock() e.mu.RUnlock()
if !exists { if !exists {
e.mu.Lock() e.mu.Lock()
s.Status = models.StatusPending site := models.Site{SiteConfig: cfg, SiteState: models.SiteState{Status: models.StatusPending}}
if h, ok := e.GetHistory(s.ID); ok && len(h.Statuses) > 0 { if h, ok := e.GetHistory(cfg.ID); ok && len(h.Statuses) > 0 {
if h.Statuses[len(h.Statuses)-1] { if h.Statuses[len(h.Statuses)-1] {
s.Status = models.StatusUp site.Status = models.StatusUp
} else { } else {
s.Status = models.StatusDown site.Status = models.StatusDown
} }
if len(h.Latencies) > 0 { if len(h.Latencies) > 0 {
s.Latency = h.Latencies[len(h.Latencies)-1] site.Latency = h.Latencies[len(h.Latencies)-1]
} }
} }
e.liveState[s.ID] = s e.liveState[cfg.ID] = site
e.addToTokenIndex(s) e.addToTokenIndex(site)
e.mu.Unlock() e.mu.Unlock()
e.checkerWG.Add(1) e.checkerWG.Add(1)
go func(id int) { go func(id int) {
defer e.checkerWG.Done() defer e.checkerWG.Done()
e.monitorRoutine(ctx, id) e.monitorRoutine(ctx, id)
}(s.ID) }(cfg.ID)
} }
} }
@@ -498,27 +498,17 @@ func (e *Engine) pruneMaintenanceWindows(ctx context.Context) {
} }
} }
func (e *Engine) UpdateSiteConfig(site models.Site) { func (e *Engine) UpdateSiteConfig(cfg models.SiteConfig) {
e.mu.Lock() e.mu.Lock()
if existing, ok := e.liveState[site.ID]; ok { if existing, ok := e.liveState[cfg.ID]; ok {
e.removeFromTokenIndex(site.ID) e.removeFromTokenIndex(cfg.ID)
site.Status = existing.Status existing.SiteConfig = cfg
site.StatusCode = existing.StatusCode e.liveState[cfg.ID] = existing
site.Latency = existing.Latency e.addToTokenIndex(existing)
site.CertExpiry = existing.CertExpiry
site.HasSSL = existing.HasSSL
site.LastCheck = existing.LastCheck
site.SentSSLWarning = existing.SentSSLWarning
site.FailureCount = existing.FailureCount
site.LastError = existing.LastError
site.StatusChangedAt = existing.StatusChangedAt
site.LastSuccessAt = existing.LastSuccessAt
e.liveState[site.ID] = site
e.addToTokenIndex(site)
} }
e.mu.Unlock() e.mu.Unlock()
e.signalRecheck(site.ID) e.signalRecheck(cfg.ID)
} }
func (e *Engine) getRecheckChan(id int) chan struct{} { func (e *Engine) getRecheckChan(id int) chan struct{} {
@@ -675,7 +665,7 @@ func (e *Engine) checkByID(ctx context.Context, id int) {
case "group": case "group":
e.checkGroup(ctx, site) e.checkGroup(ctx, site)
default: default:
result := RunCheck(ctx, site, e.strictClient, e.insecureClient, e.insecureSkipVerify, e.allowPrivateTargets) result := RunCheck(ctx, site.SiteConfig, e.strictClient, e.insecureClient, e.insecureSkipVerify, e.allowPrivateTargets)
updatedSite := site updatedSite := site
updatedSite.HasSSL = result.HasSSL updatedSite.HasSSL = result.HasSSL
updatedSite.CertExpiry = result.CertExpiry updatedSite.CertExpiry = result.CertExpiry
+211 -82
View File
@@ -22,7 +22,7 @@ type savedCheck struct {
type mockStore struct { type mockStore struct {
storetest.BaseMock storetest.BaseMock
mu sync.Mutex mu sync.Mutex
sites []models.Site sites []models.SiteConfig
alerts map[int]models.AlertConfig alerts map[int]models.AlertConfig
maintenance map[int]bool maintenance map[int]bool
logs []string logs []string
@@ -40,7 +40,7 @@ func newMockStore() *mockStore {
} }
} }
func (m *mockStore) GetSites(context.Context) ([]models.Site, error) { return m.sites, nil } func (m *mockStore) GetSites(context.Context) ([]models.SiteConfig, error) { return m.sites, nil }
func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.MaintenanceWindow, error) { func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.MaintenanceWindow, error) {
m.mu.Lock() m.mu.Lock()
@@ -148,7 +148,10 @@ func (m *mockStore) getAlertCallsSnapshot() []int {
func TestHandleStatusChange_PendingToUp(t *testing.T) { func TestHandleStatusChange_PendingToUp(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "PENDING", MaxRetries: 3, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 3, AlertID: 1},
SiteState: models.SiteState{Status: "PENDING"},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "UP", 200, 10*time.Millisecond, "") e.handleStatusChange(site, "UP", 200, 10*time.Millisecond, "")
@@ -169,7 +172,10 @@ func TestHandleStatusChange_PendingToUp(t *testing.T) {
func TestHandleStatusChange_UpIncrementFailure(t *testing.T) { func TestHandleStatusChange_UpIncrementFailure(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 3, FailureCount: 0} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 3},
SiteState: models.SiteState{Status: "UP", FailureCount: 0},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 500, 0, "test error") e.handleStatusChange(site, "DOWN", 500, 0, "test error")
@@ -187,7 +193,10 @@ func TestHandleStatusChange_UpToDown_ExceedsRetries(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "discord", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "discord", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 2, FailureCount: 2, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 2, AlertID: 1},
SiteState: models.SiteState{Status: "UP", FailureCount: 2},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 500, 0, "test error") e.handleStatusChange(site, "DOWN", 500, 0, "test error")
@@ -210,7 +219,10 @@ func TestHandleStatusChange_UpToDown_ZeroRetries(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0, FailureCount: 0, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0, AlertID: 1},
SiteState: models.SiteState{Status: "UP", FailureCount: 0},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 0, 0, "test error") e.handleStatusChange(site, "DOWN", 0, 0, "test error")
@@ -229,7 +241,10 @@ func TestHandleStatusChange_DownToUp_Recovery(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "DOWN", FailureCount: 4, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", AlertID: 1},
SiteState: models.SiteState{Status: "DOWN", FailureCount: 4},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "UP", 200, 5*time.Millisecond, "") e.handleStatusChange(site, "UP", 200, 5*time.Millisecond, "")
@@ -250,7 +265,10 @@ func TestHandleStatusChange_DownToUp_Recovery(t *testing.T) {
func TestHandleStatusChange_DownStaysDown(t *testing.T) { func TestHandleStatusChange_DownStaysDown(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "DOWN", MaxRetries: 2, FailureCount: 3} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 2},
SiteState: models.SiteState{Status: "DOWN", FailureCount: 3},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 0, 0, "test error") e.handleStatusChange(site, "DOWN", 0, 0, "test error")
@@ -269,7 +287,10 @@ func TestHandleStatusChange_SSLExpired(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0, AlertID: 1},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "SSL EXP", 0, 0, "SSL certificate expired") e.handleStatusChange(site, "SSL EXP", 0, 0, "SSL certificate expired")
@@ -289,7 +310,10 @@ func TestHandleStatusChange_AlertSuppressedMaintenance(t *testing.T) {
ms.maintenance[1] = true ms.maintenance[1] = true
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0, AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0, AlertID: 1},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.refreshMaintenanceCache(context.Background()) e.refreshMaintenanceCache(context.Background())
@@ -321,7 +345,10 @@ func TestHandleStatusChange_RecoverySuppressedMaintenance(t *testing.T) {
ms.maintenance[1] = true ms.maintenance[1] = true
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "DOWN", AlertID: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", AlertID: 1},
SiteState: models.SiteState{Status: "DOWN"},
}
injectSite(e, site) injectSite(e, site)
e.refreshMaintenanceCache(context.Background()) e.refreshMaintenanceCache(context.Background())
@@ -342,10 +369,8 @@ func TestHandleStatusChange_SSLWarning(t *testing.T) {
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "test", Status: "UP", Type: "http", SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http", CheckSSL: true, ExpiryThreshold: 30, AlertID: 1},
CheckSSL: true, HasSSL: true, ExpiryThreshold: 30, SiteState: models.SiteState{Status: "UP", HasSSL: true, SentSSLWarning: false, CertExpiry: time.Now().Add(15 * 24 * time.Hour)},
SentSSLWarning: false, AlertID: 1,
CertExpiry: time.Now().Add(15 * 24 * time.Hour),
} }
injectSite(e, site) injectSite(e, site)
@@ -365,10 +390,8 @@ func TestHandleStatusChange_SSLWarningNotRepeated(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "test", Status: "UP", Type: "http", SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http", CheckSSL: true, ExpiryThreshold: 30, AlertID: 1},
CheckSSL: true, HasSSL: true, ExpiryThreshold: 30, SiteState: models.SiteState{Status: "UP", HasSSL: true, SentSSLWarning: true, CertExpiry: time.Now().Add(15 * 24 * time.Hour)},
SentSSLWarning: true, AlertID: 1,
CertExpiry: time.Now().Add(15 * 24 * time.Hour),
} }
injectSite(e, site) injectSite(e, site)
@@ -384,10 +407,8 @@ func TestHandleStatusChange_SSLWarningReset(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "test", Status: "UP", Type: "http", SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http", CheckSSL: true, ExpiryThreshold: 30},
CheckSSL: true, HasSSL: true, ExpiryThreshold: 30, SiteState: models.SiteState{Status: "UP", HasSSL: true, SentSSLWarning: true, CertExpiry: time.Now().Add(60 * 24 * time.Hour)},
SentSSLWarning: true,
CertExpiry: time.Now().Add(60 * 24 * time.Hour),
} }
injectSite(e, site) injectSite(e, site)
@@ -405,10 +426,8 @@ func TestHandleStatusChange_SSLWarningSuppressedMaint(t *testing.T) {
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "test", Status: "UP", Type: "http", SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http", CheckSSL: true, ExpiryThreshold: 30, AlertID: 1},
CheckSSL: true, HasSSL: true, ExpiryThreshold: 30, SiteState: models.SiteState{Status: "UP", HasSSL: true, SentSSLWarning: false, CertExpiry: time.Now().Add(15 * 24 * time.Hour)},
SentSSLWarning: false, AlertID: 1,
CertExpiry: time.Now().Add(15 * 24 * time.Hour),
} }
injectSite(e, site) injectSite(e, site)
e.refreshMaintenanceCache(context.Background()) e.refreshMaintenanceCache(context.Background())
@@ -428,7 +447,10 @@ func TestHandleStatusChange_SSLWarningSuppressedMaint(t *testing.T) {
func TestHandleStatusChange_InactiveEngine(t *testing.T) { func TestHandleStatusChange_InactiveEngine(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.SetActive(false) e.SetActive(false)
@@ -445,7 +467,10 @@ func TestHandleStatusChange_InactiveEngine(t *testing.T) {
func TestRecordHeartbeat_ValidToken(t *testing.T) { func TestRecordHeartbeat_ValidToken(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "push-test", Type: "push", Token: "abc123", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "push-test", Type: "push", Token: "abc123"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
if !e.RecordHeartbeat("abc123") { if !e.RecordHeartbeat("abc123") {
@@ -465,7 +490,10 @@ func TestRecordHeartbeat_RecoveryFromDown(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}} ms.alerts[1] = models.AlertConfig{ID: 1, Name: "test", Type: "webhook", Settings: map[string]string{"url": "http://example.com"}}
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "push-test", Type: "push", Token: "abc123", Status: "DOWN", AlertID: 1, FailureCount: 3} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "push-test", Type: "push", Token: "abc123", AlertID: 1},
SiteState: models.SiteState{Status: "DOWN", FailureCount: 3},
}
injectSite(e, site) injectSite(e, site)
if !e.RecordHeartbeat("abc123") { if !e.RecordHeartbeat("abc123") {
@@ -497,7 +525,10 @@ func TestRecordHeartbeat_UnknownToken(t *testing.T) {
func TestRecordHeartbeat_InactiveEngine(t *testing.T) { func TestRecordHeartbeat_InactiveEngine(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Type: "push", Token: "abc123", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Type: "push", Token: "abc123"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.SetActive(false) e.SetActive(false)
@@ -512,9 +543,8 @@ func TestCheckPush_DeadlineMissed(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "push", Type: "push", Status: "UP", SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Interval: 10, MaxRetries: 0},
Interval: 10, MaxRetries: 0, SiteState: models.SiteState{Status: "UP", LastCheck: time.Now().Add(-120 * time.Second)},
LastCheck: time.Now().Add(-120 * time.Second),
} }
injectSite(e, site) injectSite(e, site)
@@ -530,9 +560,8 @@ func TestCheckPush_OverdueBecomesLate(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "push", Type: "push", Status: "UP", SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Interval: 300},
Interval: 300, SiteState: models.SiteState{Status: "UP", LastCheck: time.Now().Add(-310 * time.Second)},
LastCheck: time.Now().Add(-310 * time.Second),
} }
injectSite(e, site) injectSite(e, site)
@@ -550,9 +579,8 @@ func TestCheckPush_OverdueBecomesStale(t *testing.T) {
// interval=300, grace=150 (300/2), staleMark=overdue+75 // interval=300, grace=150 (300/2), staleMark=overdue+75
// at 380s: past staleMark(375) but before graceEnd(450) // at 380s: past staleMark(375) but before graceEnd(450)
site := models.Site{ site := models.Site{
ID: 1, Name: "push", Type: "push", Status: "UP", SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Interval: 300},
Interval: 300, SiteState: models.SiteState{Status: "UP", LastCheck: time.Now().Add(-380 * time.Second)},
LastCheck: time.Now().Add(-380 * time.Second),
} }
injectSite(e, site) injectSite(e, site)
@@ -568,8 +596,8 @@ func TestCheckPush_WithinDeadline(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "push", Type: "push", Status: "UP", SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Interval: 60},
Interval: 60, LastCheck: time.Now(), SiteState: models.SiteState{Status: "UP", LastCheck: time.Now()},
} }
injectSite(e, site) injectSite(e, site)
@@ -585,8 +613,8 @@ func TestCheckPush_PendingStaysPending(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ site := models.Site{
ID: 1, Name: "push", Type: "push", Status: "PENDING", SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Interval: 60},
Interval: 60, SiteState: models.SiteState{Status: "PENDING"},
} }
injectSite(e, site) injectSite(e, site)
@@ -603,9 +631,18 @@ func TestCheckPush_PendingStaysPending(t *testing.T) {
func TestCheckGroup_AllChildrenUp(t *testing.T) { func TestCheckGroup_AllChildrenUp(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group", Status: "PENDING"} group := models.Site{
child1 := models.Site{ID: 2, Name: "child1", Type: "http", ParentID: 1, Status: "UP"} SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
child2 := models.Site{ID: 3, Name: "child2", Type: "http", ParentID: 1, Status: "UP"} SiteState: models.SiteState{Status: "PENDING"},
}
child1 := models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "child1", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "UP"},
}
child2 := models.Site{
SiteConfig: models.SiteConfig{ID: 3, Name: "child2", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child1) injectSite(e, child1)
injectSite(e, child2) injectSite(e, child2)
@@ -621,9 +658,18 @@ func TestCheckGroup_AllChildrenUp(t *testing.T) {
func TestCheckGroup_OneChildDown(t *testing.T) { func TestCheckGroup_OneChildDown(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group", Status: "UP"} group := models.Site{
child1 := models.Site{ID: 2, Name: "child1", Type: "http", ParentID: 1, Status: "UP"} SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
child2 := models.Site{ID: 3, Name: "child2", Type: "http", ParentID: 1, Status: "DOWN"} SiteState: models.SiteState{Status: "UP"},
}
child1 := models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "child1", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "UP"},
}
child2 := models.Site{
SiteConfig: models.SiteConfig{ID: 3, Name: "child2", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "DOWN"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child1) injectSite(e, child1)
injectSite(e, child2) injectSite(e, child2)
@@ -639,9 +685,17 @@ func TestCheckGroup_OneChildDown(t *testing.T) {
func TestCheckGroup_PausedChildIgnored(t *testing.T) { func TestCheckGroup_PausedChildIgnored(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group"} group := models.Site{
child1 := models.Site{ID: 2, Name: "child1", Type: "http", ParentID: 1, Status: "UP"} SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
child2 := models.Site{ID: 3, Name: "child2", Type: "http", ParentID: 1, Status: "DOWN", Paused: true} }
child1 := models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "child1", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "UP"},
}
child2 := models.Site{
SiteConfig: models.SiteConfig{ID: 3, Name: "child2", Type: "http", ParentID: 1, Paused: true},
SiteState: models.SiteState{Status: "DOWN"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child1) injectSite(e, child1)
injectSite(e, child2) injectSite(e, child2)
@@ -658,9 +712,17 @@ func TestCheckGroup_MaintenanceChildIgnored(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.maintenance[3] = true ms.maintenance[3] = true
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group"} group := models.Site{
child1 := models.Site{ID: 2, Name: "child1", Type: "http", ParentID: 1, Status: "UP"} SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
child2 := models.Site{ID: 3, Name: "child2", Type: "http", ParentID: 1, Status: "DOWN"} }
child1 := models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "child1", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "UP"},
}
child2 := models.Site{
SiteConfig: models.SiteConfig{ID: 3, Name: "child2", Type: "http", ParentID: 1},
SiteState: models.SiteState{Status: "DOWN"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child1) injectSite(e, child1)
injectSite(e, child2) injectSite(e, child2)
@@ -677,7 +739,10 @@ func TestCheckGroup_MaintenanceChildIgnored(t *testing.T) {
func TestCheckGroup_NoChildren(t *testing.T) { func TestCheckGroup_NoChildren(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group", Status: "UP"} group := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, group) injectSite(e, group)
e.checkGroup(context.Background(), group) e.checkGroup(context.Background(), group)
@@ -772,10 +837,13 @@ func TestInitHistory_LoadsFromDB(t *testing.T) {
func TestUpdateSiteConfig_PreservesRuntime(t *testing.T) { func TestUpdateSiteConfig_PreservesRuntime(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", URL: "http://old.com", Status: "DOWN", FailureCount: 3, Latency: 100 * time.Millisecond} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", URL: "http://old.com"},
SiteState: models.SiteState{Status: "DOWN", FailureCount: 3, Latency: 100 * time.Millisecond},
}
injectSite(e, site) injectSite(e, site)
updated := models.Site{ID: 1, Name: "test", URL: "http://new.com", Interval: 60} updated := models.SiteConfig{ID: 1, Name: "test", URL: "http://new.com", Interval: 60}
e.UpdateSiteConfig(updated) e.UpdateSiteConfig(updated)
s, _ := getSite(e, 1) s, _ := getSite(e, 1)
@@ -796,7 +864,10 @@ func TestUpdateSiteConfig_PreservesRuntime(t *testing.T) {
func TestRemoveSite_CleansUp(t *testing.T) { func TestRemoveSite_CleansUp(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Type: "push", Token: "tok1", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "push", Token: "tok1"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.recordCheck(1, 5*time.Millisecond, true) e.recordCheck(1, 5*time.Millisecond, true)
@@ -816,7 +887,10 @@ func TestRemoveSite_CleansUp(t *testing.T) {
func TestToggleSitePause(t *testing.T) { func TestToggleSitePause(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
paused := e.ToggleSitePause(1) paused := e.ToggleSitePause(1)
@@ -845,8 +919,14 @@ func TestToggleSitePause_NonexistentSite(t *testing.T) {
func TestGetAllSites_ReturnsCopy(t *testing.T) { func TestGetAllSites_ReturnsCopy(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
injectSite(e, models.Site{ID: 1, Name: "s1", Status: "UP"}) injectSite(e, models.Site{
injectSite(e, models.Site{ID: 2, Name: "s2", Status: "DOWN"}) SiteConfig: models.SiteConfig{ID: 1, Name: "s1"},
SiteState: models.SiteState{Status: "UP"},
})
injectSite(e, models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "s2"},
SiteState: models.SiteState{Status: "DOWN"},
})
sites := e.GetAllSites() sites := e.GetAllSites()
if len(sites) != 2 { if len(sites) != 2 {
@@ -865,10 +945,13 @@ func TestGetAllSites_ReturnsCopy(t *testing.T) {
func TestGetLiveState_ReturnsCopy(t *testing.T) { func TestGetLiveState_ReturnsCopy(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
injectSite(e, models.Site{ID: 1, Name: "s1", Status: "UP"}) injectSite(e, models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "s1"},
SiteState: models.SiteState{Status: "UP"},
})
state := e.GetLiveState() state := e.GetLiveState()
state[1] = models.Site{Name: "mutated"} state[1] = models.Site{SiteConfig: models.SiteConfig{Name: "mutated"}}
fresh := e.GetLiveState() fresh := e.GetLiveState()
if fresh[1].Name == "mutated" { if fresh[1].Name == "mutated" {
@@ -984,7 +1067,8 @@ func TestConcurrent_RecordHeartbeat(t *testing.T) {
e := newTestEngine(ms) e := newTestEngine(ms)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
injectSite(e, models.Site{ injectSite(e, models.Site{
ID: i + 1, Type: "push", Token: fmt.Sprintf("tok-%d", i+1), Status: "UP", SiteConfig: models.SiteConfig{ID: i + 1, Type: "push", Token: fmt.Sprintf("tok-%d", i+1)},
SiteState: models.SiteState{Status: "UP"},
}) })
} }
@@ -1002,7 +1086,10 @@ func TestConcurrent_RecordHeartbeat(t *testing.T) {
func TestConcurrent_HandleStatusChangeAndGetState(t *testing.T) { func TestConcurrent_HandleStatusChangeAndGetState(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 100} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 100},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -1055,7 +1142,10 @@ func TestConcurrent_RecordCheckAndGetHistory(t *testing.T) {
func TestHandleStatusChange_PauseDuringCheckSurvives(t *testing.T) { func TestHandleStatusChange_PauseDuringCheckSurvives(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
// `site` is the stale snapshot the check ran against (Paused=false). // `site` is the stale snapshot the check ran against (Paused=false).
@@ -1079,11 +1169,14 @@ func TestHandleStatusChange_PauseDuringCheckSurvives(t *testing.T) {
func TestHandleStatusChange_ConfigEditDuringCheckSurvives(t *testing.T) { func TestHandleStatusChange_ConfigEditDuringCheckSurvives(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", URL: "http://old.com", Type: "http", Status: "UP", MaxRetries: 0, Interval: 30} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", URL: "http://old.com", Type: "http", MaxRetries: 0, Interval: 30},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
// Config changes mid-check. // Config changes mid-check.
e.UpdateSiteConfig(models.Site{ID: 1, Name: "test", URL: "http://new.com", Type: "http", Interval: 60}) e.UpdateSiteConfig(models.SiteConfig{ID: 1, Name: "test", URL: "http://new.com", Type: "http", Interval: 60})
// Stale check (ran against http://old.com) folds its result in. // Stale check (ran against http://old.com) folds its result in.
e.handleStatusChange(site, "UP", 200, 5*time.Millisecond, "") e.handleStatusChange(site, "UP", 200, 5*time.Millisecond, "")
@@ -1105,7 +1198,10 @@ func TestHandleStatusChange_HeartbeatNotOverwrittenByStaleDown(t *testing.T) {
e := newTestEngine(ms) e := newTestEngine(ms)
// Snapshot the engine would have taken before evaluating staleness: // Snapshot the engine would have taken before evaluating staleness:
// LastCheck is old, so checkPush decided "DOWN". // LastCheck is old, so checkPush decided "DOWN".
snap := models.Site{ID: 1, Name: "push", Type: "push", Token: "tok", Status: "UP", Interval: 10, LastCheck: time.Now().Add(-120 * time.Second)} snap := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "push", Type: "push", Token: "tok", Interval: 10},
SiteState: models.SiteState{Status: "UP", LastCheck: time.Now().Add(-120 * time.Second)},
}
injectSite(e, snap) injectSite(e, snap)
// A heartbeat lands first, advancing LastCheck and confirming UP. // A heartbeat lands first, advancing LastCheck and confirming UP.
@@ -1126,7 +1222,10 @@ func TestHandleStatusChange_HeartbeatNotOverwrittenByStaleDown(t *testing.T) {
func TestHandleStatusChange_RemovedSiteDropped(t *testing.T) { func TestHandleStatusChange_RemovedSiteDropped(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Status: "UP", MaxRetries: 0} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", MaxRetries: 0},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.RemoveSite(1) e.RemoveSite(1)
@@ -1189,9 +1288,18 @@ func TestEngineStop_Idempotent(t *testing.T) {
func TestCheckGroup_AllPausedNoAutoFreeze(t *testing.T) { func TestCheckGroup_AllPausedNoAutoFreeze(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 1, Name: "group", Type: "group", Status: "UP"} group := models.Site{
child1 := models.Site{ID: 2, Name: "child1", Type: "http", ParentID: 1, Status: "UP", Paused: true} SiteConfig: models.SiteConfig{ID: 1, Name: "group", Type: "group"},
child2 := models.Site{ID: 3, Name: "child2", Type: "http", ParentID: 1, Status: "UP", Paused: true} SiteState: models.SiteState{Status: "UP"},
}
child1 := models.Site{
SiteConfig: models.SiteConfig{ID: 2, Name: "child1", Type: "http", ParentID: 1, Paused: true},
SiteState: models.SiteState{Status: "UP"},
}
child2 := models.Site{
SiteConfig: models.SiteConfig{ID: 3, Name: "child2", Type: "http", ParentID: 1, Paused: true},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child1) injectSite(e, child1)
injectSite(e, child2) injectSite(e, child2)
@@ -1208,7 +1316,10 @@ func TestCheckGroup_AllPausedNoAutoFreeze(t *testing.T) {
func TestHandleStatusChange_PendingRetriesBeforeDown(t *testing.T) { func TestHandleStatusChange_PendingRetriesBeforeDown(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "new-monitor", Status: "PENDING", MaxRetries: 2} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "new-monitor", MaxRetries: 2},
SiteState: models.SiteState{Status: "PENDING"},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 0, 0, "timeout") e.handleStatusChange(site, "DOWN", 0, 0, "timeout")
@@ -1237,7 +1348,10 @@ func TestHandleStatusChange_PendingRetriesBeforeDown(t *testing.T) {
func TestHandleStatusChange_LateRetriesBeforeDown(t *testing.T) { func TestHandleStatusChange_LateRetriesBeforeDown(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "push-mon", Status: "LATE", MaxRetries: 1} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "push-mon", MaxRetries: 1},
SiteState: models.SiteState{Status: "LATE"},
}
injectSite(e, site) injectSite(e, site)
e.handleStatusChange(site, "DOWN", 0, 0, "missed heartbeat") e.handleStatusChange(site, "DOWN", 0, 0, "missed heartbeat")
@@ -1257,7 +1371,10 @@ func TestHandleStatusChange_LateRetriesBeforeDown(t *testing.T) {
func TestIngestProbeResult_ExpiresStaleProbes(t *testing.T) { func TestIngestProbeResult_ExpiresStaleProbes(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Type: "http", Status: "UP", Interval: 30} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http", Interval: 30},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.probeResultsMu.Lock() e.probeResultsMu.Lock()
@@ -1289,7 +1406,10 @@ func TestIngestProbeResult_ExpiresStaleProbes(t *testing.T) {
func TestRemoveSite_CleansProbeResults(t *testing.T) { func TestRemoveSite_CleansProbeResults(t *testing.T) {
ms := newMockStore() ms := newMockStore()
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Type: "http", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.probeResultsMu.Lock() e.probeResultsMu.Lock()
@@ -1312,8 +1432,14 @@ func TestIsInMaintenance_UsesCache(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.maintenance[10] = true // direct maintenance on group ms.maintenance[10] = true // direct maintenance on group
e := newTestEngine(ms) e := newTestEngine(ms)
group := models.Site{ID: 10, Name: "group", Type: "group", Status: "UP"} group := models.Site{
child := models.Site{ID: 20, Name: "child", Type: "http", ParentID: 10, Status: "UP"} SiteConfig: models.SiteConfig{ID: 10, Name: "group", Type: "group"},
SiteState: models.SiteState{Status: "UP"},
}
child := models.Site{
SiteConfig: models.SiteConfig{ID: 20, Name: "child", Type: "http", ParentID: 10},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, group) injectSite(e, group)
injectSite(e, child) injectSite(e, child)
e.refreshMaintenanceCache(context.Background()) e.refreshMaintenanceCache(context.Background())
@@ -1334,7 +1460,10 @@ func TestIsInMaintenance_GlobalMaintenance(t *testing.T) {
ms := newMockStore() ms := newMockStore()
ms.maintenance[0] = true ms.maintenance[0] = true
e := newTestEngine(ms) e := newTestEngine(ms)
site := models.Site{ID: 1, Name: "test", Type: "http", Status: "UP"} site := models.Site{
SiteConfig: models.SiteConfig{ID: 1, Name: "test", Type: "http"},
SiteState: models.SiteState{Status: "UP"},
}
injectSite(e, site) injectSite(e, site)
e.refreshMaintenanceCache(context.Background()) e.refreshMaintenanceCache(context.Background())
+6 -6
View File
@@ -21,7 +21,7 @@ import (
type mockStore struct { type mockStore struct {
storetest.BaseMock storetest.BaseMock
mu sync.Mutex mu sync.Mutex
sites []models.Site sites []models.SiteConfig
alerts []models.AlertConfig alerts []models.AlertConfig
nodes map[string]models.ProbeNode nodes map[string]models.ProbeNode
importedData *models.Backup importedData *models.Backup
@@ -35,7 +35,7 @@ func newMockStore() *mockStore {
} }
} }
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { return m.sites, nil } func (m *mockStore) GetSites(_ context.Context) ([]models.SiteConfig, error) { return m.sites, nil }
func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) { func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) {
return m.alerts, nil return m.alerts, nil
} }
@@ -252,7 +252,7 @@ func TestExport_Unauthorized_WrongKey(t *testing.T) {
func TestExport_Success(t *testing.T) { func TestExport_Success(t *testing.T) {
ts := newTestServer(t, "secret", false) ts := newTestServer(t, "secret", false)
ts.store.sites = []models.Site{{ID: 1, Name: "example", URL: "http://example.com"}} ts.store.sites = []models.SiteConfig{{ID: 1, Name: "example", URL: "http://example.com"}}
resp, err := authReq("GET", ts.baseURL+"/api/backup/export", "secret", nil) resp, err := authReq("GET", ts.baseURL+"/api/backup/export", "secret", nil)
if err != nil { if err != nil {
@@ -299,7 +299,7 @@ func TestImport_Unauthorized(t *testing.T) {
func TestImport_Success(t *testing.T) { func TestImport_Success(t *testing.T) {
ts := newTestServer(t, "secret", false) ts := newTestServer(t, "secret", false)
backup := models.Backup{ backup := models.Backup{
Sites: []models.Site{{Name: "imported", URL: "http://example.com"}}, Sites: []models.SiteConfig{{Name: "imported", URL: "http://example.com"}},
} }
body, _ := json.Marshal(backup) body, _ := json.Marshal(backup)
resp, err := authReq("POST", ts.baseURL+"/api/backup/import", "secret", body) resp, err := authReq("POST", ts.baseURL+"/api/backup/import", "secret", body)
@@ -437,9 +437,9 @@ func TestStatusJSON_PublicDTOOnly(t *testing.T) {
// take. The old version of this test injected via UpdateSiteConfig, which // take. The old version of this test injected via UpdateSiteConfig, which
// no-ops for unknown IDs, so it asserted over zero sites and passed // no-ops for unknown IDs, so it asserted over zero sites and passed
// against a server that leaked tokens. // against a server that leaked tokens.
ts.store.sites = []models.Site{{ ts.store.sites = []models.SiteConfig{{
ID: 1, Name: "test", Type: "push", Token: "secret-token", ID: 1, Name: "test", Type: "push", Token: "secret-token",
Hostname: "internal-host", LastError: "internal failure detail", AlertID: 3, Hostname: "internal-host", AlertID: 3,
}} }}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
ts.engine.Start(ctx) ts.engine.Start(ctx)
+8 -8
View File
@@ -112,7 +112,7 @@ func (s *SQLStore) Init(ctx context.Context) error {
return nil return nil
} }
func (s *SQLStore) GetSites(ctx context.Context) ([]models.Site, error) { func (s *SQLStore) GetSites(ctx context.Context) ([]models.SiteConfig, error) {
bf := s.dialect.BoolFalse() bf := s.dialect.BoolFalse()
query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites", "SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites",
@@ -123,9 +123,9 @@ func (s *SQLStore) GetSites(ctx context.Context) ([]models.Site, error) {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var sites []models.Site var sites []models.SiteConfig
for rows.Next() { for rows.Next() {
var st models.Site var st models.SiteConfig
if err := rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID, if err := rows.Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID,
&st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout, &st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout,
&st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType, &st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType,
@@ -137,7 +137,7 @@ func (s *SQLStore) GetSites(ctx context.Context) ([]models.Site, error) {
return sites, rows.Err() return sites, rows.Err()
} }
func (s *SQLStore) AddSite(ctx context.Context, site models.Site) error { func (s *SQLStore) AddSite(ctx context.Context, site models.SiteConfig) error {
token := "" token := ""
if site.Type == "push" { if site.Type == "push" {
var err error var err error
@@ -152,7 +152,7 @@ func (s *SQLStore) AddSite(ctx context.Context, site models.Site) error {
return err return err
} }
func (s *SQLStore) UpdateSite(ctx context.Context, site models.Site) error { func (s *SQLStore) UpdateSite(ctx context.Context, site models.SiteConfig) error {
var existingToken string var existingToken string
_ = s.db.QueryRowContext(ctx, s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken) //nolint:errcheck _ = s.db.QueryRowContext(ctx, s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken) //nolint:errcheck
if site.Type == "push" && existingToken == "" { if site.Type == "push" && existingToken == "" {
@@ -198,13 +198,13 @@ func (s *SQLStore) DeleteSite(ctx context.Context, id int) error {
return nil return nil
} }
func (s *SQLStore) GetSiteByName(ctx context.Context, name string) (models.Site, error) { func (s *SQLStore) GetSiteByName(ctx context.Context, name string) (models.SiteConfig, error) {
bf := s.dialect.BoolFalse() bf := s.dialect.BoolFalse()
query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input query := fmt.Sprintf( //nolint:gosec // bf is a dialect boolean literal, not user input
"SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites WHERE name = %s", "SELECT id, COALESCE(name, url), url, COALESCE(type, 'http'), COALESCE(token, ''), interval, alert_id, check_ssl, threshold, max_retries, COALESCE(hostname, ''), COALESCE(port, 0), COALESCE(timeout, 0), COALESCE(method, 'GET'), COALESCE(description, ''), COALESCE(parent_id, 0), COALESCE(accepted_codes, '200-299'), COALESCE(dns_resolve_type, ''), COALESCE(dns_server, ''), COALESCE(ignore_tls, %s), COALESCE(paused, %s), COALESCE(regions, '') FROM sites WHERE name = %s",
bf, bf, s.q("?"), bf, bf, s.q("?"),
) )
var st models.Site var st models.SiteConfig
err := s.db.QueryRowContext(ctx, query, name).Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID, err := s.db.QueryRowContext(ctx, query, name).Scan(&st.ID, &st.Name, &st.URL, &st.Type, &st.Token, &st.Interval, &st.AlertID,
&st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout, &st.CheckSSL, &st.ExpiryThreshold, &st.MaxRetries, &st.Hostname, &st.Port, &st.Timeout,
&st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType, &st.Method, &st.Description, &st.ParentID, &st.AcceptedCodes, &st.DNSResolveType,
@@ -246,7 +246,7 @@ func (s *SQLStore) GetAlertByName(ctx context.Context, name string) (models.Aler
return a, nil return a, nil
} }
func (s *SQLStore) AddSiteReturningID(ctx context.Context, site models.Site) (int, error) { func (s *SQLStore) AddSiteReturningID(ctx context.Context, site models.SiteConfig) (int, error) {
token := "" token := ""
if site.Type == "push" { if site.Type == "push" {
var err error var err error
+6 -6
View File
@@ -33,7 +33,7 @@ func TestSiteCRUD(t *testing.T) {
t.Fatalf("expected 0 sites, got %d", len(sites)) t.Fatalf("expected 0 sites, got %d", len(sites))
} }
if err := s.AddSite(context.Background(), models.Site{Name: "Test", URL: "https://example.com", Type: "http", Interval: 30}); err != nil { if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Test", URL: "https://example.com", Type: "http", Interval: 30}); err != nil {
t.Fatalf("AddSite: %v", err) t.Fatalf("AddSite: %v", err)
} }
@@ -174,7 +174,7 @@ func TestUserCRUD(t *testing.T) {
func TestPushTokenGeneration(t *testing.T) { func TestPushTokenGeneration(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
if err := s.AddSite(context.Background(), models.Site{Name: "Push Monitor", Type: "push", Interval: 60}); err != nil { if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Push Monitor", Type: "push", Interval: 60}); err != nil {
t.Fatalf("AddSite: %v", err) t.Fatalf("AddSite: %v", err)
} }
@@ -199,7 +199,7 @@ func TestImportExport(t *testing.T) {
if err := s.AddAlert(context.Background(), "Test Alert", "webhook", map[string]string{"url": "https://example.com"}); err != nil { if err := s.AddAlert(context.Background(), "Test Alert", "webhook", map[string]string{"url": "https://example.com"}); err != nil {
t.Fatalf("AddAlert: %v", err) t.Fatalf("AddAlert: %v", err)
} }
if err := s.AddSite(context.Background(), models.Site{Name: "Site1", URL: "https://example.com", Type: "http", Interval: 30}); err != nil { if err := s.AddSite(context.Background(), models.SiteConfig{Name: "Site1", URL: "https://example.com", Type: "http", Interval: 30}); err != nil {
t.Fatalf("AddSite: %v", err) t.Fatalf("AddSite: %v", err)
} }
if err := s.AddUser(context.Background(), "user1", "ssh-ed25519 KEY", "user"); err != nil { if err := s.AddUser(context.Background(), "user1", "ssh-ed25519 KEY", "user"); err != nil {
@@ -239,7 +239,7 @@ func TestImportExport(t *testing.T) {
func TestImportData_WipesHistory(t *testing.T) { func TestImportData_WipesHistory(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
if err := s.AddSite(context.Background(), models.Site{Name: "OldSite", URL: "https://old.com", Type: "http", Interval: 30}); err != nil { if err := s.AddSite(context.Background(), models.SiteConfig{Name: "OldSite", URL: "https://old.com", Type: "http", Interval: 30}); err != nil {
t.Fatalf("AddSite: %v", err) t.Fatalf("AddSite: %v", err)
} }
if err := s.SaveCheck(context.Background(), 1, 5000, true); err != nil { if err := s.SaveCheck(context.Background(), 1, 5000, true); err != nil {
@@ -253,7 +253,7 @@ func TestImportData_WipesHistory(t *testing.T) {
} }
backup := models.Backup{ backup := models.Backup{
Sites: []models.Site{{ID: 1, Name: "NewSite", URL: "https://new.com", Type: "http", Interval: 60}}, Sites: []models.SiteConfig{{ID: 1, Name: "NewSite", URL: "https://new.com", Type: "http", Interval: 60}},
} }
if err := s.ImportData(context.Background(), backup); err != nil { if err := s.ImportData(context.Background(), backup); err != nil {
t.Fatalf("ImportData: %v", err) t.Fatalf("ImportData: %v", err)
@@ -314,7 +314,7 @@ func TestCheckHistory(t *testing.T) {
func TestDeleteSiteCascade(t *testing.T) { func TestDeleteSiteCascade(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
site := models.Site{Name: "Cascade Test", URL: "https://example.com", Interval: 30} site := models.SiteConfig{Name: "Cascade Test", URL: "https://example.com", Interval: 30}
if err := s.AddSite(context.Background(), site); err != nil { if err := s.AddSite(context.Background(), site); err != nil {
t.Fatalf("AddSite: %v", err) t.Fatalf("AddSite: %v", err)
} }
+5 -5
View File
@@ -11,9 +11,9 @@ type Store interface {
Init(ctx context.Context) error Init(ctx context.Context) error
// Sites // Sites
GetSites(ctx context.Context) ([]models.Site, error) GetSites(ctx context.Context) ([]models.SiteConfig, error)
AddSite(ctx context.Context, site models.Site) error AddSite(ctx context.Context, site models.SiteConfig) error
UpdateSite(ctx context.Context, site models.Site) error UpdateSite(ctx context.Context, site models.SiteConfig) error
UpdateSitePaused(ctx context.Context, id int, paused bool) error UpdateSitePaused(ctx context.Context, id int, paused bool) error
DeleteSite(ctx context.Context, id int) error DeleteSite(ctx context.Context, id int) error
@@ -25,9 +25,9 @@ type Store interface {
DeleteAlert(ctx context.Context, id int) error DeleteAlert(ctx context.Context, id int) error
// Declarative config support // Declarative config support
GetSiteByName(ctx context.Context, name string) (models.Site, error) GetSiteByName(ctx context.Context, name string) (models.SiteConfig, error)
GetAlertByName(ctx context.Context, name string) (models.AlertConfig, error) GetAlertByName(ctx context.Context, name string) (models.AlertConfig, error)
AddSiteReturningID(ctx context.Context, site models.Site) (int, error) AddSiteReturningID(ctx context.Context, site models.SiteConfig) (int, error)
AddAlertReturningID(ctx context.Context, name, aType string, settings map[string]string) (int, error) AddAlertReturningID(ctx context.Context, name, aType string, settings map[string]string) (int, error)
// Users // Users
+11 -9
View File
@@ -11,9 +11,9 @@ import (
// mocks and override only the methods you need via the exported Func fields or // mocks and override only the methods you need via the exported Func fields or
// by shadowing the method on the embedding struct. // by shadowing the method on the embedding struct.
type BaseMock struct { type BaseMock struct {
GetSitesFunc func(ctx context.Context) ([]models.Site, error) GetSitesFunc func(ctx context.Context) ([]models.SiteConfig, error)
AddSiteFunc func(ctx context.Context, site models.Site) error AddSiteFunc func(ctx context.Context, site models.SiteConfig) error
UpdateSiteFunc func(ctx context.Context, site models.Site) error UpdateSiteFunc func(ctx context.Context, site models.SiteConfig) error
GetAllAlertsFunc func(ctx context.Context) ([]models.AlertConfig, error) GetAllAlertsFunc func(ctx context.Context) ([]models.AlertConfig, error)
GetAlertFunc func(ctx context.Context, id int) (models.AlertConfig, error) GetAlertFunc func(ctx context.Context, id int) (models.AlertConfig, error)
GetAllUsersFunc func(ctx context.Context) ([]models.User, error) GetAllUsersFunc func(ctx context.Context) ([]models.User, error)
@@ -41,21 +41,21 @@ type BaseMock struct {
func (m *BaseMock) Init(_ context.Context) error { return nil } func (m *BaseMock) Init(_ context.Context) error { return nil }
func (m *BaseMock) Close() error { return nil } func (m *BaseMock) Close() error { return nil }
func (m *BaseMock) GetSites(ctx context.Context) ([]models.Site, error) { func (m *BaseMock) GetSites(ctx context.Context) ([]models.SiteConfig, error) {
if m.GetSitesFunc != nil { if m.GetSitesFunc != nil {
return m.GetSitesFunc(ctx) return m.GetSitesFunc(ctx)
} }
return nil, nil return nil, nil
} }
func (m *BaseMock) AddSite(ctx context.Context, site models.Site) error { func (m *BaseMock) AddSite(ctx context.Context, site models.SiteConfig) error {
if m.AddSiteFunc != nil { if m.AddSiteFunc != nil {
return m.AddSiteFunc(ctx, site) return m.AddSiteFunc(ctx, site)
} }
return nil return nil
} }
func (m *BaseMock) UpdateSite(ctx context.Context, site models.Site) error { func (m *BaseMock) UpdateSite(ctx context.Context, site models.SiteConfig) error {
if m.UpdateSiteFunc != nil { if m.UpdateSiteFunc != nil {
return m.UpdateSiteFunc(ctx, site) return m.UpdateSiteFunc(ctx, site)
} }
@@ -90,15 +90,17 @@ func (m *BaseMock) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ m
func (m *BaseMock) DeleteAlert(_ context.Context, _ int) error { return nil } func (m *BaseMock) DeleteAlert(_ context.Context, _ int) error { return nil }
func (m *BaseMock) GetSiteByName(_ context.Context, _ string) (models.Site, error) { func (m *BaseMock) GetSiteByName(_ context.Context, _ string) (models.SiteConfig, error) {
return models.Site{}, nil return models.SiteConfig{}, nil
} }
func (m *BaseMock) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) { func (m *BaseMock) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
return models.AlertConfig{}, nil return models.AlertConfig{}, nil
} }
func (m *BaseMock) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) { return 0, nil } func (m *BaseMock) AddSiteReturningID(_ context.Context, _ models.SiteConfig) (int, error) {
return 0, nil
}
func (m *BaseMock) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) { func (m *BaseMock) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
return 0, nil return 0, nil
+13 -13
View File
@@ -8,9 +8,9 @@ import (
func TestSortSitesForDisplay_GroupsFirst(t *testing.T) { func TestSortSitesForDisplay_GroupsFirst(t *testing.T) {
sites := []models.Site{ sites := []models.Site{
{ID: 3, Name: "ungrouped", Type: "http", Status: "UP"}, {SiteConfig: models.SiteConfig{ID: 3, Name: "ungrouped", Type: "http"}, SiteState: models.SiteState{Status: "UP"}},
{ID: 1, Name: "group-a", Type: "group", Status: "UP"}, {SiteConfig: models.SiteConfig{ID: 1, Name: "group-a", Type: "group"}, SiteState: models.SiteState{Status: "UP"}},
{ID: 2, Name: "child", Type: "http", Status: "UP", ParentID: 1}, {SiteConfig: models.SiteConfig{ID: 2, Name: "child", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
} }
result := sortSitesForDisplay(sites, nil) result := sortSitesForDisplay(sites, nil)
if len(result) != 3 { if len(result) != 3 {
@@ -29,9 +29,9 @@ func TestSortSitesForDisplay_GroupsFirst(t *testing.T) {
func TestSortSitesForDisplay_CollapsedHidesChildren(t *testing.T) { func TestSortSitesForDisplay_CollapsedHidesChildren(t *testing.T) {
sites := []models.Site{ sites := []models.Site{
{ID: 1, Name: "group-a", Type: "group", Status: "UP"}, {SiteConfig: models.SiteConfig{ID: 1, Name: "group-a", Type: "group"}, SiteState: models.SiteState{Status: "UP"}},
{ID: 2, Name: "child-1", Type: "http", Status: "UP", ParentID: 1}, {SiteConfig: models.SiteConfig{ID: 2, Name: "child-1", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
{ID: 3, Name: "child-2", Type: "http", Status: "UP", ParentID: 1}, {SiteConfig: models.SiteConfig{ID: 3, Name: "child-2", Type: "http", ParentID: 1}, SiteState: models.SiteState{Status: "UP"}},
} }
collapsed := map[int]bool{1: true} collapsed := map[int]bool{1: true}
result := sortSitesForDisplay(sites, collapsed) result := sortSitesForDisplay(sites, collapsed)
@@ -45,9 +45,9 @@ func TestSortSitesForDisplay_CollapsedHidesChildren(t *testing.T) {
func TestSortSitesForDisplay_StatusOrdering(t *testing.T) { func TestSortSitesForDisplay_StatusOrdering(t *testing.T) {
sites := []models.Site{ sites := []models.Site{
{ID: 1, Name: "up-site", Type: "http", Status: "UP"}, {SiteConfig: models.SiteConfig{ID: 1, Name: "up-site", Type: "http"}, SiteState: models.SiteState{Status: "UP"}},
{ID: 2, Name: "down-site", Type: "http", Status: "DOWN"}, {SiteConfig: models.SiteConfig{ID: 2, Name: "down-site", Type: "http"}, SiteState: models.SiteState{Status: "DOWN"}},
{ID: 3, Name: "late-site", Type: "http", Status: "LATE"}, {SiteConfig: models.SiteConfig{ID: 3, Name: "late-site", Type: "http"}, SiteState: models.SiteState{Status: "LATE"}},
} }
result := sortSitesForDisplay(sites, nil) result := sortSitesForDisplay(sites, nil)
if result[0].Status != "DOWN" { if result[0].Status != "DOWN" {
@@ -63,9 +63,9 @@ func TestSortSitesForDisplay_StatusOrdering(t *testing.T) {
func TestFilterSites(t *testing.T) { func TestFilterSites(t *testing.T) {
sites := []models.Site{ sites := []models.Site{
{Name: "Production API"}, {SiteConfig: models.SiteConfig{Name: "Production API"}},
{Name: "Staging API"}, {SiteConfig: models.SiteConfig{Name: "Staging API"}},
{Name: "Database"}, {SiteConfig: models.SiteConfig{Name: "Database"}},
} }
tests := []struct { tests := []struct {
@@ -87,7 +87,7 @@ func TestFilterSites(t *testing.T) {
} }
func TestFilterSites_EmptyNeedle(t *testing.T) { func TestFilterSites_EmptyNeedle(t *testing.T) {
sites := []models.Site{{Name: "a"}, {Name: "b"}} sites := []models.Site{{SiteConfig: models.SiteConfig{Name: "a"}}, {SiteConfig: models.SiteConfig{Name: "b"}}}
got := filterSites(sites, "") got := filterSites(sites, "")
if len(got) != 2 { if len(got) != 2 {
t.Errorf("empty needle should return all, got %d", len(got)) t.Errorf("empty needle should return all, got %d", len(got))
+7 -7
View File
@@ -38,13 +38,13 @@ func TestSiteOrder(t *testing.T) {
site models.Site site models.Site
want int want int
}{ }{
{"down", models.Site{Status: "DOWN"}, 0}, {"down", models.Site{SiteState: models.SiteState{Status: "DOWN"}}, 0},
{"ssl exp", models.Site{Status: "SSL EXP"}, 0}, {"ssl exp", models.Site{SiteState: models.SiteState{Status: "SSL EXP"}}, 0},
{"late", models.Site{Status: "LATE"}, 1}, {"late", models.Site{SiteState: models.SiteState{Status: "LATE"}}, 1},
{"up", models.Site{Status: "UP"}, 2}, {"up", models.Site{SiteState: models.SiteState{Status: "UP"}}, 2},
{"pending", models.Site{Status: "PENDING"}, 3}, {"pending", models.Site{SiteState: models.SiteState{Status: "PENDING"}}, 3},
{"paused up", models.Site{Status: "UP", Paused: true}, 3}, {"paused up", models.Site{SiteConfig: models.SiteConfig{Paused: true}, SiteState: models.SiteState{Status: "UP"}}, 3},
{"paused down", models.Site{Status: "DOWN", Paused: true}, 3}, {"paused down", models.Site{SiteConfig: models.SiteConfig{Paused: true}, SiteState: models.SiteState{Status: "DOWN"}}, 3},
} }
for _, tt := range tests { for _, tt := range tests {
got := siteOrder(tt.site) got := siteOrder(tt.site)
+4 -7
View File
@@ -535,7 +535,7 @@ func (m *Model) submitSiteForm() tea.Cmd {
threshold = 7 threshold = 7
} }
site := models.Site{ cfg := models.SiteConfig{
ID: m.editID, ID: m.editID,
Name: d.Name, Name: d.Name,
URL: d.URL, URL: d.URL,
@@ -559,11 +559,8 @@ func (m *Model) submitSiteForm() tea.Cmd {
st := m.store st := m.store
m.state = stateDashboard m.state = stateDashboard
if m.editID > 0 { if m.editID > 0 {
// The engine's in-memory config updates immediately; the DB write m.engine.UpdateSiteConfig(cfg)
// follows in the Cmd. New sites enter the engine via its poll loop return writeCmd("Update site", func() error { return st.UpdateSite(context.Background(), cfg) })
// once the insert lands.
m.engine.UpdateSiteConfig(site)
return writeCmd("Update site", func() error { return st.UpdateSite(context.Background(), site) })
} }
return writeCmd("Add site", func() error { return st.AddSite(context.Background(), site) }) return writeCmd("Add site", func() error { return st.AddSite(context.Background(), cfg) })
} }
+5 -5
View File
@@ -116,7 +116,7 @@ func (*stubErr) Error() string { return "boom" }
func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) { func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) {
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}} ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
m := newTestModel(ms) m := newTestModel(ms)
m.sites = []models.Site{{ID: 1, Name: "site", Status: "DOWN"}} m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "DOWN"}}}
m.cursor = 0 m.cursor = 0
m.state = stateDetail m.state = stateDetail
m.termWidth = 120 m.termWidth = 120
@@ -201,7 +201,7 @@ func TestHandleTabData_DropsStaleSeq(t *testing.T) {
func TestHistoryKey_LoadsOffUIGoroutine(t *testing.T) { func TestHistoryKey_LoadsOffUIGoroutine(t *testing.T) {
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}} ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
m := newTestModel(ms) m := newTestModel(ms)
m.sites = []models.Site{{ID: 7, Name: "site"}} m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 7, Name: "site"}}}
m.state = stateDetail m.state = stateDetail
m.termWidth, m.termHeight = 120, 40 m.termWidth, m.termHeight = 120, 40
@@ -240,7 +240,7 @@ func TestHistoryKey_LoadsOffUIGoroutine(t *testing.T) {
func TestSLAData_DropsStaleReply(t *testing.T) { func TestSLAData_DropsStaleReply(t *testing.T) {
m := newTestModel(&tuiMockStore{}) m := newTestModel(&tuiMockStore{})
m.termWidth, m.termHeight = 120, 40 m.termWidth, m.termHeight = 120, 40
m.sites = []models.Site{{ID: 3, Status: "UP"}} m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 3}, SiteState: models.SiteState{Status: "UP"}}}
if cmd := (&m).openSLAView(m.sites[0]); cmd == nil { if cmd := (&m).openSLAView(m.sites[0]); cmd == nil {
t.Fatal("openSLAView should return a load Cmd") t.Fatal("openSLAView should return a load Cmd")
@@ -264,7 +264,7 @@ func TestSLAData_DropsStaleReply(t *testing.T) {
func TestConfirmDelete_WritesOffUIGoroutine(t *testing.T) { func TestConfirmDelete_WritesOffUIGoroutine(t *testing.T) {
ms := &tuiMockStore{} ms := &tuiMockStore{}
m := newTestModel(ms) m := newTestModel(ms)
m.sites = []models.Site{{ID: 4, Name: "s"}} m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 4, Name: "s"}}}
m.state = stateConfirmDelete m.state = stateConfirmDelete
m.deleteTab = 0 m.deleteTab = 0
m.deleteID = 4 m.deleteID = 4
@@ -312,7 +312,7 @@ func TestWriteDoneMsg_LogsErrorAndReloads(t *testing.T) {
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) { func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}} ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
m := newTestModel(ms) m := newTestModel(ms)
m.sites = []models.Site{{ID: 5, Name: "site"}} m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 5, Name: "site"}}}
m.state = stateDashboard m.state = stateDashboard
if (&m).detailRefreshCmd() != nil { if (&m).detailRefreshCmd() != nil {