216 строки
7.3 KiB
Go
216 строки
7.3 KiB
Go
package models_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/datatypes"
|
|
|
|
"rsgit.ru/rsmon/rsmon/app/models"
|
|
)
|
|
|
|
// monitorStateTestWorld bundles the account/group/monitor fixture the
|
|
// monitor-state tests need. It deliberately re-creates the row in each
|
|
// test rather than sharing, because Monitor.UpdateStatusFromChecks
|
|
// mutates the row in place and the per-test assertions need a clean
|
|
// baseline.
|
|
type monitorStateTestWorld struct {
|
|
plan models.Plan
|
|
account models.Account
|
|
group models.Group
|
|
monitor models.Monitor
|
|
}
|
|
|
|
// seedMonitorStateWorld provisions one plan/account/group/monitor with
|
|
// the requested initial state. The monitor is enabled so
|
|
// UpdateStatusFromChecks treats its checks as live.
|
|
func seedMonitorStateWorld(t *testing.T, initialState string) monitorStateTestWorld {
|
|
t.Helper()
|
|
models.Drop()
|
|
models.Migrate()
|
|
|
|
plan := models.Plan{Name: "ms-plan", Default: true}
|
|
require.NoError(t, models.DB().Create(&plan).Error)
|
|
|
|
account := models.Account{Name: "ms-acc", PlanID: &plan.ID}
|
|
require.NoError(t, models.DB().Create(&account).Error)
|
|
|
|
group := models.Group{AccountID: account.ID, Name: "ms"}
|
|
require.NoError(t, models.DB().Create(&group).Error)
|
|
|
|
mon := models.Monitor{
|
|
Name: stringPtrAgg("mon.test"),
|
|
Host: "mon.test",
|
|
GroupID: group.ID,
|
|
Enabled: true,
|
|
State: initialState,
|
|
}
|
|
require.NoError(t, models.DB().Create(&mon).Error)
|
|
|
|
return monitorStateTestWorld{
|
|
plan: plan,
|
|
account: account,
|
|
group: group,
|
|
monitor: mon,
|
|
}
|
|
}
|
|
|
|
// attachCheck creates an enabled check on the monitor with the given
|
|
// state. Returns the persisted check so the test can re-load it.
|
|
func attachCheck(t *testing.T, monitorID int64, kind string, state string) models.Check {
|
|
t.Helper()
|
|
enTrue := true
|
|
c := models.Check{
|
|
MonitorID: monitorID,
|
|
Kind: kind,
|
|
Interval: 60,
|
|
Enabled: &enTrue,
|
|
State: state,
|
|
Settings: datatypes.JSON([]byte(`{}`)),
|
|
}
|
|
require.NoError(t, models.DB().Create(&c).Error)
|
|
return c
|
|
}
|
|
|
|
// reloadMonitor pulls the latest monitor state from the DB so the test
|
|
// can compare against the post-UpdateStatusFromChecks row.
|
|
func reloadMonitor(t *testing.T, id int64) models.Monitor {
|
|
t.Helper()
|
|
var m models.Monitor
|
|
require.NoError(t, models.DB().First(&m, id).Error)
|
|
return m
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Phase 3 severity rules: FAIL > ERR > DEGRADED > WARN > OK.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TestMonitorStateFromChecks_Degraded: monitor with 1 OK + 1 DEGRADED
|
|
// child → monitor.State = DEGRADED. The new severity rank must
|
|
// correctly promote DEGRADED above OK.
|
|
func TestMonitorStateFromChecks_Degraded(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
attachCheck(t, world.monitor.ID, "http", "OK")
|
|
attachCheck(t, world.monitor.ID, "http", "DEGRADED")
|
|
|
|
world.monitor.UpdateStatusFromChecks()
|
|
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, "DEGRADED", got.State,
|
|
"DEGRADED child must promote monitor above OK")
|
|
}
|
|
|
|
// TestMonitorStateFromChecks_DegradedWithError: monitor with 1 ERR +
|
|
// 1 DEGRADED → monitor.State = ERR. ERR beats DEGRADED in the
|
|
// severity order.
|
|
func TestMonitorStateFromChecks_DegradedWithError(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
attachCheck(t, world.monitor.ID, "http", "ERR")
|
|
attachCheck(t, world.monitor.ID, "http", "DEGRADED")
|
|
|
|
world.monitor.UpdateStatusFromChecks()
|
|
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, "ERR", got.State,
|
|
"ERR must beat DEGRADED — the order is FAIL > ERR > DEGRADED > WARN > OK")
|
|
}
|
|
|
|
// TestMonitorStateFromChecks_DegradedOnlyOK covers the single-DEGRADED
|
|
// case explicitly so a regression that treats DEGRADED as "WARN-ish"
|
|
// would flip this assertion.
|
|
func TestMonitorStateFromChecks_DegradedOnlyOK(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
attachCheck(t, world.monitor.ID, "http", "OK")
|
|
attachCheck(t, world.monitor.ID, "http", "DEGRADED")
|
|
|
|
world.monitor.UpdateStatusFromChecks()
|
|
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, "DEGRADED", got.State,
|
|
"mixed OK+DEGRADED monitor must be DEGRADED, not OK")
|
|
}
|
|
|
|
// TestMonitorStateFromChecks_SeverityOrderingTable is a table-driven
|
|
// sweep of the FAIL > ERR > DEGRADED > WARN > OK ladder. The case set
|
|
// is intentionally small — every pair that could reveal a wrong
|
|
// winner under the new severity rank. Keeping it table-driven makes
|
|
// it trivial to add more cases if a future state is introduced.
|
|
func TestMonitorStateFromChecks_SeverityOrderingTable(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
checks []string
|
|
wantMon string
|
|
}{
|
|
{"all_ok", []string{"OK", "OK"}, "OK"},
|
|
{"all_warn", []string{"WARN", "WARN"}, "WARN"},
|
|
{"ok_with_warn", []string{"OK", "WARN"}, "WARN"},
|
|
{"warn_with_ok", []string{"WARN", "OK"}, "WARN"}, // order independence
|
|
{"ok_with_degraded", []string{"OK", "DEGRADED"}, "DEGRADED"},
|
|
{"warn_with_degraded", []string{"WARN", "DEGRADED"}, "DEGRADED"},
|
|
{"degraded_with_warn", []string{"DEGRADED", "WARN"}, "DEGRADED"},
|
|
{"err_with_degraded", []string{"ERR", "DEGRADED"}, "ERR"},
|
|
{"degraded_with_err", []string{"DEGRADED", "ERR"}, "ERR"},
|
|
{"fail_with_err", []string{"FAIL", "ERR"}, "FAIL"},
|
|
{"err_with_fail", []string{"ERR", "FAIL"}, "FAIL"}, // order independence
|
|
{"fail_alone", []string{"FAIL"}, "FAIL"},
|
|
{"all_degraded", []string{"DEGRADED", "DEGRADED"}, "DEGRADED"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
for i, st := range c.checks {
|
|
attachCheck(t, world.monitor.ID,
|
|
"http-"+string(rune('a'+i)), st)
|
|
}
|
|
world.monitor.UpdateStatusFromChecks()
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, c.wantMon, got.State,
|
|
"severity winner for child states %v must be %s", c.checks, c.wantMon)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMonitorStateFromChecks_NoChecksIsWarn pins the existing "no
|
|
// enabled checks → WARN" behavior — the new severity rank must not
|
|
// accidentally produce OK for an empty monitor.
|
|
func TestMonitorStateFromChecks_NoChecksIsWarn(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
world.monitor.UpdateStatusFromChecks()
|
|
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, "WARN", got.State,
|
|
"an enabled monitor with zero checks must remain WARN")
|
|
}
|
|
|
|
// TestMonitorStateFromChecks_DisabledCheckIgnored verifies that a
|
|
// disabled check is not folded into the severity decision. Otherwise
|
|
// a stuck-in-ERR check that has been disabled would keep tripping the
|
|
// monitor forever.
|
|
func TestMonitorStateFromChecks_DisabledCheckIgnored(t *testing.T) {
|
|
world := seedMonitorStateWorld(t, "UNK")
|
|
attachCheck(t, world.monitor.ID, "http", "OK")
|
|
|
|
enFalse := false
|
|
disabled := models.Check{
|
|
MonitorID: world.monitor.ID,
|
|
Kind: "http-disabled",
|
|
Interval: 60,
|
|
Enabled: &enFalse,
|
|
State: "ERR",
|
|
Settings: datatypes.JSON([]byte(`{}`)),
|
|
}
|
|
require.NoError(t, models.DB().Create(&disabled).Error)
|
|
|
|
world.monitor.UpdateStatusFromChecks()
|
|
|
|
got := reloadMonitor(t, world.monitor.ID)
|
|
assert.Equal(t, "OK", got.State,
|
|
"disabled check must be ignored — only the enabled OK check counts")
|
|
}
|
|
|
|
// guard against time import being pruned by an editor when individual
|
|
// test bodies stop referencing it directly.
|
|
var _ = time.Second
|