feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
160
internal/notifier/run_exp_test.go
Обычный файл
160
internal/notifier/run_exp_test.go
Обычный файл
@@ -0,0 +1,160 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/config/database"
|
||||
"rsgit.ru/rsmon/rsmon/spec/factories"
|
||||
)
|
||||
|
||||
func init() {
|
||||
database.Init()
|
||||
}
|
||||
|
||||
func TestRunExp(t *testing.T) {
|
||||
log.Println("TestRunExp")
|
||||
models.Drop()
|
||||
models.Migrate()
|
||||
var err error
|
||||
contact, notification, monitor := factories.MonitorWithNotification()
|
||||
|
||||
check := factories.PersistedCheck(&monitor, "ssl")
|
||||
assert.Equal(t, "ssl", check.Kind, "check kind should be ssl")
|
||||
assert.Equal(t, monitor.ID, check.MonitorID, "check should have correct monitor id")
|
||||
|
||||
exp := time.Now().Add(14 * 24 * time.Hour)
|
||||
check.Expires = &exp
|
||||
err = models.DB().Save(&check).Error
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
RunExp()
|
||||
shoudHaveMessages("exp1 - contact should receive no messages", t, "exp", notification.ID, contact.ID, []int64{})
|
||||
|
||||
exp = time.Now().Add(1 * time.Hour)
|
||||
check.Expires = &exp
|
||||
err = models.DB().Save(&check).Error
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
RunExp()
|
||||
shoudHaveMessages("exp2 - contact should receive messages", t, "exp", notification.ID, contact.ID, []int64{check.ID})
|
||||
|
||||
RunExp()
|
||||
shoudHaveMessages("exp3 - contact should not receive duplicate messages", t, "exp", notification.ID, contact.ID, []int64{check.ID})
|
||||
}
|
||||
|
||||
// TestRunExpExpiresSystemContact exercises the regression scenario that the
|
||||
// dev DB hit after being restored from the production dump: the
|
||||
// contacts.is_system column was missing from the production schema and the
|
||||
// notifier's RunExp panic'd on the GORM preload.
|
||||
//
|
||||
// The fix has three layers: AutoMigrate adds the column, GetContacts logs
|
||||
// instead of panicking, and RunExp/RunExpCheck recover from panics. This
|
||||
// test verifies all three by setting a contact's is_system=true, queueing an
|
||||
// expiring SSL check, and confirming RunExp:
|
||||
// - does not panic;
|
||||
// - writes a queued exp message for the is_system contact (i.e. the schema
|
||||
// has the column and the field round-trips); and
|
||||
// - leaves RunExp returnable to its caller.
|
||||
func TestRunExpExpiresSystemContact(t *testing.T) {
|
||||
log.Println("TestRunExpExpiresSystemContact")
|
||||
models.Drop()
|
||||
models.Migrate()
|
||||
|
||||
account := &models.Account{Name: "acct-system-contact"}
|
||||
require.NoError(t, models.DB().Create(account).Error)
|
||||
accountID := account.ID
|
||||
|
||||
trueVal := true
|
||||
contact := &models.Contact{
|
||||
AccountID: &accountID,
|
||||
Name: "system-admin",
|
||||
Kind: "email",
|
||||
Value: "ops@example.com",
|
||||
IsSystem: &trueVal,
|
||||
}
|
||||
require.NoError(t, models.DB().Create(contact).Error)
|
||||
|
||||
group := factories.PersistedGroup(account)
|
||||
notification := factories.PersistedNotification(
|
||||
account, []int64{contact.ID}, []int64{group.ID}, 300, false,
|
||||
)
|
||||
monitor := factories.PersistedMonitor(&group)
|
||||
|
||||
check := factories.PersistedCheck(&monitor, "ssl")
|
||||
exp := time.Now().Add(1 * time.Hour)
|
||||
check.Expires = &exp
|
||||
require.NoError(t, models.DB().Save(&check).Error)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
RunExp()
|
||||
}, "RunExp must not panic when processing an is_system contact")
|
||||
|
||||
shoudHaveMessages(
|
||||
"is_system contact must receive the exp message",
|
||||
t, "exp", notification.ID, contact.ID, []int64{check.ID},
|
||||
)
|
||||
}
|
||||
|
||||
// TestRunExpDoesNotPanicOnBrokenAssociation replays the original prod-dump
|
||||
// panic in a contained way: the notification_contacts join row references a
|
||||
// non-existent contact id, which forces GORM's preload of Contacts to fail.
|
||||
// The fix's defensive recover() must keep RunExpCheck returning cleanly so
|
||||
// the scheduler loop survives a single bad row.
|
||||
//
|
||||
// We deliberately bypass the contacts.is_system column-drop path because
|
||||
// Postgres caches prepared statements per session; mutating the contacts
|
||||
// schema mid-test triggers SQLSTATE 0A000 (cached plan must not change
|
||||
// result type) on the pool's other connections and masks the panic we want
|
||||
// to verify.
|
||||
func TestRunExpDoesNotPanicOnBrokenAssociation(t *testing.T) {
|
||||
log.Println("TestRunExpDoesNotPanicOnBrokenAssociation")
|
||||
models.Drop()
|
||||
models.Migrate()
|
||||
|
||||
_, notification, monitor := factories.MonitorWithNotification()
|
||||
|
||||
// Force a broken association by deleting the contact that the
|
||||
// notification points to. GORM's preload of Contacts will then have no
|
||||
// rows for that notification, exercising the empty-contacts path
|
||||
// without involving DDL or FK violations.
|
||||
require.NoError(t, models.DB().
|
||||
Exec("DELETE FROM notification_contacts WHERE notification_id = ?", notification.ID).Error)
|
||||
// Re-add a join row pointing to a contact id that has been deleted
|
||||
// from contacts. We disable the FK temporarily so the join row sticks.
|
||||
require.NoError(t, models.DB().
|
||||
Exec("SET session_replication_role = 'replica'").Error)
|
||||
t.Cleanup(func() {
|
||||
_ = models.DB().
|
||||
Exec("SET session_replication_role = 'origin'").Error
|
||||
})
|
||||
bogusContactID := int64(9999999)
|
||||
require.NoError(t, models.DB().
|
||||
Exec(
|
||||
"INSERT INTO notification_contacts (notification_id, contact_id) VALUES (?, ?)",
|
||||
notification.ID, bogusContactID,
|
||||
).Error)
|
||||
|
||||
check := factories.PersistedCheck(&monitor, "ssl")
|
||||
exp := time.Now().Add(1 * time.Hour)
|
||||
check.Expires = &exp
|
||||
require.NoError(t, models.DB().Save(&check).Error)
|
||||
|
||||
require.NoError(t, models.DB().
|
||||
Preload("Monitor").
|
||||
Preload("Monitor.Group").
|
||||
Preload("Monitor.Group.Notifications").
|
||||
Preload("Monitor.Group.Notifications.Contacts").
|
||||
First(&check, check.ID).Error)
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
RunExpCheck(&check)
|
||||
}, "RunExpCheck must not panic when Contact preload encounters a broken association")
|
||||
}
|
||||
Ссылка в новой задаче
Block a user