package server_test import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strconv" "strings" "testing" "github.com/bouk/httprouter" "github.com/google/go-cmp/cmp" "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/mocks" "github.com/influxdata/chronograf/server" ) const tickScript = ` stream |from() .measurement('cpu') |alert() .crit(lambda: "usage_idle" < 10) .log('/tmp/alert') ` func TestValidRuleRequest(t *testing.T) { tests := []struct { name string rule chronograf.AlertRule wantErr bool }{ { name: "No every with functions", rule: chronograf.AlertRule{ Query: &chronograf.QueryConfig{ Fields: []chronograf.Field{ { Value: "max", Type: "func", Args: []chronograf.Field{ { Value: "oldmanpeabody", Type: "field", }, }, }, }, }, }, wantErr: true, }, { name: "With every", rule: chronograf.AlertRule{ Every: "10s", Query: &chronograf.QueryConfig{ Fields: []chronograf.Field{ { Value: "max", Type: "func", Args: []chronograf.Field{ { Value: "oldmanpeabody", Type: "field", }, }, }, }, }, }, }, { name: "No query config", rule: chronograf.AlertRule{}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := server.ValidRuleRequest(tt.rule); (err != nil) != tt.wantErr { t.Errorf("ValidRuleRequest() error = %v, wantErr %v", err, tt.wantErr) } }) } } func Test_KapacitorRulesGet(t *testing.T) { kapaTests := []struct { name string requestPath string mockAlerts []chronograf.AlertRule expected []chronograf.AlertRule }{ { name: "basic", requestPath: "/chronograf/v1/sources/1/kapacitors/1/rules", mockAlerts: []chronograf.AlertRule{ { ID: "cpu_alert", Name: "cpu_alert", Status: "enabled", Type: "stream", DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}}, TICKScript: tickScript, }, }, expected: []chronograf.AlertRule{ { ID: "cpu_alert", Name: "cpu_alert", Status: "enabled", Type: "stream", DBRPs: []chronograf.DBRP{{DB: "telegraf", RP: "autogen"}}, TICKScript: tickScript, AlertNodes: chronograf.AlertNodes{ Posts: []*chronograf.Post{}, TCPs: []*chronograf.TCP{}, Email: []*chronograf.Email{}, Exec: []*chronograf.Exec{}, Log: []*chronograf.Log{}, VictorOps: []*chronograf.VictorOps{}, PagerDuty: []*chronograf.PagerDuty{}, PagerDuty2: []*chronograf.PagerDuty{}, Pushover: []*chronograf.Pushover{}, Sensu: []*chronograf.Sensu{}, Slack: []*chronograf.Slack{}, Telegram: []*chronograf.Telegram{}, Alerta: []*chronograf.Alerta{}, OpsGenie: []*chronograf.OpsGenie{}, OpsGenie2: []*chronograf.OpsGenie{}, Talk: []*chronograf.Talk{}, Kafka: []*chronograf.Kafka{}, }, }, }, }, } for _, test := range kapaTests { test := test // needed to avoid data race t.Run(test.name, func(t *testing.T) { t.Parallel() // setup mock kapa API kapaSrv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { params := r.URL.Query() limit, err := strconv.Atoi(params.Get("limit")) if err != nil { rw.WriteHeader(http.StatusBadRequest) return } offset, err := strconv.Atoi(params.Get("offset")) if err != nil { rw.WriteHeader(http.StatusBadRequest) return } tsks := []map[string]interface{}{} for _, task := range test.mockAlerts { tsks = append(tsks, map[string]interface{}{ "id": task.ID, "script": tickScript, "status": "enabled", "type": "stream", "dbrps": []chronograf.DBRP{ { DB: "telegraf", RP: "autogen", }, }, "link": map[string]interface{}{ "rel": "self", "href": "/kapacitor/v1/tasks/cpu_alert", }, }) } var tasks map[string]interface{} if offset >= len(tsks) { tasks = map[string]interface{}{ "tasks": []map[string]interface{}{}, } } else if limit+offset > len(tsks) { tasks = map[string]interface{}{ "tasks": tsks[offset:], } } //} else { //tasks = map[string]interface{}{ //"tasks": tsks[offset : offset+limit], //} //} err = json.NewEncoder(rw).Encode(&tasks) if err != nil { t.Error("Failed to encode JSON. err:", err) } })) defer kapaSrv.Close() // setup mock service and test logger testLogger := mocks.TestLogger{} svc := &server.Service{ Store: &mocks.Store{ SourcesStore: &mocks.SourcesStore{ GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { return chronograf.Source{ ID: ID, InsecureSkipVerify: true, }, nil }, }, ServersStore: &mocks.ServersStore{ GetF: func(ctx context.Context, ID int) (chronograf.Server, error) { return chronograf.Server{ SrcID: ID, URL: kapaSrv.URL, }, nil }, }, }, Logger: &testLogger, } // setup request and response recorder req := httptest.NewRequest("GET", test.requestPath, strings.NewReader("")) rr := httptest.NewRecorder() // setup context and request params bg := context.Background() params := httprouter.Params{ { Key: "id", Value: "1", }, { Key: "kid", Value: "1", }, } ctx := httprouter.WithParams(bg, params) req = req.WithContext(ctx) // invoke KapacitorRulesGet endpoint svc.KapacitorRulesGet(rr, req) // destructure response frame := struct { Rules []struct { chronograf.AlertRule Links json.RawMessage `json:"links"` } `json:"rules"` }{} resp := rr.Result() err := json.NewDecoder(resp.Body).Decode(&frame) if err != nil { t.Fatal("Err decoding kapa rule response: err:", err) } actual := make([]chronograf.AlertRule, len(frame.Rules)) for i := range frame.Rules { actual[i] = frame.Rules[i].AlertRule } if resp.StatusCode != http.StatusOK { t.Fatal("Expected HTTP 200 OK but got", resp.Status) } if !cmp.Equal(test.expected, actual) { t.Fatalf("%q - Alert rules differ! diff:\n%s\n", test.name, cmp.Diff(test.expected, actual)) } }) } } // Test_KapacitorActivation tests that activation of one kapacitor make other kapacitors inactive func Test_KapacitorActivation(t *testing.T) { // setup mock service and test logger sourceId := 1 sourceIdStr := strconv.FormatInt(int64(sourceId), 10) kapacitors := make([]chronograf.Server, 0, 5) testLogger := mocks.TestLogger{} svc := &server.Service{ Store: &mocks.Store{ SourcesStore: &mocks.SourcesStore{ GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { return chronograf.Source{ ID: sourceId, InsecureSkipVerify: true, }, nil }, }, ServersStore: &mocks.ServersStore{ GetF: func(ctx context.Context, ID int) (chronograf.Server, error) { for _, item := range kapacitors { if item.ID == ID { return item, nil } } return chronograf.Server{}, errors.New("Not Found") }, AllF: func(ctx context.Context) ([]chronograf.Server, error) { return kapacitors, nil }, AddF: func(ctx context.Context, svr chronograf.Server) (chronograf.Server, error) { for _, item := range kapacitors { if item.ID == svr.ID { return chronograf.Server{}, errors.New("Already Exists") } } svr.ID = len(kapacitors) + 1 kapacitors = append(kapacitors, svr) return svr, nil }, UpdateF: func(ctx context.Context, svr chronograf.Server) error { for i, item := range kapacitors { if item.ID == svr.ID { kapacitors[i] = svr return nil } } return errors.New("Not Found") }, }, OrganizationsStore: &mocks.OrganizationsStore{ AllF: func(ctx context.Context) ([]chronograf.Organization, error) { return nil, errors.New("No Organizations") }, DefaultOrganizationF: func(ctx context.Context) (*chronograf.Organization, error) { return &chronograf.Organization{ ID: "0", Name: "Default", }, nil }, }, }, Logger: &testLogger, } newKapacitor := func(t *testing.T, active bool) int { // setup request and response recorder id := len(kapacitors) + 1 kapacitor := chronograf.Server{ ID: id, Active: active, SrcID: sourceId, Name: strconv.Itoa(id), URL: "http://test/" + strconv.Itoa(id), } requestBody, _ := json.Marshal(kapacitor) req := httptest.NewRequest("POST", "/chronograf/v1/sources/"+sourceIdStr+"/kapacitors", bytes.NewReader(requestBody)) rr := httptest.NewRecorder() // setup context and request params bg := context.Background() params := httprouter.Params{ { Key: "id", Value: sourceIdStr, }, } ctx := httprouter.WithParams(bg, params) req = req.WithContext(ctx) svc.NewKapacitor(rr, req) if rr.Result().StatusCode/100 != 2 { t.Fatalf("unable to create kapacitor #%d", id) } return id } updateKapacitor := func(t *testing.T, id int, active bool) { // setup request and response recorder kapacitor := chronograf.Server{ ID: id, Active: active, SrcID: sourceId, Name: strconv.Itoa(id), URL: "http://test/" + strconv.Itoa(id), } requestBody, _ := json.Marshal(kapacitor) kid := strconv.Itoa(id) req := httptest.NewRequest("PATCH", "/chronograf/v1/sources/"+sourceIdStr+"/kapacitors/"+kid, bytes.NewReader(requestBody)) rr := httptest.NewRecorder() // setup context and request params bg := context.Background() params := httprouter.Params{ { Key: "id", Value: sourceIdStr, }, { Key: "kid", Value: kid, }, } ctx := httprouter.WithParams(bg, params) req = req.WithContext(ctx) svc.UpdateKapacitor(rr, req) if rr.Result().StatusCode/100 != 2 { t.Fatalf("unable to update kapacitor #%d", id) } } // checks that the expected kapacitor (ID) is active , -1 if there is none, -2 if there are more assertActiveKapacitor := func(t *testing.T, name string, expected int) { // setup request and response recorder req := httptest.NewRequest("GET", "/chronograf/v1/sources/"+sourceIdStr+"/kapacitors", strings.NewReader("")) rr := httptest.NewRecorder() // setup context and request params bg := context.Background() params := httprouter.Params{ { Key: "id", Value: sourceIdStr, }, } ctx := httprouter.WithParams(bg, params) req = req.WithContext(ctx) // invoke KapacitorRulesGet endpoint svc.Kapacitors(rr, req) type kapacitor struct { ID int `json:"id,string"` // Unique identifier representing a kapacitor instance. Active bool `json:"active"` } type kapacitors struct { Kapacitors []kapacitor `json:"kapacitors"` } responseData := kapacitors{} resp := rr.Result() err := json.NewDecoder(resp.Body).Decode(&responseData) retVal := -1 if err == nil { for _, kapacitor := range responseData.Kapacitors { if kapacitor.Active { if retVal != -1 { t.Fatalf("%s - more active kapacitors detected!", name) return } retVal = kapacitor.ID } } } if expected != retVal { t.Fatalf("%s - expected active kapacitor %d, received %d", name, expected, retVal) } } // test expectation when creating/activating/deactivating kapacitors newKapacitor(t, false) assertActiveKapacitor(t, "create inactive #1", -1) // no kapacitor is now active id2 := newKapacitor(t, true) assertActiveKapacitor(t, "create active #2", id2) id3 := newKapacitor(t, false) assertActiveKapacitor(t, "create inactive #3", id2) updateKapacitor(t, id2, false) assertActiveKapacitor(t, "deactivate current #2", -1) // no kapacitor is now active id4 := newKapacitor(t, true) assertActiveKapacitor(t, "create active #4", id4) updateKapacitor(t, id3, true) assertActiveKapacitor(t, "activate #3", id3) }