Alerting: Add integration tests for mute timings provisioning API (#79810)

This commit is contained in:
Yuri Tseretyan 2023-12-21 14:38:38 -05:00 committed by GitHub
parent 14ff9ac679
commit d160638c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 360 additions and 0 deletions

View File

@ -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 != "" {

View File

@ -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)
}