The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/ngalert/provisioning/templates_test.go

1285 lines
46 KiB

package provisioning
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"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/util"
)
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",
},
},
}
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)
expected := []definitions.NotificationTemplate{
{
UID: legacy_storage.NameToUid("template1"),
Name: "template1",
Template: "test1",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: calculateTemplateFingerprint("test1"),
},
{
UID: legacy_storage.NameToUid("template2"),
Name: "template2",
Template: "test2",
Provenance: definitions.Provenance(models.ProvenanceFile),
ResourceVersion: calculateTemplateFingerprint("test2"),
},
{
UID: legacy_storage.NameToUid("template3"),
Name: "template3",
Template: "test3",
Provenance: definitions.Provenance(models.ProvenanceNone),
ResourceVersion: calculateTemplateFingerprint("test3"),
},
}
require.EqualValues(t, expected, result)
prov.AssertCalled(t, "GetProvenances", mock.Anything, orgID, (&definitions.NotificationTemplate{}).ResourceType())
prov.AssertExpectations(t)
})
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{
UID: legacy_storage.NameToUid(templateName),
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) {
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.GetTemplate(context.Background(), 1, templateName)
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, 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.GetTemplate(context.Background(), orgID, templateName)
require.ErrorIs(t, err, expectedErr)
prov.AssertExpectations(t)
})
})
}
func TestUpsertTemplate(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,
}
}
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)
tmpl := definitions.NotificationTemplate{
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: "",
}
result, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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.UpsertTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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("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.UpsertTemplate(context.Background(), orgID, tmpl)
expectedContent := fmt.Sprintf("{{ define \"%s\" }}\n content\n{{ end }}", templateName)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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.UpsertTemplate(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.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
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
}
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
}
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
}
template.Provenance = definitions.Provenance(models.ProvenanceNone)
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, expectedErr)
})
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)
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
ResourceVersion: "bad-version",
Provenance: definitions.Provenance(models.ProvenanceNone),
}
_, err := sut.UpsertTemplate(context.Background(), orgID, template)
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.UpsertTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrTemplateNotFound)
})
t.Run("rejects new template has UID ", 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{
UID: "new-template",
Name: "template2",
Template: "asdf-new",
Provenance: definitions.Provenance(models.ProvenanceNone),
}
_, err := sut.UpsertTemplate(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")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return nil, expectedErr
}
_, err := sut.UpsertTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
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.UpsertTemplate(context.Background(), orgID, tmpl)
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.UpsertTemplate(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.UpsertTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})
}
func TestCreateTemplate(t *testing.T) {
orgID := int64(1)
amConfigToken := util.GenerateShortUID()
tmpl := definitions.NotificationTemplate{
Name: "new-template",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
}
revision := func() *legacy_storage.ConfigRevision {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{},
ConcurrencyToken: amConfigToken,
}
}
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 revision(), 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)
result, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
require.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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("returns ErrTemplateExists if template exists", func(t *testing.T) {
sut, store, _ := 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{
TemplateFiles: map[string]string{
tmpl.Name: "test",
},
},
ConcurrencyToken: amConfigToken,
}, nil
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateExists)
})
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.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return nil, expectedErr
}
_, err := sut.CreateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
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().SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedErr)
_, err := sut.CreateTemplate(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.CreateTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})
}
func TestUpdateTemplate(t *testing.T) {
orgID := int64(1)
currentTemplateContent := "test1"
tmpl := definitions.NotificationTemplate{
Name: "template1",
Template: "{{ define \"test\"}} test {{ end }}",
Provenance: definitions.Provenance(models.ProvenanceAPI),
ResourceVersion: "",
}
amConfigToken := util.GenerateShortUID()
revision := func() *legacy_storage.ConfigRevision {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
tmpl.Name: currentTemplateContent,
},
},
ConcurrencyToken: amConfigToken,
}
}
t.Run("returns ErrTemplateNotFound if template name 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 &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{},
ConcurrencyToken: amConfigToken,
}, nil
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateNotFound)
require.Len(t, store.Calls, 1)
prov.AssertExpectations(t)
})
t.Run("returns ErrTemplateNotFound if template UID 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 &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
"not-found": "test", // create a template with name that matches UID to make sure we do not search by name
tmpl.Name: "test",
},
},
ConcurrencyToken: amConfigToken,
}, nil
}
tmpl := tmpl
tmpl.UID = "not-found"
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateNotFound)
require.Len(t, store.Calls, 1)
prov.AssertExpectations(t)
})
testcases := []struct {
name string
templateUid string
}{
{
name: "by name",
templateUid: "",
},
{
name: "by uid",
templateUid: legacy_storage.NameToUid(tmpl.UID),
},
}
for _, tt := range testcases {
t.Run(fmt.Sprintf("updates current template %s", tt.name), 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.UID = tt.templateUid
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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)
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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("creates a new template and delete old one when template is renamed", 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().DeleteProvenance(mock.Anything, mock.Anything, mock.Anything).Return(nil).Run(func(ctx context.Context, o models.Provisionable, org int64) {
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)
oldName := tmpl.Name
tmpl := tmpl
tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name is different
result, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.NoError(t, err)
assert.Equal(t, definitions.NotificationTemplate{
UID: legacy_storage.NameToUid(tmpl.Name),
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])
assert.NotContains(t, saved.Config.TemplateFiles, oldName)
prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
return t.Name == oldName
}), mock.Anything)
prov.AssertExpectations(t)
})
t.Run("rejects rename operation if template with the new name exists", func(t *testing.T) {
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, org int64) (*legacy_storage.ConfigRevision, error) {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
tmpl.Name: currentTemplateContent,
"new-template-name": "test",
},
},
ConcurrencyToken: amConfigToken,
}, nil
}
tmpl := tmpl
tmpl.UID = legacy_storage.NameToUid(tmpl.Name) // UID matches the current template
tmpl.Name = "new-template-name" // but name matches another existing template
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateExists)
prov.AssertExpectations(t)
})
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.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
t.Run("invalid content", func(t *testing.T) {
tmpl := definitions.NotificationTemplate{
Name: "",
Template: "{{ .MyField }",
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, ErrTemplateInvalid)
})
require.Empty(t, store.Calls)
prov.AssertExpectations(t)
})
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
}
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
}
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
}
template.Provenance = definitions.Provenance(models.ProvenanceNone)
_, err := sut.UpdateTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, expectedErr)
})
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)
template := definitions.NotificationTemplate{
Name: "template1",
Template: "asdf-new",
ResourceVersion: "bad-version",
Provenance: definitions.Provenance(models.ProvenanceNone),
}
_, err := sut.UpdateTemplate(context.Background(), orgID, template)
require.ErrorIs(t, err, ErrVersionConflict)
prov.AssertExpectations(t)
})
t.Run("propagates errors", func(t *testing.T) {
t.Run("when unable to read config", func(t *testing.T) {
sut, store, _ := createTemplateServiceSut()
expectedErr := errors.New("test")
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return nil, expectedErr
}
_, err := sut.UpdateTemplate(context.Background(), orgID, tmpl)
require.ErrorIs(t, err, expectedErr)
})
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.UpdateTemplate(context.Background(), orgID, tmpl)
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.UpdateTemplate(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.UpdateTemplate(context.Background(), 1, tmpl)
require.ErrorIs(t, err, expectedErr)
})
})
}
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,
}
}
testCase := []struct {
name string
templateNameOrUid string
}{
{
name: "by name",
templateNameOrUid: templateName,
},
{
name: "by uid",
templateNameOrUid: legacy_storage.NameToUid(templateName),
},
}
for _, tt := range testCase {
t.Run(fmt.Sprintf("deletes template from config file %s", tt.name), 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, tt.templateNameOrUid, 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, tt.templateNameOrUid, 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("should look by name before uid", func(t *testing.T) {
expectedToDelete := legacy_storage.NameToUid(templateName)
sut, store, prov := createTemplateServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &legacy_storage.ConfigRevision{
Config: &definitions.PostableUserConfig{
TemplateFiles: map[string]string{
templateName: templateContent,
expectedToDelete: templateContent,
},
},
ConcurrencyToken: amConfigToken,
}, 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, expectedToDelete, 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, expectedToDelete)
assert.Contains(t, saved.Config.TemplateFiles, templateName)
prov.AssertCalled(t, "DeleteProvenance", mock.Anything, mock.MatchedBy(func(t *definitions.NotificationTemplate) bool {
return t.Name == expectedToDelete
}), 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: store,
provenanceStore: provStore,
xact: newNopTransactionManager(),
log: log.NewNopLogger(),
validator: validation.ValidateProvenanceRelaxed,
}, store, provStore
}