diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index ed645537d34..262c8a05753 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -45,6 +45,7 @@ type ContactPointService interface { type TemplateService interface { GetTemplates(ctx context.Context, orgID int64) ([]definitions.NotificationTemplate, error) + GetTemplate(ctx context.Context, orgID int64, name string) (definitions.NotificationTemplate, error) SetTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) DeleteTemplate(ctx context.Context, orgID int64, name string, provenance definitions.Provenance, version string) error } diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index bb2cbf6b2dc..bf9be3b2462 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -271,7 +271,7 @@ func createTestRoutingTree() definitions.Route { } func createTestAlertingConfig() *definitions.PostableUserConfig { - cfg, _ := legacy_storage.DeserializeAlertmanagerConfig([]byte(defaultConfig)) + cfg, _ := legacy_storage.DeserializeAlertmanagerConfig([]byte(setting.GetAlertmanagerDefaultConfiguration())) cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{ Receiver: config.Receiver{ diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 06c878e4742..bed740aa763 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -36,6 +36,15 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi return nil, err } + if len(revision.Config.TemplateFiles) == 0 { + return nil, nil + } + + provenances, err := t.provenanceStore.GetProvenances(ctx, orgID, (&definitions.NotificationTemplate{}).ResourceType()) + if err != nil { + return nil, err + } + templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles)) for name, tmpl := range revision.Config.TemplateFiles { tmpl := definitions.NotificationTemplate{ @@ -43,17 +52,41 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi Template: tmpl, ResourceVersion: calculateTemplateFingerprint(tmpl), } + provenance, ok := provenances[tmpl.ResourceID()] + if !ok { + provenance = models.ProvenanceNone + } + tmpl.Provenance = definitions.Provenance(provenance) + templates = append(templates, tmpl) + } + + return templates, nil +} + +func (t *TemplateService) GetTemplate(ctx context.Context, orgID int64, name string) (definitions.NotificationTemplate, error) { + revision, err := t.configStore.Get(ctx, orgID) + if err != nil { + return definitions.NotificationTemplate{}, err + } + + for tmplName, tmpl := range revision.Config.TemplateFiles { + if tmplName != name { + continue + } + tmpl := definitions.NotificationTemplate{ + Name: name, + Template: tmpl, + ResourceVersion: calculateTemplateFingerprint(tmpl), + } provenance, err := t.provenanceStore.GetProvenance(ctx, &tmpl, orgID) if err != nil { - return nil, err + return definitions.NotificationTemplate{}, err } tmpl.Provenance = definitions.Provenance(provenance) - - templates = append(templates, tmpl) + return tmpl, nil } - - return templates, nil + return definitions.NotificationTemplate{}, ErrTemplateNotFound.Errorf("") } func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl definitions.NotificationTemplate) (definitions.NotificationTemplate, error) { diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 82123fea5b7..2921a8b1499 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/log" @@ -15,592 +15,713 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" - "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) -func TestTemplateService(t *testing.T) { - t.Run("service returns templates from config file", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) +func TestGetTemplates(t *testing.T) { + orgID := int64(1) + revision := &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ + TemplateFiles: map[string]string{ + "template1": "test1", + "template2": "test2", + "template3": "test3", + }, + }, + } - result, err := sut.GetTemplates(context.Background(), 1) + t.Run("returns templates from config file", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(map[string]models.Provenance{ + "template1": models.ProvenanceAPI, + "template2": models.ProvenanceFile, + }, nil) + result, err := sut.GetTemplates(context.Background(), orgID) require.NoError(t, err) - require.Len(t, result, 1) + + expected := []definitions.NotificationTemplate{ + { + Name: "template1", + Template: "test1", + Provenance: definitions.Provenance(models.ProvenanceAPI), + ResourceVersion: calculateTemplateFingerprint("test1"), + }, + { + Name: "template2", + Template: "test2", + Provenance: definitions.Provenance(models.ProvenanceFile), + ResourceVersion: calculateTemplateFingerprint("test2"), + }, + { + Name: "template3", + Template: "test3", + Provenance: definitions.Provenance(models.ProvenanceNone), + ResourceVersion: calculateTemplateFingerprint("test3"), + }, + } + + require.ElementsMatch(t, expected, result) + + prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType()) + prov.AssertExpectations(t) }) - t.Run("service returns empty map when config file contains no templates", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) + t.Run("returns empty list when config file contains no templates", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{}, + }, nil + } result, err := sut.GetTemplates(context.Background(), 1) require.NoError(t, err) require.Empty(t, result) + prov.AssertExpectations(t) + }) + + t.Run("propagates errors", func(t *testing.T) { + t.Run("when unable to read config", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + expectedErr := errors.New("test") + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return nil, expectedErr + } + + _, err := sut.GetTemplates(context.Background(), 1) + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when provenance status fails", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision, nil + } + + expectedErr := errors.New("test") + prov.EXPECT().GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(nil, expectedErr) + + _, err := sut.GetTemplates(context.Background(), 1) + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + }) +} + +func TestGetTemplate(t *testing.T) { + orgID := int64(1) + templateName := "template1" + templateContent := "test1" + revision := &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ + TemplateFiles: map[string]string{ + templateName: templateContent, + }, + }, + } + + t.Run("return a template from config file by name", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + + result, err := sut.GetTemplate(context.Background(), orgID, templateName) + require.NoError(t, err) + + expected := definitions.NotificationTemplate{ + Name: templateName, + Template: templateContent, + Provenance: definitions.Provenance(models.ProvenanceAPI), + ResourceVersion: calculateTemplateFingerprint(templateContent), + } + + require.Equal(t, expected, result) + + prov.AssertCalled(t, "GetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool { + return t.Name == expected.Name + }), orgID) + prov.AssertExpectations(t) + }) + + t.Run("returns ErrTemplateNotFound when template does not exist", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision, nil + } + _, err := sut.GetTemplate(context.Background(), orgID, "not-found") + require.ErrorIs(t, err, ErrTemplateNotFound) + prov.AssertExpectations(t) }) t.Run("service propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("failed")) + sut, store, prov := createTemplateServiceSut() + expectedErr := errors.New("test") + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return nil, expectedErr + } - _, err := sut.GetTemplates(context.Background(), 1) + _, err := sut.GetTemplate(context.Background(), 1, templateName) - require.Error(t, err) + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) }) - t.Run("when config is invalid", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: brokenConfig, - }) + t.Run("when provenance status fails", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision, nil + } + expectedErr := errors.New("test") + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr) - _, err := sut.GetTemplates(context.Background(), 1) + _, err := sut.GetTemplate(context.Background(), orgID, templateName) + require.ErrorIs(t, err, expectedErr) - require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) + prov.AssertExpectations(t) }) + }) +} - t.Run("when no AM config in current org", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, nil) +func TestSetTemplate(t *testing.T) { + orgID := int64(1) + templateName := "template1" + currentTemplateContent := "test1" + amConfigToken := util.GenerateShortUID() + revision := func() *legacy_storage.ConfigRevision { + return &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ + TemplateFiles: map[string]string{ + templateName: currentTemplateContent, + }, + }, + ConcurrencyToken: amConfigToken, + } + } - _, err := sut.GetTemplates(context.Background(), 1) + t.Run("adds new template to config file", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{}, + ConcurrencyToken: amConfigToken, + }, nil + } + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { + assertInTransaction(t, ctx) + return nil + } + prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) { + assertInTransaction(t, ctx) + }).Return(nil) - require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) + tmpl := definitions.NotificationTemplate{ + Name: "new-template", + Template: "{{ define \"test\"}} test {{ end }}", + Provenance: definitions.Provenance(models.ProvenanceAPI), + ResourceVersion: "", + } + + result, err := sut.SetTemplate(context.Background(), orgID, tmpl) + + require.NoError(t, err) + require.Equal(t, definitions.NotificationTemplate{ + Name: tmpl.Name, + Template: tmpl.Template, + Provenance: tmpl.Provenance, + ResourceVersion: calculateTemplateFingerprint(tmpl.Template), + }, result) + + require.Len(t, store.Calls, 2) + + require.Equal(t, "Save", store.Calls[1].Method) + saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) + assert.Equal(t, amConfigToken, saved.ConcurrencyToken) + assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name) + assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name]) + + prov.AssertCalled(t, "SetProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool { + return t.Name == tmpl.Name + }), orgID, models.ProvenanceAPI) + }) + + t.Run("updates current template", func(t *testing.T) { + t.Run("when version matches", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) { + assertInTransaction(t, ctx) + }).Return(nil) + + tmpl := definitions.NotificationTemplate{ + Name: templateName, + Template: "{{ define \"test\"}} test {{ end }}", + Provenance: definitions.Provenance(models.ProvenanceAPI), + ResourceVersion: calculateTemplateFingerprint("test1"), + } + + result, err := sut.SetTemplate(context.Background(), orgID, tmpl) + + require.NoError(t, err) + assert.Equal(t, definitions.NotificationTemplate{ + Name: tmpl.Name, + Template: tmpl.Template, + Provenance: tmpl.Provenance, + ResourceVersion: calculateTemplateFingerprint(tmpl.Template), + }, result) + + require.Len(t, store.Calls, 2) + require.Equal(t, "Save", store.Calls[1].Method) + saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) + assert.Equal(t, amConfigToken, saved.ConcurrencyToken) + assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name) + assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name]) + + prov.AssertExpectations(t) + }) + t.Run("bypasses optimistic concurrency validation when version is empty", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) { + assertInTransaction(t, ctx) + }).Return(nil) + + tmpl := definitions.NotificationTemplate{ + Name: templateName, + Template: "{{ define \"test\"}} test {{ end }}", + Provenance: definitions.Provenance(models.ProvenanceAPI), + ResourceVersion: "", + } + + result, err := sut.SetTemplate(context.Background(), orgID, tmpl) + + require.NoError(t, err) + assert.Equal(t, definitions.NotificationTemplate{ + Name: tmpl.Name, + Template: tmpl.Template, + Provenance: tmpl.Provenance, + ResourceVersion: calculateTemplateFingerprint(tmpl.Template), + }, result) + + require.Equal(t, "Save", store.Calls[1].Method) + saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) + assert.Equal(t, amConfigToken, saved.ConcurrencyToken) + assert.Contains(t, saved.Config.TemplateFiles, tmpl.Name) + assert.Equal(t, tmpl.Template, saved.Config.TemplateFiles[tmpl.Name]) }) }) - t.Run("setting templates", func(t *testing.T) { - t.Run("rejects templates that fail validation", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + t.Run("normalizes template content with no define", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + prov.EXPECT().SaveSucceeds() + + tmpl := definitions.NotificationTemplate{ + Name: templateName, + Template: "content", + Provenance: definitions.Provenance(models.ProvenanceNone), + ResourceVersion: calculateTemplateFingerprint(currentTemplateContent), + } + + result, _ := sut.SetTemplate(context.Background(), orgID, tmpl) + + expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n content\n{{ end }}", templateName) + require.Equal(t, definitions.NotificationTemplate{ + Name: tmpl.Name, + Template: expectedContent, + Provenance: tmpl.Provenance, + ResourceVersion: calculateTemplateFingerprint(expectedContent), + }, result) + }) + + t.Run("does not reject template with unknown field", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + assert.Equal(t, orgID, org) + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + prov.EXPECT().SaveSucceeds() + + tmpl := definitions.NotificationTemplate{ + Name: "name", + Template: "{{ .NotAField }}", + } + _, err := sut.SetTemplate(context.Background(), 1, tmpl) + + require.NoError(t, err) + }) + + t.Run("rejects templates that fail validation", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + + t.Run("empty content", func(t *testing.T) { tmpl := definitions.NotificationTemplate{ Name: "", Template: "", } - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - + _, err := sut.SetTemplate(context.Background(), orgID, tmpl) require.ErrorIs(t, err, ErrTemplateInvalid) }) - t.Run("rejects existing templates if provenance is not right", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - - expectedErr := errors.New("test") - sut.validator = func(from, to models.Provenance) error { - assert.Equal(t, models.ProvenanceAPI, from) - assert.Equal(t, models.ProvenanceNone, to) - return expectedErr - } - template := definitions.NotificationTemplate{ - Name: "a", - Template: "asdf-new", - } - template.Provenance = definitions.Provenance(models.ProvenanceNone) - - _, err := sut.SetTemplate(context.Background(), 1, template) - - require.ErrorIs(t, err, expectedErr) - }) - - t.Run("rejects existing templates if version is not right", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - - template := definitions.NotificationTemplate{ - Name: "a", - Template: "asdf-new", - ResourceVersion: "bad-version", - Provenance: definitions.Provenance(models.ProvenanceNone), - } - - _, err := sut.SetTemplate(context.Background(), 1, template) - - require.ErrorIs(t, err, ErrVersionConflict) - }) - - t.Run("rejects new template if version is set", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - tmpl.ResourceVersion = "test" - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.ErrorIs(t, err, ErrTemplateNotFound) - }) - - t.Run("propagates errors", func(t *testing.T) { - t.Run("when unable to read config", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("failed")) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.Error(t, err) - }) - - t.Run("when config is invalid", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: brokenConfig, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) - }) - - t.Run("when no AM config in current org", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) - }) - - t.Run("when provenance fails to save", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT(). - SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return(fmt.Errorf("failed to save provenance")) - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.ErrorContains(t, err, "failed to save provenance") - }) - - t.Run("when AM config fails to save", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT(). - UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(fmt.Errorf("failed to save config")) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.ErrorContains(t, err, "failed to save config") - }) - }) - - t.Run("adds new template to config file on success", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.NoError(t, err) - }) - - t.Run("succeeds when stitching config file with no templates", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := createNotificationTemplate() - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.NoError(t, err) - }) - - t.Run("normalizes template content with no define", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + t.Run("invalid content", func(t *testing.T) { tmpl := definitions.NotificationTemplate{ - Name: "name", - Template: "content", - } - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - result, _ := sut.SetTemplate(context.Background(), 1, tmpl) - - exp := "{{ define \"name\" }}\n content\n{{ end }}" - require.Equal(t, exp, result.Template) - }) - - t.Run("avoids normalizing template content with define", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := definitions.NotificationTemplate{ - Name: "name", - Template: "{{define \"name\"}}content{{end}}", - } - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - result, _ := sut.SetTemplate(context.Background(), 1, tmpl) - - require.Equal(t, tmpl.Template, result.Template) - }) - - t.Run("rejects syntactically invalid template", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := definitions.NotificationTemplate{ - Name: "name", + Name: "", Template: "{{ .MyField }", } - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - + _, err := sut.SetTemplate(context.Background(), orgID, tmpl) require.ErrorIs(t, err, ErrTemplateInvalid) }) - t.Run("does not reject template with unknown field", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - tmpl := definitions.NotificationTemplate{ - Name: "name", - Template: "{{ .NotAField }}", - } - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - _, err := sut.SetTemplate(context.Background(), 1, tmpl) - - require.NoError(t, err) - }) + require.Empty(t, store.Calls) + prov.AssertExpectations(t) }) - t.Run("deleting templates", func(t *testing.T) { - t.Run("propagates errors", func(t *testing.T) { - t.Run("when unable to read config", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, fmt.Errorf("failed")) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + t.Run("rejects existing templates if provenance is not right", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } - err := sut.DeleteTemplate(context.Background(), 1, "template", definitions.Provenance(models.ProvenanceAPI), "") + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - require.Error(t, err) - }) + expectedErr := errors.New("test") + sut.validator = func(from, to models.Provenance) error { + assert.Equal(t, models.ProvenanceAPI, from) + assert.Equal(t, models.ProvenanceNone, to) + return expectedErr + } - t.Run("when config is invalid", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: brokenConfig, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + template := definitions.NotificationTemplate{ + Name: "template1", + Template: "asdf-new", + } + template.Provenance = definitions.Provenance(models.ProvenanceNone) - err := sut.DeleteTemplate(context.Background(), 1, "template", definitions.Provenance(models.ProvenanceAPI), "") + _, err := sut.SetTemplate(context.Background(), orgID, template) - require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) - }) + require.ErrorIs(t, err, expectedErr) + }) - t.Run("when no AM config in current org", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + t.Run("rejects existing templates if version is not right", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - err := sut.DeleteTemplate(context.Background(), 1, "template", definitions.Provenance(models.ProvenanceAPI), "") + template := definitions.NotificationTemplate{ + Name: "template1", + Template: "asdf-new", + ResourceVersion: "bad-version", + Provenance: definitions.Provenance(models.ProvenanceNone), + } - require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) - }) + _, err := sut.SetTemplate(context.Background(), orgID, template) - t.Run("when provenance fails to save", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT(). - DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). - Return(fmt.Errorf("failed to save provenance")) - - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceAPI), "") - - require.ErrorContains(t, err, "failed to save provenance") - }) - - t.Run("when AM config fails to save", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT(). - UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(fmt.Errorf("failed to save config")) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceAPI), "") - - require.ErrorContains(t, err, "failed to save config") - }) - }) - - t.Run("deletes template from config file on success", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceAPI), "") - - require.NoError(t, err) - }) - - t.Run("deletes template from config file on success ignoring optimistic concurrency", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceAPI), "b26e328af4bb9aaf") - - require.NoError(t, err) - }) - - t.Run("does not error when deleting templates that do not exist", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - err := sut.DeleteTemplate(context.Background(), 1, "does not exist", definitions.Provenance(models.ProvenanceAPI), "") - - require.NoError(t, err) - }) - - t.Run("succeeds when deleting from config file with no template section", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: defaultConfig, - }) - mockStore.EXPECT().SaveSucceeds() - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() - - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceAPI), "") - - require.NoError(t, err) - }) - - t.Run("errors if provenance is not right", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + require.ErrorIs(t, err, ErrVersionConflict) + prov.AssertExpectations(t) + }) + t.Run("rejects new template if version is set", func(t *testing.T) { + sut, store, _ := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + template := definitions.NotificationTemplate{ + Name: "template2", + Template: "asdf-new", + ResourceVersion: "version", + Provenance: definitions.Provenance(models.ProvenanceNone), + } + _, err := sut.SetTemplate(context.Background(), orgID, template) + require.ErrorIs(t, err, ErrTemplateNotFound) + }) + t.Run("propagates errors", func(t *testing.T) { + tmpl := definitions.NotificationTemplate{ + Name: templateName, + Template: "content", + Provenance: definitions.Provenance(models.ProvenanceNone), + } + t.Run("when unable to read config", func(t *testing.T) { + sut, store, _ := createTemplateServiceSut() expectedErr := errors.New("test") - sut.validator = func(from, to models.Provenance) error { - assert.Equal(t, models.ProvenanceAPI, from) - assert.Equal(t, models.ProvenanceNone, to) - return expectedErr + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return nil, expectedErr } - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceNone), "") - + _, err := sut.SetTemplate(context.Background(), orgID, tmpl) require.ErrorIs(t, err, expectedErr) }) - t.Run("errors if version is not right", func(t *testing.T) { - mockStore := &legacy_storage.MockAMConfigStore{} - sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) - mockStore.EXPECT(). - GetsConfig(models.AlertConfiguration{ - AlertmanagerConfiguration: configWithTemplates, - }) - sut.provenanceStore.(*MockProvisioningStore).EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + t.Run("when reading provenance status fails", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + expectedErr := errors.New("test") + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr) - err := sut.DeleteTemplate(context.Background(), 1, "a", definitions.Provenance(models.ProvenanceNone), "bad-version") + _, err := sut.SetTemplate(context.Background(), orgID, tmpl) - require.ErrorIs(t, err, ErrVersionConflict) + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when provenance fails to save", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + expectedErr := errors.New("test") + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + prov.EXPECT().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr) + + _, err := sut.SetTemplate(context.Background(), orgID, tmpl) + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when AM config fails to save", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + expectedErr := errors.New("test") + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { + return expectedErr + } + prov.EXPECT().SaveSucceeds() + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + + _, err := sut.SetTemplate(context.Background(), 1, tmpl) + require.ErrorIs(t, err, expectedErr) }) }) } -func createTemplateServiceSut(configStore alertmanagerConfigStore) *TemplateService { +func TestDeleteTemplate(t *testing.T) { + orgID := int64(1) + templateName := "template1" + templateContent := "test-1" + templateVersion := calculateTemplateFingerprint(templateContent) + amConfigToken := util.GenerateShortUID() + revision := func() *legacy_storage.ConfigRevision { + return &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ + TemplateFiles: map[string]string{ + templateName: templateContent, + }, + }, + ConcurrencyToken: amConfigToken, + } + } + + t.Run("deletes template from config file on success", func(t *testing.T) { + t.Run("when version matches", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceFile, nil) + prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64) { + assertInTransaction(t, ctx) + }).Return(nil) + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceFile), templateVersion) + + require.NoError(t, err) + + require.Len(t, store.Calls, 2) + + require.Equal(t, "Save", store.Calls[1].Method) + saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) + assert.Equal(t, amConfigToken, saved.ConcurrencyToken) + assert.NotContains(t, saved.Config.TemplateFiles, templateName) + + prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool { + return t.Name == templateName + }), orgID) + + prov.AssertExpectations(t) + }) + + t.Run("bypasses optimistic concurrency when version is empty", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceFile, nil) + prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Run(func(ctx context.Context, o models.Provisionable, org int64) { + assertInTransaction(t, ctx) + }).Return(nil) + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceFile), "") + + require.NoError(t, err) + require.Len(t, store.Calls, 2) + + require.Equal(t, "Save", store.Calls[1].Method) + saved := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) + assert.Equal(t, amConfigToken, saved.ConcurrencyToken) + assert.NotContains(t, saved.Config.TemplateFiles, templateName) + + prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool { + return t.Name == templateName + }), orgID) + + prov.AssertExpectations(t) + }) + }) + + t.Run("does not error when deleting templates that do not exist", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + + err := sut.DeleteTemplate(context.Background(), orgID, "not-found", definitions.Provenance(models.ProvenanceNone), "") + + require.NoError(t, err) + + prov.AssertExpectations(t) + }) + + t.Run("errors if provenance is not right", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + + expectedErr := errors.New("test") + sut.validator = func(from, to models.Provenance) error { + assert.Equal(t, models.ProvenanceAPI, from) + assert.Equal(t, models.ProvenanceNone, to) + return expectedErr + } + + err := sut.DeleteTemplate(context.Background(), 1, templateName, definitions.Provenance(models.ProvenanceNone), "") + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("errors if version is not right", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) + + err := sut.DeleteTemplate(context.Background(), 1, templateName, definitions.Provenance(models.ProvenanceNone), "bad-version") + + require.ErrorIs(t, err, ErrVersionConflict) + }) + + t.Run("propagates errors", func(t *testing.T) { + t.Run("when unable to read config", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + expectedErr := errors.New("test") + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return nil, expectedErr + } + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion) + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when reading provenance status fails", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + expectedErr := errors.New("test") + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, expectedErr) + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion) + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when provenance fails to save", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + expectedErr := errors.New("test") + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + prov.EXPECT().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedErr) + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion) + + require.ErrorIs(t, err, expectedErr) + + prov.AssertExpectations(t) + }) + + t.Run("when AM config fails to save", func(t *testing.T) { + sut, store, prov := createTemplateServiceSut() + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return revision(), nil + } + expectedErr := errors.New("test") + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { + return expectedErr + } + prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) + + err := sut.DeleteTemplate(context.Background(), orgID, templateName, definitions.Provenance(models.ProvenanceNone), templateVersion) + + require.ErrorIs(t, err, expectedErr) + }) + }) +} + +func createTemplateServiceSut() (*TemplateService, *legacy_storage.AlertmanagerConfigStoreFake, *MockProvisioningStore) { + store := &legacy_storage.AlertmanagerConfigStoreFake{} + provStore := &MockProvisioningStore{} return &TemplateService{ - configStore: configStore, - provenanceStore: &MockProvisioningStore{}, + configStore: store, + provenanceStore: provStore, xact: newNopTransactionManager(), log: log.NewNopLogger(), validator: validation.ValidateProvenanceRelaxed, - } + }, store, provStore } - -func createNotificationTemplate() definitions.NotificationTemplate { - return definitions.NotificationTemplate{ - Name: "test", - Template: "asdf", - } -} - -var defaultConfig = setting.GetAlertmanagerDefaultConfiguration() - -var configWithTemplates = ` -{ - "template_files": { - "a": "template" - }, - "alertmanager_config": { - "route": { - "receiver": "grafana-default-email" - }, - "receivers": [{ - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [{ - "uid": "", - "name": "email receiver", - "type": "email", - "settings": { - "addresses": "" - } - }] - }] - } -} -` - -var brokenConfig = ` - "alertmanager_config": { - "route": { - "receiver": "grafana-default-email" - }, - "receivers": [{ - "name": "grafana-default-email", - "grafana_managed_receiver_configs": [{ - "uid": "abc", - "name": "default-email", - "type": "email", - "settings": {} - }] - }] - } -}`