220 строки
7.6 KiB
Go
220 строки
7.6 KiB
Go
package notifier
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/datatypes"
|
|
|
|
"rsgit.ru/rsmon/rsmon/app/models"
|
|
"rsgit.ru/rsmon/rsmon/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
|