feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
215
app/models/monitor_state_test.go
Обычный файл
215
app/models/monitor_state_test.go
Обычный файл
@@ -0,0 +1,215 @@
|
||||
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
|
||||
Ссылка в новой задаче
Block a user