package notifier import ( "encoding/json" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/datatypes" "rocketgit.ru/rsmon/worker/app/models" "rocketgit.ru/rsmon/worker/config/database" ) func init() { database.Init() } // TestContactKindToMethod is the table that drives producer + selector + // executor dispatch. func TestContactKindToMethod(t *testing.T) { cases := map[string]string{ "email": "email", "telegram_private": "telegram", "telegram_group": "telegram", "webhook": "webhook", "mattermost": "mattermost", "sms": "sms", "voice": "voice", "": "", "unknown": "", } for in, want := range cases { assert.Equal(t, want, ContactKindToMethod(in), "kind=%q", in) } } // TestEnqueueNotificationTaskFromMessage_SeedsTaskWithPreRenderedBody // exercises the producer end-to-end against the test DB. It seeds an account, // notification, contact, and event; calls EnqueueNotificationTaskFromMessage; // and checks the resulting Task row has the pre-rendered subject/body in the // payload (i.e. the worker does not need to know templating). func TestEnqueueNotificationTaskFromMessage_SeedsTaskWithPreRenderedBody(t *testing.T) { models.Drop() models.Migrate() models.DB().Exec( "INSERT INTO regions (code, name, enabled, created_at, updated_at) VALUES (?, ?, true, now(), now())", "test", "test", ) plan := models.Plan{Name: "producer-test"} require.NoError(t, models.DB().Create(&plan).Error) user := models.User{Name: "u", Email: producerStringPtr("u@example.com"), Timezone: "UTC"} require.NoError(t, models.DB().Create(&user).Error) account := models.Account{Name: "a", Timezone: "UTC", Language: "en", PlanID: &plan.ID} require.NoError(t, models.DB().Create(&account).Error) group := models.Group{Name: "g", AccountID: account.ID} require.NoError(t, models.DB().Create(&group).Error) monitor := models.Monitor{ Name: producerStringPtr("m"), Host: "example.com", GroupID: group.ID, Enabled: true, } require.NoError(t, models.DB().Create(&monitor).Error) notification := models.Notification{ Name: "default", AccountID: account.ID, Enabled: true, NotifyDown: true, NotifyRestore: true, } require.NoError(t, models.DB().Create(¬ification).Error) contact := models.Contact{Name: "ops", Kind: "email", Value: "ops@example.com", AccountID: &account.ID} require.NoError(t, models.DB().Create(&contact).Error) start := time.Now().Add(-time.Minute) event := models.Event{ MonitorID: monitor.ID, StartTime: &start, State: "current", Errors: 5, } require.NoError(t, models.DB().Create(&event).Error) msg := models.Message{ NotificationID: notification.ID, ContactID: contact.ID, Events: []models.Event{event}, Kind: "down", State: "queued", } require.NoError(t, models.DB().Create(&msg).Error) w := &models.WorkerNode{ WorkerID: "worker-producer", RegionCode: "test", Status: "active", AuthToken: "tok", Concurrency: 4, LastSeen: producerTimePtr(time.Now()), Capabilities: datatypes.JSON([]byte( `{"check_types":["http"],"task_envelope":true,"notification_methods":["email"],"notification_accounts":[]}`, )), } require.NoError(t, models.DB().Create(w).Error) // Load the message back with the scope GetContent expects (Monitor + Group // + Notification preloaded). The sender's GetContent panics on nil fields. loaded := models.Message{} require.NoError(t, models.MessageScope(models.DB()).First(&loaded, msg.ID).Error) require.Len(t, loaded.Events, 1) require.NotNil(t, loaded.Events[0].Monitor) row, err := EnqueueNotificationTaskFromMessage(¬ification, &contact, &loaded) require.NoError(t, err) require.NotNil(t, row, "expected a Task row from the producer") assert.Equal(t, models.TaskKindNotification, row.Kind) assert.Equal(t, models.TaskStateQueued, row.State) assert.NotEmpty(t, row.Payload) assert.Equal(t, account.ID, row.AccountID) assert.Equal(t, &contact.ID, row.ContactID) require.NotNil(t, row.MessageID) assert.Equal(t, loaded.ID, *row.MessageID) assert.Equal(t, models.NotificationIdempotencyKey(notification.ID, contact.ID, event.ID), row.IdempotencyKey) var payload map[string]interface{} require.NoError(t, json.Unmarshal(row.Payload, &payload)) assert.Equal(t, "email", payload["method"]) assert.Equal(t, "down", payload["message_kind"]) assert.NotEmpty(t, payload["subject"]) } // TestEnqueueNotificationTaskFromMessage_Idempotent exercises the producer's // idempotency contract: a second call with the same (notification, contact, // event) tuple must not create a second Task row. func TestEnqueueNotificationTaskFromMessage_Idempotent(t *testing.T) { models.Drop() models.Migrate() models.DB().Exec( "INSERT INTO regions (code, name, enabled, created_at, updated_at) VALUES (?, ?, true, now(), now())", "test", "test", ) plan := models.Plan{Name: "p"} require.NoError(t, models.DB().Create(&plan).Error) user := models.User{Name: "u", Email: producerStringPtr("u@example.com"), Timezone: "UTC"} require.NoError(t, models.DB().Create(&user).Error) account := models.Account{Name: "a", Timezone: "UTC", Language: "en", PlanID: &plan.ID} require.NoError(t, models.DB().Create(&account).Error) group := models.Group{Name: "g", AccountID: account.ID} require.NoError(t, models.DB().Create(&group).Error) monitor := models.Monitor{ Name: producerStringPtr("m"), Host: "example.com", GroupID: group.ID, Enabled: true, } require.NoError(t, models.DB().Create(&monitor).Error) notification := models.Notification{ Name: "default", AccountID: account.ID, Enabled: true, NotifyDown: true, NotifyRestore: true, } require.NoError(t, models.DB().Create(¬ification).Error) contact := models.Contact{Name: "ops", Kind: "email", Value: "ops@example.com", AccountID: &account.ID} require.NoError(t, models.DB().Create(&contact).Error) start := time.Now().Add(-time.Minute) event := models.Event{MonitorID: monitor.ID, StartTime: &start, State: "current", Errors: 5} require.NoError(t, models.DB().Create(&event).Error) w := &models.WorkerNode{ WorkerID: "worker-idem", RegionCode: "test", Status: "active", AuthToken: "tok", Concurrency: 4, LastSeen: producerTimePtr(time.Now()), Capabilities: datatypes.JSON([]byte( `{"check_types":["http"],"task_envelope":true,"notification_methods":["email"],"notification_accounts":[]}`, )), } require.NoError(t, models.DB().Create(w).Error) msg := models.Message{ NotificationID: notification.ID, ContactID: contact.ID, Events: []models.Event{event}, Kind: "down", State: "queued", } require.NoError(t, models.DB().Create(&msg).Error) loaded := models.Message{} require.NoError(t, models.MessageScope(models.DB()).First(&loaded, msg.ID).Error) first, err := EnqueueNotificationTaskFromMessage(¬ification, &contact, &loaded) require.NoError(t, err) require.NotNil(t, first) second, err := EnqueueNotificationTaskFromMessage(¬ification, &contact, &loaded) require.NoError(t, err) require.NotNil(t, second) assert.Equal(t, first.ID, second.ID, "second producer call must reuse the first task row") var count int64 require.NoError(t, models.DB().Model(&models.Task{}). Where("idempotency_key = ?", first.IdempotencyKey). Count(&count).Error) assert.EqualValues(t, 1, count) } func producerStringPtr(s string) *string { return &s } func producerTimePtr(value time.Time) *time.Time { return &value } // guard against uuid being accidentally dropped from the imports. var _ = uuid.New