grafana/pkg/services/publicdashboards/api/api_test.go
Jeff Levin eacee08135
public dashboards: move into into its own service (#51358)
This PR moves public dashboards into its own self contained service including API, Service, Database, and Models. Routes are mounted on the Grafana HTTPServer by the API service at injection time with wire.go. The main  route that loads the frontend for public dashboards is still handled by the API package.

Co-authored-by: Jesse Weaver <jesse.weaver@grafana.com>
Co-authored-by: Owen Smallwood <owen.smallwood@grafana.com>
2022-07-06 17:51:44 -06:00

590 lines
17 KiB
Go

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/datasources"
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
"github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
func TestAPIGetPublicDashboard(t *testing.T) {
t.Run("It should 404 if featureflag is not enabled", func(t *testing.T) {
cfg := setting.NewCfg()
qs := buildQueryDataService(t, nil, nil, nil)
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(&models.Dashboard{}, nil).Maybe()
testServer := setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(), service, nil)
response := callAPI(testServer, http.MethodGet, "/api/public/dashboards", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.Equal(t, http.StatusNotFound, response.Code)
// control set. make sure routes are mounted
testServer = setupTestServer(t, cfg, qs, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil)
response = callAPI(testServer, http.MethodGet, "/api/public/dashboards/asdf", nil, t)
assert.NotEqual(t, http.StatusNotFound, response.Code)
})
DashboardUid := "dashboard-abcd1234"
token, err := uuid.NewRandom()
require.NoError(t, err)
accessToken := fmt.Sprintf("%x", token)
testCases := []struct {
Name string
AccessToken string
ExpectedHttpResponse int
PublicDashboardResult *models.Dashboard
PublicDashboardErr error
}{
{
Name: "It gets a public dashboard",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: &models.Dashboard{
Data: simplejson.NewFromAny(map[string]interface{}{
"Uid": DashboardUid,
}),
},
PublicDashboardErr: nil,
},
{
Name: "It should return 404 if no public dashboard",
AccessToken: accessToken,
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: ErrPublicDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboard", mock.Anything, mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr).Maybe()
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(testServer, http.MethodGet,
fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken),
nil,
t,
)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.PublicDashboardErr == nil {
var dashResp dtos.DashboardFullWithMeta
err := json.Unmarshal(response.Body.Bytes(), &dashResp)
require.NoError(t, err)
assert.Equal(t, DashboardUid, dashResp.Dashboard.Get("Uid").MustString())
assert.Equal(t, false, dashResp.Meta.CanEdit)
assert.Equal(t, false, dashResp.Meta.CanDelete)
assert.Equal(t, false, dashResp.Meta.CanSave)
} else {
var errResp struct {
Error string `json:"error"`
}
err := json.Unmarshal(response.Body.Bytes(), &errResp)
require.NoError(t, err)
assert.Equal(t, test.PublicDashboardErr.Error(), errResp.Error)
}
})
}
}
func TestAPIGetPublicDashboardConfig(t *testing.T) {
pubdash := &PublicDashboard{IsEnabled: true}
testCases := []struct {
Name string
DashboardUid string
ExpectedHttpResponse int
PublicDashboardResult *PublicDashboard
PublicDashboardErr error
}{
{
Name: "retrieves public dashboard config when dashboard is found",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusOK,
PublicDashboardResult: pubdash,
PublicDashboardErr: nil,
},
{
Name: "returns 404 when dashboard not found",
DashboardUid: "77777",
ExpectedHttpResponse: http.StatusNotFound,
PublicDashboardResult: nil,
PublicDashboardErr: dashboards.ErrDashboardNotFound,
},
{
Name: "returns 500 when internal server error",
DashboardUid: "1",
ExpectedHttpResponse: http.StatusInternalServerError,
PublicDashboardResult: nil,
PublicDashboardErr: errors.New("database broken"),
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("GetPublicDashboardConfig", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).
Return(test.PublicDashboardResult, test.PublicDashboardErr)
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(
testServer,
http.MethodGet,
"/api/dashboards/uid/1/public-config",
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 TestApiSavePublicDashboardConfig(t *testing.T) {
testCases := []struct {
Name string
DashboardUid string
publicDashboardConfig *PublicDashboard
ExpectedHttpResponse int
SaveDashboardErr error
}{
{
Name: "returns 200 when update persists",
DashboardUid: "1",
publicDashboardConfig: &PublicDashboard{IsEnabled: true},
ExpectedHttpResponse: http.StatusOK,
SaveDashboardErr: nil,
},
{
Name: "returns 500 when not persisted",
ExpectedHttpResponse: http.StatusInternalServerError,
publicDashboardConfig: &PublicDashboard{},
SaveDashboardErr: errors.New("backend failed to save"),
},
{
Name: "returns 404 when dashboard not found",
ExpectedHttpResponse: http.StatusNotFound,
publicDashboardConfig: &PublicDashboard{},
SaveDashboardErr: dashboards.ErrDashboardNotFound,
},
}
for _, test := range testCases {
t.Run(test.Name, func(t *testing.T) {
service := publicdashboards.NewFakePublicDashboardService(t)
service.On("SavePublicDashboardConfig", mock.Anything, mock.AnythingOfType("*models.SavePublicDashboardConfigDTO")).
Return(&PublicDashboard{IsEnabled: true}, test.SaveDashboardErr)
testServer := setupTestServer(
t,
setting.NewCfg(),
buildQueryDataService(t, nil, nil, nil),
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
nil,
)
response := callAPI(
testServer,
http.MethodPost,
"/api/dashboards/uid/1/public-config",
strings.NewReader(`{ "isPublic": true }`),
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.publicDashboardConfig)
require.NoError(t, err)
assert.Equal(t, string(val), response.Body.String())
}
})
}
}
// `/public/dashboards/:uid/query`` endpoint test
func TestAPIQueryPublicDashboard(t *testing.T) {
cacheService := &fakeDatasources.FakeCacheService{
DataSources: []*datasources.DataSource{
{Uid: "mysqlds"},
{Uid: "promds"},
{Uid: "promds2"},
},
}
// used to determine whether fakePluginClient returns an error
queryReturnsError := false
fakePluginClient := &fakePluginClient{
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if queryReturnsError {
return nil, errors.New("error")
}
resp := backend.Responses{}
for _, query := range req.Queries {
resp[query.RefID] = backend.DataResponse{
Frames: []*data.Frame{
{
RefID: query.RefID,
Name: "query-" + query.RefID,
},
},
}
}
return &backend.QueryDataResponse{Responses: resp}, nil
},
}
qds := buildQueryDataService(t, cacheService, fakePluginClient, nil)
setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) {
service := publicdashboards.NewFakePublicDashboardService(t)
testServer := setupTestServer(
t,
setting.NewCfg(),
qds,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled),
service,
nil,
)
return testServer, service
}
t.Run("Status code is 404 when feature toggle is disabled", func(t *testing.T) {
server, _ := setup(false)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusNotFound, resp.Code)
})
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
server, _ := setup(true)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
})
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil)
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
},
}, nil)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "A",
"name": "query-A"
}
}
]
}
}
}`,
resp.Body.String(),
)
require.Equal(t, http.StatusOK, resp.Code)
})
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil)
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
},
}, nil)
queryReturnsError = true
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.Equal(t, http.StatusInternalServerError, resp.Code)
queryReturnsError = false
})
t.Run("Status code is 200 when a panel has queries from multiple datasources", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetPublicDashboard", mock.Anything, mock.Anything).Return(&models.Dashboard{}, nil)
fakeDashboardService.On("GetPublicDashboardConfig", mock.Anything, mock.Anything, mock.Anything).Return(&PublicDashboard{}, nil)
fakeDashboardService.On("BuildAnonymousUser", mock.Anything, mock.Anything, mock.Anything).Return(&models.SignedInUser{}, nil)
fakeDashboardService.On("BuildPublicDashboardMetricRequest", mock.Anything, mock.Anything, mock.Anything, int64(2)).Return(dtos.MetricRequest{
Queries: []*simplejson.Json{
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds"
},
"exemplar": true,
"expr": "query_2_A",
"interval": "",
"legendFormat": "",
"refId": "A"
}
`)),
simplejson.MustJson([]byte(`
{
"datasource": {
"type": "prometheus",
"uid": "promds2"
},
"exemplar": true,
"expr": "query_2_B",
"interval": "",
"legendFormat": "",
"refId": "B"
}
`)),
},
}, nil)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "A",
"name": "query-A"
}
}
]
},
"B": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": [],
"refId": "B",
"name": "query-B"
}
}
]
}
}
}`,
resp.Body.String(),
)
require.Equal(t, http.StatusOK, resp.Code)
})
}
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
db := sqlstore.InitTestDB(t)
cacheService := service.ProvideCacheService(localcache.ProvideService(), db)
qds := buildQueryDataService(t, cacheService, nil, db)
_ = db.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
Uid: "ds1",
OrgId: 1,
Name: "laban",
Type: datasources.DS_MYSQL,
Access: datasources.DS_ACCESS_DIRECT,
Url: "http://test",
Database: "site",
ReadOnly: true,
})
// Create Dashboard
saveDashboardCmd := models.SaveDashboardCommand{
OrgId: 1,
FolderId: 1,
IsFolder: false,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
"id": nil,
"title": "test",
"panels": []map[string]interface{}{
{
"id": 1,
"targets": []map[string]interface{}{
{
"datasource": map[string]string{
"type": "mysql",
"uid": "ds1",
},
"refId": "A",
},
},
},
},
}),
}
// create dashboard
dashboardStore := dashboardStore.ProvideDashboardStore(db)
dashboard, err := dashboardStore.SaveDashboard(saveDashboardCmd)
require.NoError(t, err)
// Create public dashboard
savePubDashboardCmd := &SavePublicDashboardConfigDTO{
DashboardUid: dashboard.Uid,
OrgId: dashboard.OrgId,
PublicDashboard: &PublicDashboard{
IsEnabled: true,
},
}
// create public dashboard
store := publicdashboardsStore.ProvideStore(db)
service := publicdashboardsService.ProvideService(setting.NewCfg(), store)
pubdash, err := service.SavePublicDashboardConfig(context.Background(), savePubDashboardCmd)
require.NoError(t, err)
// setup test server
server := setupTestServer(t,
setting.NewCfg(),
qds,
featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards),
service,
db,
)
resp := callAPI(server, http.MethodPost,
fmt.Sprintf("/api/public/dashboards/%s/panels/1/query", pubdash.AccessToken),
strings.NewReader(`{}`),
t,
)
require.Equal(t, http.StatusOK, resp.Code)
require.NoError(t, err)
require.JSONEq(
t,
`{
"results": {
"A": {
"frames": [
{
"data": {
"values": []
},
"schema": {
"fields": []
}
}
]
}
}
}`,
resp.Body.String(),
)
}