diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index bf811c43e8d..f86e28d6291 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -3,7 +3,7 @@ package service import ( "context" "encoding/json" - "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -11,6 +11,7 @@ import ( "time" "github.com/google/uuid" + "github.com/grafana/grafana-plugin-sdk-go/backend" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -37,8 +38,7 @@ import ( secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testsuite" - testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" - "github.com/grafana/grafana/pkg/util/errutil" + // testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" ) func TestMain(m *testing.M) { @@ -62,60 +62,183 @@ func (d *dataSourceMockRetriever) GetDataSource(ctx context.Context, query *data } func TestService_AddDataSource(t *testing.T) { - cfg := &setting.Cfg{} + t.Run("should not fail if the plugin is not installed", func(t *testing.T) { + dsService := initDSService(t) + dsService.pluginStore = &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{}, // empty list + } - t.Run("should return validation error if command validation failed", func(t *testing.T) { - dsplugin := &testdatasource.Service{} - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{{ - JSONData: plugins.JSONData{ - ID: "test", - Type: plugins.TypeDataSource, - Name: "test", - APIVersion: "v0alpha1", // When a value exists in plugin.json, the callback will be executed - }, - }}, - }, &pluginfakes.FakePluginClient{ - ValidateAdmissionFunc: dsplugin.ValidateAdmission, - MutateAdmissionFunc: dsplugin.MutateAdmission, - ConvertObjectFunc: dsplugin.ConvertObject, - }, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) + cmd := &datasources.AddDataSourceCommand{ + OrgID: 1, + Type: datasources.DS_TESTDATA, + Name: "test", + } + + ds, err := dsService.AddDataSource(context.Background(), cmd) require.NoError(t, err) + require.Equal(t, "test", ds.Name) + }) + + t.Run("should fail if the datasource name is too long", func(t *testing.T) { + dsService := initDSService(t) cmd := &datasources.AddDataSourceCommand{ OrgID: 1, Name: string(make([]byte, 256)), } - _, err = dsService.AddDataSource(context.Background(), cmd) + _, err := dsService.AddDataSource(context.Background(), cmd) require.EqualError(t, err, "[datasource.nameInvalid] max length is 190") + }) - cmd = &datasources.AddDataSourceCommand{ + t.Run("should fail if the datasource url is invalid", func(t *testing.T) { + dsService := initDSService(t) + + cmd := &datasources.AddDataSourceCommand{ OrgID: 1, URL: string(make([]byte, 256)), } - _, err = dsService.AddDataSource(context.Background(), cmd) + _, err := dsService.AddDataSource(context.Background(), cmd) require.EqualError(t, err, "[datasource.urlInvalid] max length is 255") + }) - cmd = &datasources.AddDataSourceCommand{ - OrgID: 1, - Type: "test", // required to validate apiserver - Name: "test", - APIVersion: "v123", // invalid apiVersion - } + t.Run("if a plugin has an API version defined (EXPERIMENTAL)", func(t *testing.T) { + t.Run("should success to run admission hooks", func(t *testing.T) { + dsService := initDSService(t) + validateExecuted := false + dsService.pluginStore = &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{{ + JSONData: plugins.JSONData{ + ID: "test", + Type: plugins.TypeDataSource, + Name: "test", + APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed + }, + }}, + } + dsService.pluginClient = &pluginfakes.FakePluginClient{ + ValidateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { + validateExecuted = true + return &backend.ValidationResponse{ + Allowed: true, + }, nil + }, + MutateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { + return &backend.MutationResponse{ + Allowed: true, + ObjectBytes: req.ObjectBytes, + }, nil + }, + ConvertObjectFunc: func(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + } + cmd := &datasources.AddDataSourceCommand{ + OrgID: 1, + Type: "test", // required to validate apiserver + Name: "test", + APIVersion: "v0alpha1", + } + _, err := dsService.AddDataSource(context.Background(), cmd) + require.NoError(t, err) + require.True(t, validateExecuted) + }) - _, err = dsService.AddDataSource(context.Background(), cmd) + t.Run("should fail at validation", func(t *testing.T) { + dsService := initDSService(t) + dsService.pluginStore = &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{{ + JSONData: plugins.JSONData{ + ID: "test", + Type: plugins.TypeDataSource, + Name: "test", + APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed + }, + }}, + } + dsService.pluginClient = &pluginfakes.FakePluginClient{ + ValidateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { + settings, err := backend.DataSourceInstanceSettingsFromProto(req.ObjectBytes, "") + if err != nil { + return nil, err + } + if settings.APIVersion != "v0alpha1" { + return &backend.ValidationResponse{ + Allowed: false, + Result: &backend.StatusResult{ + Status: "Failure", + Message: fmt.Sprintf("expected apiVersion: v0alpha1, found: %s", settings.APIVersion), + Reason: "badRequest", + Code: http.StatusBadRequest, + }, + }, nil + } + return &backend.ValidationResponse{ + Allowed: true, + }, nil + }, + MutateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + ConvertObjectFunc: func(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + } + cmd := &datasources.AddDataSourceCommand{ + OrgID: 1, + Type: "test", // required to validate apiserver + Name: "test", + APIVersion: "v123", // invalid apiVersion + } + _, err := dsService.AddDataSource(context.Background(), cmd) + assert.ErrorContains(t, err, "expected apiVersion: v0alpha1, found: v123") + }) - var gErr errutil.Error - require.True(t, errors.As(err, &gErr)) - require.Equal(t, "expected apiVersion: v0alpha1, found: v123", gErr.PublicMessage) - require.Equal(t, 400, gErr.Public().StatusCode) + t.Run("should mutate a request", func(t *testing.T) { + dsService := initDSService(t) + dsService.pluginStore = &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{{ + JSONData: plugins.JSONData{ + ID: "test", + Type: plugins.TypeDataSource, + Name: "test", + APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed + }, + }}, + } + dsService.pluginClient = &pluginfakes.FakePluginClient{ + ValidateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { + return &backend.ValidationResponse{ + Allowed: true, + }, nil + }, + MutateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { + settings, err := backend.DataSourceInstanceSettingsFromProto(req.ObjectBytes, "") + if err != nil { + return nil, err + } + settings.URL = "url-mutated" + pb, err := backend.DataSourceInstanceSettingsToProtoBytes(settings) + return &backend.MutationResponse{ + Allowed: true, + ObjectBytes: pb, + }, err + }, + ConvertObjectFunc: func(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + } + cmd := &datasources.AddDataSourceCommand{ + OrgID: 1, + Type: "test", // required to validate apiserver + Name: "test", + APIVersion: "v0alpha1", + } + ds, err := dsService.AddDataSource(context.Background(), cmd) + require.NoError(t, err) + require.Equal(t, "url-mutated", ds.URL) + }) }) } @@ -199,16 +322,8 @@ func TestService_getAvailableName(t *testing.T) { } func TestService_UpdateDataSource(t *testing.T) { - cfg := &setting.Cfg{} - t.Run("should return not found error if datasource not found", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) - require.NoError(t, err) + dsService := initDSService(t) cmd := &datasources.UpdateDataSourceCommand{ UID: uuid.New().String(), @@ -216,29 +331,12 @@ func TestService_UpdateDataSource(t *testing.T) { OrgID: 1, } - _, err = dsService.UpdateDataSource(context.Background(), cmd) + _, err := dsService.UpdateDataSource(context.Background(), cmd) require.ErrorIs(t, err, datasources.ErrDataSourceNotFound) }) t.Run("should return validation error if command validation failed", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, - &pluginstore.FakePluginStore{ - PluginList: []pluginstore.Plugin{ - { - JSONData: plugins.JSONData{ - ID: "test", - }, - }, - }, - }, &pluginfakes.FakePluginClient{}, - plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) - require.NoError(t, err) + dsService := initDSService(t) // First add the datasource ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ @@ -271,15 +369,7 @@ func TestService_UpdateDataSource(t *testing.T) { }) t.Run("should return no error if updated datasource", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) - require.NoError(t, err) - - mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + dsService := initDSService(t) ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ OrgID: 1, @@ -298,15 +388,7 @@ func TestService_UpdateDataSource(t *testing.T) { }) t.Run("should return error if datasource with same name exist", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) - require.NoError(t, err) - - mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + dsService := initDSService(t) dsToUpdate, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ OrgID: 1, @@ -331,15 +413,7 @@ func TestService_UpdateDataSource(t *testing.T) { }) t.Run("should merge cmd.SecureJsonData with db data", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) - require.NoError(t, err) - - mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + dsService := initDSService(t) expectedDbKey := "db-secure-key" expectedDbValue := "db-secure-value" @@ -375,16 +449,7 @@ func TestService_UpdateDataSource(t *testing.T) { }) t.Run("should preserve cmd.SecureJsonData when cmd.IgnoreOldSecureJsonData=true", func(t *testing.T) { - sqlStore := db.InitTestDB(t) - secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) - secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) - quotaService := quotatest.New(false, nil) - mockPermission := acmock.NewMockedPermissionsService() - dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, - mockPermission, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil) - require.NoError(t, err) - - mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + dsService := initDSService(t) notExpectedDbKey := "db-secure-key" dbValue := "db-secure-value" @@ -420,6 +485,61 @@ func TestService_UpdateDataSource(t *testing.T) { _, ok := secret[notExpectedDbKey] assert.False(t, ok) }) + + t.Run("should run validation and mutation hooks", func(t *testing.T) { + dsService := initDSService(t) + dsService.pluginStore = &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{{ + JSONData: plugins.JSONData{ + ID: "test", + Type: plugins.TypeDataSource, + Name: "test", + APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed + }, + }}, + } + validateExecuted := false + mutateExecuted := false + dsService.pluginClient = &pluginfakes.FakePluginClient{ + ValidateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { + validateExecuted = true + return &backend.ValidationResponse{ + Allowed: true, + }, nil + }, + MutateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { + mutateExecuted = true + return &backend.MutationResponse{ + Allowed: true, + ObjectBytes: req.ObjectBytes, + }, nil + }, + ConvertObjectFunc: func(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + } + ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 1, + Name: "test-datasource", + APIVersion: "v0alpha1", + Type: "test", + }) + require.NoError(t, err) + + cmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + Name: "test-datasource-updated", + APIVersion: "v0alpha1", + Type: "test", + } + + dsUpdated, err := dsService.UpdateDataSource(context.Background(), cmd) + require.NoError(t, err) + require.True(t, validateExecuted) + require.True(t, mutateExecuted) + require.Equal(t, "test-datasource-updated", dsUpdated.Name) + }) } func TestService_NameScopeResolver(t *testing.T) { @@ -1291,6 +1411,43 @@ func TestDataSource_CustomHeaders(t *testing.T) { } } +func initDSService(t *testing.T) *Service { + cfg := &setting.Cfg{} + sqlStore := db.InitTestDB(t) + secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) + quotaService := quotatest.New(false, nil) + mockPermission := acmock.NewMockedPermissionsService() + mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService, &pluginstore.FakePluginStore{ + PluginList: []pluginstore.Plugin{{ + JSONData: plugins.JSONData{ + ID: "test", + Type: plugins.TypeDataSource, + Name: "test", + }, + }}, + }, &pluginfakes.FakePluginClient{ + ValidateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) { + return &backend.ValidationResponse{ + Allowed: true, + }, nil + }, + MutateAdmissionFunc: func(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) { + return &backend.MutationResponse{ + Allowed: true, + ObjectBytes: req.ObjectBytes, + }, nil + }, + ConvertObjectFunc: func(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) { + return nil, fmt.Errorf("not implemented") + }, + }, plugincontext.ProvideBaseService(cfg, pluginconfig.NewFakePluginRequestConfigProvider())) + require.NoError(t, err) + + return dsService +} + const caCert string = `-----BEGIN CERTIFICATE----- MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda