d50e2a94a3
Part 2 of adding version messages to the `alert_rule_version` able. This allows setting the message via a header when using the Prometheus conversion API, which can be useful for e.g. linking changes back to source control.
2645 lines
99 KiB
Go
2645 lines
99 KiB
Go
package provisioning
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/util/testutil"
|
|
)
|
|
|
|
// note: additional integration tests are in /pkg/tests/api/alerting/api_provisioning_test.go
|
|
|
|
func TestIntegrationAlertRuleService(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
ruleService := createAlertRuleService(t, nil)
|
|
var orgID int64 = 1
|
|
u := &user.SignedInUser{
|
|
UserUID: util.GenerateShortUID(),
|
|
UserID: 1,
|
|
OrgID: orgID,
|
|
}
|
|
|
|
t.Run("group creation should set the right provenance", func(t *testing.T) {
|
|
group := createDummyGroup("group-test-1", orgID)
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-1")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
for _, rule := range readGroup.Rules {
|
|
_, provenance, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceAPI, provenance)
|
|
}
|
|
})
|
|
|
|
t.Run("alert rule group should be updated correctly", func(t *testing.T) {
|
|
rule := dummyRule("test#3", orgID)
|
|
rule.RuleGroup = "a"
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
var interval int64 = 120
|
|
err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, interval, rule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("if a folder was renamed the interval should be fetched from the renamed folder", func(t *testing.T) {
|
|
var orgID int64 = 2
|
|
rule := dummyRule("test#1", orgID)
|
|
rule.NamespaceUID = "123abc"
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
rule.NamespaceUID = "abc123"
|
|
_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("group update should propagate folderUID from group to rules", func(t *testing.T) {
|
|
ruleService := createAlertRuleService(t, nil)
|
|
group := createDummyGroup("namespace-test", orgID)
|
|
group.Rules[0].NamespaceUID = ""
|
|
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "namespace-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Equal(t, "my-namespace", readGroup.Rules[0].NamespaceUID)
|
|
})
|
|
|
|
t.Run("group creation should propagate group title correctly", func(t *testing.T) {
|
|
group := createDummyGroup("group-test-3", orgID)
|
|
group.Rules[0].RuleGroup = "something different"
|
|
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-3")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
for _, rule := range readGroup.Rules {
|
|
require.Equal(t, "group-test-3", rule.RuleGroup)
|
|
}
|
|
})
|
|
|
|
t.Run("alert rule should get interval from existing rule group", func(t *testing.T) {
|
|
rule := dummyRule("test#4", orgID)
|
|
rule.RuleGroup = "b"
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
var interval int64 = 120
|
|
err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
|
|
require.NoError(t, err)
|
|
|
|
rule = dummyRule("test#4-1", orgID)
|
|
rule.RuleGroup = "b"
|
|
rule, err = ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, interval, rule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("updating a rule group's top level fields should bump the version number", func(t *testing.T) {
|
|
const (
|
|
orgID = 123
|
|
namespaceUID = "abc"
|
|
ruleUID = "some_rule_uid"
|
|
ruleGroup = "abc"
|
|
newInterval int64 = 120
|
|
)
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
rule := dummyRule("my_rule", orgID)
|
|
rule.UID = ruleUID
|
|
rule.RuleGroup = ruleGroup
|
|
rule.NamespaceUID = namespaceUID
|
|
_, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, ruleUID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), rule.Version)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
err = ruleService.UpdateRuleGroup(context.Background(), u, namespaceUID, ruleGroup, newInterval)
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, ruleUID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(2), rule.Version)
|
|
require.Equal(t, newInterval, rule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("updating a group by updating a rule should bump that rule's data and version number", func(t *testing.T) {
|
|
group := createDummyGroup("group-test-5", orgID)
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
|
|
require.NoError(t, err)
|
|
|
|
updatedGroup.Rules[0].Title = "some-other-title-asdf"
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 1)
|
|
require.Equal(t, "some-other-title-asdf", readGroup.Rules[0].Title)
|
|
require.Equal(t, int64(2), readGroup.Rules[0].Version)
|
|
})
|
|
|
|
t.Run("updating a group should not override its rules editor settings", func(t *testing.T) {
|
|
namespaceUID := "my-namespace"
|
|
groupTitle := "test-group-123"
|
|
|
|
// create the rule group via the rule store, to persist the editor settings
|
|
rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
|
|
ruleMetadata := models.AlertRuleMetadata{
|
|
EditorSettings: models.EditorSettings{
|
|
SimplifiedQueryAndExpressionsSection: true,
|
|
},
|
|
}
|
|
rule.Metadata = ruleMetadata
|
|
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
|
|
require.NoError(t, err)
|
|
require.Len(t, r, 1)
|
|
|
|
// Set the UID for the rule to update it
|
|
rule.UID = r[0].UID
|
|
// clear the metadata to check that the existing metadata is not overridden
|
|
rule.Metadata = models.AlertRuleMetadata{}
|
|
|
|
// Now update the rule group with the rule to update its metadata
|
|
group := models.AlertRuleGroup{
|
|
Title: groupTitle,
|
|
Interval: 60,
|
|
FolderUID: namespaceUID,
|
|
Rules: []models.AlertRule{rule},
|
|
}
|
|
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 1)
|
|
|
|
// check that the metadata is still there
|
|
require.Equal(t, ruleMetadata, readGroup.Rules[0].Metadata)
|
|
})
|
|
|
|
t.Run("updating a group with editor settings should override its prometheus rule definition", func(t *testing.T) {
|
|
namespaceUID := "my-namespace"
|
|
groupTitle := "test-group-123"
|
|
|
|
// create the rule group via the rule store, to persist the editor settings
|
|
rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
|
|
ruleMetadata := models.AlertRuleMetadata{
|
|
EditorSettings: models.EditorSettings{
|
|
SimplifiedQueryAndExpressionsSection: true,
|
|
},
|
|
PrometheusStyleRule: &models.PrometheusStyleRule{
|
|
OriginalRuleDefinition: "old",
|
|
},
|
|
}
|
|
rule.Metadata = ruleMetadata
|
|
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
|
|
require.NoError(t, err)
|
|
require.Len(t, r, 1)
|
|
|
|
// Set the UID for the rule to update it
|
|
rule.UID = r[0].UID
|
|
// clear the editor settings in the metadata to check that the existing setting is not overridden
|
|
rule.Metadata = models.AlertRuleMetadata{
|
|
PrometheusStyleRule: &models.PrometheusStyleRule{
|
|
OriginalRuleDefinition: "new",
|
|
},
|
|
}
|
|
|
|
// Now update the rule group with the rule to update its metadata
|
|
group := models.AlertRuleGroup{
|
|
Title: groupTitle,
|
|
Interval: 60,
|
|
FolderUID: namespaceUID,
|
|
Rules: []models.AlertRule{rule},
|
|
}
|
|
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 1)
|
|
|
|
// check that the editor settings are still there
|
|
require.True(t, readGroup.Rules[0].Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection)
|
|
// check the new prometheus rule definition
|
|
require.Equal(t, "new", readGroup.Rules[0].Metadata.PrometheusStyleRule.OriginalRuleDefinition)
|
|
})
|
|
|
|
t.Run("updating a group should override its prometheus rule definition", func(t *testing.T) {
|
|
namespaceUID := "my-namespace"
|
|
groupTitle := "test-group-123"
|
|
|
|
// create the rule group via the rule store, to persist the editor settings
|
|
rule := createTestRule(util.GenerateShortUID(), groupTitle, orgID, namespaceUID)
|
|
ruleMetadata := models.AlertRuleMetadata{
|
|
PrometheusStyleRule: &models.PrometheusStyleRule{
|
|
OriginalRuleDefinition: "old",
|
|
},
|
|
}
|
|
rule.Metadata = ruleMetadata
|
|
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
|
|
require.NoError(t, err)
|
|
require.Len(t, r, 1)
|
|
|
|
// Set the UID for the rule to update it
|
|
rule.UID = r[0].UID
|
|
// make the metadata empty
|
|
rule.Metadata = models.AlertRuleMetadata{}
|
|
|
|
// Now update the rule group with the rule to update its metadata
|
|
group := models.AlertRuleGroup{
|
|
Title: groupTitle,
|
|
Interval: 60,
|
|
FolderUID: namespaceUID,
|
|
Rules: []models.AlertRule{rule},
|
|
}
|
|
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, namespaceUID, groupTitle)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 1)
|
|
|
|
// check that the prometheus rule definition is empty
|
|
require.Nil(t, readGroup.Rules[0].Metadata.PrometheusStyleRule)
|
|
})
|
|
|
|
t.Run("updating a rule should not override its editor settings", func(t *testing.T) {
|
|
rule := createTestRule(util.GenerateShortUID(), "my-group", orgID, "my-folder")
|
|
ruleMetadata := models.AlertRuleMetadata{
|
|
EditorSettings: models.EditorSettings{
|
|
SimplifiedQueryAndExpressionsSection: true,
|
|
},
|
|
}
|
|
rule.Metadata = ruleMetadata
|
|
r, err := ruleService.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
|
|
require.NoError(t, err)
|
|
require.Len(t, r, 1)
|
|
|
|
// Set the UID for the rule to update it
|
|
rule.UID = r[0].UID
|
|
// clear the metadata to check that the existing metadata is not overridden
|
|
rule.Metadata = models.AlertRuleMetadata{}
|
|
|
|
// Update the rule
|
|
_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
// Read the rule and check that the editor settings are preserved
|
|
readRule, _, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, ruleMetadata, readRule.Metadata)
|
|
})
|
|
|
|
t.Run("updating a group to temporarily overlap rule names should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "overlap-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("overlap-test-rule-1", orgID),
|
|
dummyRule("overlap-test-rule-2", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test")
|
|
require.NoError(t, err)
|
|
|
|
updatedGroup.Rules[0].Title = "overlap-test-rule-2"
|
|
updatedGroup.Rules[1].Title = "overlap-test-rule-3"
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "overlap-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 2)
|
|
require.Equal(t, "overlap-test-rule-2", readGroup.Rules[0].Title)
|
|
require.Equal(t, "overlap-test-rule-3", readGroup.Rules[1].Title)
|
|
require.Equal(t, int64(3), readGroup.Rules[0].Version)
|
|
require.Equal(t, int64(3), readGroup.Rules[1].Version)
|
|
})
|
|
|
|
t.Run("updating a group to swap the name of two rules should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "swap-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("swap-test-rule-1", orgID),
|
|
dummyRule("swap-test-rule-2", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test")
|
|
require.NoError(t, err)
|
|
|
|
updatedGroup.Rules[0].Title = "swap-test-rule-2"
|
|
updatedGroup.Rules[1].Title = "swap-test-rule-1"
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "swap-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 2)
|
|
require.Equal(t, "swap-test-rule-2", readGroup.Rules[0].Title)
|
|
require.Equal(t, "swap-test-rule-1", readGroup.Rules[1].Title)
|
|
require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
|
|
require.Equal(t, int64(3), readGroup.Rules[1].Version)
|
|
})
|
|
|
|
t.Run("updating a group that has a rule name cycle should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "cycle-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("cycle-test-rule-1", orgID),
|
|
dummyRule("cycle-test-rule-2", orgID),
|
|
dummyRule("cycle-test-rule-3", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test")
|
|
require.NoError(t, err)
|
|
|
|
updatedGroup.Rules[0].Title = "cycle-test-rule-2"
|
|
updatedGroup.Rules[1].Title = "cycle-test-rule-3"
|
|
updatedGroup.Rules[2].Title = "cycle-test-rule-1"
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "cycle-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 3)
|
|
require.Equal(t, "cycle-test-rule-2", readGroup.Rules[0].Title)
|
|
require.Equal(t, "cycle-test-rule-3", readGroup.Rules[1].Title)
|
|
require.Equal(t, "cycle-test-rule-1", readGroup.Rules[2].Title)
|
|
require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
|
|
require.Equal(t, int64(3), readGroup.Rules[1].Version)
|
|
require.Equal(t, int64(3), readGroup.Rules[2].Version)
|
|
})
|
|
|
|
t.Run("updating a group that has multiple rule name cycles should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "multi-cycle-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("multi-cycle-test-rule-1", orgID),
|
|
dummyRule("multi-cycle-test-rule-2", orgID),
|
|
|
|
dummyRule("multi-cycle-test-rule-3", orgID),
|
|
dummyRule("multi-cycle-test-rule-4", orgID),
|
|
dummyRule("multi-cycle-test-rule-5", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test")
|
|
require.NoError(t, err)
|
|
|
|
updatedGroup.Rules[0].Title = "multi-cycle-test-rule-2"
|
|
updatedGroup.Rules[1].Title = "multi-cycle-test-rule-1"
|
|
|
|
updatedGroup.Rules[2].Title = "multi-cycle-test-rule-4"
|
|
updatedGroup.Rules[3].Title = "multi-cycle-test-rule-5"
|
|
updatedGroup.Rules[4].Title = "multi-cycle-test-rule-3"
|
|
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "multi-cycle-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 5)
|
|
require.Equal(t, "multi-cycle-test-rule-2", readGroup.Rules[0].Title)
|
|
require.Equal(t, "multi-cycle-test-rule-1", readGroup.Rules[1].Title)
|
|
require.Equal(t, "multi-cycle-test-rule-4", readGroup.Rules[2].Title)
|
|
require.Equal(t, "multi-cycle-test-rule-5", readGroup.Rules[3].Title)
|
|
require.Equal(t, "multi-cycle-test-rule-3", readGroup.Rules[4].Title)
|
|
require.Equal(t, int64(3), readGroup.Rules[0].Version) // Needed an extra update to break the update cycle.
|
|
require.Equal(t, int64(3), readGroup.Rules[1].Version)
|
|
require.Equal(t, int64(3), readGroup.Rules[2].Version) // Needed an extra update to break the update cycle.
|
|
require.Equal(t, int64(3), readGroup.Rules[3].Version)
|
|
require.Equal(t, int64(3), readGroup.Rules[4].Version)
|
|
})
|
|
|
|
t.Run("updating a group to recreate a rule using the same name should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "recreate-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("recreate-test-rule-1", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup := models.AlertRuleGroup{
|
|
Title: "recreate-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("recreate-test-rule-1", orgID),
|
|
},
|
|
}
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "recreate-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 1)
|
|
require.Equal(t, "recreate-test-rule-1", readGroup.Rules[0].Title)
|
|
require.Equal(t, int64(1), readGroup.Rules[0].Version)
|
|
})
|
|
|
|
t.Run("updating a group to create a rule that temporarily overlaps an existing should not throw unique constraint", func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := models.AlertRuleGroup{
|
|
Title: "create-overlap-test",
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule("create-overlap-test-rule-1", orgID),
|
|
},
|
|
}
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test")
|
|
require.NoError(t, err)
|
|
updatedGroup.Rules[0].Title = "create-overlap-test-rule-2"
|
|
updatedGroup.Rules = append(updatedGroup.Rules, dummyRule("create-overlap-test-rule-1", orgID))
|
|
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, updatedGroup, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
readGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "create-overlap-test")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, readGroup.Rules)
|
|
require.Len(t, readGroup.Rules, 2)
|
|
require.Equal(t, "create-overlap-test-rule-2", readGroup.Rules[0].Title)
|
|
require.Equal(t, "create-overlap-test-rule-1", readGroup.Rules[1].Title)
|
|
require.Equal(t, int64(2), readGroup.Rules[0].Version)
|
|
require.Equal(t, int64(1), readGroup.Rules[1].Version)
|
|
})
|
|
|
|
t.Run("updating a group by updating a rule should not remove dashboard and panel ids", func(t *testing.T) {
|
|
dashboardUid := "huYnkl7H"
|
|
panelId := int64(5678)
|
|
group := createDummyGroup("group-test-5", orgID)
|
|
group.Rules[0].Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: dashboardUid,
|
|
models.PanelIDAnnotation: strconv.FormatInt(panelId, 10),
|
|
}
|
|
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
updatedGroup, err := ruleService.GetRuleGroup(context.Background(), u, "my-namespace", "group-test-5")
|
|
require.NoError(t, err)
|
|
|
|
require.NotNil(t, updatedGroup.Rules[0].DashboardUID)
|
|
require.NotNil(t, updatedGroup.Rules[0].PanelID)
|
|
require.Equal(t, dashboardUid, *updatedGroup.Rules[0].DashboardUID)
|
|
require.Equal(t, panelId, *updatedGroup.Rules[0].PanelID)
|
|
})
|
|
|
|
t.Run("alert rule provenace should be correctly checked", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
from models.Provenance
|
|
to models.Provenance
|
|
errNil bool
|
|
}{
|
|
{
|
|
name: "should be able to update from provenance none to api",
|
|
from: models.ProvenanceNone,
|
|
to: models.ProvenanceAPI,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should be able to update from provenance none to file",
|
|
from: models.ProvenanceNone,
|
|
to: models.ProvenanceFile,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance api to file",
|
|
from: models.ProvenanceAPI,
|
|
to: models.ProvenanceFile,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance api to none",
|
|
from: models.ProvenanceAPI,
|
|
to: models.ProvenanceNone,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance file to api",
|
|
from: models.ProvenanceFile,
|
|
to: models.ProvenanceAPI,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance file to none",
|
|
from: models.ProvenanceFile,
|
|
to: models.ProvenanceNone,
|
|
errNil: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
rule := dummyRule(t.Name(), orgID)
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, test.from)
|
|
require.NoError(t, err)
|
|
|
|
_, err = ruleService.UpdateAlertRule(context.Background(), u, rule, test.to)
|
|
if test.errNil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("alert rule provenace should be correctly checked when writing groups", func(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
from models.Provenance
|
|
to models.Provenance
|
|
errNil bool
|
|
}{
|
|
{
|
|
name: "should be able to update from provenance none to api",
|
|
from: models.ProvenanceNone,
|
|
to: models.ProvenanceAPI,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should be able to update from provenance none to file",
|
|
from: models.ProvenanceNone,
|
|
to: models.ProvenanceFile,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance api to file",
|
|
from: models.ProvenanceAPI,
|
|
to: models.ProvenanceFile,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should be able to update from provenance api to none",
|
|
from: models.ProvenanceAPI,
|
|
to: models.ProvenanceNone,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance file to api",
|
|
from: models.ProvenanceFile,
|
|
to: models.ProvenanceAPI,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance file to none",
|
|
from: models.ProvenanceFile,
|
|
to: models.ProvenanceNone,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should be able to update from provenance none to 'converted prometheus'",
|
|
from: models.ProvenanceNone,
|
|
to: models.ProvenanceConvertedPrometheus,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should be able to update from provenance 'converted prometheus' to none",
|
|
from: models.ProvenanceConvertedPrometheus,
|
|
to: models.ProvenanceNone,
|
|
errNil: true,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance 'converted prometheus' to api",
|
|
from: models.ProvenanceConvertedPrometheus,
|
|
to: models.ProvenanceAPI,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance 'converted prometheus' to file",
|
|
from: models.ProvenanceConvertedPrometheus,
|
|
to: models.ProvenanceFile,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance api to 'converted prometheus'",
|
|
from: models.ProvenanceAPI,
|
|
to: models.ProvenanceConvertedPrometheus,
|
|
errNil: false,
|
|
},
|
|
{
|
|
name: "should not be able to update from provenance file to 'converted prometheus'",
|
|
from: models.ProvenanceFile,
|
|
to: models.ProvenanceConvertedPrometheus,
|
|
errNil: false,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
var orgID int64 = 1
|
|
group := createDummyGroup(t.Name(), orgID)
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, test.from, "")
|
|
require.NoError(t, err)
|
|
|
|
group.Rules[0].Title = t.Name()
|
|
err = ruleService.ReplaceRuleGroup(context.Background(), u, group, test.to, "")
|
|
if test.errNil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("quota met causes create to be rejected", func(t *testing.T) {
|
|
ruleService := createAlertRuleService(t, nil)
|
|
checker := &MockQuotaChecker{}
|
|
checker.EXPECT().LimitExceeded()
|
|
ruleService.quotas = checker
|
|
|
|
_, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone)
|
|
|
|
require.ErrorIs(t, err, models.ErrQuotaReached)
|
|
})
|
|
|
|
t.Run("quota met causes group write to be rejected", func(t *testing.T) {
|
|
ruleService := createAlertRuleService(t, nil)
|
|
checker := &MockQuotaChecker{}
|
|
checker.EXPECT().LimitExceeded()
|
|
ruleService.quotas = checker
|
|
|
|
group := createDummyGroup("quota-reached", orgID)
|
|
err := ruleService.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
|
|
require.ErrorIs(t, err, models.ErrQuotaReached)
|
|
})
|
|
|
|
t.Run("alert rules created without a group should be considered NoGroup rules", func(t *testing.T) {
|
|
rule := createNoGroupRule("test-no-group-rule", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule.RuleGroup = ""
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
ruleGroup, err := ruleService.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ruleGroup)
|
|
require.True(t, models.IsNoGroupRuleGroup(ruleGroup.Title), "Rule group should be NoGroup rule group")
|
|
require.Len(t, ruleGroup.Rules, 1, "Rule group should only contain one NoGroup rule")
|
|
})
|
|
|
|
t.Run("multiple alert rules created without a group should be considered NoGroup rules, and be returned in separate groups", func(t *testing.T) {
|
|
rule := createNoGroupRule("test-no-group-rule-1", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule.RuleGroup = ""
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
rule2 := createNoGroupRule("test-no-group-rule-2", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule2.RuleGroup = ""
|
|
rule2, err = ruleService.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule2.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
require.NotEqual(t, rule.RuleGroup, rule2.RuleGroup, "Both rules should have different NoGroup rule groups")
|
|
|
|
ruleGroup, err := ruleService.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ruleGroup)
|
|
require.True(t, models.IsNoGroupRuleGroup(ruleGroup.Title), "Rule group should be NoGroup rule group")
|
|
require.Len(t, ruleGroup.Rules, 1, "Rule group should only contain one NoGroup rule")
|
|
|
|
ruleGroup2, err := ruleService.GetRuleGroup(context.Background(), u, rule2.NamespaceUID, rule2.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ruleGroup2)
|
|
require.True(t, models.IsNoGroupRuleGroup(ruleGroup2.Title), "Rule group should be NoGroup rule group")
|
|
require.Len(t, ruleGroup2.Rules, 1, "Rule group should only contain one NoGroup rule")
|
|
|
|
require.NotEqual(t, ruleGroup, ruleGroup2, "Both NoGroup rule groups should be different")
|
|
})
|
|
|
|
t.Run("setting the group interval on NoGroup rules should only affect 1 rule", func(t *testing.T) {
|
|
rule := createNoGroupRule("test-no-group-rule-1", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule.RuleGroup = ""
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
rule2 := createNoGroupRule("test-no-group-rule-2", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule2.RuleGroup = ""
|
|
rule2, err = ruleService.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule2.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
require.NotEqual(t, rule.RuleGroup, rule2.RuleGroup, "Both rules should have different NoGroup rule groups")
|
|
|
|
var updatedInterval int64 = 120
|
|
err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, updatedInterval)
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, updatedInterval, rule.IntervalSeconds, "Rule should have updated interval")
|
|
rule2, _, err = ruleService.GetAlertRule(context.Background(), u, rule2.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule2.IntervalSeconds, "Rule should not have updated interval")
|
|
require.NotEqual(t, updatedInterval, rule2.IntervalSeconds, "Both rules should not have updated interval")
|
|
})
|
|
|
|
t.Run("alert rule in NoGroup should be updated correctly", func(t *testing.T) {
|
|
rule := createNoGroupRule("test-no-group-rule", orgID, "my-namespace")
|
|
// This is the way legacy storage creates rules without a group
|
|
rule.RuleGroup = ""
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(60), rule.IntervalSeconds)
|
|
|
|
// get the actual calculated group for use with the api
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(rule.RuleGroup), "Rule should be considered NoGroup rule")
|
|
|
|
err = ruleService.UpdateRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup, 120)
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(120), rule.IntervalSeconds)
|
|
})
|
|
}
|
|
|
|
func TestIntegrationCreateAlertRule(t *testing.T) {
|
|
testutil.SkipIntegrationTestInShortMode(t)
|
|
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID, UserUID: util.GenerateShortUID()}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
groupIntervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
|
|
groupProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
t.Run("and a new rule creates a new group", func(t *testing.T) {
|
|
rule := gen.With(gen.WithOrgID(orgID)).Generate()
|
|
service, ruleStore, provenanceStore, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
t.Run("it should assign default interval", func(t *testing.T) {
|
|
require.Equal(t, service.defaultIntervalSeconds, actualRule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("inserts to database", func(t *testing.T) {
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, inserts, 1)
|
|
cmd := inserts[0].([]models.InsertRule)
|
|
require.Len(t, cmd, 1)
|
|
})
|
|
|
|
t.Run("set correct provenance", func(t *testing.T) {
|
|
p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceFile, p)
|
|
})
|
|
})
|
|
t.Run("and it adds a rule to a group", func(t *testing.T) {
|
|
rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
|
|
service, ruleStore, provenanceStore, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
t.Run("it should assign group interval", func(t *testing.T) {
|
|
require.Equal(t, groupIntervalSeconds, actualRule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("inserts to database", func(t *testing.T) {
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, inserts, 1)
|
|
cmd := inserts[0].([]models.InsertRule)
|
|
require.Len(t, cmd, 1)
|
|
})
|
|
|
|
t.Run("set correct provenance", func(t *testing.T) {
|
|
p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceNone, p)
|
|
})
|
|
})
|
|
})
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
t.Run("and it creates a new group", func(t *testing.T) {
|
|
rule := gen.With(gen.WithOrgID(orgID)).Generate()
|
|
t.Run("it should authorize the change", func(t *testing.T) {
|
|
service, ruleStore, provenanceStore, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, rule.GetGroupKey(), change.GroupKey)
|
|
assert.Len(t, change.New, 1)
|
|
assert.Empty(t, change.Update)
|
|
assert.Empty(t, change.Delete)
|
|
assert.Empty(t, change.AffectedGroups)
|
|
return nil
|
|
}
|
|
|
|
actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
t.Run("it should assign default interval", func(t *testing.T) {
|
|
require.Equal(t, service.defaultIntervalSeconds, actualRule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("inserts to database", func(t *testing.T) {
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, inserts, 1)
|
|
cmd := inserts[0].([]models.InsertRule)
|
|
require.Len(t, cmd, 1)
|
|
})
|
|
|
|
t.Run("set correct provenance", func(t *testing.T) {
|
|
p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceFile, p)
|
|
})
|
|
})
|
|
})
|
|
t.Run("and it adds a rule to a group", func(t *testing.T) {
|
|
rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
|
|
t.Run("it should authorize the change to whole group", func(t *testing.T) {
|
|
service, ruleStore, provenanceStore, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, rule.GetGroupKey(), change.GroupKey)
|
|
assert.Contains(t, change.AffectedGroups, change.GroupKey)
|
|
assert.EqualValues(t, change.AffectedGroups[change.GroupKey], rules)
|
|
assert.Len(t, change.New, 1)
|
|
assert.Empty(t, change.Update)
|
|
assert.Empty(t, change.Delete)
|
|
return nil
|
|
}
|
|
|
|
actualRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
t.Run("it should assign group interval", func(t *testing.T) {
|
|
require.Equal(t, groupIntervalSeconds, actualRule.IntervalSeconds)
|
|
})
|
|
|
|
t.Run("inserts to database", func(t *testing.T) {
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, inserts, 1)
|
|
cmd := inserts[0].([]models.InsertRule)
|
|
require.Len(t, cmd, 1)
|
|
})
|
|
|
|
t.Run("set correct provenance", func(t *testing.T) {
|
|
p, err := provenanceStore.GetProvenance(context.Background(), &actualRule, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceNone, p)
|
|
})
|
|
})
|
|
})
|
|
t.Run("it should not insert if not authorized", func(t *testing.T) {
|
|
rule := gen.With(gen.WithGroupKey(groupKey)).Generate()
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
_, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceFile)
|
|
require.ErrorIs(t, expectedErr, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
inserts := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.InsertRule)
|
|
return a, ok
|
|
})
|
|
require.Empty(t, inserts)
|
|
})
|
|
})
|
|
|
|
ruleService := createAlertRuleService(t, nil)
|
|
t.Run("should return the created id", func(t *testing.T) {
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#1", orgID), models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value")
|
|
})
|
|
|
|
t.Run("should set the right provenance", func(t *testing.T) {
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, dummyRule("test#2", orgID), models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
_, provenance, err := ruleService.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, models.ProvenanceAPI, provenance)
|
|
})
|
|
|
|
t.Run("when UID is specified", func(t *testing.T) {
|
|
t.Run("return error if it is not valid UID", func(t *testing.T) {
|
|
rule := dummyRule("test#3", orgID)
|
|
rule.UID = strings.Repeat("1", util.MaxUIDLength+1)
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
|
|
})
|
|
t.Run("should create a new rule with this UID", func(t *testing.T) {
|
|
rule := dummyRule("test#3", orgID)
|
|
uid := util.GenerateShortUID()
|
|
rule.UID = uid
|
|
created, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uid, created.UID)
|
|
_, _, err = ruleService.GetAlertRule(context.Background(), u, uid)
|
|
require.NoError(t, err)
|
|
})
|
|
})
|
|
t.Run("when dashboard is specified", func(t *testing.T) {
|
|
t.Run("return no error when both specified", func(t *testing.T) {
|
|
rule := dummyRule("test#4", orgID)
|
|
dashboardUid := "oinwerfgiuac"
|
|
panelId := int64(42)
|
|
rule.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: dashboardUid,
|
|
models.PanelIDAnnotation: strconv.FormatInt(panelId, 10),
|
|
}
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
})
|
|
t.Run("return 4xx error when missing dashboard uid", func(t *testing.T) {
|
|
rule := dummyRule("test#3", orgID)
|
|
panelId := int64(42)
|
|
rule.Annotations = map[string]string{
|
|
models.PanelIDAnnotation: strconv.FormatInt(panelId, 10),
|
|
}
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
|
|
})
|
|
t.Run("return 4xx error when missing panel id", func(t *testing.T) {
|
|
rule := dummyRule("test#3", orgID)
|
|
dashboardUid := "oinwerfgiuac"
|
|
rule.Annotations = map[string]string{
|
|
models.DashboardUIDAnnotation: dashboardUid,
|
|
}
|
|
rule, err := ruleService.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
|
|
})
|
|
})
|
|
|
|
t.Run("should not allow creating with a NoGroup Rule", func(t *testing.T) {
|
|
// NoGroup rules are not allowed to be created directly via provisioning, they must be created via new k8s apis
|
|
ruleWNoGroup := createNoGroupRule("test_No_group_create_disallowed", orgID, "test-no-group-ns")
|
|
_, err := ruleService.CreateAlertRule(context.Background(), u, ruleWNoGroup, models.ProvenanceNone)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleFailedValidation)
|
|
require.ErrorContains(t, err, "rules must have a valid group")
|
|
})
|
|
|
|
t.Run("should allow creating Rule without a group", func(t *testing.T) {
|
|
ruleWNoGroup := createNoGroupRule("test_No_group_create_allowed", orgID, "test-no-group-ns")
|
|
ruleWNoGroup.RuleGroup = "" // This is the way legacy storage creates rules without a group
|
|
_, err := ruleService.CreateAlertRule(context.Background(), u, ruleWNoGroup, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
// We should be able to retrieve the rule and see that it is a NoGroup rule
|
|
retrievedRule, _, err := ruleService.GetAlertRule(context.Background(), u, ruleWNoGroup.UID)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(retrievedRule.RuleGroup), "Rule should be considered NoGroup rule")
|
|
})
|
|
}
|
|
|
|
func TestUpdateAlertRule(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
groupIntervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
|
|
groupProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
rule := models.CopyRule(rules[0])
|
|
rule.RuleGroup = rule.RuleGroup + "_new"
|
|
rule.Title = rule.Title + "_new"
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
_, err := service.UpdateAlertRule(context.Background(), u, *rule, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, updates, 1)
|
|
})
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
rule := models.CopyRule(rules[0])
|
|
rule.Title = rule.Title + "_new"
|
|
|
|
t.Run("it should authorize the change to whole group", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, groupKey, change.GroupKey)
|
|
assert.Contains(t, change.AffectedGroups, groupKey)
|
|
assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
|
|
assert.Len(t, change.Update, 1)
|
|
assert.Empty(t, change.New)
|
|
assert.Empty(t, change.Delete)
|
|
return nil
|
|
}
|
|
|
|
_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, updates, 1)
|
|
})
|
|
t.Run("it should not update if not authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
|
|
require.ErrorIs(t, expectedErr, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Empty(t, updates)
|
|
})
|
|
|
|
t.Run("when there are no changes it should be successful", func(t *testing.T) {
|
|
// For this test we will not change the rule, and we will not use "admin" (CanWriteAllRules)
|
|
// permissions. The response of the service should still be successful.
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
rule := models.CopyRule(rules[0])
|
|
|
|
_, err := service.ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: *rule}})
|
|
require.NoError(t, err)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
_, err = service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Empty(t, updates)
|
|
})
|
|
})
|
|
|
|
// NoGroup-specific tests for UpdateAlertRule
|
|
t.Run("NoGroup: UpdateAlertRule preserves interval and sentinel group", func(t *testing.T) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }
|
|
|
|
rule := createNoGroupRule("nogroup-update", orgID, "my-namespace")
|
|
_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: rule}})
|
|
require.NoError(t, err)
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), &rule, orgID, models.ProvenanceNone))
|
|
|
|
// mutate fields and attempt to change interval via UpdateAlertRule
|
|
rule.Title = "nogroup-update-new"
|
|
originalInterval := int64(60)
|
|
require.Equal(t, originalInterval, rule.IntervalSeconds)
|
|
rule.IntervalSeconds = originalInterval + 60
|
|
|
|
updated, err := service.UpdateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(updated.RuleGroup))
|
|
require.Equal(t, "nogroup-update-new", updated.Title)
|
|
require.Equal(t, originalInterval, updated.IntervalSeconds)
|
|
})
|
|
}
|
|
|
|
func TestDeleteAlertRule(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
groupIntervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
|
|
groupProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
rule := rules[0]
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteAlertRule(context.Background(), u, rule.UID, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
})
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
rule := models.CopyRule(rules[0])
|
|
rule.Title = rule.Title + "_new"
|
|
|
|
t.Run("it should authorize the change to whole group", func(t *testing.T) {
|
|
rule := rules[0]
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, groupKey, change.GroupKey)
|
|
assert.Contains(t, change.AffectedGroups, groupKey)
|
|
assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
|
|
assert.Empty(t, change.Update)
|
|
assert.Empty(t, change.New)
|
|
assert.Len(t, change.Delete, 1)
|
|
return nil
|
|
}
|
|
|
|
err := service.DeleteAlertRule(context.Background(), u, rule.UID, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
})
|
|
t.Run("it should not delete if not authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
_, err := service.UpdateAlertRule(context.Background(), u, *rule, groupProvenance)
|
|
require.ErrorIs(t, expectedErr, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Empty(t, deletes)
|
|
})
|
|
})
|
|
|
|
// NoGroup-specific behaviors
|
|
t.Run("deleting a NoGroup rule removes only that rule", func(t *testing.T) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }
|
|
|
|
// create two NoGroup rules in the same namespace (distinct sentinel groups)
|
|
r1 := createNoGroupRule("nogroup-del-1", orgID, "my-namespace")
|
|
r2 := createNoGroupRule("nogroup-del-2", orgID, "my-namespace")
|
|
_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r1}, {AlertRule: r2}})
|
|
require.NoError(t, err)
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r1, orgID, models.ProvenanceNone))
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r2, orgID, models.ProvenanceNone))
|
|
|
|
err = service.DeleteAlertRule(context.Background(), u, r1.UID, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
uids := deletes[0].Params[3].([]string)
|
|
require.Contains(t, uids, r1.UID)
|
|
|
|
// verify r2 remains in store
|
|
remaining := ruleStore.Rules[orgID]
|
|
require.Len(t, remaining, 1)
|
|
require.Equal(t, r2.UID, remaining[0].UID)
|
|
// and its sentinel group remains intact
|
|
require.True(t, models.IsNoGroupRuleGroup(remaining[0].RuleGroup))
|
|
})
|
|
|
|
t.Run("when user cannot write all rules, deleting a NoGroup rule authorizes and succeeds", func(t *testing.T) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return false, nil }
|
|
|
|
r := createNoGroupRule("nogroup-del-auth", orgID, "my-namespace")
|
|
_, err := ruleStore.InsertAlertRules(context.Background(), models.NewUserUID(u), []models.InsertRule{{AlertRule: r}})
|
|
require.NoError(t, err)
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), &r, orgID, models.ProvenanceNone))
|
|
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
// expect single delete and affected group contains exactly the rule
|
|
require.Len(t, change.Delete, 1)
|
|
require.Contains(t, change.AffectedGroups, change.GroupKey)
|
|
require.Len(t, change.AffectedGroups[change.GroupKey], 1)
|
|
require.Equal(t, r.UID, change.AffectedGroups[change.GroupKey][0].UID)
|
|
return nil
|
|
}
|
|
|
|
err = service.DeleteAlertRule(context.Background(), u, r.UID, models.ProvenanceNone)
|
|
require.NoError(t, err)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
uids := deletes[0].Params[3].([]string)
|
|
require.Contains(t, uids, r.UID)
|
|
})
|
|
}
|
|
|
|
func TestGetAlertRule(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey)).GenerateManyRef(3)
|
|
rule := rules[0]
|
|
expectedProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))
|
|
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("should authorize access to rule", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
|
|
expected := errors.New("test")
|
|
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
|
|
assert.Equal(t, u, user)
|
|
assert.EqualValues(t, rule, namespaced)
|
|
return expected
|
|
}
|
|
|
|
_, _, err := service.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.Error(t, err)
|
|
require.Equal(t, expected, err)
|
|
|
|
assert.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "AuthorizeRuleRead", ac.Calls[0].Method)
|
|
|
|
ac.Calls = nil
|
|
ac.AuthorizeAccessInFolderFunc = func(ctx context.Context, user identity.Requester, namespaced models.Namespaced) error {
|
|
return nil
|
|
}
|
|
|
|
actual, provenance, err := service.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, *rule, actual)
|
|
assert.Equal(t, expectedProvenance, provenance)
|
|
})
|
|
|
|
t.Run("should return ErrAlertRuleNotFound if rule does not exist", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
_, _, err := service.GetAlertRule(context.Background(), u, "no-rule-uid")
|
|
require.ErrorIs(t, err, models.ErrAlertRuleNotFound)
|
|
|
|
assert.Len(t, ac.Calls, 0)
|
|
require.IsType(t, ruleStore.RecordedOps[0], models.GetAlertRuleByUIDQuery{})
|
|
query := ruleStore.RecordedOps[0].(models.GetAlertRuleByUIDQuery)
|
|
assert.Equal(t, models.GetAlertRuleByUIDQuery{
|
|
OrgID: orgID,
|
|
UID: "no-rule-uid",
|
|
}, query)
|
|
})
|
|
}
|
|
|
|
func TestGetRuleGroup(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
intervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(intervalSeconds)).GenerateManyRef(3)
|
|
derefRules := make([]models.AlertRule, 0, len(rules))
|
|
for _, rule := range rules {
|
|
derefRules = append(derefRules, *rule)
|
|
}
|
|
expectedProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))
|
|
}
|
|
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("return ErrAlertRuleGroupNotFound when rule group does not exist", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
|
|
_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, "no-rule-group")
|
|
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
|
require.Empty(t, ac.Calls)
|
|
})
|
|
|
|
t.Run("when user cannot read all rules", func(t *testing.T) {
|
|
t.Run("it should authorize access to entire group", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
assert.Equal(t, u, user)
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("error")
|
|
ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, r models.RulesGroup) error {
|
|
assert.Equal(t, u, user)
|
|
assert.EqualValues(t, rules, r)
|
|
return expectedErr
|
|
}
|
|
|
|
_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
|
|
require.Error(t, err)
|
|
require.Equal(t, expectedErr, err)
|
|
|
|
assert.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[1].Method)
|
|
|
|
ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
|
|
return nil
|
|
}
|
|
|
|
group, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, groupKey.RuleGroup, group.Title)
|
|
assert.Equal(t, groupKey.NamespaceUID, group.FolderUID)
|
|
assert.Equal(t, intervalSeconds, group.Interval)
|
|
assert.Equal(t, derefRules, group.Rules)
|
|
})
|
|
})
|
|
|
|
t.Run("when user can read all rules", func(t *testing.T) {
|
|
t.Run("it should skip AuthorizeRuleGroupRead", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
assert.Equal(t, u, user)
|
|
return true, nil
|
|
}
|
|
|
|
group, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
|
|
assert.Equal(t, groupKey.RuleGroup, group.Title)
|
|
assert.Equal(t, groupKey.NamespaceUID, group.FolderUID)
|
|
assert.Equal(t, intervalSeconds, group.Interval)
|
|
assert.Equal(t, derefRules, group.Rules)
|
|
})
|
|
})
|
|
|
|
t.Run("return error immediately when CanReadAllRules returns error", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
|
|
expectedErr := errors.New("test")
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, expectedErr
|
|
}
|
|
|
|
_, err := service.GetRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup)
|
|
require.Error(t, err)
|
|
require.Equal(t, expectedErr, err)
|
|
|
|
assert.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
})
|
|
|
|
// NoGroup-specific behaviors
|
|
// A NoGroup rule should be returned as a one-rule group addressed by its sentinel group title
|
|
t.Run("should return NoGroup rule group with exactly one rule", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initService(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }
|
|
|
|
rule := createNoGroupRule("nogroup-rule-1", orgID, "my-namespace")
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: {&rule},
|
|
}
|
|
|
|
group, err := service.GetRuleGroup(context.Background(), u, rule.NamespaceUID, rule.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(group.Title))
|
|
require.Equal(t, rule.NamespaceUID, group.FolderUID)
|
|
require.Equal(t, rule.IntervalSeconds, group.Interval)
|
|
require.Len(t, group.Rules, 1)
|
|
require.Equal(t, rule.UID, group.Rules[0].UID)
|
|
})
|
|
|
|
// Multiple NoGroup rules in the same namespace must produce separate sentinel groups
|
|
t.Run("should return distinct NoGroup groups for multiple rules", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initService(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) { return true, nil }
|
|
|
|
rule1 := createNoGroupRule("nogroup-rule-a", orgID, "my-namespace")
|
|
rule2 := createNoGroupRule("nogroup-rule-b", orgID, "my-namespace")
|
|
require.NotEqual(t, rule1.RuleGroup, rule2.RuleGroup)
|
|
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: {&rule1, &rule2},
|
|
}
|
|
|
|
group1, err := service.GetRuleGroup(context.Background(), u, rule1.NamespaceUID, rule1.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(group1.Title))
|
|
require.Len(t, group1.Rules, 1)
|
|
require.Equal(t, rule1.UID, group1.Rules[0].UID)
|
|
|
|
group2, err := service.GetRuleGroup(context.Background(), u, rule2.NamespaceUID, rule2.RuleGroup)
|
|
require.NoError(t, err)
|
|
require.True(t, models.IsNoGroupRuleGroup(group2.Title))
|
|
require.Len(t, group2.Rules, 1)
|
|
require.Equal(t, rule2.UID, group2.Rules[0].UID)
|
|
})
|
|
}
|
|
|
|
func TestListAlertRules(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey1 := models.GenerateGroupKey(orgID)
|
|
groupKey2 := models.GenerateGroupKey(orgID)
|
|
gen := models.RuleGen
|
|
rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3)
|
|
models.RulesGroup(rules1).SortByGroupIndex()
|
|
rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4)
|
|
models.RulesGroup(rules2).SortByGroupIndex()
|
|
allRules := append(rules1, rules2...)
|
|
|
|
fs := foldertest.NewFakeService()
|
|
fs.AddFolder(&folder.Folder{
|
|
OrgID: orgID,
|
|
UID: groupKey1.NamespaceUID,
|
|
Title: "folder1",
|
|
})
|
|
fs.AddFolder(&folder.Folder{
|
|
OrgID: orgID,
|
|
UID: groupKey2.NamespaceUID,
|
|
Title: "folder2",
|
|
})
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
service.folderService = fs
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: allRules,
|
|
}
|
|
ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can read all rules", func(t *testing.T) {
|
|
t.Run("should skip AuthorizeRuleGroupRead and return all rules", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
|
|
require.NoError(t, err)
|
|
// check that rules contain all uids from allRules
|
|
ruleUIDs := make(map[string]bool)
|
|
for _, r := range rules {
|
|
ruleUIDs[r.UID] = true
|
|
}
|
|
for _, r := range allRules {
|
|
assert.True(t, ruleUIDs[r.UID])
|
|
}
|
|
require.Len(t, ruleUIDs, len(allRules))
|
|
require.Empty(t, token)
|
|
|
|
assert.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
})
|
|
})
|
|
|
|
t.Run("when user cannot read all rules", func(t *testing.T) {
|
|
t.Run("should return only rules in accessible folders", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.HasAccessInFolderFunc = func(ctx context.Context, user identity.Requester, folder models.Namespaced) (bool, error) {
|
|
return folder.GetNamespaceUID() == groupKey2.NamespaceUID, nil
|
|
}
|
|
|
|
rules, _, token, err := service.ListAlertRules(context.Background(), u, ListAlertRulesOptions{})
|
|
require.NoError(t, err)
|
|
// check that rules contain all uids from rules1
|
|
ruleUIDs := make(map[string]bool)
|
|
for _, r := range rules {
|
|
ruleUIDs[r.UID] = true
|
|
}
|
|
for _, r := range rules2 {
|
|
assert.True(t, ruleUIDs[r.UID])
|
|
}
|
|
require.Len(t, ruleUIDs, len(rules2))
|
|
require.Empty(t, token)
|
|
|
|
assert.Len(t, ac.Calls, 3)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "HasAccessInFolder", ac.Calls[1].Method)
|
|
assert.Equal(t, "HasAccessInFolder", ac.Calls[2].Method)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestGetAlertRules(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey1 := models.GenerateGroupKey(orgID)
|
|
groupKey2 := models.GenerateGroupKey(orgID)
|
|
gen := models.RuleGen
|
|
rules1 := gen.With(gen.WithGroupKey(groupKey1), gen.WithUniqueGroupIndex()).GenerateManyRef(3)
|
|
models.RulesGroup(rules1).SortByGroupIndex()
|
|
rules2 := gen.With(gen.WithGroupKey(groupKey2), gen.WithUniqueGroupIndex()).GenerateManyRef(4)
|
|
models.RulesGroup(rules2).SortByGroupIndex()
|
|
allRules := append(rules1, rules2...)
|
|
expectedProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: allRules,
|
|
}
|
|
for _, rule := range rules1 {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, expectedProvenance))
|
|
}
|
|
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("return error when CanReadAllRules return error", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
expectedErr := errors.New("test")
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, expectedErr
|
|
}
|
|
|
|
_, _, err := service.GetAlertRules(context.Background(), u)
|
|
require.ErrorIs(t, err, expectedErr)
|
|
})
|
|
|
|
t.Run("when user can read all rules", func(t *testing.T) {
|
|
t.Run("should skip AuthorizeRuleGroupRead and return all rules", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
rules, provenance, err := service.GetAlertRules(context.Background(), u)
|
|
require.NoError(t, err)
|
|
require.Equal(t, allRules, rules)
|
|
require.Len(t, provenance, len(rules1))
|
|
|
|
assert.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
})
|
|
})
|
|
|
|
t.Run("when user cannot read all rules", func(t *testing.T) {
|
|
t.Run("should group rules and check AuthorizeRuleGroupRead and return only available rules", func(t *testing.T) {
|
|
t.Run("should remove group from output if AuthorizeRuleGroupRead returns authorization error", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
|
|
if rules[0].GetGroupKey() == groupKey1 {
|
|
return accesscontrol.NewAuthorizationErrorGeneric("test")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
rules, provenance, err := service.GetAlertRules(context.Background(), u)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, rules2, rules)
|
|
assert.Empty(t, provenance)
|
|
|
|
assert.Len(t, ac.Calls, 3)
|
|
assert.Equal(t, "CanReadAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[1].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupRead", ac.Calls[2].Method)
|
|
|
|
group1 := ac.Calls[1].Args[2].(models.RulesGroup)
|
|
group2 := ac.Calls[2].Args[2].(models.RulesGroup)
|
|
require.Len(t, append(group1, group2...), len(allRules))
|
|
})
|
|
|
|
t.Run("should immediately exist if AuthorizeRuleGroupRead returns another error", func(t *testing.T) {
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanReadAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test")
|
|
ac.AuthorizeAccessToRuleGroupFunc = func(ctx context.Context, user identity.Requester, rules models.RulesGroup) error {
|
|
return expectedErr
|
|
}
|
|
|
|
_, _, err := service.GetAlertRules(context.Background(), u)
|
|
require.ErrorIs(t, err, expectedErr)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestReplaceGroup(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
groupIntervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
|
|
groupProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
group := models.AlertRuleGroup{
|
|
Title: groupKey.RuleGroup,
|
|
FolderUID: groupKey.NamespaceUID,
|
|
Interval: groupIntervalSeconds,
|
|
Provenance: groupProvenance,
|
|
}
|
|
for _, rule := range rules {
|
|
r := models.CopyRule(rule)
|
|
r.Title = r.Title + "_new"
|
|
group.Rules = append(group.Rules, *r)
|
|
}
|
|
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, updates, 1)
|
|
})
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
group := models.AlertRuleGroup{
|
|
Title: groupKey.RuleGroup,
|
|
FolderUID: groupKey.NamespaceUID,
|
|
Interval: groupIntervalSeconds,
|
|
Provenance: groupProvenance,
|
|
}
|
|
for _, rule := range rules {
|
|
r := models.CopyRule(rule)
|
|
r.Title = r.Title + "_new"
|
|
group.Rules = append(group.Rules, *r)
|
|
}
|
|
|
|
t.Run("it should not update if not authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.ErrorIs(t, err, expectedErr)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Empty(t, updates)
|
|
})
|
|
t.Run("it should update if authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return nil
|
|
}
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceAPI, "")
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
updates := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.([]models.UpdateRule)
|
|
return a, ok
|
|
})
|
|
require.Len(t, updates, 1)
|
|
})
|
|
})
|
|
|
|
t.Run("alert rule metadata should be updated correctly", func(t *testing.T) {
|
|
service, _, _, _ := initServiceWithData(t)
|
|
|
|
rule := dummyRule("test#3", orgID)
|
|
// the rule must have a UID to be updated, otherwise it will be created as new
|
|
// and the previous version will be deleted
|
|
rule.UID = util.GenerateShortUID()
|
|
rule.Metadata = models.AlertRuleMetadata{
|
|
EditorSettings: models.EditorSettings{
|
|
SimplifiedQueryAndExpressionsSection: true,
|
|
},
|
|
PrometheusStyleRule: &models.PrometheusStyleRule{
|
|
OriginalRuleDefinition: "old",
|
|
},
|
|
}
|
|
group := models.AlertRuleGroup{
|
|
Title: rule.RuleGroup,
|
|
Interval: rule.IntervalSeconds,
|
|
FolderUID: rule.NamespaceUID,
|
|
Rules: []models.AlertRule{rule},
|
|
}
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
|
|
require.NoError(t, err)
|
|
|
|
rule.Metadata.PrometheusStyleRule.OriginalRuleDefinition = "new"
|
|
err = service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
|
|
require.NoError(t, err)
|
|
|
|
rule, _, err = service.GetAlertRule(context.Background(), u, rule.UID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "new", rule.Metadata.PrometheusStyleRule.OriginalRuleDefinition)
|
|
})
|
|
|
|
// NoGroup rule group should not allow more than one rule
|
|
t.Run("should reject multiple rules in a NoGroup rule group", func(t *testing.T) {
|
|
service, _, _, _ := initServiceWithData(t)
|
|
|
|
// Build a NoGroup group title and attempt to place 2 rules under it
|
|
group := createNoGroupRuleGroup("nogroup-multi", orgID, "my-namespace")
|
|
second := createNoGroupRule("nogroup-second", orgID, "my-namespace")
|
|
// Ensure the second rule is assigned to the same sentinel group
|
|
second.RuleGroup = group.Title
|
|
group.Rules = append(group.Rules, second)
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, group, models.ProvenanceNone, "")
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "cannot be used for rule groups with multiple rules")
|
|
})
|
|
|
|
t.Run("should reject changing the group name in a NoGroup rule group", func(t *testing.T) {
|
|
service, store, _, _ := initServiceWithData(t)
|
|
|
|
// Create a NoGroup rule and attempt to change its group name
|
|
groupSeed := createNoGroupRuleGroup("nogroup-multi", orgID, "my-namespace")
|
|
store.Rules[orgID] = []*models.AlertRule{models.CopyRule(&groupSeed.Rules[0])}
|
|
// change the group name away from the sentinel value
|
|
groupSeed.Title = "some-other-group" // not the sentinel group name
|
|
|
|
err := service.ReplaceRuleGroup(context.Background(), u, groupSeed, models.ProvenanceNone, "")
|
|
require.Error(t, err)
|
|
require.ErrorContains(t, err, "cannot move rule out of this group")
|
|
})
|
|
}
|
|
|
|
func TestDeleteRuleGroup(t *testing.T) {
|
|
orgID := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID}
|
|
groupKey := models.GenerateGroupKey(orgID)
|
|
groupIntervalSeconds := int64(30)
|
|
gen := models.RuleGen
|
|
rules := gen.With(gen.WithGroupKey(groupKey), gen.WithIntervalSeconds(groupIntervalSeconds)).GenerateManyRef(3)
|
|
groupProvenance := models.ProvenanceAPI
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID: rules,
|
|
}
|
|
for _, rule := range rules {
|
|
require.NoError(t, provenanceStore.SetProvenance(context.Background(), rule, orgID, groupProvenance))
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
})
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
t.Run("it should not update if not authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
|
|
require.ErrorIs(t, err, expectedErr)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Empty(t, deletes)
|
|
})
|
|
t.Run("it should update if authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, groupKey, change.GroupKey)
|
|
assert.Contains(t, change.AffectedGroups, groupKey)
|
|
assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey])
|
|
assert.Empty(t, change.Update)
|
|
assert.Empty(t, change.New)
|
|
assert.Len(t, change.Delete, len(rules))
|
|
return nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroup(context.Background(), u, groupKey.NamespaceUID, groupKey.RuleGroup, groupProvenance)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeleteQueries(ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestDeleteRuleGroups(t *testing.T) {
|
|
orgID1 := rand.Int63()
|
|
orgID2 := rand.Int63()
|
|
u := &user.SignedInUser{OrgID: orgID1, UserUID: "test-test"}
|
|
|
|
// Create groups across different orgs and namespaces
|
|
groupKey1 := models.AlertRuleGroupKey{
|
|
OrgID: orgID1,
|
|
NamespaceUID: "namespace1",
|
|
RuleGroup: "group1",
|
|
}
|
|
groupKey2 := models.AlertRuleGroupKey{
|
|
OrgID: orgID1,
|
|
NamespaceUID: "namespace2",
|
|
RuleGroup: "group2",
|
|
}
|
|
groupKey3 := models.AlertRuleGroupKey{
|
|
OrgID: orgID1,
|
|
NamespaceUID: "namespace3",
|
|
RuleGroup: "group3",
|
|
}
|
|
groupKey4 := models.AlertRuleGroupKey{
|
|
OrgID: orgID2, // Different org
|
|
NamespaceUID: "namespace1",
|
|
RuleGroup: "group1",
|
|
}
|
|
|
|
gen := models.RuleGen
|
|
// Create rules for each group
|
|
rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2)
|
|
rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3)
|
|
rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2)
|
|
rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2)
|
|
|
|
org1Rules := slices.Concat(rules1, rules2, rules3)
|
|
org2Rules := rules4
|
|
|
|
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
service, ruleStore, provenanceStore, ac := initService(t)
|
|
ruleStore.Rules = map[int64][]*models.AlertRule{
|
|
orgID1: org1Rules,
|
|
orgID2: org2Rules,
|
|
}
|
|
// Set provenance for all rules
|
|
for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} {
|
|
for _, rule := range rules {
|
|
err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
getUIDs := func(rules []*models.AlertRule) []string {
|
|
uids := make([]string, 0, len(rules))
|
|
for _, rule := range rules {
|
|
uids = append(uids, rule.UID)
|
|
}
|
|
return uids
|
|
}
|
|
|
|
t.Run("when deleting specific groups", func(t *testing.T) {
|
|
filterOpts := &FilterOptions{
|
|
NamespaceUIDs: []string{"namespace1"},
|
|
RuleGroups: []string{"group1"},
|
|
}
|
|
|
|
t.Run("when user can write all rules", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 1)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
|
|
// Verify only rules from group1 in org1 were deleted
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
require.Equal(t, "test-test", deletes[0].userID)
|
|
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
|
|
})
|
|
|
|
t.Run("when user cannot write all rules", func(t *testing.T) {
|
|
t.Run("should not delete if not authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
expectedErr := errors.New("test error")
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
return expectedErr
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.ErrorIs(t, err, expectedErr)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Empty(t, deletes)
|
|
})
|
|
|
|
t.Run("should delete group1 when authorized", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return false, nil
|
|
}
|
|
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
|
assert.Equal(t, u, user)
|
|
assert.Equal(t, groupKey1, change.GroupKey)
|
|
assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1])
|
|
assert.Empty(t, change.Update)
|
|
assert.Empty(t, change.New)
|
|
assert.Len(t, change.Delete, len(rules1))
|
|
return nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, ac.Calls, 2)
|
|
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
|
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
|
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
|
|
})
|
|
})
|
|
})
|
|
|
|
t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) {
|
|
filterOpts := &FilterOptions{
|
|
NamespaceUIDs: []string{"namespace1", "namespace2"},
|
|
RuleGroups: []string{"group1", "group2"},
|
|
}
|
|
|
|
t.Run("should delete all matching groups from correct org", func(t *testing.T) {
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.NoError(t, err)
|
|
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Len(t, deletes, 2)
|
|
require.ElementsMatch(
|
|
t,
|
|
slices.Concat(getUIDs(rules1), getUIDs(rules2)),
|
|
slices.Concat(deletes[0].uids, deletes[1].uids),
|
|
)
|
|
})
|
|
})
|
|
|
|
t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
|
|
filterOpts := &FilterOptions{
|
|
HasPrometheusRuleDefinition: util.Pointer(true),
|
|
NamespaceUIDs: []string{"namespace1"},
|
|
}
|
|
|
|
t.Run("when the group is not imported", func(t *testing.T) {
|
|
filterOpts.RuleGroups = []string{groupKey1.RuleGroup}
|
|
service, _, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
|
})
|
|
|
|
t.Run("when the group is imported", func(t *testing.T) {
|
|
importedGroup := models.AlertRuleGroupKey{
|
|
OrgID: orgID1,
|
|
NamespaceUID: "namespace1",
|
|
RuleGroup: "newgroup",
|
|
}
|
|
importedRules := gen.With(
|
|
gen.WithGroupKey(importedGroup),
|
|
gen.WithPrometheusOriginalRuleDefinition("something"),
|
|
).GenerateManyRef(2)
|
|
filterOpts.RuleGroups = []string{importedGroup.RuleGroup}
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.NoError(t, err)
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Len(t, deletes, 1)
|
|
require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids)
|
|
})
|
|
})
|
|
|
|
t.Run("with no matching rule groups", func(t *testing.T) {
|
|
filterOpts := &FilterOptions{
|
|
NamespaceUIDs: []string{"non-existent"},
|
|
RuleGroups: []string{"non-existent"},
|
|
}
|
|
|
|
service, ruleStore, _, ac := initServiceWithData(t)
|
|
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
|
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
|
|
|
deletes := getDeletedRules(t, ruleStore)
|
|
require.Empty(t, deletes)
|
|
})
|
|
}
|
|
|
|
func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
|
|
generic := ruleStore.GetRecordedCommands(func(cmd any) (any, bool) {
|
|
a, ok := cmd.(fakes.GenericRecordedQuery)
|
|
if !ok || a.Name != "DeleteAlertRulesByUID" {
|
|
return nil, false
|
|
}
|
|
return a, ok
|
|
})
|
|
result := make([]fakes.GenericRecordedQuery, 0, len(generic))
|
|
for _, g := range generic {
|
|
result = append(result, g.(fakes.GenericRecordedQuery))
|
|
}
|
|
return result
|
|
}
|
|
|
|
type deleteRuleOperation struct {
|
|
orgID int64
|
|
userID string
|
|
uids []string
|
|
}
|
|
|
|
func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
|
|
t.Helper()
|
|
|
|
queries := getDeleteQueries(ruleStore)
|
|
operations := make([]deleteRuleOperation, 0, len(queries))
|
|
for _, q := range queries {
|
|
orgID, ok := q.Params[0].(int64)
|
|
require.True(t, ok, "orgID parameter should be int64")
|
|
|
|
uid := ""
|
|
userUID, ok := q.Params[1].(*models.UserUID)
|
|
require.True(t, ok, "parameter should be UserUID")
|
|
if userUID != nil {
|
|
uid = string(*userUID)
|
|
}
|
|
|
|
uids, ok := q.Params[3].([]string)
|
|
require.True(t, ok, "uids parameter should be []string")
|
|
|
|
operations = append(operations, deleteRuleOperation{
|
|
orgID: orgID,
|
|
userID: uid,
|
|
uids: uids,
|
|
})
|
|
}
|
|
return operations
|
|
}
|
|
|
|
func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
|
|
t.Helper()
|
|
sqlStore := db.InitTestDB(t)
|
|
store := store.DBstore{
|
|
SQLStore: sqlStore,
|
|
Cfg: setting.UnifiedAlertingSettings{
|
|
BaseInterval: time.Second * 10,
|
|
},
|
|
Logger: log.NewNopLogger(),
|
|
FolderService: folderService,
|
|
Bus: bus.ProvideBus(tracing.InitializeTracerForTest()),
|
|
FeatureToggles: featuremgmt.WithFeatures(),
|
|
}
|
|
// store := fakes.NewRuleStore(t)
|
|
quotas := MockQuotaChecker{}
|
|
quotas.EXPECT().LimitOK()
|
|
|
|
if folderService == nil {
|
|
folderService = foldertest.NewFakeService()
|
|
}
|
|
|
|
return AlertRuleService{
|
|
ruleStore: store,
|
|
provenanceStore: store,
|
|
quotas: "as,
|
|
xact: sqlStore,
|
|
log: log.New("testing"),
|
|
baseIntervalSeconds: 10,
|
|
defaultIntervalSeconds: 60,
|
|
folderService: folderService,
|
|
authz: &fakeRuleAccessControlService{},
|
|
nsValidatorProvider: &NotificationSettingsValidatorProviderFake{},
|
|
}
|
|
}
|
|
|
|
func dummyRule(title string, orgID int64) models.AlertRule {
|
|
return createTestRule(title, "my-cool-group", orgID, "my-namespace")
|
|
}
|
|
|
|
func createNoGroupRuleGroup(title string, orgID int64, namespace string) models.AlertRuleGroup {
|
|
uid := util.GenerateShortUID()
|
|
group, err := models.NewNoGroupRuleGroup(uid)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to create NoGroupRuleGroup: %v", err))
|
|
}
|
|
|
|
return models.AlertRuleGroup{
|
|
Title: group.String(),
|
|
Interval: 60,
|
|
FolderUID: namespace,
|
|
Rules: []models.AlertRule{
|
|
createNoGroupRule(title, orgID, namespace),
|
|
},
|
|
}
|
|
}
|
|
|
|
func createNoGroupRule(title string, orgID int64, namespace string) models.AlertRule {
|
|
uid := util.GenerateShortUID()
|
|
group, err := models.NewNoGroupRuleGroup(uid)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to create NoGroupRuleGroup: %v", err))
|
|
}
|
|
|
|
return models.AlertRule{
|
|
UID: uid,
|
|
OrgID: orgID,
|
|
Title: title,
|
|
Condition: "A",
|
|
Version: 1,
|
|
IntervalSeconds: 60,
|
|
Data: []models.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
Model: json.RawMessage("{}"),
|
|
DatasourceUID: expr.DatasourceUID,
|
|
RelativeTimeRange: models.RelativeTimeRange{
|
|
From: models.Duration(60),
|
|
To: models.Duration(0),
|
|
},
|
|
},
|
|
},
|
|
NamespaceUID: namespace,
|
|
RuleGroup: group.String(),
|
|
For: time.Second * 60,
|
|
NoDataState: models.OK,
|
|
ExecErrState: models.OkErrState,
|
|
}
|
|
}
|
|
|
|
func createTestRule(title string, groupTitle string, orgID int64, namespace string) models.AlertRule {
|
|
return models.AlertRule{
|
|
OrgID: orgID,
|
|
Title: title,
|
|
Condition: "A",
|
|
Version: 1,
|
|
IntervalSeconds: 60,
|
|
Data: []models.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
Model: json.RawMessage("{}"),
|
|
DatasourceUID: expr.DatasourceUID,
|
|
RelativeTimeRange: models.RelativeTimeRange{
|
|
From: models.Duration(60),
|
|
To: models.Duration(0),
|
|
},
|
|
},
|
|
},
|
|
NamespaceUID: namespace,
|
|
RuleGroup: groupTitle,
|
|
For: time.Second * 60,
|
|
NoDataState: models.OK,
|
|
ExecErrState: models.OkErrState,
|
|
}
|
|
}
|
|
|
|
func createDummyGroup(title string, orgID int64) models.AlertRuleGroup {
|
|
return models.AlertRuleGroup{
|
|
Title: title,
|
|
Interval: 60,
|
|
FolderUID: "my-namespace",
|
|
Rules: []models.AlertRule{
|
|
dummyRule(title+"-"+"rule-1", orgID),
|
|
},
|
|
}
|
|
}
|
|
|
|
func initService(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
|
t.Helper()
|
|
|
|
ac := &fakeRuleAccessControlService{}
|
|
ruleStore := fakes.NewRuleStore(t)
|
|
provenanceStore := fakes.NewFakeProvisioningStore()
|
|
folderService := foldertest.NewFakeService()
|
|
|
|
quotas := MockQuotaChecker{}
|
|
quotas.EXPECT().LimitOK()
|
|
|
|
service := &AlertRuleService{
|
|
folderService: folderService,
|
|
ruleStore: ruleStore,
|
|
provenanceStore: provenanceStore,
|
|
quotas: "as,
|
|
xact: newNopTransactionManager(),
|
|
log: log.New("testing"),
|
|
baseIntervalSeconds: 10,
|
|
defaultIntervalSeconds: 60,
|
|
authz: ac,
|
|
nsValidatorProvider: &NotificationSettingsValidatorProviderFake{},
|
|
}
|
|
|
|
return service, ruleStore, provenanceStore, ac
|
|
}
|
|
|
|
// func TestNoGroupRuleGroupIntervalHandling(t *testing.T) {
|
|
// orgID := rand.Int63()
|
|
// u := &user.SignedInUser{OrgID: orgID, UserUID: util.GenerateShortUID()}
|
|
|
|
// t.Run("UpdateRuleGroup with NoGroupRuleGroup", func(t *testing.T) {
|
|
// t.Run("should allow interval updates for NoGroupRuleGroup via UpdateRuleGroup", func(t *testing.T) {
|
|
// service, store, _, ac := initService(t)
|
|
// ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
// return true, nil
|
|
// }
|
|
|
|
// // pre populate a rule with NoGroupRuleGroup
|
|
// rule := createNoGroupRule("test-rule", orgID, "my-namespace")
|
|
// store.Rules[orgID] = []*models.AlertRule{&rule} // Pre-populate the store with the rule
|
|
|
|
// // Update the rule with a new interval via UpdateRuleGroup
|
|
// newInterval := int64(120)
|
|
// createdRule, _, err := service.GetAlertRule(context.Background(), u, rule.UID)
|
|
// require.NoError(t, err)
|
|
// err = service.UpdateRuleGroup(context.Background(), u, createdRule.NamespaceUID, createdRule.RuleGroup, newInterval)
|
|
// require.NoError(t, err)
|
|
// updatedRule, _, err := service.GetAlertRule(context.Background(), u, createdRule.UID)
|
|
// require.NoError(t, err)
|
|
// assert.Equal(t, newInterval, updatedRule.IntervalSeconds, "Rule interval should be updated for NoGroupRuleGroup")
|
|
|
|
// })
|
|
|
|
// // t.Run("should preserve group interval for normal groups", func(t *testing.T) {
|
|
// // service, _, _, ac := initService(t)
|
|
// // ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
// // return true, nil
|
|
// // }
|
|
|
|
// // // Create a rule in a normal group
|
|
// // groupInterval := int64(90)
|
|
// // rule := createTestRule("test-rule", "normal-group", orgID, "my-namespace")
|
|
// // rule2 := createTestRule("test-rule-2", "normal-group", orgID, "my-namespace")
|
|
// // rule2.IntervalSeconds = groupInterval // Set the group interval
|
|
// // rule.IntervalSeconds = groupInterval
|
|
// // createdRule, err := service.CreateAlertRule(context.Background(), u, rule, models.ProvenanceNone)
|
|
// // require.NoError(t, err)
|
|
// // createdRule2, err := service.CreateAlertRule(context.Background(), u, rule2, models.ProvenanceNone)
|
|
// // require.NoError(t, err)
|
|
|
|
// // // Try to update the rule with a different interval
|
|
// // createdRule.IntervalSeconds = 120
|
|
// // updatedRule, err := service.UpdateAlertRule(context.Background(), u, createdRule, models.ProvenanceNone)
|
|
// // require.NoError(t, err)
|
|
// // assert.Equal(t, int64(120), updatedRule.IntervalSeconds, "Rule interval should be changed for all rules in normal groups")
|
|
// // updatedRule2, _, err := service.GetAlertRule(context.Background(), u, createdRule2.UID)
|
|
// // require.NoError(t, err)
|
|
// // assert.Equal(t, int64(120), updatedRule2.IntervalSeconds, "All rules in the same group should have the same interval after update")
|
|
// // })
|
|
// })
|
|
|
|
// t.Run("GetRuleGroup with a NoGroupRuleGroup", func(t *testing.T) {
|
|
// t.Run("should allow retrieval of sentinel group", func(t *testing.T) {
|
|
// service, store, _, ac := initService(t)
|
|
// ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
|
// return true, nil
|
|
// }
|
|
|
|
// // pre populate a rule with NoGroupRuleGroup
|
|
// rule := createNoGroupRule("test-rule", orgID, "my-namespace")
|
|
// store.Rules[orgID] = []*models.AlertRule{&rule} // Pre-populate the store with the rule
|
|
|
|
// noGroupGroup, err := service.GetRuleGroup(context.Background(), u, "my-namespace", rule.RuleGroup)
|
|
// require.NoError(t, err)
|
|
// assert.Equal(t, rule.RuleGroup, noGroupGroup.Title, "NoGroupRuleGroup should be retrievable by its group title")
|
|
// assert.Len(t, noGroupGroup.Rules, 1, "NoGroupRuleGroup should contain only the one rule")
|
|
// })
|
|
// })
|
|
// }
|