The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/publicdashboards/api/api_test.go

624 lines
21 KiB

package api
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/publicdashboards"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util/errutil"
)
var userNoRBACPerms = &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleAdmin, Login: "testAdminUserNoRBACPerms"}
var userAdmin = &user.SignedInUser{UserID: 2, OrgID: 1, OrgRole: org.RoleAdmin, Login: "testAdminUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}}
var userViewer = &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleViewer, Login: "testViewerUserRBAC", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsRead: {dashboards.ScopeDashboardsAll}}}}
var anonymousUser = &user.SignedInUser{IsAnonymous: true}
type JsonErrResponse struct {
Error string `json:"error"`
}
func TestAPIFeatureFlag(t *testing.T) {
testCases := []struct {
Name string
Method string
Path string
}{
{
Name: "API: Load Dashboard",
Method: http.MethodGet,
Path: "/api/public/dashboards/acbc123",
},
{
Name: "API: Query Dashboard",
Method: http.MethodGet,
Path: "/api/public/dashboards/abc123/panels/2/query",
},
{
Name: "API: List Dashboards",
Method: http.MethodGet,
Path: "/api/dashboards/public-dashboards",
},
{
Name: "API: Get Public Dashboard",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Create Public Dashboard",
Method: http.MethodPost,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Update Public Dashboard",
Method: http.MethodPut,
Path: "/api/dashboards/uid/abc123/public-dashboards",
},
{
Name: "API: Delete Public Dashboard",
Method: http.MethodDelete,
Path: "/api/dashboards/uid/:dashboardUid/public-dashboards/:uid",
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
cfg := setting.NewCfg()
service := publicdashboards.NewFakePublicDashboardService(t)
features := featuremgmt.WithFeatures()
testServer := setupTestServer(t, cfg, features, service, nil, userAdmin)
response := callAPI(testServer, test.Method, test.Path, nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
})
}
}
func TestAPIListPublicDashboard(t *testing.T) {
successResp := &PublicDashboardListResponseWithPagination{
PublicDashboards: []*PublicDashboardListResponse{
{
Uid: "1234asdfasdf",
AccessToken: "asdfasdf",
DashboardUid: "abc1234",
IsEnabled: true,
},
},
}
testCases := []struct {
Name string
User *user.SignedInUser
Response *PublicDashboardListResponseWithPagination
ResponseErr error
ExpectedHttpResponse int
}{
{
Name: "Anonymous user cannot list dashboards",
User: anonymousUser,
Response: successResp,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusUnauthorized,
},
{
Name: "User viewer can see public dashboards",
User: userViewer,
Response: successResp,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusOK,
},
{
Name: "Handles Service error",
User: userViewer,
Response: nil,
ResponseErr: ErrInternalServerError.Errorf(""),
ExpectedHttpResponse: http.StatusInternalServerError,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("FindAllWithPagination", mock.Anything, mock.Anything, mock.Anything).
Return(test.Response, test.ResponseErr).Maybe()
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
response := callAPI(testServer, http.MethodGet, "/api/dashboards/public-dashboards", nil, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.ExpectedHttpResponse == http.StatusOK {
var jsonResp PublicDashboardListResponseWithPagination
err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
require.NoError(t, err)
assert.Equal(t, jsonResp.PublicDashboards[0].Uid, "1234asdfasdf")
}
if test.ResponseErr != nil {
var errResp errutil.PublicError
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, "Internal server error", errResp.Message)
assert.Equal(t, "publicdashboards.internalServerError", errResp.MessageID)
service.AssertNotCalled(t, "FindAllWithPagination")
}
})
}
}
func TestAPIDeletePublicDashboard(t *testing.T) {
dashboardUid := "abc1234"
publicDashboardUid := "1234asdfasdf"
userEditorAllPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}}
userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}}
userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}}
testCases := []struct {
Name string
User *user.SignedInUser
DashboardUid string
PublicDashboardUid string
ResponseErr error
ExpectedHttpResponse int
ExpectedMessageResponse string
ShouldCallService bool
}{
{
Name: "User viewer cannot delete public dashboard",
User: userViewer,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
{
Name: "User editor without specific dashboard access cannot delete public dashboard",
User: userEditorAnotherPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
{
Name: "User editor with all dashboard accesses can delete public dashboard",
User: userEditorAllPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
{
Name: "User editor with dashboard access can delete public dashboard",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
{
Name: "Internal server error returns an error",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: ErrInternalServerError.Errorf(""),
ExpectedHttpResponse: ErrInternalServerError.Errorf("").Reason.Status().HTTPStatus(),
ExpectedMessageResponse: ErrInternalServerError.Errorf("").PublicMessage,
ShouldCallService: true,
},
{
Name: "PublicDashboard error returns correct status code instead of 500",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
ResponseErr: ErrPublicDashboardIdentifierNotSet.Errorf(""),
ExpectedHttpResponse: ErrPublicDashboardIdentifierNotSet.Errorf("").Reason.Status().HTTPStatus(),
ExpectedMessageResponse: ErrPublicDashboardIdentifierNotSet.Errorf("").PublicMessage,
ShouldCallService: true,
},
{
Name: "Invalid publicDashboardUid throws an error",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: "inv@lid-publicd@shboard-uid!",
ResponseErr: nil,
ExpectedHttpResponse: http.StatusBadRequest,
ShouldCallService: false,
},
{
Name: "Public dashboard uid does not exist",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: "UIDDOESNOTEXIST",
ResponseErr: ErrPublicDashboardNotFound.Errorf(""),
ExpectedHttpResponse: ErrPublicDashboardNotFound.Errorf("").Reason.Status().HTTPStatus(),
ExpectedMessageResponse: ErrPublicDashboardNotFound.Errorf("").PublicMessage,
ShouldCallService: true,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("Delete", mock.Anything, mock.Anything, mock.Anything).
Return(test.ResponseErr)
}
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
response := callAPI(testServer, http.MethodDelete, fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid), nil, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.ExpectedHttpResponse == http.StatusOK {
var jsonResp any
err := json.Unmarshal(response.Body.Bytes(), &jsonResp)
require.NoError(t, err)
assert.Equal(t, jsonResp, nil)
}
if !test.ShouldCallService {
service.AssertNotCalled(t, "Delete")
}
if test.ResponseErr != nil {
var errResp errutil.PublicError
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.ExpectedHttpResponse, errResp.StatusCode)
assert.Equal(t, test.ExpectedMessageResponse, errResp.Message)
}
})
}
}
func TestAPIGetPublicDashboard(t *testing.T) {
pubdash := &PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *PublicDashboard
PublicDashboardErr error
User *user.SignedInUser
ShouldCallService bool
}{
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: ErrDashboardNotFound.Errorf(""),
User: userViewer,
ShouldCallService: true,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardErr: errors.New("database broken"),
User: userViewer,
ShouldCallService: true,
},
{
Name: "retrieves public dashboard when dashboard is found RBAC on",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userViewer,
ShouldCallService: true,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
User: userNoRBACPerms,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("FindByDashboardUid", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr)
}
cfg := setting.NewCfg()
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodGet,
"/api/dashboards/uid/1/public-dashboards",
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if response.Code == http.StatusOK {
var pdcResp PublicDashboard
err := json.Unmarshal(response.Body.Bytes(), &pdcResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardResult, &pdcResp)
}
})
}
}
func TestApiCreatePublicDashboard(t *testing.T) {
testCases := []struct {
Name string
DashboardUid string
publicDashboard *PublicDashboard
ExpectedHttpResponse int
SaveDashboardErr error
User *user.SignedInUser
ShouldCallService bool
JsonBody string
}{
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: ErrInternalServerError.Errorf(""),
User: userAdmin,
ShouldCallService: true,
JsonBody: `{ "isPublic": true }`,
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboard: &PublicDashboard{},
SaveDashboardErr: ErrDashboardNotFound.Errorf(""),
User: userAdmin,
ShouldCallService: true,
JsonBody: `{ "isPublic": true }`,
},
{
Name: "returns 200 when update persists RBAC on",
DashboardUid: "1",
publicDashboard: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
User: userAdmin,
ShouldCallService: true,
JsonBody: `{ "isPublic": true }`,
},
{
Name: "returns 403 when no permissions RBAC on",
ExpectedHttpResponse: http.StatusForbidden,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userNoRBACPerms,
ShouldCallService: false,
JsonBody: `{ "isPublic": true }`,
},
{
Name: "returns 400 when uid is invalid",
ExpectedHttpResponse: http.StatusBadRequest,
publicDashboard: nil,
SaveDashboardErr: nil,
User: userAdmin,
ShouldCallService: false,
JsonBody: `{ "uid": "*", "isEnabled": true }`,
},
{
Name: "returns 200 when uid is valid",
ExpectedHttpResponse: http.StatusOK,
publicDashboard: &PublicDashboard{IsEnabled: true},
SaveDashboardErr: nil,
User: userAdmin,
ShouldCallService: true,
JsonBody: `{ "uid": "123abc", "isEnabled": true}`,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
// this is to avoid AssertExpectations fail at t.Cleanup when the middleware returns before calling the service
if test.ShouldCallService {
service.On("Create", mock.Anything, mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardDTO")).
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
}
cfg := setting.NewCfg()
testServer := setupTestServer(
t,
cfg,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
test.User,
)
response := callAPI(
testServer,
http.MethodPost,
"/api/dashboards/uid/1/public-dashboards",
strings.NewReader(test.JsonBody),
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
//check the result if it's a 200
if response.Code == http.StatusOK {
val, err := json.Marshal(test.publicDashboard)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
}
})
}
}
func TestAPIUpdatePublicDashboard(t *testing.T) {
dashboardUid := "abc1234"
publicDashboardUid := "1234asdfasdf"
adminUser := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {dashboards.ScopeDashboardsAll}}}}
userEditorPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {fmt.Sprintf("dashboards:uid:%s", dashboardUid)}}}}
userEditorAnotherPublicDashboard := &user.SignedInUser{UserID: 4, OrgID: 1, OrgRole: org.RoleEditor, Login: "testEditorUser", Permissions: map[int64]map[string][]string{1: {dashboards.ActionDashboardsPublicWrite: {"another-uid"}}}}
testCases := []struct {
Name string
User *user.SignedInUser
DashboardUid string
PublicDashboardUid string
PublicDashboardRes *PublicDashboard
PublicDashboardErr error
ExpectedHttpResponse int
ShouldCallService bool
}{
{
Name: "Invalid dashboardUid",
User: adminUser,
DashboardUid: "",
PublicDashboardUid: "",
PublicDashboardRes: nil,
PublicDashboardErr: ErrPublicDashboardIdentifierNotSet.Errorf(""),
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: false,
},
{
Name: "Invalid public dashboard uid",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: "",
PublicDashboardRes: nil,
PublicDashboardErr: ErrPublicDashboardNotFound.Errorf(""),
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: false,
},
{
Name: "Service Error",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: nil,
PublicDashboardErr: ErrDashboardNotFound.Errorf(""),
ExpectedHttpResponse: http.StatusNotFound,
ShouldCallService: true,
},
{
Name: "Success",
User: adminUser,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: &PublicDashboard{Uid: "success"},
PublicDashboardErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
// permissions
{
Name: "User can update this public dashboard",
User: userEditorPublicDashboard,
DashboardUid: dashboardUid,
PublicDashboardUid: publicDashboardUid,
PublicDashboardRes: &PublicDashboard{Uid: "success"},
PublicDashboardErr: nil,
ExpectedHttpResponse: http.StatusOK,
ShouldCallService: true,
},
{
Name: "User has permissions on another dashboard",
User: userEditorAnotherPublicDashboard,
PublicDashboardUid: publicDashboardUid,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
{
Name: "Viewer cannot update any dashboard",
User: userViewer,
PublicDashboardUid: publicDashboardUid,
ExpectedHttpResponse: http.StatusForbidden,
ShouldCallService: false,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ShouldCallService {
service.On("Update", mock.Anything, mock.Anything, mock.Anything).
Return(test.PublicDashboardRes, test.PublicDashboardErr)
}
cfg := setting.NewCfg()
features := featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards)
testServer := setupTestServer(t, cfg, features, service, nil, test.User)
url := fmt.Sprintf("/api/dashboards/uid/%s/public-dashboards/%s", test.DashboardUid, test.PublicDashboardUid)
body := strings.NewReader(fmt.Sprintf(`{ "uid": "%s"}`, test.PublicDashboardUid))
response := callAPI(testServer, http.MethodPatch, url, body, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
// check whether service called
if !test.ShouldCallService {
service.AssertNotCalled(t, "Update")
}
fmt.Println(response.Body.String())
// check response
if response.Code == http.StatusOK {
val, err := json.Marshal(test.PublicDashboardRes)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
// verify 4XXs except 403 && 404
} else if test.ExpectedHttpResponse > 200 &&
test.ExpectedHttpResponse != 403 &&
test.ExpectedHttpResponse != 404 {
var errResp JsonErrResponse
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error)
}
})
}
}