From d160638c671fc5161c804c917a5453ac501ef2d6 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 21 Dec 2023 14:38:38 -0500 Subject: [PATCH] Alerting: Add integration tests for mute timings provisioning API (#79810) --- .../api/alerting/api_provisioning_test.go | 235 ++++++++++++++++++ pkg/tests/api/alerting/testing.go | 125 ++++++++++ 2 files changed, 360 insertions(+) diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index 9a8c424abe5..6346b2689bb 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -6,12 +6,18 @@ import ( "fmt" "io" "net/http" + "slices" "sort" + "strings" "testing" + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/timeinterval" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -371,6 +377,235 @@ func TestIntegrationProvisioning(t *testing.T) { }) } +func TestMuteTimings(t *testing.T) { + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + }) + + grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) + + createUser(t, store, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "admin", + Login: "admin", + }) + + apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin") + + t.Run("default config should return empty list", func(t *testing.T) { + mt, status, body := apiClient.GetAllMuteTimingsWithStatus(t) + requireStatusCode(t, http.StatusOK, status, body) + require.Empty(t, mt) + }) + + emptyMuteTiming := definitions.MuteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: "Empty Mute Timing", + TimeIntervals: []timeinterval.TimeInterval{}, + }, + } + + t.Run("should create a new mute timing without any intervals", func(t *testing.T) { + mt, status, body := apiClient.CreateMuteTimingWithStatus(t, emptyMuteTiming) + requireStatusCode(t, http.StatusCreated, status, body) + require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt.MuteTimeInterval) + require.EqualValues(t, models.ProvenanceAPI, mt.Provenance) + }) + + anotherMuteTiming := definitions.MuteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: "Not Empty Mute Timing", + TimeIntervals: []timeinterval.TimeInterval{ + { + Times: []timeinterval.TimeRange{ + { + StartMinute: 10, + EndMinute: 45, + }, + }, + Weekdays: []timeinterval.WeekdayRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 0, + End: 2, + }, + }, + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 4, + End: 5, + }, + }, + }, + }, + }, + }, + } + + t.Run("should create a new mute timing with some settings", func(t *testing.T) { + mt, status, body := apiClient.CreateMuteTimingWithStatus(t, anotherMuteTiming) + requireStatusCode(t, http.StatusCreated, status, body) + require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval) + require.EqualValues(t, models.ProvenanceAPI, mt.Provenance) + }) + + t.Run("should return mute timing by name", func(t *testing.T) { + mt, status, body := apiClient.GetMuteTimingByNameWithStatus(t, emptyMuteTiming.Name) + requireStatusCode(t, http.StatusOK, status, body) + require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt.MuteTimeInterval) + require.EqualValues(t, "", mt.Provenance) // TODO this is a bug + + mt, status, body = apiClient.GetMuteTimingByNameWithStatus(t, anotherMuteTiming.Name) + requireStatusCode(t, http.StatusOK, status, body) + require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval) + require.EqualValues(t, "", mt.Provenance) // TODO this is a bug + }) + + t.Run("should return NotFound if mute timing does not exist", func(t *testing.T) { + _, status, body := apiClient.GetMuteTimingByNameWithStatus(t, "some-missing-timing") + requireStatusCode(t, http.StatusNotFound, status, body) + }) + + t.Run("should return all mute timings", func(t *testing.T) { + mt, status, body := apiClient.GetAllMuteTimingsWithStatus(t) + requireStatusCode(t, http.StatusOK, status, body) + require.Len(t, mt, 2) + + slices.SortFunc(mt, func(a, b definitions.MuteTimeInterval) int { + return strings.Compare(a.Name, b.Name) + }) + + require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt[0].MuteTimeInterval) + require.EqualValues(t, "", mt[0].Provenance) // TODO this is a bug + + require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt[1].MuteTimeInterval) + require.EqualValues(t, "", mt[1].Provenance) // TODO this is a bug + }) + + t.Run("should get BadRequest if creates a new mute timing with the same name", func(t *testing.T) { + m := anotherMuteTiming + m.TimeIntervals = nil + _, status, body := apiClient.CreateMuteTimingWithStatus(t, m) + t.Log(body) + requireStatusCode(t, http.StatusBadRequest, status, body) + var validationError map[string]any + assert.NoError(t, json.Unmarshal([]byte(body), &validationError)) + assert.Contains(t, validationError, "message") + if t.Failed() { + t.Fatalf("response: %s", body) + } + }) + + t.Run("should get BadRequest if creates an invalid mute timing", func(t *testing.T) { + m := definitions.MuteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: "Invalid", + TimeIntervals: []timeinterval.TimeInterval{ + { + Times: []timeinterval.TimeRange{ + { + StartMinute: 20000, + EndMinute: 90000, + }, + }, + }, + }, + }, + } + _, status, body := apiClient.CreateMuteTimingWithStatus(t, m) + t.Log(body) + requireStatusCode(t, http.StatusBadRequest, status, body) + var validationError map[string]any + assert.NoError(t, json.Unmarshal([]byte(body), &validationError)) + assert.Contains(t, validationError, "message") + if t.Failed() { + t.Fatalf("response: %s", body) + } + }) + + t.Run("should update existing mute timing", func(t *testing.T) { + anotherMuteTiming.TimeIntervals = []timeinterval.TimeInterval{ + { + Times: []timeinterval.TimeRange{ + { + StartMinute: 36, + EndMinute: 49, + }, + }, + }, + } + + mt, status, body := apiClient.UpdateMuteTimingWithStatus(t, anotherMuteTiming) + requireStatusCode(t, http.StatusAccepted, status, body) + require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval) + }) + + t.Run("should fail to update existing mute timing with invalid one", func(t *testing.T) { + mt := anotherMuteTiming + mt.TimeIntervals = []timeinterval.TimeInterval{ + { + Times: []timeinterval.TimeRange{ + { + StartMinute: 360000, + EndMinute: 490000, + }, + }, + }, + } + + _, status, body := apiClient.UpdateMuteTimingWithStatus(t, mt) + + requireStatusCode(t, http.StatusBadRequest, status, body) + var validationError map[string]any + assert.NoError(t, json.Unmarshal([]byte(body), &validationError)) + assert.Contains(t, validationError, "message") + if t.Failed() { + t.Fatalf("response: %s", body) + } + }) + + t.Run("should get NotFound if updates mute timing that does not exist", func(t *testing.T) { + mt := definitions.MuteTimeInterval{ + MuteTimeInterval: config.MuteTimeInterval{ + Name: "Missing Mute Timing", + }, + } + _, status, body := apiClient.UpdateMuteTimingWithStatus(t, mt) + requireStatusCode(t, http.StatusNotFound, status, body) + }) + + t.Run("should delete unused mute timing", func(t *testing.T) { + status, body := apiClient.DeleteMuteTimingWithStatus(t, emptyMuteTiming.Name) + requireStatusCode(t, http.StatusNoContent, status, body) + + _, status, body = apiClient.GetMuteTimingByNameWithStatus(t, emptyMuteTiming.Name) + requireStatusCode(t, http.StatusNotFound, status, body) + }) + + t.Run("should get BadRequest if deletes used mute-timing", func(t *testing.T) { + route, status, response := apiClient.GetRouteWithStatus(t) + requireStatusCode(t, http.StatusOK, status, response) + route.Routes = append(route.Routes, &definitions.Route{ + Receiver: route.Receiver, + ObjectMatchers: definitions.ObjectMatchers{ + { + Name: "a", + Value: "b", + }, + }, + MuteTimeIntervals: []string{anotherMuteTiming.Name}, + }) + status, response = apiClient.UpdateRouteWithStatus(t, route) + requireStatusCode(t, http.StatusAccepted, status, response) + + status, response = apiClient.DeleteMuteTimingWithStatus(t, anotherMuteTiming.Name) + requireStatusCode(t, http.StatusInternalServerError, status, response) // TODO should be bad request + }) +} + func createTestRequest(method string, url string, user string, body string) *http.Request { var bodyBuf io.Reader if body != "" { diff --git a/pkg/tests/api/alerting/testing.go b/pkg/tests/api/alerting/testing.go index ae4d1bcb649..2371d2bcc55 100644 --- a/pkg/tests/api/alerting/testing.go +++ b/pkg/tests/api/alerting/testing.go @@ -594,3 +594,128 @@ func (a apiClient) DeleteDatasource(t *testing.T, uid string) { require.Failf(t, "failed to create data source", "API request to create a datasource failed. Status code: %d, response: %s", resp.StatusCode, string(b)) } } + +func (a apiClient) GetAllMuteTimingsWithStatus(t *testing.T) (apimodels.MuteTimings, int, string) { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings", a.url), nil) + require.NoError(t, err) + + return sendRequest[apimodels.MuteTimings](t, req, http.StatusOK) +} + +func (a apiClient) GetMuteTimingByNameWithStatus(t *testing.T, name string) (apimodels.MuteTimeInterval, int, string) { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s", a.url, name), nil) + require.NoError(t, err) + + return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusOK) +} + +func (a apiClient) CreateMuteTimingWithStatus(t *testing.T, interval apimodels.MuteTimeInterval) (apimodels.MuteTimeInterval, int, string) { + t.Helper() + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err := enc.Encode(interval) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/provisioning/mute-timings", a.url), &buf) + req.Header.Add("Content-Type", "application/json") + require.NoError(t, err) + + return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusCreated) +} + +func (a apiClient) UpdateMuteTimingWithStatus(t *testing.T, interval apimodels.MuteTimeInterval) (apimodels.MuteTimeInterval, int, string) { + t.Helper() + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err := enc.Encode(interval) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s", a.url, interval.Name), &buf) + req.Header.Add("Content-Type", "application/json") + require.NoError(t, err) + + return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusAccepted) +} + +func (a apiClient) DeleteMuteTimingWithStatus(t *testing.T, name string) (int, string) { + t.Helper() + + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s", a.url, name), nil) + req.Header.Add("Content-Type", "application/json") + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return resp.StatusCode, string(body) +} + +func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, string) { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/policies", a.url), nil) + require.NoError(t, err) + + return sendRequest[apimodels.Route](t, req, http.StatusOK) +} + +func (a apiClient) UpdateRouteWithStatus(t *testing.T, route apimodels.Route) (int, string) { + t.Helper() + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + err := enc.Encode(route) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/provisioning/policies", a.url), &buf) + req.Header.Add("Content-Type", "application/json") + require.NoError(t, err) + + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + return resp.StatusCode, string(body) +} + +func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) { + client := &http.Client{} + resp, err := client.Do(req) + require.NoError(t, err) + defer func() { + _ = resp.Body.Close() + }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + var result T + + if resp.StatusCode != successStatusCode { + return result, resp.StatusCode, string(body) + } + + err = json.Unmarshal(body, &result) + require.NoError(t, err) + return result, resp.StatusCode, string(body) +} + +func requireStatusCode(t *testing.T, expected, actual int, response string) { + require.Equalf(t, expected, actual, "Unexpected status. Response: %s", response) +}