2025-09-26 18:54:58 +05:30
|
|
|
package rulebasednotification
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"sync"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
|
2025-10-03 19:47:15 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/nfroutingstore/nfroutingstoretest"
|
2025-09-26 18:54:58 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/factory"
|
|
|
|
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
2025-10-03 19:47:15 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/types"
|
2025-09-26 18:54:58 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
2025-10-03 19:47:15 +05:30
|
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
|
|
|
|
2025-09-26 18:54:58 +05:30
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
2025-10-03 19:47:15 +05:30
|
|
|
|
|
|
|
|
"github.com/prometheus/common/model"
|
2025-09-26 18:54:58 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func createTestProviderSettings() factory.ProviderSettings {
|
|
|
|
|
return instrumentationtest.New().ToProviderSettings()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewFactory(t *testing.T) {
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
providerFactory := NewFactory(routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
assert.NotNil(t, providerFactory)
|
|
|
|
|
assert.Equal(t, "rulebased", providerFactory.Name().String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNew(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.NotNil(t, provider)
|
|
|
|
|
|
|
|
|
|
// Verify provider implements the interface correctly
|
|
|
|
|
assert.Implements(t, (*nfmanager.NotificationManager)(nil), provider)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_SetNotificationConfig(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
orgID string
|
|
|
|
|
ruleID string
|
|
|
|
|
config *alertmanagertypes.NotificationConfig
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "valid parameters",
|
|
|
|
|
orgID: "org1",
|
|
|
|
|
ruleID: "rule1",
|
|
|
|
|
config: &alertmanagertypes.NotificationConfig{
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: 2 * time.Hour,
|
|
|
|
|
NoDataInterval: 2 * time.Hour,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty orgID",
|
|
|
|
|
orgID: "",
|
|
|
|
|
ruleID: "rule1",
|
|
|
|
|
config: &alertmanagertypes.NotificationConfig{
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: time.Hour,
|
|
|
|
|
NoDataInterval: time.Hour,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantErr: true, // Should error due to validation
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty ruleID",
|
|
|
|
|
orgID: "org1",
|
|
|
|
|
ruleID: "",
|
|
|
|
|
config: &alertmanagertypes.NotificationConfig{
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: time.Hour,
|
|
|
|
|
NoDataInterval: time.Hour,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
wantErr: true, // Should error due to validation
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "nil config",
|
|
|
|
|
orgID: "org1",
|
|
|
|
|
ruleID: "rule1",
|
|
|
|
|
config: nil,
|
|
|
|
|
wantErr: true, // Should error due to nil config
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
err := provider.SetNotificationConfig(tt.orgID, tt.ruleID, tt.config)
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
} else {
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// If we set a config successfully, we should be able to retrieve it
|
|
|
|
|
if tt.orgID != "" && tt.ruleID != "" && tt.config != nil {
|
|
|
|
|
retrievedConfig, retrieveErr := provider.GetNotificationConfig(tt.orgID, tt.ruleID)
|
|
|
|
|
assert.NoError(t, retrieveErr)
|
|
|
|
|
assert.NotNil(t, retrievedConfig)
|
|
|
|
|
assert.Equal(t, tt.config.Renotify, retrievedConfig.Renotify)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_GetNotificationConfig(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
orgID := "test-org"
|
2025-10-03 19:47:15 +05:30
|
|
|
ruleID := "ruleId"
|
2025-09-26 18:54:58 +05:30
|
|
|
customConfig := &alertmanagertypes.NotificationConfig{
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: 30 * time.Minute,
|
|
|
|
|
NoDataInterval: 30 * time.Minute,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ruleId1 := "rule-1"
|
|
|
|
|
customConfig1 := &alertmanagertypes.NotificationConfig{
|
|
|
|
|
NotificationGroup: map[model.LabelName]struct{}{
|
|
|
|
|
model.LabelName("group1"): {},
|
|
|
|
|
model.LabelName("group2"): {},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = provider.SetNotificationConfig(orgID, ruleID, customConfig)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
err = provider.SetNotificationConfig(orgID, ruleId1, customConfig1)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
orgID string
|
|
|
|
|
ruleID string
|
2025-10-03 19:47:15 +05:30
|
|
|
alert *alertmanagertypes.Alert
|
2025-09-26 18:54:58 +05:30
|
|
|
expectedConfig *alertmanagertypes.NotificationConfig
|
|
|
|
|
shouldFallback bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "existing config",
|
|
|
|
|
orgID: orgID,
|
|
|
|
|
ruleID: ruleID,
|
|
|
|
|
expectedConfig: &alertmanagertypes.NotificationConfig{
|
|
|
|
|
NotificationGroup: map[model.LabelName]struct{}{
|
2025-10-03 19:47:15 +05:30
|
|
|
model.LabelName(ruleID): {},
|
2025-09-26 18:54:58 +05:30
|
|
|
},
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: 30 * time.Minute,
|
|
|
|
|
NoDataInterval: 30 * time.Minute,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
shouldFallback: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "non-existing config - fallback",
|
|
|
|
|
orgID: orgID,
|
|
|
|
|
ruleID: ruleId1,
|
|
|
|
|
expectedConfig: &alertmanagertypes.NotificationConfig{
|
|
|
|
|
NotificationGroup: map[model.LabelName]struct{}{
|
|
|
|
|
model.LabelName("group1"): {},
|
|
|
|
|
model.LabelName("group2"): {},
|
2025-10-03 19:47:15 +05:30
|
|
|
model.LabelName(ruleID): {},
|
2025-09-26 18:54:58 +05:30
|
|
|
},
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: 4 * time.Hour,
|
|
|
|
|
NoDataInterval: 4 * time.Hour,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
},
|
2025-09-26 18:54:58 +05:30
|
|
|
shouldFallback: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty orgID - fallback",
|
|
|
|
|
orgID: "",
|
|
|
|
|
ruleID: ruleID,
|
|
|
|
|
expectedConfig: nil, // Will get fallback
|
|
|
|
|
shouldFallback: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "nil alert - fallback",
|
|
|
|
|
orgID: orgID,
|
|
|
|
|
ruleID: "rule3", // Different ruleID to get fallback
|
|
|
|
|
alert: nil,
|
|
|
|
|
expectedConfig: nil, // Will get fallback
|
|
|
|
|
shouldFallback: true,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
config, err := provider.GetNotificationConfig(tt.orgID, tt.ruleID)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
if tt.shouldFallback {
|
|
|
|
|
// Should get fallback config (4 hour default)
|
|
|
|
|
assert.NotNil(t, config)
|
|
|
|
|
assert.Equal(t, 4*time.Hour, config.Renotify.RenotifyInterval)
|
|
|
|
|
} else {
|
|
|
|
|
// Should get our custom config
|
|
|
|
|
assert.NotNil(t, config)
|
|
|
|
|
assert.Equal(t, tt.expectedConfig, config)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_ConcurrentAccess(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
2025-10-03 19:47:15 +05:30
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
2025-09-26 18:54:58 +05:30
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
orgID := "test-org"
|
|
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
|
|
|
|
// Writer goroutine
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
for i := 0; i < 50; i++ {
|
|
|
|
|
config := &alertmanagertypes.NotificationConfig{
|
|
|
|
|
Renotify: alertmanagertypes.ReNotificationConfig{
|
|
|
|
|
RenotifyInterval: time.Duration(i+1) * time.Minute,
|
|
|
|
|
NoDataInterval: time.Duration(i+1) * time.Minute,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
err := provider.SetNotificationConfig(orgID, "rule1", config)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Reader goroutine
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
for i := 0; i < 50; i++ {
|
|
|
|
|
config, err := provider.GetNotificationConfig(orgID, "rule1")
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NotNil(t, config)
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// Wait for both goroutines to complete
|
|
|
|
|
wg.Wait()
|
|
|
|
|
}
|
2025-10-03 19:47:15 +05:30
|
|
|
|
|
|
|
|
func TestProvider_EvaluateExpression(t *testing.T) {
|
|
|
|
|
provider := &provider{}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
expression string
|
|
|
|
|
labelSet model.LabelSet
|
|
|
|
|
expected bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "simple equality check - match",
|
|
|
|
|
expression: `threshold.name == 'auth' && ruleId == 'rule1'`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "auth",
|
|
|
|
|
"ruleId": "rule1",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "simple equality check - match",
|
|
|
|
|
expression: `threshold.name = 'auth' AND ruleId = 'rule1'`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "auth",
|
|
|
|
|
"ruleId": "rule1",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "simple equality check - no match",
|
|
|
|
|
expression: `service == "payment"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "simple equality check - no match",
|
|
|
|
|
expression: `service = "payment"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with AND - both match",
|
|
|
|
|
expression: `service == "auth" && env == "production"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with AND - both match",
|
|
|
|
|
expression: `service = "auth" AND env = "production"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with AND - one doesn't match",
|
|
|
|
|
expression: `service == "auth" && env == "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with AND - one doesn't match",
|
|
|
|
|
expression: `service = "auth" AND env = "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with OR - one matches",
|
|
|
|
|
expression: `service == "payment" || env == "production"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with OR - one matches",
|
|
|
|
|
expression: `service = "payment" OR env = "production"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with OR - none match",
|
|
|
|
|
expression: `service == "payment" || env == "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "multiple conditions with OR - none match",
|
|
|
|
|
expression: `service = "payment" OR env = "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"env": "production",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "in operator - value in list",
|
|
|
|
|
expression: `service in ["auth", "payment", "notification"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "in operator - value in list",
|
|
|
|
|
expression: `service IN ["auth", "payment", "notification"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "in operator - value not in list",
|
|
|
|
|
expression: `service in ["payment", "notification"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "in operator - value not in list",
|
|
|
|
|
expression: `service IN ["payment", "notification"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "contains operator - substring match",
|
|
|
|
|
expression: `host contains "prod"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"host": "prod-server-01",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "contains operator - substring match",
|
|
|
|
|
expression: `host CONTAINS "prod"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"host": "prod-server-01",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "contains operator - no substring match",
|
|
|
|
|
expression: `host contains "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"host": "prod-server-01",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "contains operator - no substring match",
|
|
|
|
|
expression: `host CONTAINS "staging"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"host": "prod-server-01",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "complex expression with parentheses",
|
|
|
|
|
expression: `(service == "auth" && env == "production") || critical == "true"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "payment",
|
|
|
|
|
"env": "staging",
|
|
|
|
|
"critical": "true",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "complex expression with parentheses",
|
|
|
|
|
expression: `(service = "auth" AND env = "production") OR critical = "true"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "payment",
|
|
|
|
|
"env": "staging",
|
|
|
|
|
"critical": "true",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "missing label key",
|
|
|
|
|
expression: `"missing_key" == "value"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "missing label key",
|
|
|
|
|
expression: `"missing_key" = "value"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "rule-based expression with threshold name and ruleId",
|
|
|
|
|
expression: `'threshold.name' == "high-cpu" && ruleId == "rule-123"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "high-cpu",
|
|
|
|
|
"ruleId": "rule-123",
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false, //no commas
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "rule-based expression with threshold name and ruleId",
|
|
|
|
|
expression: `'threshold.name' = "high-cpu" AND ruleId == "rule-123"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "high-cpu",
|
|
|
|
|
"ruleId": "rule-123",
|
|
|
|
|
"service": "auth",
|
|
|
|
|
},
|
|
|
|
|
expected: false, //no commas
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "alertname and ruleId combination",
|
|
|
|
|
expression: `alertname == "HighCPUUsage" && ruleId == "cpu-alert-001"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"alertname": "HighCPUUsage",
|
|
|
|
|
"ruleId": "cpu-alert-001",
|
|
|
|
|
"severity": "critical",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "alertname and ruleId combination",
|
|
|
|
|
expression: `alertname = "HighCPUUsage" AND ruleId = "cpu-alert-001"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"alertname": "HighCPUUsage",
|
|
|
|
|
"ruleId": "cpu-alert-001",
|
|
|
|
|
"severity": "critical",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "kubernetes namespace filtering",
|
|
|
|
|
expression: `k8s.namespace.name == "auth" && service in ["auth", "payment"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"k8s.namespace.name": "auth",
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"host": "k8s-node-1",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "kubernetes namespace filtering",
|
|
|
|
|
expression: `k8s.namespace.name = "auth" && service IN ["auth", "payment"]`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"k8s.namespace.name": "auth",
|
|
|
|
|
"service": "auth",
|
|
|
|
|
"host": "k8s-node-1",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "migration expression format from SQL migration",
|
|
|
|
|
expression: `threshold.name == "HighCPUUsage" && ruleId == "rule-uuid-123"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "HighCPUUsage",
|
|
|
|
|
"ruleId": "rule-uuid-123",
|
|
|
|
|
"severity": "warning",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "migration expression format from SQL migration",
|
|
|
|
|
expression: `threshold.name = "HighCPUUsage" && ruleId = "rule-uuid-123"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"threshold.name": "HighCPUUsage",
|
|
|
|
|
"ruleId": "rule-uuid-123",
|
|
|
|
|
"severity": "warning",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "case sensitive matching",
|
|
|
|
|
expression: `service == "Auth"`, // capital A
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth", // lowercase a
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "case sensitive matching",
|
|
|
|
|
expression: `service = "Auth"`, // capital A
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth", // lowercase a
|
|
|
|
|
},
|
|
|
|
|
expected: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "numeric comparison as strings",
|
|
|
|
|
expression: `port == "8080"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"port": "8080",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "numeric comparison as strings",
|
|
|
|
|
expression: `port = "8080"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"port": "8080",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "quoted string with special characters",
|
|
|
|
|
expression: `service == "auth-service-v2"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth-service-v2",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "quoted string with special characters",
|
|
|
|
|
expression: `service = "auth-service-v2"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "auth-service-v2",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "boolean operators precedence",
|
|
|
|
|
expression: `service == "auth" && env == "prod" || critical == "true"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "payment",
|
|
|
|
|
"env": "staging",
|
|
|
|
|
"critical": "true",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "boolean operators precedence",
|
|
|
|
|
expression: `service = "auth" AND env = "prod" OR critical = "true"`,
|
|
|
|
|
labelSet: model.LabelSet{
|
|
|
|
|
"service": "payment",
|
|
|
|
|
"env": "staging",
|
|
|
|
|
"critical": "true",
|
|
|
|
|
},
|
|
|
|
|
expected: true,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
result, err := provider.evaluateExpr(tt.expression, tt.labelSet)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.Equal(t, tt.expected, result, "Expression: %s", tt.expression)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_DeleteRoute(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
orgID string
|
|
|
|
|
routeID string
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "valid parameters",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
routeID: "route-uuid-456",
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty routeID",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
routeID: "",
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "valid orgID with valid routeID",
|
|
|
|
|
orgID: "another-org",
|
|
|
|
|
routeID: "another-route-id",
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
if !tt.wantErr {
|
|
|
|
|
routeStore.ExpectDelete(tt.orgID, tt.routeID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = provider.DeleteRoutePolicy(ctx, tt.orgID, tt.routeID)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
} else {
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NoError(t, routeStore.ExpectationsWereMet())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_CreateRoute(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
orgID string
|
|
|
|
|
route *alertmanagertypes.RoutePolicy
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "valid route",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
|
|
|
|
Expression: `service == "auth"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "auth-service-route",
|
|
|
|
|
Description: "Route for auth service alerts",
|
|
|
|
|
Enabled: true,
|
|
|
|
|
OrgID: "test-org-123",
|
|
|
|
|
Channels: []string{"slack-channel"},
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
2025-10-03 21:50:56 +05:30
|
|
|
{
|
|
|
|
|
name: "valid route qb format",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
|
|
|
|
|
Expression: `service = "auth"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "auth-service-route",
|
|
|
|
|
Description: "Route for auth service alerts",
|
|
|
|
|
Enabled: true,
|
|
|
|
|
OrgID: "test-org-123",
|
|
|
|
|
Channels: []string{"slack-channel"},
|
|
|
|
|
},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
2025-10-03 19:47:15 +05:30
|
|
|
{
|
|
|
|
|
name: "nil route",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: nil,
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid route - missing expression",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: "", // empty expression
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "invalid-route",
|
|
|
|
|
OrgID: "test-org-123",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid route - missing name",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: `service == "auth"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "", // empty name
|
|
|
|
|
OrgID: "test-org-123",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
2025-10-03 21:50:56 +05:30
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "invalid route - missing name",
|
|
|
|
|
orgID: "test-org-123",
|
|
|
|
|
route: &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: `service = "auth"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "", // empty name
|
|
|
|
|
OrgID: "test-org-123",
|
|
|
|
|
},
|
|
|
|
|
wantErr: true,
|
2025-10-03 19:47:15 +05:30
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
if !tt.wantErr && tt.route != nil {
|
|
|
|
|
routeStore.ExpectCreate(tt.route)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = provider.CreateRoutePolicy(ctx, tt.orgID, tt.route)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
} else {
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NoError(t, routeStore.ExpectationsWereMet())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestProvider_CreateRoutes(t *testing.T) {
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
providerSettings := createTestProviderSettings()
|
|
|
|
|
config := nfmanager.Config{}
|
|
|
|
|
|
|
|
|
|
routeStore := nfroutingstoretest.NewMockSQLRouteStore()
|
|
|
|
|
provider, err := New(ctx, providerSettings, config, routeStore)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
validRoute1 := &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: `service == "auth"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "auth-route",
|
|
|
|
|
Description: "Auth service route",
|
|
|
|
|
Enabled: true,
|
|
|
|
|
OrgID: "test-org",
|
|
|
|
|
Channels: []string{"slack-auth"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validRoute2 := &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: `service == "payment"`,
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "payment-route",
|
|
|
|
|
Description: "Payment service route",
|
|
|
|
|
Enabled: true,
|
|
|
|
|
OrgID: "test-org",
|
|
|
|
|
Channels: []string{"slack-payment"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
invalidRoute := &alertmanagertypes.RoutePolicy{
|
|
|
|
|
Expression: "", // empty expression - invalid
|
|
|
|
|
ExpressionKind: alertmanagertypes.PolicyBasedExpression,
|
|
|
|
|
Name: "invalid-route",
|
|
|
|
|
OrgID: "test-org",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
orgID string
|
|
|
|
|
routes []*alertmanagertypes.RoutePolicy
|
|
|
|
|
wantErr bool
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "valid routes",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: []*alertmanagertypes.RoutePolicy{validRoute1, validRoute2},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "empty routes list",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: []*alertmanagertypes.RoutePolicy{},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "nil routes list",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: nil,
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "routes with nil route",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: []*alertmanagertypes.RoutePolicy{validRoute1, nil},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "routes with invalid route",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: []*alertmanagertypes.RoutePolicy{validRoute1, invalidRoute},
|
|
|
|
|
wantErr: true,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "single valid route",
|
|
|
|
|
orgID: "test-org",
|
|
|
|
|
routes: []*alertmanagertypes.RoutePolicy{validRoute1},
|
|
|
|
|
wantErr: false,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
if !tt.wantErr && len(tt.routes) > 0 {
|
|
|
|
|
routeStore.ExpectCreateBatch(tt.routes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := provider.CreateRoutePolicies(ctx, tt.orgID, tt.routes)
|
|
|
|
|
|
|
|
|
|
if tt.wantErr {
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
} else {
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NoError(t, routeStore.ExpectationsWereMet())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|