2022-03-07 09:33:01 -09:00
package api
import (
2022-07-13 09:27:03 -04:00
"bytes"
2022-03-07 09:33:01 -09:00
"context"
2022-07-13 09:27:03 -04:00
"encoding/json"
2022-09-14 17:19:57 +01:00
"errors"
2022-03-07 09:33:01 -09:00
"fmt"
2022-09-14 17:19:57 +01:00
"io"
2022-03-07 09:33:01 -09:00
"net/http"
"strings"
"testing"
2022-05-17 14:52:22 -04:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2022-06-27 12:23:15 -04:00
"github.com/stretchr/testify/require"
2023-06-08 13:59:51 +02:00
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/localcache"
2022-09-14 17:19:57 +01:00
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
2022-11-02 14:51:51 +01:00
"github.com/grafana/grafana/pkg/plugins/config"
2022-09-14 17:19:57 +01:00
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
2023-07-17 12:21:44 +02:00
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
2022-09-14 17:19:57 +01:00
"github.com/grafana/grafana/pkg/plugins/manager/registry"
2023-05-23 15:29:20 +01:00
"github.com/grafana/grafana/pkg/services/datasources"
2022-09-14 17:19:57 +01:00
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
2022-06-13 17:23:56 -06:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2023-06-08 13:59:51 +02:00
"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"
2022-09-14 17:19:57 +01:00
"github.com/grafana/grafana/pkg/services/query"
2022-07-15 18:06:44 +02:00
"github.com/grafana/grafana/pkg/services/quota/quotatest"
2023-06-08 13:59:51 +02:00
secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes"
2022-08-10 11:56:48 +02:00
"github.com/grafana/grafana/pkg/services/user"
2022-10-21 13:54:55 +02:00
"github.com/grafana/grafana/pkg/setting"
2022-09-14 17:19:57 +01:00
"github.com/grafana/grafana/pkg/util/errutil"
2022-06-13 17:23:56 -06:00
"github.com/grafana/grafana/pkg/web/webtest"
2022-03-07 09:33:01 -09:00
)
type fakePluginRequestValidator struct {
err error
}
2022-07-13 09:27:03 -04:00
type secretsErrorResponseBody struct {
Error string `json:"error"`
Message string `json:"message"`
}
2022-03-07 09:33:01 -09:00
func ( rv * fakePluginRequestValidator ) Validate ( dsURL string , req * http . Request ) error {
return rv . err
}
2022-05-03 18:02:20 +02:00
// `/ds/query` endpoint test
func TestAPIEndpoint_Metrics_QueryMetricsV2 ( t * testing . T ) {
qds := query . ProvideService (
2022-10-21 13:54:55 +02:00
setting . NewCfg (),
2022-05-03 18:02:20 +02:00
nil ,
nil ,
& fakePluginRequestValidator {},
& fakePluginClient {
QueryDataHandlerFunc : func ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
resp := backend . Responses {
"A" : backend . DataResponse {
2022-12-01 22:51:12 +03:00
Error : errors . New ( "query failed" ),
2022-05-03 18:02:20 +02:00
},
}
return & backend . QueryDataResponse { Responses : resp }, nil
},
},
2023-07-17 12:21:44 +02:00
plugincontext . ProvideService ( localcache . ProvideService (), & fakes . FakePluginStore {
2023-06-08 13:59:51 +02:00
PluginList : [] plugins . PluginDTO {
{
JSONData : plugins . JSONData {
ID : "grafana" ,
},
},
},
}, & fakeDatasources . FakeDataSourceService {}, pluginSettings . ProvideService ( dbtest . NewFakeDB (),
secretstest . NewFakeSecretsService ()),
),
2022-05-03 18:02:20 +02:00
)
serverFeatureEnabled := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
hs . Features = featuremgmt . WithFeatures ( featuremgmt . FlagDatasourceQueryMultiStatus , true )
2022-11-14 21:08:10 +02:00
hs . QuotaService = quotatest . New ( false , nil )
2022-05-03 18:02:20 +02:00
})
serverFeatureDisabled := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
hs . Features = featuremgmt . WithFeatures ( featuremgmt . FlagDatasourceQueryMultiStatus , false )
2022-11-14 21:08:10 +02:00
hs . QuotaService = quotatest . New ( false , nil )
2022-05-03 18:02:20 +02:00
})
t . Run ( "Status code is 400 when data source response has an error and feature toggle is disabled" , func ( t * testing . T ) {
2022-09-14 17:19:57 +01:00
req := serverFeatureDisabled . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ))
2023-05-23 15:29:20 +01:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , Permissions : map [ int64 ] map [ string ][] string { 1 : { datasources . ActionQuery : [] string { datasources . ScopeAll }}}})
2022-05-03 18:02:20 +02:00
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 ) {
2022-09-14 17:19:57 +01:00
req := serverFeatureEnabled . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ))
2023-05-23 15:29:20 +01:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , Permissions : map [ int64 ] map [ string ][] string { 1 : { datasources . ActionQuery : [] string { datasources . ScopeAll }}}})
2022-05-03 18:02:20 +02:00
resp , err := serverFeatureEnabled . SendJSON ( req )
require . NoError ( t , err )
require . NoError ( t , resp . Body . Close ())
require . Equal ( t , http . StatusMultiStatus , resp . StatusCode )
})
}
2022-07-13 09:27:03 -04:00
func TestAPIEndpoint_Metrics_PluginDecryptionFailure ( t * testing . T ) {
2023-06-08 13:59:51 +02:00
ds := & fakeDatasources . FakeDataSourceService { SimulatePluginFailure : true }
db := & dbtest . FakeDB { ExpectedError : pluginsettings . ErrPluginSettingNotFound }
pcp := plugincontext . ProvideService ( localcache . ProvideService (),
2023-07-17 12:21:44 +02:00
& fakes . FakePluginStore {
2023-06-08 13:59:51 +02:00
PluginList : [] plugins . PluginDTO {
{
JSONData : plugins . JSONData {
ID : "grafana" ,
},
},
},
},
ds , pluginSettings . ProvideService ( db , secretstest . NewFakeSecretsService ()),
)
2022-07-13 09:27:03 -04:00
qds := query . ProvideService (
2022-10-21 13:54:55 +02:00
setting . NewCfg (),
2022-07-13 09:27:03 -04:00
nil ,
nil ,
& fakePluginRequestValidator {},
& fakePluginClient {
QueryDataHandlerFunc : func ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
resp := backend . Responses {
"A" : backend . DataResponse {
2022-12-01 22:51:12 +03:00
Error : errors . New ( "query failed" ),
2022-07-13 09:27:03 -04:00
},
}
return & backend . QueryDataResponse { Responses : resp }, nil
},
},
2023-06-08 13:59:51 +02:00
pcp ,
2022-07-13 09:27:03 -04:00
)
httpServer := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
2022-11-14 21:08:10 +02:00
hs . QuotaService = quotatest . New ( false , nil )
2023-06-08 13:59:51 +02:00
hs . pluginContextProvider = pcp
2022-07-13 09:27:03 -04:00
})
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 ) {
2022-09-14 17:19:57 +01:00
req := httpServer . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ))
2023-05-23 15:29:20 +01:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , Permissions : map [ int64 ] map [ string ][] string { 1 : { datasources . ActionQuery : [] string { datasources . ScopeAll }}}})
2022-07-13 09:27:03 -04:00
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 , "unknown error" , resObj . Error )
require . Contains ( t , resObj . Message , "Secrets Plugin error:" )
})
}
2022-09-14 17:19:57 +01:00
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 : backendplugin . ErrPluginUnavailable ,
expectedStatus : http . StatusInternalServerError ,
expectedBody : `{"message":"Internal server error","messageId":"plugin.unavailable","statusCode":500,"traceID":""}` ,
},
{
request : reqValid ,
clientErr : backendplugin . ErrMethodNotImplemented ,
expectedStatus : http . StatusNotImplemented ,
expectedBody : `{"message":"Not implemented","messageId":"plugin.notImplemented","statusCode":501,"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 : `{"error":"data source not found","message":"Data source not found","traceID":""}` ,
},
{
request : reqDatasourceByIdNotFound ,
expectedStatus : http . StatusNotFound ,
expectedBody : `{"error":"data source not found","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 ) {
r := registry . NewInMemory ()
err := r . Add ( context . Background (), p )
require . NoError ( t , err )
2023-06-08 13:59:51 +02:00
ds := & fakeDatasources . FakeDataSourceService {}
2022-09-14 17:19:57 +01:00
hs . queryDataService = query . ProvideService (
2022-10-21 13:54:55 +02:00
setting . NewCfg (),
2022-09-14 17:19:57 +01:00
& fakeDatasources . FakeCacheService {},
nil ,
& fakePluginRequestValidator {},
2022-11-02 14:51:51 +01:00
pluginClient . ProvideService ( r , & config . Cfg {}),
2023-07-17 12:21:44 +02:00
plugincontext . ProvideService ( localcache . ProvideService (), & fakes . FakePluginStore {
2023-06-08 13:59:51 +02:00
PluginList : [] plugins . PluginDTO { p . ToDTO ()},
},
ds , pluginSettings . ProvideService ( dbtest . NewFakeDB (),
secretstest . NewFakeSecretsService ()),
),
2022-09-14 17:19:57 +01:00
)
2022-11-14 21:08:10 +02:00
hs . QuotaService = quotatest . New ( false , nil )
2022-09-14 17:19:57 +01:00
})
req := srv . NewPostRequest ( "/api/ds/query" , strings . NewReader ( tc . request ))
2023-05-23 15:29:20 +01:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , Permissions : map [ int64 ] map [ string ][] string { 1 : { datasources . ActionQuery : [] string { datasources . ScopeAll }}}})
2022-09-14 17:19:57 +01:00
resp , err := srv . SendJSON ( req )
require . NoError ( t , err )
require . Equal ( t , tc . expectedStatus , resp . StatusCode )
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
}