package base import ( "context" "encoding/json" "errors" "io" "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-kit/log" "github.com/gorilla/mux" "github.com/grafana/dskit/services" "github.com/stretchr/testify/require" "github.com/weaveworks/common/user" "github.com/grafana/loki/pkg/ruler/rulespb" ) func TestRuler_rules(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(mockRules)) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck a := NewAPI(r, r.store, log.NewNopLogger()) req := requestFor(t, "GET", "https://localhost:8080/api/prom/api/v1/rules", nil, "user1") w := httptest.NewRecorder() a.PrometheusRules(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) // Check status code and status response responseJSON := response{} err := json.Unmarshal(body, &responseJSON) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, responseJSON.Status, "success") // Testing the running rules for user1 in the mock store expectedResponse, _ := json.Marshal(response{ Status: "success", Data: &RuleDiscovery{ RuleGroups: []*RuleGroup{ { Name: "group1", File: "namespace1", Rules: []rule{ &recordingRule{ Name: "UP_RULE", Query: "up", Health: "unknown", Type: "recording", }, &alertingRule{ Name: "UP_ALERT", Query: "up < 1", State: "inactive", Health: "unknown", Type: "alerting", Alerts: []*Alert{}, }, }, Interval: 60, }, }, }, }) require.Equal(t, string(expectedResponse), string(body)) } func TestRuler_rules_special_characters(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(mockSpecialCharRules)) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck a := NewAPI(r, r.store, log.NewNopLogger()) req := requestFor(t, http.MethodGet, "https://localhost:8080/api/prom/api/v1/rules", nil, "user1") w := httptest.NewRecorder() a.PrometheusRules(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) // Check status code and status response responseJSON := response{} err := json.Unmarshal(body, &responseJSON) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, responseJSON.Status, "success") // Testing the running rules for user1 in the mock store expectedResponse, _ := json.Marshal(response{ Status: "success", Data: &RuleDiscovery{ RuleGroups: []*RuleGroup{ { Name: ")(_+?/|group1+/?", File: ")(_+?/|namespace1+/?", Rules: []rule{ &recordingRule{ Name: "UP_RULE", Query: "up", Health: "unknown", Type: "recording", }, &alertingRule{ Name: "UP_ALERT", Query: "up < 1", State: "inactive", Health: "unknown", Type: "alerting", Alerts: []*Alert{}, }, }, Interval: 60, }, }, }, }) require.Equal(t, string(expectedResponse), string(body)) } func TestRuler_alerts(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(mockRules)) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer r.StopAsync() a := NewAPI(r, r.store, log.NewNopLogger()) req := requestFor(t, http.MethodGet, "https://localhost:8080/api/prom/api/v1/alerts", nil, "user1") w := httptest.NewRecorder() a.PrometheusAlerts(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) // Check status code and status response responseJSON := response{} err := json.Unmarshal(body, &responseJSON) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, responseJSON.Status, "success") // Currently there is not an easy way to mock firing alerts. The empty // response case is tested instead. expectedResponse, _ := json.Marshal(response{ Status: "success", Data: &AlertDiscovery{ Alerts: []*Alert{}, }, }) require.Equal(t, string(expectedResponse), string(body)) } func TestRuler_Create(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList))) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck a := NewAPI(r, r.store, log.NewNopLogger()) tc := []struct { name string input string output string err error status int }{ { name: "with an empty payload", input: "", status: 400, err: errors.New("invalid rules config: rule group name must not be empty"), }, { name: "with no rule group name", input: ` interval: 15s rules: - record: up_rule expr: up `, status: 400, err: errors.New("invalid rules config: rule group name must not be empty"), }, { name: "with no rules", input: ` name: rg_name interval: 15s `, status: 400, err: errors.New("invalid rules config: rule group 'rg_name' has no rules"), }, { name: "with a a valid rules file", status: 202, input: ` name: test interval: 15s rules: - record: up_rule expr: up{} - alert: up_alert expr: sum(up{}) > 1 for: 30s annotations: test: test labels: test: test `, output: "name: test\ninterval: 15s\nrules:\n - record: up_rule\n expr: up{}\n - alert: up_alert\n expr: sum(up{}) > 1\n for: 30s\n labels:\n test: test\n annotations:\n test: test\n", }, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { router := mux.NewRouter() router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup) router.Path("/api/v1/rules/{namespace}/{groupName}").Methods("GET").HandlerFunc(a.GetRuleGroup) // POST req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, tt.status, w.Code) if tt.err == nil { // GET req = requestFor(t, http.MethodGet, "https://localhost:8080/api/v1/rules/namespace/test", nil, "user1") w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, 200, w.Code) require.Equal(t, tt.output, w.Body.String()) } else { require.Equal(t, tt.err.Error()+"\n", w.Body.String()) } }) } } func TestRuler_DeleteNamespace(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(mockRulesNamespaces)) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck a := NewAPI(r, r.store, log.NewNopLogger()) router := mux.NewRouter() router.Path("/api/v1/rules/{namespace}").Methods(http.MethodDelete).HandlerFunc(a.DeleteNamespace) router.Path("/api/v1/rules/{namespace}/{groupName}").Methods(http.MethodGet).HandlerFunc(a.GetRuleGroup) // Verify namespace1 rules are there. req := requestFor(t, http.MethodGet, "https://localhost:8080/api/v1/rules/namespace1/group1", nil, "user1") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code) require.Equal(t, "name: group1\ninterval: 1m\nrules:\n - record: UP_RULE\n expr: up\n - alert: UP_ALERT\n expr: up < 1\n", w.Body.String()) // Delete namespace1 req = requestFor(t, http.MethodDelete, "https://localhost:8080/api/v1/rules/namespace1", nil, "user1") w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusAccepted, w.Code) require.Equal(t, "{\"status\":\"success\",\"data\":null,\"errorType\":\"\",\"error\":\"\"}", w.Body.String()) // On Partial failures req = requestFor(t, http.MethodDelete, "https://localhost:8080/api/v1/rules/namespace2", nil, "user1") w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusInternalServerError, w.Code) require.Equal(t, "{\"status\":\"error\",\"data\":null,\"errorType\":\"server_error\",\"error\":\"unable to delete rg\"}", w.Body.String()) } func TestRuler_LimitsPerGroup(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList))) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck r.limits = ruleLimits{maxRuleGroups: 1, maxRulesPerRuleGroup: 1} a := NewAPI(r, r.store, log.NewNopLogger()) tc := []struct { name string input string output string err error status int }{ { name: "when exceeding the rules per rule group limit", status: 400, input: ` name: test interval: 15s rules: - record: up_rule expr: up{} - alert: up_alert expr: sum(up{}) > 1 for: 30s annotations: test: test labels: test: test `, output: "per-user rules per rule group limit (limit: 1 actual: 2) exceeded\n", }, } for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { router := mux.NewRouter() router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup) // POST req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, tt.status, w.Code) require.Equal(t, tt.output, w.Body.String()) }) } } func TestRuler_RulerGroupLimits(t *testing.T) { cfg, cleanup := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList))) defer cleanup() r, rcleanup := newTestRuler(t, cfg) defer rcleanup() defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck r.limits = ruleLimits{maxRuleGroups: 1, maxRulesPerRuleGroup: 1} a := NewAPI(r, r.store, log.NewNopLogger()) tc := []struct { name string input string output string err error status int }{ { name: "when pushing the first group within bounds of the limit", status: 202, input: ` name: test_first_group_will_succeed interval: 15s rules: - record: up_rule expr: up{} `, output: "{\"status\":\"success\",\"data\":null,\"errorType\":\"\",\"error\":\"\"}", }, { name: "when exceeding the rule group limit after sending the first group", status: 400, input: ` name: test_second_group_will_fail interval: 15s rules: - record: up_rule expr: up{} `, output: "per-user rule groups limit (limit: 1 actual: 2) exceeded\n", }, } // define once so the requests build on each other so the number of rules can be tested router := mux.NewRouter() router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup) for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { // POST req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1") w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, tt.status, w.Code) require.Equal(t, tt.output, w.Body.String()) }) } } func requestFor(t *testing.T, method string, url string, body io.Reader, userID string) *http.Request { t.Helper() req := httptest.NewRequest(method, url, body) ctx := user.InjectOrgID(req.Context(), userID) return req.WithContext(ctx) }