440 lines
14 KiB
Go
440 lines
14 KiB
Go
package publicdashboards
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/tests"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
testsuite.Run(m)
|
|
}
|
|
|
|
func TestPublicDashboardsAPI(t *testing.T) {
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
AppModeProduction: false,
|
|
EnableFeatureToggles: []string{
|
|
featuremgmt.FlagPublicDashboardsEmailSharing,
|
|
},
|
|
})
|
|
|
|
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
|
adminUsername := fmt.Sprintf("testadmin-%d", time.Now().UnixNano())
|
|
tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleAdmin),
|
|
Login: adminUsername,
|
|
Password: "admin",
|
|
IsAdmin: true,
|
|
})
|
|
adminClient := createHTTPClient(grafanaListedAddr, adminUsername, "admin")
|
|
|
|
t.Run("should create, get, update, and delete public dashboard", func(t *testing.T) {
|
|
dashboardPayload := map[string]interface{}{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "Test Dashboard",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(dashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
var dashboardResult map[string]interface{}
|
|
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
|
require.Equal(t, 200, createDashboardResp.StatusCode)
|
|
|
|
dashboardUID := dashboardResult["uid"].(string)
|
|
|
|
var listResult map[string]interface{}
|
|
doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listResult)
|
|
publicDashboardPayload := map[string]interface{}{
|
|
"isEnabled": true,
|
|
"annotationsEnabled": false,
|
|
"timeSelectionEnabled": true,
|
|
"share": "public",
|
|
}
|
|
|
|
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
|
var publicDashboard map[string]interface{}
|
|
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
|
require.Equal(t, 200, createResp.StatusCode)
|
|
assert.Equal(t, true, publicDashboard["isEnabled"])
|
|
assert.Equal(t, false, publicDashboard["annotationsEnabled"])
|
|
assert.Equal(t, true, publicDashboard["timeSelectionEnabled"])
|
|
assert.Equal(t, "public", publicDashboard["share"])
|
|
assert.NotEmpty(t, publicDashboard["accessToken"])
|
|
assert.NotEmpty(t, publicDashboard["uid"])
|
|
|
|
accessToken := publicDashboard["accessToken"].(string)
|
|
publicDashboardUID := publicDashboard["uid"].(string)
|
|
|
|
// get the public dashboard
|
|
getURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
|
var retrievedPD map[string]interface{}
|
|
getResp := doRequest(t, adminClient, "GET", getURL, nil, &retrievedPD)
|
|
require.Equal(t, 200, getResp.StatusCode)
|
|
|
|
// view the public dashboard
|
|
viewURL := fmt.Sprintf("/api/public/dashboards/%s", accessToken)
|
|
var dashboardData map[string]interface{}
|
|
viewResp := doRequest(t, adminClient, "GET", viewURL, nil, &dashboardData)
|
|
require.Equal(t, 200, viewResp.StatusCode)
|
|
assert.Equal(t, "Test Dashboard", dashboardData["dashboard"].(map[string]interface{})["title"])
|
|
assert.Equal(t, "Test Panel", dashboardData["dashboard"].(map[string]interface{})["panels"].([]interface{})[0].(map[string]interface{})["title"])
|
|
|
|
updatePayload := map[string]interface{}{
|
|
"isEnabled": false,
|
|
"annotationsEnabled": true,
|
|
"timeSelectionEnabled": false,
|
|
"share": "email",
|
|
}
|
|
updateBytes, err := json.Marshal(updatePayload)
|
|
require.NoError(t, err)
|
|
updateURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID)
|
|
var updatedPD map[string]interface{}
|
|
updateResp := doRequest(t, adminClient, "PATCH", updateURL, updateBytes, &updatedPD)
|
|
require.Equal(t, 200, updateResp.StatusCode)
|
|
assert.Equal(t, false, updatedPD["isEnabled"])
|
|
assert.Equal(t, true, updatedPD["annotationsEnabled"])
|
|
assert.Equal(t, false, updatedPD["timeSelectionEnabled"])
|
|
assert.Equal(t, "email", updatedPD["share"])
|
|
|
|
deleteURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", dashboardUID, publicDashboardUID)
|
|
var deleteResult map[string]interface{}
|
|
deleteResp := doRequest(t, adminClient, "DELETE", deleteURL, nil, &deleteResult)
|
|
require.Equal(t, 200, deleteResp.StatusCode)
|
|
var getAfterDeleteResult map[string]interface{}
|
|
getAfterDeleteResp := doRequest(t, adminClient, "GET", getURL, nil, &getAfterDeleteResult)
|
|
require.Equal(t, 404, getAfterDeleteResp.StatusCode)
|
|
})
|
|
|
|
t.Run("should list public dashboards", func(t *testing.T) {
|
|
dashboardPayload := map[string]interface{}{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "Test Dashboard for List",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(dashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
var dashboardResult map[string]interface{}
|
|
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
|
require.Equal(t, 200, createDashboardResp.StatusCode)
|
|
dashboardUID := dashboardResult["uid"].(string)
|
|
|
|
publicDashboardPayload := map[string]interface{}{
|
|
"isEnabled": true,
|
|
"annotationsEnabled": false,
|
|
"timeSelectionEnabled": true,
|
|
"share": "public",
|
|
}
|
|
|
|
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
|
var createResult map[string]interface{}
|
|
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &createResult)
|
|
require.Equal(t, 200, createResp.StatusCode)
|
|
|
|
var listData map[string]interface{}
|
|
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards", nil, &listData)
|
|
require.Equal(t, 200, listResp.StatusCode)
|
|
assert.NotEmpty(t, listData["publicDashboards"])
|
|
publicDashboards := listData["publicDashboards"].([]interface{})
|
|
assert.GreaterOrEqual(t, len(publicDashboards), 1)
|
|
})
|
|
|
|
t.Run("should handle invalid access token", func(t *testing.T) {
|
|
var viewResult map[string]interface{}
|
|
viewResp := doRequest(t, adminClient, "GET", "/api/public/dashboards/invalid-token", nil, &viewResult)
|
|
require.Equal(t, 400, viewResp.StatusCode)
|
|
})
|
|
|
|
t.Run("should handle disabled public dashboard", func(t *testing.T) {
|
|
dashboardPayload := map[string]interface{}{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "Test Dashboard Disabled",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(dashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
var dashboardResult map[string]interface{}
|
|
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
|
require.Equal(t, 200, createDashboardResp.StatusCode)
|
|
|
|
dashboardUID := dashboardResult["uid"].(string)
|
|
publicDashboardPayload := map[string]interface{}{
|
|
"isEnabled": false,
|
|
"annotationsEnabled": false,
|
|
"timeSelectionEnabled": true,
|
|
"share": "public",
|
|
}
|
|
|
|
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUID)
|
|
var publicDashboard map[string]interface{}
|
|
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
|
require.Equal(t, 200, createResp.StatusCode)
|
|
accessToken := publicDashboard["accessToken"].(string)
|
|
|
|
var viewResult map[string]interface{}
|
|
viewResp := doRequest(t, adminClient, "GET", fmt.Sprintf("/api/public/dashboards/%s", accessToken), nil, &viewResult)
|
|
require.Equal(t, 403, viewResp.StatusCode)
|
|
})
|
|
|
|
t.Run("permission test", func(t *testing.T) {
|
|
dashboards := []map[string]interface{}{
|
|
{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "test",
|
|
"uid": "9S6TmO67z",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
},
|
|
{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "my first dashboard",
|
|
"uid": "1S6TmO67z",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
},
|
|
{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "my second dashboard",
|
|
"uid": "2S6TmO67z",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
},
|
|
{
|
|
"dashboard": map[string]interface{}{
|
|
"title": "my zero dashboard",
|
|
"uid": "0S6TmO67z",
|
|
"panels": []map[string]interface{}{
|
|
{
|
|
"id": 1,
|
|
"type": "stat",
|
|
"title": "Test Panel",
|
|
},
|
|
},
|
|
},
|
|
"folderUid": "",
|
|
"overwrite": false,
|
|
},
|
|
}
|
|
|
|
dashboardUIDs := make([]string, len(dashboards))
|
|
publicDashboardUIDs := make([]string, len(dashboards))
|
|
|
|
for i, dashboardPayload := range dashboards {
|
|
payloadBytes, err := json.Marshal(dashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
var dashboardResult map[string]interface{}
|
|
createDashboardResp := doRequest(t, adminClient, "POST", "/api/dashboards/db", payloadBytes, &dashboardResult)
|
|
require.Equal(t, 200, createDashboardResp.StatusCode)
|
|
dashboardUIDs[i] = dashboardResult["uid"].(string)
|
|
|
|
isEnabled := i != 1
|
|
publicDashboardPayload := map[string]interface{}{
|
|
"isEnabled": isEnabled,
|
|
"annotationsEnabled": false,
|
|
"timeSelectionEnabled": true,
|
|
"share": "public",
|
|
}
|
|
|
|
payloadBytes, err = json.Marshal(publicDashboardPayload)
|
|
require.NoError(t, err)
|
|
|
|
createURL := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards", dashboardUIDs[i])
|
|
var publicDashboard map[string]interface{}
|
|
createResp := doRequest(t, adminClient, "POST", createURL, payloadBytes, &publicDashboard)
|
|
require.Equal(t, 200, createResp.StatusCode)
|
|
publicDashboardUIDs[i] = publicDashboard["uid"].(string)
|
|
}
|
|
|
|
t.Run("admin user should see all dashboards", func(t *testing.T) {
|
|
var listData map[string]interface{}
|
|
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData)
|
|
require.Equal(t, 200, listResp.StatusCode)
|
|
|
|
totalCount := int64(listData["totalCount"].(float64))
|
|
assert.GreaterOrEqual(t, totalCount, int64(4))
|
|
})
|
|
|
|
t.Run("user with access to just one dashboard should see only that dashboard", func(t *testing.T) {
|
|
limitedUserUsername := fmt.Sprintf("limiteduser-%d", time.Now().UnixNano())
|
|
limitedUserID := tests.CreateUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleNone),
|
|
Login: limitedUserUsername,
|
|
Password: "password",
|
|
IsAdmin: false,
|
|
})
|
|
limitedUserClient := createHTTPClient(grafanaListedAddr, limitedUserUsername, "password")
|
|
permissionPayload := map[string]interface{}{
|
|
"permission": "View",
|
|
}
|
|
permissionBytes, err := json.Marshal(permissionPayload)
|
|
require.NoError(t, err)
|
|
|
|
permissionURL := fmt.Sprintf("/api/access-control/dashboards/9S6TmO67z/users/%d", limitedUserID)
|
|
var permissionResult map[string]interface{}
|
|
permissionResp := doRequest(t, adminClient, "POST", permissionURL, permissionBytes, &permissionResult)
|
|
require.Equal(t, 200, permissionResp.StatusCode)
|
|
|
|
var listData map[string]interface{}
|
|
listResp := doRequest(t, limitedUserClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=50", nil, &listData)
|
|
require.Equal(t, 200, listResp.StatusCode)
|
|
|
|
totalCount := int64(listData["totalCount"].(float64))
|
|
assert.Equal(t, int64(1), totalCount)
|
|
})
|
|
|
|
t.Run("pagination should work correctly", func(t *testing.T) {
|
|
var listData map[string]interface{}
|
|
listResp := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=1&perpage=2", nil, &listData)
|
|
require.Equal(t, 200, listResp.StatusCode)
|
|
assert.NotEmpty(t, listData["publicDashboards"])
|
|
publicDashboards := listData["publicDashboards"].([]interface{})
|
|
assert.Equal(t, 2, len(publicDashboards))
|
|
totalCount := int64(listData["totalCount"].(float64))
|
|
assert.GreaterOrEqual(t, totalCount, int64(4))
|
|
|
|
var listDataPage2 map[string]interface{}
|
|
listRespPage2 := doRequest(t, adminClient, "GET", "/api/dashboards/public-dashboards?page=2&perpage=2", nil, &listDataPage2)
|
|
require.Equal(t, 200, listRespPage2.StatusCode)
|
|
publicDashboardsPage2 := listDataPage2["publicDashboards"].([]interface{})
|
|
assert.Equal(t, 2, len(publicDashboardsPage2))
|
|
})
|
|
})
|
|
}
|
|
|
|
type httpClient struct {
|
|
baseURL string
|
|
client *http.Client
|
|
}
|
|
|
|
func createHTTPClient(host, username, password string) *httpClient {
|
|
baseURL := fmt.Sprintf("http://%s:%s@%s", username, password, host)
|
|
return &httpClient{
|
|
baseURL: baseURL,
|
|
client: &http.Client{
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type httpResponse struct {
|
|
StatusCode int
|
|
Body []byte
|
|
}
|
|
|
|
func doRequest(t *testing.T, client *httpClient, method, path string, body []byte, result interface{}) httpResponse {
|
|
t.Helper()
|
|
|
|
var req *http.Request
|
|
var err error
|
|
|
|
url := client.baseURL + path
|
|
if body != nil {
|
|
req, err = http.NewRequest(method, url, bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req, err = http.NewRequest(method, url, nil)
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
resp, err := client.client.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close() // nolint:errcheck
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
response := httpResponse{
|
|
StatusCode: resp.StatusCode,
|
|
Body: respBody,
|
|
}
|
|
|
|
if result != nil && len(respBody) > 0 {
|
|
err = json.Unmarshal(respBody, result)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
return response
|
|
}
|