Files
grafana/pkg/tests/api/alerting/api_alertmanager_test.go
Will Browne edb0865caa Chore: Ensure we save correct default admin user in integration test DB setup (#105752)
* fix helper + amend tests

* fix import + remove unused var

* remove more users

* remove unused code

* update test comment
2025-05-28 11:25:01 +01:00

632 lines
18 KiB
Go

package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"testing"
"time"
"github.com/go-openapi/strfmt"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util"
)
func TestIntegrationAMConfigAccess(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "viewer",
Login: "viewer",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
type testCase struct {
desc string
url string
expStatus int
expBody string
}
// Create alertmanager config
cfg := apimodels.PostableUserConfig{}
amConfig := `
{
"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>"
}
}]
}]
}
}
`
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
require.NoError(t, err)
t.Run("when retrieve alertmanager configuration", func(t *testing.T) {
cfgTemplate := `
{
"template_files": null,
"alertmanager_config": {
"route": %s,
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"disableResolveMessage": false,
"uid": "",
"name": "email receiver",
"type": "email",
"secureFields": {},
"settings": {
"addresses": "<example@email.com>"
}
}]
}]
}
}
`
cfgWithoutAutogen := fmt.Sprintf(cfgTemplate, `{
"receiver": "grafana-default-email"
}`)
cfgWithAutogen := fmt.Sprintf(cfgTemplate, `{
"receiver": "grafana-default-email",
"routes": [{
"receiver": "grafana-default-email",
"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
"routes": [{
"receiver": "grafana-default-email",
"group_by": ["grafana_folder", "alertname"],
"object_matchers": [["__grafana_receiver__", "=", "grafana-default-email"]]
}]
}]
}`)
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusUnauthorized,
expBody: `{"extra":null,"message":"Unauthorized","messageId":"auth.unauthorized","statusCode":401,"traceID":""}`,
},
{
desc: "viewer request should succeed",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusOK,
expBody: cfgWithoutAutogen,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusOK,
expBody: cfgWithoutAutogen,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusOK,
expBody: cfgWithAutogen,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf(tc.url, grafanaListedAddr))
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
if tc.expStatus == http.StatusOK {
re := regexp.MustCompile(`"uid":"([\w|-]+)"`)
b = re.ReplaceAll(b, []byte(`"uid":""`))
}
require.NoError(t, err)
require.JSONEq(t, tc.expBody, string(b))
})
}
})
t.Run("when creating silence", func(t *testing.T) {
now := time.Now()
body := fmt.Sprintf(`
{
"comment": "string",
"createdBy": "string",
"matchers": [
{
"isRegex": true,
"name": "string",
"value": "string"
}
],
"startsAt": "%s",
"endsAt": "%s"
}
`, now.Format(time.RFC3339), now.Add(10*time.Second).Format(time.RFC3339))
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v2/silences",
expStatus: http.StatusUnauthorized,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusForbidden,
expBody: `"title":"Access denied"`,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusAccepted,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusAccepted,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
url := fmt.Sprintf(tc.url, grafanaListedAddr)
buf := bytes.NewReader([]byte(body))
// nolint:gosec
resp, err := http.Post(url, "application/json", buf)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
if tc.expStatus == http.StatusAccepted {
response := apimodels.PostSilencesOKBody{}
require.NoError(t, json.Unmarshal(b, &response))
require.NotEmpty(t, response.SilenceID)
return
}
require.Contains(t, string(b), tc.expBody)
})
}
})
var blob []byte
t.Run("when getting silences", func(t *testing.T) {
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusUnauthorized,
expBody: `"message": "Unauthorized"`,
},
{
desc: "viewer request should succeed",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusOK,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusOK,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
expStatus: http.StatusOK,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
url := fmt.Sprintf(tc.url, grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(url)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
require.NoError(t, err)
if tc.expStatus == http.StatusOK {
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
blob = b
}
})
}
})
var silences apimodels.GettableSilences
err = json.Unmarshal(blob, &silences)
require.NoError(t, err)
assert.Len(t, silences, 2)
silenceIDs := make([]string, 0, len(silences))
for _, s := range silences {
silenceIDs = append(silenceIDs, *s.ID)
}
unconsumedSilenceIdx := 0
t.Run("when deleting a silence", func(t *testing.T) {
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/api/v2/silence/%s",
expStatus: http.StatusUnauthorized,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silence/%s",
expStatus: http.StatusForbidden,
expBody: `"title":"Access denied"`,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silence/%s",
expStatus: http.StatusOK,
expBody: `{"message":"silence deleted"}`,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silence/%s",
expStatus: http.StatusOK,
expBody: `{"message":"silence deleted"}`,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
url := fmt.Sprintf(tc.url, grafanaListedAddr, silenceIDs[unconsumedSilenceIdx])
// Create client
client := &http.Client{}
// Create request
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
fmt.Println(err)
return
}
// Fetch Request
resp, err := client.Do(req)
if err != nil {
return
}
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
if tc.expStatus == http.StatusOK {
unconsumedSilenceIdx++
}
require.Contains(t, string(b), tc.expBody)
})
}
})
}
func TestIntegrationAlertmanagerCreateSilence(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, _ := testinfra.StartGrafanaEnv(t, dir, path)
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
cases := []struct {
name string
silence apimodels.PostableSilence
expErr string
}{{
name: "can create silence for foo=bar",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("foo"),
Value: util.Pointer("bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
}, {
name: "can create silence for _foo1=bar",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("_foo1"),
Value: util.Pointer("bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
}, {
name: "can create silence for 0foo=bar",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("0foo"),
Value: util.Pointer("bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
}, {
name: "can create silence for foo=🙂bar",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("foo"),
Value: util.Pointer("🙂bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
}, {
name: "can create silence for foo🙂=bar",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("foo🙂"),
Value: util.Pointer("bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
}, {
name: "can't create silence for missing label name",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer(""),
Value: util.Pointer("bar"),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
expErr: "unable to upsert silence: invalid silence: invalid label matcher 0: invalid label name \"\": unable to create silence",
}, {
name: "can't create silence for missing label value",
silence: apimodels.PostableSilence{
Silence: amv2.Silence{
Comment: util.Pointer("This is a comment"),
CreatedBy: util.Pointer("test"),
EndsAt: util.Pointer(strfmt.DateTime(time.Now().Add(time.Minute))),
Matchers: amv2.Matchers{{
IsEqual: util.Pointer(true),
IsRegex: util.Pointer(false),
Name: util.Pointer("foo"),
Value: util.Pointer(""),
}},
StartsAt: util.Pointer(strfmt.DateTime(time.Now())),
},
},
expErr: "unable to upsert silence: invalid silence: at least one matcher must not match the empty string: unable to create silence",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
silenceOkBody, status, body := client.PostSilence(t, tc.silence)
t.Log(body)
if tc.expErr != "" {
assert.NotEqual(t, http.StatusAccepted, status)
var validationError errutil.PublicError
assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
assert.Contains(t, validationError.Message, tc.expErr)
assert.Empty(t, silenceOkBody.SilenceID)
} else {
assert.Equal(t, http.StatusAccepted, status)
assert.NotEmpty(t, silenceOkBody.SilenceID)
}
})
}
}
func TestIntegrationAlertmanagerStatus(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "viewer",
Login: "viewer",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
type testCase struct {
desc string
url string
expStatus int
expBody string
}
cfgTemplate := `
{
"cluster": {
"peers": [],
"status": "disabled"
},
"config": {
"route": %s,
"receivers": [{
"name": "grafana-default-email",
"grafana_managed_receiver_configs": [{
"uid": "",
"name": "email receiver",
"type": "email",
"disableResolveMessage": false,
"settings": {
"addresses": "\u003cexample@email.com\u003e"
}
}]
}]
},
"uptime": null,
"versionInfo": {
"branch": "N/A",
"buildDate": "N/A",
"buildUser": "N/A",
"goVersion": "N/A",
"revision": "N/A",
"version": "N/A"
}
}
`
cfgWithoutAutogen := fmt.Sprintf(cfgTemplate, `{
"receiver": "grafana-default-email",
"group_by": ["grafana_folder", "alertname"]
}`)
cfgWithAutogen := fmt.Sprintf(cfgTemplate, `{
"receiver": "grafana-default-email",
"routes": [{
"receiver": "grafana-default-email",
"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
"routes": [{
"receiver": "grafana-default-email",
"group_by": ["grafana_folder", "alertname"],
"object_matchers": [["__grafana_receiver__", "=", "grafana-default-email"]]
}]
}],
"group_by": ["grafana_folder", "alertname"]
}`)
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/api/v2/status",
expStatus: http.StatusUnauthorized,
expBody: `{"extra":null,"message":"Unauthorized","messageId":"auth.unauthorized","statusCode":401,"traceID":""}`,
},
{
desc: "viewer request should succeed",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/status",
expStatus: http.StatusOK,
expBody: cfgWithoutAutogen,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/status",
expStatus: http.StatusOK,
expBody: cfgWithoutAutogen,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/status",
expStatus: http.StatusOK,
expBody: cfgWithAutogen,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
resp, err := http.Get(fmt.Sprintf(tc.url, grafanaListedAddr))
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
if tc.expStatus == http.StatusOK {
re := regexp.MustCompile(`"uid":"([\w|-]+)"`)
b = re.ReplaceAll(b, []byte(`"uid":""`))
}
require.NoError(t, err)
require.JSONEq(t, tc.expBody, string(b))
})
}
}