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 }