@ -2,147 +2,308 @@ package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
domain "github.com/grafana/grafana/pkg/services/ngalert/models"
gfcore "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
"github.com/grafana/grafana/pkg/services/ngalert/store"
secrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/web"
prometheus "github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/stretchr/testify/require"
)
func TestProvisioningApi ( t * testing . T ) {
t . Run ( "successful GET policies returns 200" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
rc := createTestRequestCtx ( )
t . Run ( "policies" , func ( t * testing . T ) {
t . Run ( "successful GET returns 200" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
response := sut . RouteGetPolicyTree ( & rc )
require . Equal ( t , 200 , response . Status ( ) )
} )
t . Run ( "successful PUT returns 202" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
tree := definitions . Route { }
response := sut . RoutePutPolicyTree ( & rc , tree )
require . Equal ( t , 202 , response . Status ( ) )
} )
t . Run ( "when new policy tree is invalid" , func ( t * testing . T ) {
t . Run ( "PUT returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
sut . policies = & fakeRejectingNotificationPolicyService { }
rc := createTestRequestCtx ( )
tree := definitions . Route { }
response := sut . RoutePutPolicyTree ( & rc , tree )
require . Equal ( t , 400 , response . Status ( ) )
expBody := ` { "error":"invalid object specification: invalid policy tree","message":"invalid object specification: invalid policy tree"} `
require . Equal ( t , expBody , string ( response . Body ( ) ) )
} )
} )
t . Run ( "when org has no AM config" , func ( t * testing . T ) {
t . Run ( "GET returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
rc . SignedInUser . OrgId = 2
response := sut . RouteGetPolicyTree ( & rc )
require . Equal ( t , 404 , response . Status ( ) )
} )
response := sut . RouteGetPolicyTree ( & rc )
t . Run ( "POST returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
rc . SignedInUser . OrgId = 2
require . Equal ( t , 200 , response . Status ( ) )
response := sut . RouteGetPolicyTree ( & rc )
require . Equal ( t , 404 , response . Status ( ) )
} )
} )
t . Run ( "when an unspecified error occurrs" , func ( t * testing . T ) {
t . Run ( "GET returns 500" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
sut . policies = & fakeFailingNotificationPolicyService { }
rc := createTestRequestCtx ( )
response := sut . RouteGetPolicyTree ( & rc )
require . Equal ( t , 500 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "something went wrong" )
} )
t . Run ( "PUT returns 500" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
sut . policies = & fakeFailingNotificationPolicyService { }
rc := createTestRequestCtx ( )
tree := definitions . Route { }
response := sut . RoutePutPolicyTree ( & rc , tree )
require . Equal ( t , 500 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "something went wrong" )
} )
} )
} )
t . Run ( "successful PUT policies returns 202" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
rc := createTestRequestCtx ( )
tree := apimodels . Route { }
t . Run ( "contact points" , func ( t * testing . T ) {
t . Run ( "are invalid" , func ( t * testing . T ) {
t . Run ( "POST returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
cp := createInvalidContactPoint ( )
response := sut . RoutePutPolicyTree ( & rc , tree )
response := sut . RoutePostContactPoint ( & rc , cp )
require . Equal ( t , 202 , response . Status ( ) )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "recipient must be specified" )
} )
t . Run ( "PUT returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
cp := createInvalidContactPoint ( )
response := sut . RoutePutContactPoint ( & rc , cp )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "recipient must be specified" )
} )
} )
} )
t . Run ( "when new policy tree is invalid" , func ( t * testing . T ) {
t . Run ( "PUT policies returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
sut . policies = & fakeRejectingNotificationPolicyService { }
rc := createTestRequestCtx ( )
tree := apimodels . Route { }
t . Run ( "templates" , func ( t * testing . T ) {
t . Run ( "are invalid" , func ( t * testing . T ) {
t . Run ( "PUT returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
withURLParams ( rc , namePathParam , "test" )
tmpl := definitions . MessageTemplateContent { Template : "" }
response := sut . RoutePutPolicyTree ( & rc , tree )
response := sut . RoutePutTemplat e ( & rc , tmpl )
require . Equal ( t , 400 , response . Status ( ) )
expBody := ` { "error":"invalid object specification: invalid policy tree","message":"invalid object specification: invalid policy tree"} `
require . Equal ( t , expBody , string ( response . Body ( ) ) )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "template must have content" )
} )
} )
} )
t . Run ( "when org has no AM config" , func ( t * testing . T ) {
t . Run ( "GET policies returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
rc := createTestRequestCtx ( )
rc . SignedInUser . OrgId = 2
t . Run ( "mute timings" , func ( t * testing . T ) {
t . Run ( "are invalid" , func ( t * testing . T ) {
t . Run ( "POST returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
mti := createInvalidMuteTiming ( )
response := sut . RouteGetPolicyTree ( & rc )
response := sut . RoutePostMuteTiming ( & rc , mti )
require . Equal ( t , 404 , response . Status ( ) )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "invalid" )
} )
t . Run ( "PUT returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
withURLParams ( rc , namePathParam , "interval" )
mti := createInvalidMuteTiming ( )
response := sut . RoutePutMuteTiming ( & rc , mti )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "invalid" )
} )
} )
t . Run ( "POST policies returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
t . Run ( "are missing, PUT returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
rc . SignedInUser . OrgId = 2
withURLParams ( rc , namePathParam , "does not exist" )
mti := definitions . MuteTimeInterval { }
response := sut . RouteGetPolicyTree ( & rc )
response := sut . RoutePutMuteTiming ( & rc , mti )
require . Equal ( t , 404 , response . Status ( ) )
} )
} )
t . Run ( "when an unspecified error occurrs" , func ( t * testing . T ) {
t . Run ( "GET policies returns 500" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
sut . policies = & fakeFailingNotificationPolicyService { }
rc := createTestRequestCtx ( )
t . Run ( "alert rules" , func ( t * testing . T ) {
t . Run ( "are invalid" , func ( t * testing . T ) {
t . Run ( "POST returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
rule := createInvalidAlertRule ( )
response := sut . RouteGetPolicyTree ( & rc )
response := sut . RoutePostAlertRule ( & rc , rule )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "invalid alert rule" )
} )
t . Run ( "PUT returns 400" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
insertRule ( t , sut , createTestAlertRule ( "rule" , 1 ) )
rule := createInvalidAlertRule ( )
response := sut . RoutePutAlertRule ( & rc , rule )
require . Equal ( t , 500 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "something went wrong" )
require . Equal ( t , 400 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "invalid alert rule" )
} )
} )
t . Run ( "PUT policies returns 500" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( )
sut . policies = & fakeFailingNotificationPolicyService { }
t . Run ( "are missing, PUT returns 404" , func ( t * testing . T ) {
sut := createProvisioningSrvSut ( t )
rc := createTestRequestCtx ( )
tree := apimodels . Route { }
rule := createTestAlertRule ( "rule" , 1 )
response := sut . RoutePutPolicyTree ( & rc , tree )
response := sut . RoutePutAlertRul e ( & rc , rul e)
require . Equal ( t , 500 , response . Status ( ) )
require . NotEmpty ( t , response . Body ( ) )
require . Contains ( t , string ( response . Body ( ) ) , "something went wrong" )
require . Equal ( t , 404 , response . Status ( ) )
} )
} )
}
func createProvisioningSrvSut ( ) ProvisioningSrv {
func createProvisioningSrvSut ( t * testing . T ) ProvisioningSrv {
t . Helper ( )
secrets := secrets . NewFakeSecretsService ( )
log := log . NewNopLogger ( )
configs := & provisioning . MockAMConfigStore { }
configs . EXPECT ( ) .
GetsConfig ( models . AlertConfiguration {
AlertmanagerConfiguration : testConfig ,
} )
sqlStore := sqlstore . InitTestDB ( t )
store := store . DBstore {
SQLStore : sqlStore ,
BaseInterval : time . Second * 10 ,
}
xact := & provisioning . NopTransactionManager { }
prov := & provisioning . MockProvisioningStore { }
prov . EXPECT ( ) . SaveSucceeds ( )
prov . EXPECT ( ) . GetReturns ( models . ProvenanceNone )
return ProvisioningSrv {
log : log . NewNopLogger ( ) ,
policies : newFakeNotificationPolicyService ( ) ,
log : log ,
policies : newFakeNotificationPolicyService ( ) ,
contactPointService : provisioning . NewContactPointService ( configs , secrets , prov , xact , log ) ,
templates : provisioning . NewTemplateService ( configs , prov , xact , log ) ,
muteTimings : provisioning . NewMuteTimingService ( configs , prov , xact , log ) ,
alertRules : provisioning . NewAlertRuleService ( store , prov , xact , 60 , 10 , log ) ,
}
}
func createTestRequestCtx ( ) models . ReqContext {
return models . ReqContext {
func createTestRequestCtx ( ) gfcore . ReqContext {
return gfcore . ReqContext {
Context : & web . Context {
Req : & http . Request { } ,
} ,
SignedInUser : & models . SignedInUser {
SignedInUser : & gfcore . SignedInUser {
OrgId : 1 ,
} ,
}
}
func withURLParams ( rc gfcore . ReqContext , key , value string ) {
params := web . Params ( rc . Req )
params [ key ] = value
rc . Req = web . SetURLParams ( rc . Req , params )
}
type fakeNotificationPolicyService struct {
tree apimodels . Route
prov domain . Provenance
tree definition s. Route
prov models . Provenance
}
func newFakeNotificationPolicyService ( ) * fakeNotificationPolicyService {
return & fakeNotificationPolicyService {
tree : apimodel s. Route {
tree : definition s. Route {
Receiver : "some-receiver" ,
} ,
prov : domain . ProvenanceNone ,
prov : models . ProvenanceNone ,
}
}
func ( f * fakeNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( apimodel s. Route , error ) {
func ( f * fakeNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( definition s. Route , error ) {
if orgID != 1 {
return apimodel s. Route { } , store . ErrNoAlertmanagerConfiguration
return definition s. Route { } , store . ErrNoAlertmanagerConfiguration
}
result := f . tree
result . Provenance = f . prov
return result , nil
}
func ( f * fakeNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree apimodel s. Route , p domain . Provenance ) error {
func ( f * fakeNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree definition s. Route , p models . Provenance ) error {
if orgID != 1 {
return store . ErrNoAlertmanagerConfiguration
}
@ -153,20 +314,112 @@ func (f *fakeNotificationPolicyService) UpdatePolicyTree(ctx context.Context, or
type fakeFailingNotificationPolicyService struct { }
func ( f * fakeFailingNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( apimodel s. Route , error ) {
return apimodel s. Route { } , fmt . Errorf ( "something went wrong" )
func ( f * fakeFailingNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( definition s. Route , error ) {
return definition s. Route { } , fmt . Errorf ( "something went wrong" )
}
func ( f * fakeFailingNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree apimodel s. Route , p domain . Provenance ) error {
func ( f * fakeFailingNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree definition s. Route , p models . Provenance ) error {
return fmt . Errorf ( "something went wrong" )
}
type fakeRejectingNotificationPolicyService struct { }
func ( f * fakeRejectingNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( apimodel s. Route , error ) {
return apimodel s. Route { } , nil
func ( f * fakeRejectingNotificationPolicyService ) GetPolicyTree ( ctx context . Context , orgID int64 ) ( definition s. Route , error ) {
return definition s. Route { } , nil
}
func ( f * fakeRejectingNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree apimodel s. Route , p domain . Provenance ) error {
func ( f * fakeRejectingNotificationPolicyService ) UpdatePolicyTree ( ctx context . Context , orgID int64 , tree definition s. Route , p models . Provenance ) error {
return fmt . Errorf ( "%w: invalid policy tree" , provisioning . ErrValidation )
}
func createInvalidContactPoint ( ) definitions . EmbeddedContactPoint {
settings , _ := simplejson . NewJson ( [ ] byte ( ` { } ` ) )
return definitions . EmbeddedContactPoint {
Name : "test-contact-point" ,
Type : "slack" ,
Settings : settings ,
}
}
func createInvalidMuteTiming ( ) definitions . MuteTimeInterval {
return definitions . MuteTimeInterval {
MuteTimeInterval : prometheus . MuteTimeInterval {
Name : "interval" ,
TimeIntervals : [ ] timeinterval . TimeInterval {
{
Weekdays : [ ] timeinterval . WeekdayRange {
{
InclusiveRange : timeinterval . InclusiveRange {
Begin : - 1 ,
End : 7 ,
} ,
} ,
} ,
} ,
} ,
} ,
}
}
func createInvalidAlertRule ( ) definitions . AlertRule {
return definitions . AlertRule { }
}
func createTestAlertRule ( title string , orgID int64 ) definitions . AlertRule {
return definitions . AlertRule {
OrgID : orgID ,
Title : title ,
Condition : "A" ,
Data : [ ] models . AlertQuery {
{
RefID : "A" ,
Model : json . RawMessage ( "{}" ) ,
RelativeTimeRange : models . RelativeTimeRange {
From : models . Duration ( 60 ) ,
To : models . Duration ( 0 ) ,
} ,
} ,
} ,
RuleGroup : "my-cool-group" ,
For : time . Second * 60 ,
NoDataState : models . OK ,
ExecErrState : models . OkErrState ,
}
}
func insertRule ( t * testing . T , srv ProvisioningSrv , rule definitions . AlertRule ) {
t . Helper ( )
rc := createTestRequestCtx ( )
resp := srv . RoutePostAlertRule ( & rc , rule )
require . Equal ( t , 201 , resp . Status ( ) )
}
var testConfig = `
{
"template_files" : {
"a" : "template"
} ,
"alertmanager_config" : {
"route" : {
"receiver" : "grafana-default-email"
} ,
"receivers" : [ {
"name" : "grafana-default-email" ,
"grafana_managed_receiver_configs" : [ {
"uid" : "" ,
"name" : "email receiver" ,
"type" : "email" ,
"isDefault" : true ,
"settings" : {
"addresses" : "<example@email.com>"
}
} ]
} ] ,
"mute_time_intervals" : [ {
"name" : "interval" ,
"time_intervals" : [ ]
} ]
}
}
`