Files
worker/internal/notifier/run_exp_test.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

161 строка
5.4 KiB
Go

package notifier
import (
"log"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"rocketgit.ru/rsmon/worker/app/models"
"rocketgit.ru/rsmon/worker/config/database"
"rocketgit.ru/rsmon/worker/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")
}