refactor: decompose monitor.go and sqlstore.go into focused files
Split monitor.go (1131→350 lines) into alerts.go, checks.go, sites.go, and maintenance.go by concern. Split sqlstore.go (833→470 lines) into sqlstore_alerts.go, sqlstore_history.go, and sqlstore_maintenance.go by domain. Tests move with their implementation. Pure reorganization — no behavioral changes.
This commit is contained in:
@@ -3,9 +3,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
)
|
||||
@@ -74,60 +72,6 @@ func TestSiteCRUD(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlertCRUD(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
if err := s.AddAlert(context.Background(), "Discord", "discord", map[string]string{"url": "https://example.com/hook"}); err != nil {
|
||||
t.Fatalf("AddAlert: %v", err)
|
||||
}
|
||||
|
||||
alerts, err := s.GetAllAlerts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllAlerts: %v", err)
|
||||
}
|
||||
if len(alerts) != 1 {
|
||||
t.Fatalf("expected 1 alert, got %d", len(alerts))
|
||||
}
|
||||
if alerts[0].Type != "discord" {
|
||||
t.Errorf("expected type 'discord', got '%s'", alerts[0].Type)
|
||||
}
|
||||
if alerts[0].Settings["url"] != "https://example.com/hook" {
|
||||
t.Errorf("settings url mismatch")
|
||||
}
|
||||
|
||||
a, err := s.GetAlert(context.Background(), alerts[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAlert: %v", err)
|
||||
}
|
||||
if a.Name != "Discord" {
|
||||
t.Errorf("expected name 'Discord', got '%s'", a.Name)
|
||||
}
|
||||
|
||||
if err := s.UpdateAlert(context.Background(), a.ID, "Slack", "slack", map[string]string{"url": "https://slack.com/hook"}); err != nil {
|
||||
t.Fatalf("UpdateAlert: %v", err)
|
||||
}
|
||||
|
||||
a, err = s.GetAlert(context.Background(), a.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAlert: %v", err)
|
||||
}
|
||||
if a.Type != "slack" {
|
||||
t.Errorf("expected type 'slack', got '%s'", a.Type)
|
||||
}
|
||||
|
||||
if err := s.DeleteAlert(context.Background(), a.ID); err != nil {
|
||||
t.Fatalf("DeleteAlert: %v", err)
|
||||
}
|
||||
|
||||
alerts, err = s.GetAllAlerts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllAlerts: %v", err)
|
||||
}
|
||||
if len(alerts) != 0 {
|
||||
t.Fatalf("expected 0 alerts after delete, got %d", len(alerts))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCRUD(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
@@ -301,101 +245,6 @@ func TestImportData_NilUsersPreservesExisting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHistory(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
if err := s.SaveCheck(context.Background(), 1, 5000000, true); err != nil {
|
||||
t.Fatalf("SaveCheck: %v", err)
|
||||
}
|
||||
if err := s.SaveCheck(context.Background(), 1, 10000000, false); err != nil {
|
||||
t.Fatalf("SaveCheck: %v", err)
|
||||
}
|
||||
if err := s.SaveCheck(context.Background(), 2, 3000000, true); err != nil {
|
||||
t.Fatalf("SaveCheck site 2: %v", err)
|
||||
}
|
||||
|
||||
history, err := s.LoadAllHistory(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAllHistory: %v", err)
|
||||
}
|
||||
if len(history[1]) != 2 {
|
||||
t.Fatalf("expected 2 records for site 1, got %d", len(history[1]))
|
||||
}
|
||||
if len(history[2]) != 1 {
|
||||
t.Fatalf("expected 1 record for site 2, got %d", len(history[2]))
|
||||
}
|
||||
|
||||
upCount := 0
|
||||
for _, r := range history[1] {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
}
|
||||
if upCount != 1 {
|
||||
t.Errorf("expected 1 up record for site 1, got %d", upCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSiteCascade(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
site := models.SiteConfig{Name: "Cascade Test", URL: "https://example.com", Interval: 30}
|
||||
if err := s.AddSite(context.Background(), site); err != nil {
|
||||
t.Fatalf("AddSite: %v", err)
|
||||
}
|
||||
sites, err := s.GetSites(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetSites: %v", err)
|
||||
}
|
||||
siteID := sites[0].ID
|
||||
|
||||
if err := s.SaveCheck(context.Background(), siteID, 1000, true); err != nil {
|
||||
t.Fatalf("SaveCheck: %v", err)
|
||||
}
|
||||
if err := s.SaveStateChange(context.Background(), siteID, "UP", "DOWN", "timeout"); err != nil {
|
||||
t.Fatalf("SaveStateChange: %v", err)
|
||||
}
|
||||
mw := models.MaintenanceWindow{
|
||||
MonitorID: siteID,
|
||||
Title: "Test MW",
|
||||
Type: "maintenance",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(context.Background(), mw); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow: %v", err)
|
||||
}
|
||||
|
||||
if err := s.DeleteSite(context.Background(), siteID); err != nil {
|
||||
t.Fatalf("DeleteSite: %v", err)
|
||||
}
|
||||
|
||||
history, err := s.LoadAllHistory(context.Background(), 100)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAllHistory: %v", err)
|
||||
}
|
||||
if len(history[siteID]) != 0 {
|
||||
t.Errorf("expected 0 check_history rows, got %d", len(history[siteID]))
|
||||
}
|
||||
|
||||
changes, err := s.GetStateChanges(context.Background(), siteID, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStateChanges: %v", err)
|
||||
}
|
||||
if len(changes) != 0 {
|
||||
t.Errorf("expected 0 state_changes rows, got %d", len(changes))
|
||||
}
|
||||
|
||||
windows, err := s.GetActiveMaintenanceWindows(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetActiveMaintenanceWindows: %v", err)
|
||||
}
|
||||
for _, w := range windows {
|
||||
if w.MonitorID == siteID {
|
||||
t.Errorf("orphaned maintenance window found: id=%d", w.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneLogs(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
@@ -428,231 +277,3 @@ func TestPruneLogs(t *testing.T) {
|
||||
t.Error("oldest log survived prune")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneCheckHistory(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
for i := 0; i < maxCheckHistory+5; i++ {
|
||||
if err := s.SaveCheck(context.Background(), 1, int64(i), true); err != nil {
|
||||
t.Fatalf("SaveCheck site 1: %v", err)
|
||||
}
|
||||
}
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := s.SaveCheck(context.Background(), 2, int64(i), true); err != nil {
|
||||
t.Fatalf("SaveCheck site 2: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.PruneCheckHistory(context.Background()); err != nil {
|
||||
t.Fatalf("PruneCheckHistory: %v", err)
|
||||
}
|
||||
|
||||
history, err := s.LoadAllHistory(context.Background(), maxCheckHistory*2)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadAllHistory: %v", err)
|
||||
}
|
||||
if len(history[1]) != maxCheckHistory {
|
||||
t.Errorf("site 1: expected %d rows after prune, got %d", maxCheckHistory, len(history[1]))
|
||||
}
|
||||
if len(history[2]) != 3 {
|
||||
t.Errorf("site 2: expected 3 rows untouched, got %d", len(history[2]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPruneExpiredMaintenanceWindows(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// Expired 10 days ago — should be pruned with 7d retention.
|
||||
old := models.MaintenanceWindow{
|
||||
MonitorID: 0,
|
||||
Title: "Old Window",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-11 * 24 * time.Hour),
|
||||
EndTime: now.Add(-10 * 24 * time.Hour),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(context.Background(), old); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow (old): %v", err)
|
||||
}
|
||||
|
||||
// Expired 1 day ago — within 7d retention, should survive.
|
||||
recent := models.MaintenanceWindow{
|
||||
MonitorID: 0,
|
||||
Title: "Recent Window",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-2 * 24 * time.Hour),
|
||||
EndTime: now.Add(-1 * 24 * time.Hour),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(context.Background(), recent); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow (recent): %v", err)
|
||||
}
|
||||
|
||||
// Ongoing — no end time, should survive.
|
||||
ongoing := models.MaintenanceWindow{
|
||||
MonitorID: 0,
|
||||
Title: "Ongoing Window",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-1 * time.Hour),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(context.Background(), ongoing); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow (ongoing): %v", err)
|
||||
}
|
||||
|
||||
pruned, err := s.PruneExpiredMaintenanceWindows(context.Background(), 7*24*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("PruneExpiredMaintenanceWindows: %v", err)
|
||||
}
|
||||
if pruned != 1 {
|
||||
t.Errorf("expected 1 pruned, got %d", pruned)
|
||||
}
|
||||
|
||||
all, err := s.GetAllMaintenanceWindows(context.Background(), 100)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllMaintenanceWindows: %v", err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("expected 2 remaining windows, got %d", len(all))
|
||||
}
|
||||
for _, w := range all {
|
||||
if w.Title == "Old Window" {
|
||||
t.Error("old window should have been pruned")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOverlappingMaintenanceWindows(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
site := models.SiteConfig{Name: "web", URL: "https://example.com", Interval: 30}
|
||||
if err := s.AddSite(ctx, site); err != nil {
|
||||
t.Fatalf("AddSite: %v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
active := models.MaintenanceWindow{
|
||||
MonitorID: 1,
|
||||
Title: "Deploy v2",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-30 * time.Minute),
|
||||
EndTime: now.Add(30 * time.Minute),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(ctx, active); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow: %v", err)
|
||||
}
|
||||
|
||||
ended := models.MaintenanceWindow{
|
||||
MonitorID: 1,
|
||||
Title: "Old deploy",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-3 * time.Hour),
|
||||
EndTime: now.Add(-2 * time.Hour),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(ctx, ended); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow: %v", err)
|
||||
}
|
||||
|
||||
t.Run("same monitor overlaps", func(t *testing.T) {
|
||||
overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, now.Add(1*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(overlaps) != 1 {
|
||||
t.Fatalf("expected 1 overlap, got %d", len(overlaps))
|
||||
}
|
||||
if overlaps[0].Title != "Deploy v2" {
|
||||
t.Errorf("expected 'Deploy v2', got %q", overlaps[0].Title)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("different monitor no overlap", func(t *testing.T) {
|
||||
overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 99, now, now.Add(1*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(overlaps) != 0 {
|
||||
t.Errorf("expected 0 overlaps, got %d", len(overlaps))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("global window overlaps all", func(t *testing.T) {
|
||||
global := models.MaintenanceWindow{
|
||||
MonitorID: 0,
|
||||
Title: "Global freeze",
|
||||
Type: "maintenance",
|
||||
StartTime: now.Add(-10 * time.Minute),
|
||||
EndTime: now.Add(2 * time.Hour),
|
||||
}
|
||||
if err := s.AddMaintenanceWindow(ctx, global); err != nil {
|
||||
t.Fatalf("AddMaintenanceWindow: %v", err)
|
||||
}
|
||||
overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, now.Add(1*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(overlaps) != 2 {
|
||||
t.Errorf("expected 2 overlaps (specific + global), got %d", len(overlaps))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("indefinite window overlaps", func(t *testing.T) {
|
||||
overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now, time.Time{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(overlaps) < 1 {
|
||||
t.Error("expected at least 1 overlap for indefinite window")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ended window excluded", func(t *testing.T) {
|
||||
overlaps, err := s.GetOverlappingMaintenanceWindows(ctx, 1, now.Add(-4*time.Hour), now.Add(-3*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(overlaps) != 0 {
|
||||
t.Errorf("expected 0 overlaps for past range, got %d", len(overlaps))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ImportData must encrypt alert settings (like AddAlert/UpdateAlert) so a
|
||||
// restore with UPTOP_ENCRYPTION_KEY set never lands secrets in plaintext.
|
||||
func TestImportData_EncryptsAlertSettings(t *testing.T) {
|
||||
s := newTestStore(t)
|
||||
enc, err := NewEncryptor(strings.Repeat("ab", 32)) // 64 hex chars = 32 bytes
|
||||
if err != nil {
|
||||
t.Fatalf("NewEncryptor: %v", err)
|
||||
}
|
||||
s.SetEncryptor(enc)
|
||||
|
||||
backup := models.Backup{
|
||||
Alerts: []models.AlertConfig{
|
||||
{ID: 1, Name: "tg", Type: "telegram", Settings: map[string]string{"token": "123:SECRET", "chat_id": "42"}},
|
||||
},
|
||||
}
|
||||
if err := s.ImportData(context.Background(), backup); err != nil {
|
||||
t.Fatalf("ImportData: %v", err)
|
||||
}
|
||||
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT settings FROM alerts WHERE id = 1").Scan(&raw); err != nil {
|
||||
t.Fatalf("query settings: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(raw, encryptedPrefix) {
|
||||
t.Errorf("imported settings not encrypted: %q", raw)
|
||||
}
|
||||
if strings.Contains(raw, "SECRET") {
|
||||
t.Errorf("plaintext secret found in stored column: %q", raw)
|
||||
}
|
||||
|
||||
alerts, err := s.GetAllAlerts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllAlerts: %v", err)
|
||||
}
|
||||
if len(alerts) != 1 || alerts[0].Settings["token"] != "123:SECRET" {
|
||||
t.Errorf("decrypt round-trip failed: %+v", alerts)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user