Files
worker/app/models/maintenance_test.go
Gleb Tv 2c7a0236da feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
2026-07-13 17:55:14 +03:00

106 строки
5.9 KiB
Go

package models_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rsgit.ru/rsmon/rsmon/app/models"
)
func TestMaintenanceValidateGeneratesRecurringCron(t *testing.T) {
tests := []struct {
name string
m models.Maintenance
want string
}{
{"interval", models.Maintenance{Title: "interval", Strategy: models.MaintenanceRecurringInterval, StartDate: maintenanceTimePtr(time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)), StartTime: "02:30", IntervalDay: 3, DurationSec: 60, Timezone: "UTC"}, ""},
{"weekday", models.Maintenance{Title: "weekdays", Strategy: models.MaintenanceRecurringWeekday, StartTime: "02:30", Weekdays: []int64{1, 5}, DurationSec: 60, Timezone: "UTC"}, "30 02 * * 1,5"},
{"month", models.Maintenance{Title: "month", Strategy: models.MaintenanceRecurringDayOfMonth, StartTime: "02:30", DaysOfMonth: []string{"1", "lastDay1"}, DurationSec: 60, Timezone: "UTC"}, "30 02 1,28-31 * *"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) { require.NoError(t, test.m.Validate()); assert.Equal(t, test.want, test.m.Cron) })
}
}
func TestMaintenanceIsUnderMaintenanceBoundariesAndTimezone(t *testing.T) {
start := time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)
end := start.Add(time.Hour)
single := models.Maintenance{Title: "single", Strategy: models.MaintenanceSingle, StartDate: &start, EndDate: &end, Active: true, Timezone: "UTC"}
assert.True(t, single.IsUnderMaintenance(start))
assert.True(t, single.IsUnderMaintenance(end.Add(-time.Nanosecond)))
assert.False(t, single.IsUnderMaintenance(end))
cron := models.Maintenance{Title: "moscow", Strategy: models.MaintenanceCron, Cron: "0 12 * * *", DurationSec: 3600, Active: true, Timezone: "Europe/Moscow"}
require.NoError(t, cron.Validate())
assert.True(t, cron.IsUnderMaintenance(time.Date(2026, 7, 1, 9, 30, 0, 0, time.UTC)), "12:30 Moscow is 09:30 UTC in July")
assert.False(t, cron.IsUnderMaintenance(time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)))
}
func TestMaintenanceValidationRejectsInvalidSchedules(t *testing.T) {
badCron := models.Maintenance{Title: "bad", Strategy: models.MaintenanceCron, Cron: "not cron", DurationSec: 1, Timezone: "UTC"}
badSingle := models.Maintenance{Title: "bad", Strategy: models.MaintenanceSingle, Timezone: "UTC"}
assert.Error(t, badCron.Validate())
assert.Error(t, badSingle.Validate())
}
func TestMaintenanceLastDayIsNotEveryDayInExpandedCronRange(t *testing.T) {
m := models.Maintenance{Title: "last", Strategy: models.MaintenanceRecurringDayOfMonth, StartTime: "12:00", DaysOfMonth: []string{"lastDay1"}, DurationSec: 3600, Active: true, Timezone: "UTC"}
require.NoError(t, m.Validate())
assert.False(t, m.IsUnderMaintenance(time.Date(2026, 3, 28, 12, 30, 0, 0, time.UTC)))
assert.True(t, m.IsUnderMaintenance(time.Date(2026, 3, 31, 12, 30, 0, 0, time.UTC)))
}
func TestMaintenanceRecurringIntervalUsesAnchorAndIntervalDay(t *testing.T) {
anchor := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
m := models.Maintenance{Title: "every three days", Strategy: models.MaintenanceRecurringInterval, StartDate: &anchor, StartTime: "12:00", IntervalDay: 3, DurationSec: 3600, Active: true, Timezone: "UTC"}
require.NoError(t, m.Validate())
assert.True(t, m.IsUnderMaintenance(time.Date(2026, 7, 4, 12, 30, 0, 0, time.UTC)))
assert.False(t, m.IsUnderMaintenance(time.Date(2026, 7, 5, 12, 30, 0, 0, time.UTC)))
next := m.NextRun(time.Date(2026, 7, 2, 13, 0, 0, 0, time.UTC))
require.NotNil(t, next)
assert.Equal(t, time.Date(2026, 7, 4, 12, 0, 0, 0, time.UTC), *next)
}
func TestMaintenanceRecurringIntervalRequiresAnchorForMultiDayAndPositiveInterval(t *testing.T) {
m := models.Maintenance{Title: "invalid", Strategy: models.MaintenanceRecurringInterval, StartTime: "12:00", IntervalDay: 2, DurationSec: 60, Timezone: "UTC"}
assert.Error(t, m.Validate())
}
func TestMaintenanceRecurringIntervalKeepsCivilDayAcrossDST(t *testing.T) {
loc, err := time.LoadLocation("Europe/Berlin")
require.NoError(t, err)
// March 29, 2026 is the spring-forward day in Berlin. The second run is
// still two civil days after the anchor, not one because a day was 23h.
anchor := time.Date(2026, 3, 27, 0, 0, 0, 0, loc)
m := models.Maintenance{Title: "DST", Strategy: models.MaintenanceRecurringInterval, StartDate: &anchor, StartTime: "03:30", IntervalDay: 2, DurationSec: 3600, Active: true, Timezone: "Europe/Berlin"}
require.NoError(t, m.Validate())
assert.True(t, m.IsUnderMaintenance(time.Date(2026, 3, 29, 4, 0, 0, 0, loc).UTC()))
}
func TestMaintenanceStartNotificationKeyIncludesRevisionAndOccurrence(t *testing.T) {
revision := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
start := revision.Add(5 * time.Minute)
key := models.MaintenanceStartNotificationKey(7, revision, start, "Europe/Berlin", 11, 13)
assert.Equal(t, key, models.MaintenanceStartNotificationKey(7, revision, start, "Europe/Berlin", 11, 13))
assert.NotEqual(t, key, models.MaintenanceStartNotificationKey(7, revision.Add(time.Second), start, "Europe/Berlin", 11, 13))
}
func TestMaintenanceStartNotificationKeyDeduplicatesDSTFallbackCivilOccurrence(t *testing.T) {
revision := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
first := time.Date(2026, 10, 25, 0, 30, 0, 0, time.UTC)
second := first.Add(time.Hour)
assert.Equal(t, models.MaintenanceStartNotificationKey(7, revision, first, "Europe/Berlin", 11, 13), models.MaintenanceStartNotificationKey(7, revision, second, "Europe/Berlin", 11, 13))
}
func TestMaintenanceNextRunSkipsNonFinalLastDayCandidates(t *testing.T) {
m := models.Maintenance{Title: "last", Strategy: models.MaintenanceRecurringDayOfMonth, StartTime: "12:00", DaysOfMonth: []string{"lastDay1"}, DurationSec: 60, Active: true, Timezone: "UTC"}
require.NoError(t, m.Validate())
next := m.NextRun(time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC))
require.NotNil(t, next)
assert.Equal(t, time.Date(2026, 4, 30, 12, 0, 0, 0, time.UTC), *next)
}
func maintenanceTimePtr(value time.Time) *time.Time { return &value }