2022-04-01 19:00:23 -05:00
package api
import (
2023-06-08 17:59:54 -05:00
"encoding/json"
2022-04-01 19:00:23 -05:00
"net/http"
"testing"
"time"
2024-02-06 16:12:13 -06:00
"github.com/google/uuid"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
2024-01-10 13:40:00 -06:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2022-04-01 19:00:23 -05:00
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
2023-08-16 02:04:18 -05:00
"github.com/grafana/grafana/pkg/infra/tracing"
2023-11-15 10:54:54 -06:00
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
2022-04-01 19:00:23 -05:00
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
2023-01-27 01:50:36 -06:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2024-02-06 16:12:13 -06:00
"github.com/grafana/grafana/pkg/services/dashboards"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana/pkg/services/datasources"
2022-04-25 11:57:45 -05:00
fakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
2024-01-10 14:52:58 -06:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2024-02-06 16:12:13 -06:00
"github.com/grafana/grafana/pkg/services/folder"
2023-11-15 10:54:54 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/accesscontrol"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/eval"
2022-11-02 09:13:39 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/eval/eval_mocks"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/models"
2024-02-06 16:12:13 -06:00
fakes2 "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
2022-08-10 04:56:48 -05:00
"github.com/grafana/grafana/pkg/services/user"
2022-04-01 19:00:23 -05:00
"github.com/grafana/grafana/pkg/web"
)
2023-06-08 17:59:54 -05:00
func Test ( t * testing . T ) {
text := ` {
"rule" : {
"grafana_alert" : {
"condition" : "C" ,
"data" : [
{
"refId" : "A" ,
"relativeTimeRange" : {
"from" : 600 ,
"to" : 0
} ,
"queryType" : "" ,
"datasourceUid" : "PD8C576611E62080A" ,
"model" : {
"refId" : "A" ,
"hide" : false ,
"datasource" : {
"type" : "testdata" ,
"uid" : "PD8C576611E62080A"
} ,
"scenarioId" : "random_walk" ,
"seriesCount" : 5 ,
"labels" : "series=series-$seriesIndex"
}
} ,
{
"refId" : "B" ,
"datasourceUid" : "__expr__" ,
"queryType" : "" ,
"model" : {
"refId" : "B" ,
"hide" : false ,
"type" : "reduce" ,
"datasource" : {
"uid" : "__expr__" ,
"type" : "__expr__"
} ,
"reducer" : "last" ,
"expression" : "A"
} ,
"relativeTimeRange" : {
"from" : 600 ,
"to" : 0
}
} ,
{
"refId" : "C" ,
"datasourceUid" : "__expr__" ,
"queryType" : "" ,
"model" : {
"refId" : "C" ,
"hide" : false ,
"type" : "threshold" ,
"datasource" : {
"uid" : "__expr__" ,
"type" : "__expr__"
} ,
"conditions" : [
{
"type" : "query" ,
"evaluator" : {
"params" : [
0
] ,
"type" : "gt"
}
}
] ,
"expression" : "B"
} ,
"relativeTimeRange" : {
"from" : 600 ,
"to" : 0
}
}
] ,
"no_data_state" : "Alerting" ,
"title" : "string"
} ,
"for" : "0s" ,
"labels" : {
"additionalProp1" : "string" ,
"additionalProp2" : "string" ,
"additionalProp3" : "string"
} ,
"annotations" : {
"additionalProp1" : "string" ,
"additionalProp2" : "string" ,
"additionalProp3" : "string"
}
} ,
"folderUid" : "test-uid" ,
"folderTitle" : "test-folder"
} `
var conf definitions . PostableExtendedRuleNodeExtended
require . NoError ( t , json . Unmarshal ( [ ] byte ( text ) , & conf ) )
require . Equal ( t , "test-folder" , conf . NamespaceTitle )
}
2022-04-01 19:00:23 -05:00
func TestRouteTestGrafanaRuleConfig ( t * testing . T ) {
t . Run ( "when fine-grained access is enabled" , func ( t * testing . T ) {
2023-01-27 01:50:36 -06:00
rc := & contextmodel . ReqContext {
2022-04-01 19:00:23 -05:00
Context : & web . Context {
Req : & http . Request { } ,
} ,
2022-08-10 04:56:48 -05:00
SignedInUser : & user . SignedInUser {
2022-08-11 06:28:55 -05:00
OrgID : 1 ,
2022-04-01 19:00:23 -05:00
} ,
}
2024-02-06 16:12:13 -06:00
t . Run ( "should return Forbidden if user cannot access folder" , func ( t * testing . T ) {
ac := acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceAllScope ( ) } ,
} )
ruleStore := fakes2 . NewRuleStore ( t )
ruleStore . Hook = func ( cmd any ) error {
q , ok := cmd . ( fakes2 . GenericRecordedQuery )
if ! ok {
return nil
}
if q . Name == "GetNamespaceByUID" {
return dashboards . ErrFolderAccessDenied
}
return nil
}
srv := createTestingApiSrv ( t , nil , ac , eval_mocks . NewEvaluatorFactory ( & eval_mocks . ConditionEvaluatorMock { } ) , & featuremgmt . FeatureManager { } , ruleStore )
rule := validRule ( )
response := srv . RouteTestGrafanaRuleConfig ( rc , definitions . PostableExtendedRuleNodeExtended {
Rule : rule ,
NamespaceUID : uuid . NewString ( ) ,
NamespaceTitle : "test-folder" ,
} )
require . Equal ( t , http . StatusForbidden , response . Status ( ) )
} )
2023-12-07 12:43:58 -06:00
t . Run ( "should return Forbidden if user cannot query a data source" , func ( t * testing . T ) {
2022-04-01 19:00:23 -05:00
data1 := models . GenerateAlertQuery ( )
data2 := models . GenerateAlertQuery ( )
2023-11-15 10:54:54 -06:00
ac := acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
2022-04-01 19:00:23 -05:00
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data1 . DatasourceUID ) } ,
} )
2024-02-06 16:12:13 -06:00
f := randFolder ( )
ruleStore := fakes2 . NewRuleStore ( t )
ruleStore . Folders [ rc . OrgID ] = [ ] * folder . Folder { f }
srv := createTestingApiSrv ( t , nil , ac , eval_mocks . NewEvaluatorFactory ( & eval_mocks . ConditionEvaluatorMock { } ) , & featuremgmt . FeatureManager { } , ruleStore )
2022-04-01 19:00:23 -05:00
2023-06-08 17:59:54 -05:00
rule := validRule ( )
rule . GrafanaManagedAlert . Data = ApiAlertQueriesFromAlertQueries ( [ ] models . AlertQuery { data1 , data2 } )
2023-06-15 12:33:42 -05:00
rule . GrafanaManagedAlert . Condition = data2 . RefID
2023-06-08 17:59:54 -05:00
response := srv . RouteTestGrafanaRuleConfig ( rc , definitions . PostableExtendedRuleNodeExtended {
Rule : rule ,
2024-02-06 16:12:13 -06:00
NamespaceUID : f . UID ,
NamespaceTitle : f . Title ,
2022-04-01 19:00:23 -05:00
} )
2023-12-07 12:43:58 -06:00
require . Equal ( t , http . StatusForbidden , response . Status ( ) )
2022-04-01 19:00:23 -05:00
} )
t . Run ( "should return 200 if user can query all data sources" , func ( t * testing . T ) {
data1 := models . GenerateAlertQuery ( )
data2 := models . GenerateAlertQuery ( )
2023-11-15 10:54:54 -06:00
ac := acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
2022-04-01 19:00:23 -05:00
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data1 . DatasourceUID ) } ,
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data2 . DatasourceUID ) } ,
} )
2022-06-27 11:23:15 -05:00
ds := & fakes . FakeCacheService { DataSources : [ ] * datasources . DataSource {
2023-02-02 10:22:43 -06:00
{ UID : data1 . DatasourceUID } ,
{ UID : data2 . DatasourceUID } ,
2022-04-01 19:00:23 -05:00
} }
var result [ ] eval . Result
2022-11-02 09:13:39 -05:00
evaluator := & eval_mocks . ConditionEvaluatorMock { }
evaluator . EXPECT ( ) . Evaluate ( mock . Anything , mock . Anything ) . Return ( result , nil )
evalFactory := eval_mocks . NewEvaluatorFactory ( evaluator )
2022-04-01 19:00:23 -05:00
2024-02-06 16:12:13 -06:00
f := randFolder ( )
ruleStore := fakes2 . NewRuleStore ( t )
ruleStore . Folders [ rc . OrgID ] = [ ] * folder . Folder { f }
srv := createTestingApiSrv ( t , ds , ac , evalFactory , & featuremgmt . FeatureManager { } , ruleStore )
2022-04-01 19:00:23 -05:00
2023-06-08 17:59:54 -05:00
rule := validRule ( )
rule . GrafanaManagedAlert . Data = ApiAlertQueriesFromAlertQueries ( [ ] models . AlertQuery { data1 , data2 } )
2023-06-15 12:33:42 -05:00
rule . GrafanaManagedAlert . Condition = data2 . RefID
2023-06-08 17:59:54 -05:00
response := srv . RouteTestGrafanaRuleConfig ( rc , definitions . PostableExtendedRuleNodeExtended {
Rule : rule ,
2024-02-06 16:12:13 -06:00
NamespaceUID : f . UID ,
NamespaceTitle : f . Title ,
2022-04-01 19:00:23 -05:00
} )
require . Equal ( t , http . StatusOK , response . Status ( ) )
2023-06-08 17:59:54 -05:00
evaluator . AssertCalled ( t , "Evaluate" , mock . Anything , mock . Anything )
2022-04-01 19:00:23 -05:00
} )
} )
}
func TestRouteEvalQueries ( t * testing . T ) {
t . Run ( "when fine-grained access is enabled" , func ( t * testing . T ) {
2023-01-27 01:50:36 -06:00
rc := & contextmodel . ReqContext {
2022-04-01 19:00:23 -05:00
Context : & web . Context {
Req : & http . Request { } ,
} ,
2022-08-10 04:56:48 -05:00
SignedInUser : & user . SignedInUser {
2022-08-11 06:28:55 -05:00
OrgID : 1 ,
2022-04-01 19:00:23 -05:00
} ,
}
2023-12-07 12:43:58 -06:00
t . Run ( "should return Forbidden if user cannot query a data source" , func ( t * testing . T ) {
2022-04-01 19:00:23 -05:00
data1 := models . GenerateAlertQuery ( )
data2 := models . GenerateAlertQuery ( )
srv := & TestingApiSrv {
2023-11-15 10:54:54 -06:00
authz : accesscontrol . NewRuleService ( acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data1 . DatasourceUID ) } ,
} ) ) ,
tracer : tracing . InitializeTracerForTest ( ) ,
2022-04-01 19:00:23 -05:00
}
response := srv . RouteEvalQueries ( rc , definitions . EvalQueriesPayload {
2023-03-27 10:55:13 -05:00
Data : ApiAlertQueriesFromAlertQueries ( [ ] models . AlertQuery { data1 , data2 } ) ,
2022-04-01 19:00:23 -05:00
Now : time . Time { } ,
} )
2023-12-07 12:43:58 -06:00
require . Equal ( t , http . StatusForbidden , response . Status ( ) )
2022-04-01 19:00:23 -05:00
} )
t . Run ( "should return 200 if user can query all data sources" , func ( t * testing . T ) {
data1 := models . GenerateAlertQuery ( )
data2 := models . GenerateAlertQuery ( )
2022-11-02 09:13:39 -05:00
currentTime := time . Now ( )
2023-11-15 10:54:54 -06:00
ac := acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
2022-04-01 19:00:23 -05:00
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data1 . DatasourceUID ) } ,
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( data2 . DatasourceUID ) } ,
} )
2022-06-27 11:23:15 -05:00
ds := & fakes . FakeCacheService { DataSources : [ ] * datasources . DataSource {
2023-02-02 10:22:43 -06:00
{ UID : data1 . DatasourceUID } ,
{ UID : data2 . DatasourceUID } ,
2022-04-01 19:00:23 -05:00
} }
2022-11-02 09:13:39 -05:00
evaluator := & eval_mocks . ConditionEvaluatorMock { }
2022-04-01 19:00:23 -05:00
result := & backend . QueryDataResponse {
Responses : map [ string ] backend . DataResponse {
"test" : {
Frames : nil ,
Error : nil ,
} ,
} ,
}
2022-11-02 09:13:39 -05:00
evaluator . EXPECT ( ) . EvaluateRaw ( mock . Anything , mock . Anything ) . Return ( result , nil )
2022-04-01 19:00:23 -05:00
2024-02-06 16:12:13 -06:00
ruleStore := fakes2 . NewRuleStore ( t )
srv := createTestingApiSrv ( t , ds , ac , eval_mocks . NewEvaluatorFactory ( evaluator ) , & featuremgmt . FeatureManager { } , ruleStore )
2022-04-01 19:00:23 -05:00
response := srv . RouteEvalQueries ( rc , definitions . EvalQueriesPayload {
2023-03-27 10:55:13 -05:00
Data : ApiAlertQueriesFromAlertQueries ( [ ] models . AlertQuery { data1 , data2 } ) ,
2022-11-02 09:13:39 -05:00
Now : currentTime ,
2022-04-01 19:00:23 -05:00
} )
require . Equal ( t , http . StatusOK , response . Status ( ) )
2022-11-02 09:13:39 -05:00
evaluator . AssertCalled ( t , "EvaluateRaw" , mock . Anything , currentTime )
2022-04-01 19:00:23 -05:00
} )
} )
2024-01-10 13:40:00 -06:00
t . Run ( "when query is optimizable" , func ( t * testing . T ) {
rc := & contextmodel . ReqContext {
Context : & web . Context {
Req : & http . Request { } ,
} ,
SignedInUser : & user . SignedInUser {
OrgID : 1 ,
} ,
}
t . Run ( "should return warning notice on optimized queries" , func ( t * testing . T ) {
queries := [ ] models . AlertQuery {
models . CreatePrometheusQuery ( "A" , "1" , 1000 , 43200 , false , "some-ds" ) ,
models . CreatePrometheusQuery ( "B" , "1" , 1000 , 43200 , false , "some-ds" ) ,
models . CreatePrometheusQuery ( "C" , "1" , 1000 , 43200 , false , "some-ds" ) , // Not optimizable.
models . CreateReduceExpression ( "D" , "A" , "last" ) ,
models . CreateReduceExpression ( "E" , "B" , "last" ) ,
}
currentTime := time . Now ( )
ac := acMock . New ( ) . WithPermissions ( [ ] ac . Permission {
{ Action : datasources . ActionQuery , Scope : datasources . ScopeProvider . GetResourceScopeUID ( queries [ 0 ] . DatasourceUID ) } ,
} )
ds := & fakes . FakeCacheService { DataSources : [ ] * datasources . DataSource {
{ UID : queries [ 0 ] . DatasourceUID } ,
} }
evaluator := & eval_mocks . ConditionEvaluatorMock { }
createEmptyFrameResponse := func ( refId string ) backend . DataResponse {
frame := data . NewFrame ( "" )
frame . RefID = refId
frame . SetMeta ( & data . FrameMeta { } )
return backend . DataResponse {
Frames : [ ] * data . Frame { frame } ,
Error : nil ,
}
}
result := & backend . QueryDataResponse {
Responses : map [ string ] backend . DataResponse {
"A" : createEmptyFrameResponse ( "A" ) ,
"B" : createEmptyFrameResponse ( "B" ) ,
"C" : createEmptyFrameResponse ( "C" ) ,
} ,
}
evaluator . EXPECT ( ) . EvaluateRaw ( mock . Anything , mock . Anything ) . Return ( result , nil )
2024-02-06 16:12:13 -06:00
ruleStore := fakes2 . NewRuleStore ( t )
srv := createTestingApiSrv ( t , ds , ac , eval_mocks . NewEvaluatorFactory ( evaluator ) , featuremgmt . WithManager ( featuremgmt . FlagAlertingQueryOptimization ) , ruleStore )
2024-01-10 13:40:00 -06:00
response := srv . RouteEvalQueries ( rc , definitions . EvalQueriesPayload {
Data : ApiAlertQueriesFromAlertQueries ( queries ) ,
Now : currentTime ,
} )
require . Equal ( t , http . StatusOK , response . Status ( ) )
evaluator . AssertCalled ( t , "EvaluateRaw" , mock . Anything , currentTime )
require . Equal ( t , [ ] data . Notice { {
Severity : data . NoticeSeverityWarning ,
Text : "Query optimized from Range to Instant type; all uses exclusively require the last datapoint. Consider modifying your query to Instant type to ensure accuracy." ,
} } , result . Responses [ "A" ] . Frames [ 0 ] . Meta . Notices )
require . Equal ( t , [ ] data . Notice { {
Severity : data . NoticeSeverityWarning ,
Text : "Query optimized from Range to Instant type; all uses exclusively require the last datapoint. Consider modifying your query to Instant type to ensure accuracy." ,
} } , result . Responses [ "B" ] . Frames [ 0 ] . Meta . Notices )
require . Equal ( t , 0 , len ( result . Responses [ "C" ] . Frames [ 0 ] . Meta . Notices ) )
} )
} )
2022-04-01 19:00:23 -05:00
}
2024-02-06 16:12:13 -06:00
func createTestingApiSrv ( t * testing . T , ds * fakes . FakeCacheService , ac * acMock . Mock , evaluator eval . EvaluatorFactory , featureManager * featuremgmt . FeatureManager , ruleStore RuleStore ) * TestingApiSrv {
2022-04-01 19:00:23 -05:00
if ac == nil {
2023-09-05 05:04:39 -05:00
ac = acMock . New ( )
2022-04-01 19:00:23 -05:00
}
return & TestingApiSrv {
DatasourceCache : ds ,
2023-11-15 10:54:54 -06:00
authz : accesscontrol . NewRuleService ( ac ) ,
2022-04-01 19:00:23 -05:00
evaluator : evaluator ,
2023-06-08 17:59:54 -05:00
cfg : config ( t ) ,
2023-08-16 02:04:18 -05:00
tracer : tracing . InitializeTracerForTest ( ) ,
2024-01-10 14:52:58 -06:00
featureManager : featureManager ,
2024-02-06 16:12:13 -06:00
folderService : ruleStore ,
2022-04-01 19:00:23 -05:00
}
}