2022-03-07 12:33:01 -06:00
package api
import (
2022-07-13 08:27:03 -05:00
"bytes"
2022-03-07 12:33:01 -06:00
"context"
2022-07-13 08:27:03 -05:00
"encoding/json"
2022-09-14 11:19:57 -05:00
"errors"
2022-03-07 12:33:01 -06:00
"fmt"
2022-09-14 11:19:57 -05:00
"io"
2022-03-07 12:33:01 -06:00
"net/http"
"strings"
"testing"
2022-05-17 13:52:22 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2022-06-27 11:23:15 -05:00
"github.com/stretchr/testify/require"
2022-09-14 11:19:57 -05:00
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
2022-11-02 08:51:51 -05:00
"github.com/grafana/grafana/pkg/plugins/config"
2022-09-14 11:19:57 -05:00
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
2022-06-13 18:23:56 -05:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-08-10 04:56:48 -05:00
"github.com/grafana/grafana/pkg/services/org"
2022-09-14 11:19:57 -05:00
"github.com/grafana/grafana/pkg/services/query"
2022-07-15 11:06:44 -05:00
"github.com/grafana/grafana/pkg/services/quota/quotatest"
2022-08-10 04:56:48 -05:00
"github.com/grafana/grafana/pkg/services/user"
2022-10-21 06:54:55 -05:00
"github.com/grafana/grafana/pkg/setting"
2022-09-14 11:19:57 -05:00
"github.com/grafana/grafana/pkg/util/errutil"
2022-06-13 18:23:56 -05:00
"github.com/grafana/grafana/pkg/web/webtest"
2022-03-07 12:33:01 -06:00
)
type fakePluginRequestValidator struct {
err error
}
2022-07-13 08:27:03 -05:00
type secretsErrorResponseBody struct {
Error string ` json:"error" `
Message string ` json:"message" `
}
2022-03-07 12:33:01 -06:00
func ( rv * fakePluginRequestValidator ) Validate ( dsURL string , req * http . Request ) error {
return rv . err
}
2022-05-03 11:02:20 -05:00
// `/ds/query` endpoint test
func TestAPIEndpoint_Metrics_QueryMetricsV2 ( t * testing . T ) {
qds := query . ProvideService (
2022-10-21 06:54:55 -05:00
setting . NewCfg ( ) ,
2022-05-03 11:02:20 -05:00
nil ,
nil ,
& fakePluginRequestValidator { } ,
& fakeDatasources . FakeDataSourceService { } ,
& fakePluginClient {
QueryDataHandlerFunc : func ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
resp := backend . Responses {
"A" : backend . DataResponse {
2022-12-01 13:51:12 -06:00
Error : errors . New ( "query failed" ) ,
2022-05-03 11:02:20 -05:00
} ,
}
return & backend . QueryDataResponse { Responses : resp } , nil
} ,
} ,
)
serverFeatureEnabled := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
hs . Features = featuremgmt . WithFeatures ( featuremgmt . FlagDatasourceQueryMultiStatus , true )
2022-11-14 13:08:10 -06:00
hs . QuotaService = quotatest . New ( false , nil )
2022-05-03 11:02:20 -05:00
} )
serverFeatureDisabled := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
hs . Features = featuremgmt . WithFeatures ( featuremgmt . FlagDatasourceQueryMultiStatus , false )
2022-11-14 13:08:10 -06:00
hs . QuotaService = quotatest . New ( false , nil )
2022-05-03 11:02:20 -05: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 11:19:57 -05:00
req := serverFeatureDisabled . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ) )
2022-08-11 06:28:55 -05:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , OrgRole : org . RoleViewer } )
2022-05-03 11:02:20 -05: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 11:19:57 -05:00
req := serverFeatureEnabled . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ) )
2022-08-11 06:28:55 -05:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , OrgRole : org . RoleViewer } )
2022-05-03 11:02:20 -05: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 08:27:03 -05:00
func TestAPIEndpoint_Metrics_PluginDecryptionFailure ( t * testing . T ) {
qds := query . ProvideService (
2022-10-21 06:54:55 -05:00
setting . NewCfg ( ) ,
2022-07-13 08:27:03 -05:00
nil ,
nil ,
& fakePluginRequestValidator { } ,
& fakeDatasources . FakeDataSourceService { SimulatePluginFailure : true } ,
& fakePluginClient {
QueryDataHandlerFunc : func ( ctx context . Context , req * backend . QueryDataRequest ) ( * backend . QueryDataResponse , error ) {
resp := backend . Responses {
"A" : backend . DataResponse {
2022-12-01 13:51:12 -06:00
Error : errors . New ( "query failed" ) ,
2022-07-13 08:27:03 -05:00
} ,
}
return & backend . QueryDataResponse { Responses : resp } , nil
} ,
} ,
)
httpServer := SetupAPITestServer ( t , func ( hs * HTTPServer ) {
hs . queryDataService = qds
2022-11-14 13:08:10 -06:00
hs . QuotaService = quotatest . New ( false , nil )
2022-07-13 08:27:03 -05: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 11:19:57 -05:00
req := httpServer . NewPostRequest ( "/api/ds/query" , strings . NewReader ( reqValid ) )
2022-08-11 06:28:55 -05:00
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , OrgRole : org . RoleViewer } )
2022-07-13 08:27:03 -05: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 11:19:57 -05: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 )
hs . queryDataService = query . ProvideService (
2022-10-21 06:54:55 -05:00
setting . NewCfg ( ) ,
2022-09-14 11:19:57 -05:00
& fakeDatasources . FakeCacheService { } ,
nil ,
& fakePluginRequestValidator { } ,
& fakeDatasources . FakeDataSourceService { } ,
2022-11-02 08:51:51 -05:00
pluginClient . ProvideService ( r , & config . Cfg { } ) ,
2022-09-14 11:19:57 -05:00
)
2022-11-14 13:08:10 -06:00
hs . QuotaService = quotatest . New ( false , nil )
2022-09-14 11:19:57 -05:00
} )
req := srv . NewPostRequest ( "/api/ds/query" , strings . NewReader ( tc . request ) )
webtest . RequestWithSignedInUser ( req , & user . SignedInUser { UserID : 1 , OrgID : 1 , OrgRole : org . RoleViewer } )
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
}