mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
e4d1fdc3d0
When running in dev mode, error messages would contain an additional "error" property alongside "message". Since this causes confusion, that has been removed and now error messages are the same both modes (using "message").
338 lines
11 KiB
Go
338 lines
11 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
"github.com/grafana/grafana/pkg/plugins/config"
|
|
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
|
|
pluginFakes "github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
|
|
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
|
"github.com/grafana/grafana/pkg/services/query"
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
|
"github.com/grafana/grafana/pkg/web/webtest"
|
|
)
|
|
|
|
type fakePluginRequestValidator struct {
|
|
err error
|
|
}
|
|
|
|
type secretsErrorResponseBody struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
|
|
return rv.err
|
|
}
|
|
|
|
// `/ds/query` endpoint test
|
|
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
|
cfg := setting.NewCfg()
|
|
qds := query.ProvideService(
|
|
cfg,
|
|
nil,
|
|
nil,
|
|
&fakePluginRequestValidator{},
|
|
&fakePluginClient{
|
|
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
resp := backend.Responses{
|
|
"A": backend.DataResponse{
|
|
Error: errors.New("query failed"),
|
|
},
|
|
}
|
|
return &backend.QueryDataResponse{Responses: resp}, nil
|
|
},
|
|
},
|
|
plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "grafana",
|
|
},
|
|
},
|
|
},
|
|
}, &fakeDatasources.FakeDataSourceService{}, pluginSettings.ProvideService(dbtest.NewFakeDB(),
|
|
secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}),
|
|
)
|
|
serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) {
|
|
hs.queryDataService = qds
|
|
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true)
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
})
|
|
serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) {
|
|
hs.queryDataService = qds
|
|
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false)
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
})
|
|
|
|
t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) {
|
|
req := serverFeatureDisabled.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := serverFeatureDisabled.SendJSON(req)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Status code is 207 when data source response has an error and feature toggle is enabled", func(t *testing.T) {
|
|
req := serverFeatureEnabled.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := serverFeatureEnabled.SendJSON(req)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.Equal(t, http.StatusMultiStatus, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
|
cfg := setting.NewCfg()
|
|
ds := &fakeDatasources.FakeDataSourceService{SimulatePluginFailure: true}
|
|
db := &dbtest.FakeDB{ExpectedError: pluginsettings.ErrPluginSettingNotFound}
|
|
pcp := plugincontext.ProvideService(cfg, localcache.ProvideService(),
|
|
&pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "grafana",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ds, pluginSettings.ProvideService(db, secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{},
|
|
)
|
|
qds := query.ProvideService(
|
|
cfg,
|
|
nil,
|
|
nil,
|
|
&fakePluginRequestValidator{},
|
|
&fakePluginClient{
|
|
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
resp := backend.Responses{
|
|
"A": backend.DataResponse{
|
|
Error: errors.New("query failed"),
|
|
},
|
|
}
|
|
return &backend.QueryDataResponse{Responses: resp}, nil
|
|
},
|
|
},
|
|
pcp,
|
|
)
|
|
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
|
|
hs.queryDataService = qds
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
hs.pluginContextProvider = pcp
|
|
})
|
|
|
|
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {
|
|
req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := httpServer.SendJSON(req)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
|
buf := new(bytes.Buffer)
|
|
_, err = buf.ReadFrom(resp.Body)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
var resObj secretsErrorResponseBody
|
|
err = json.Unmarshal(buf.Bytes(), &resObj)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "", resObj.Error)
|
|
require.Contains(t, resObj.Message, "Secrets Plugin error:")
|
|
})
|
|
}
|
|
|
|
var reqValid = `{
|
|
"from": "",
|
|
"to": "",
|
|
"queries": [
|
|
{
|
|
"datasource": {
|
|
"type": "datasource",
|
|
"uid": "grafana"
|
|
},
|
|
"queryType": "randomWalk",
|
|
"refId": "A"
|
|
}
|
|
]
|
|
}`
|
|
|
|
var reqNoQueries = `{
|
|
"from": "",
|
|
"to": "",
|
|
"queries": []
|
|
}`
|
|
|
|
var reqQueryWithInvalidDatasourceID = `{
|
|
"from": "",
|
|
"to": "",
|
|
"queries": [
|
|
{
|
|
"queryType": "randomWalk",
|
|
"refId": "A"
|
|
}
|
|
]
|
|
}`
|
|
|
|
var reqDatasourceByUidNotFound = `{
|
|
"from": "",
|
|
"to": "",
|
|
"queries": [
|
|
{
|
|
"datasource": {
|
|
"type": "datasource",
|
|
"uid": "not-found"
|
|
},
|
|
"queryType": "randomWalk",
|
|
"refId": "A"
|
|
}
|
|
]
|
|
}`
|
|
|
|
var reqDatasourceByIdNotFound = `{
|
|
"from": "",
|
|
"to": "",
|
|
"queries": [
|
|
{
|
|
"datasourceId": 1,
|
|
"queryType": "randomWalk",
|
|
"refId": "A"
|
|
}
|
|
]
|
|
}`
|
|
|
|
func TestDataSourceQueryError(t *testing.T) {
|
|
tcs := []struct {
|
|
request string
|
|
clientErr error
|
|
expectedStatus int
|
|
expectedBody string
|
|
}{
|
|
{
|
|
request: reqValid,
|
|
clientErr: plugins.ErrPluginUnavailable,
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedBody: `{"message":"Plugin unavailable","messageId":"plugin.unavailable","statusCode":500,"traceID":""}`,
|
|
},
|
|
{
|
|
request: reqValid,
|
|
clientErr: plugins.ErrMethodNotImplemented,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: `{"message":"Method not implemented","messageId":"plugin.notImplemented","statusCode":404,"traceID":""}`,
|
|
},
|
|
{
|
|
request: reqValid,
|
|
clientErr: errors.New("surprise surprise"),
|
|
expectedStatus: errutil.StatusInternal.HTTPStatus(),
|
|
expectedBody: `{"message":"An error occurred within the plugin","messageId":"plugin.downstreamError","statusCode":500,"traceID":""}`,
|
|
},
|
|
{
|
|
request: reqNoQueries,
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedBody: `{"message":"No queries found","messageId":"query.noQueries","statusCode":400,"traceID":""}`,
|
|
},
|
|
{
|
|
request: reqQueryWithInvalidDatasourceID,
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedBody: `{"message":"Query does not contain a valid data source identifier","messageId":"query.invalidDatasourceId","statusCode":400,"traceID":""}`,
|
|
},
|
|
{
|
|
request: reqDatasourceByUidNotFound,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: `{"message":"Data source not found","traceID":""}`,
|
|
},
|
|
{
|
|
request: reqDatasourceByIdNotFound,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: `{"message":"Data source not found","traceID":""}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcs {
|
|
t.Run(fmt.Sprintf("Plugin client error %q should propagate to API", tc.clientErr), func(t *testing.T) {
|
|
p := &plugins.Plugin{
|
|
JSONData: plugins.JSONData{
|
|
ID: "grafana",
|
|
},
|
|
}
|
|
p.RegisterClient(&fakePluginBackend{
|
|
qdr: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
return nil, tc.clientErr
|
|
},
|
|
})
|
|
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
|
|
cfg := setting.NewCfg()
|
|
r := registry.NewInMemory()
|
|
err := r.Add(context.Background(), p)
|
|
require.NoError(t, err)
|
|
ds := &fakeDatasources.FakeDataSourceService{}
|
|
hs.queryDataService = query.ProvideService(
|
|
cfg,
|
|
&fakeDatasources.FakeCacheService{},
|
|
nil,
|
|
&fakePluginRequestValidator{},
|
|
pluginClient.ProvideService(r, &config.Cfg{}),
|
|
plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)},
|
|
},
|
|
ds, pluginSettings.ProvideService(dbtest.NewFakeDB(),
|
|
secretstest.NewFakeSecretsService()), pluginFakes.NewFakeLicensingService(), &config.Cfg{}),
|
|
)
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
})
|
|
req := srv.NewPostRequest("/api/ds/query", strings.NewReader(tc.request))
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := srv.SendJSON(req)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.expectedStatus, resp.StatusCode)
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expectedBody, string(body))
|
|
require.NoError(t, resp.Body.Close())
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakePluginBackend struct {
|
|
qdr backend.QueryDataHandlerFunc
|
|
|
|
backendplugin.Plugin
|
|
}
|
|
|
|
func (f *fakePluginBackend) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
if f.qdr != nil {
|
|
return f.qdr(ctx, req)
|
|
}
|
|
return backend.NewQueryDataResponse(), nil
|
|
}
|
|
|
|
func (f *fakePluginBackend) IsDecommissioned() bool {
|
|
return false
|
|
}
|