Files
worker/internal/notifier/producer_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

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(&notification).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(&notification, &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(&notification).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(&notification, &contact, &loaded)
require.NoError(t, err)
require.NotNil(t, first)
second, err := EnqueueNotificationTaskFromMessage(&notification, &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