package api import ( "context" "encoding/json" "errors" "fmt" "net/http" "strings" "testing" "github.com/aws/aws-sdk-go/aws" "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/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations/annotationstest" dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database" "github.com/grafana/grafana/pkg/services/datasources" datasourcesService "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/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestAPIViewPublicDashboard(t *testing.T) { DashboardUid := "dashboard-abcd1234" testCases := []struct { Name string AccessToken string ExpectedHttpResponse int DashboardResult *models.Dashboard Err error FixedErrorResponse string }{ { Name: "It gets a public dashboard", AccessToken: validAccessToken, ExpectedHttpResponse: http.StatusOK, DashboardResult: &models.Dashboard{ Data: simplejson.NewFromAny(map[string]interface{}{ "Uid": DashboardUid, }), }, Err: nil, FixedErrorResponse: "", }, { Name: "It should return 404 if no public dashboard", AccessToken: validAccessToken, ExpectedHttpResponse: http.StatusNotFound, DashboardResult: nil, Err: ErrPublicDashboardNotFound, FixedErrorResponse: "", }, { Name: "It should return 400 if it is an invalid access token", AccessToken: "SomeInvalidAccessToken", ExpectedHttpResponse: http.StatusBadRequest, DashboardResult: nil, Err: nil, FixedErrorResponse: "{\"message\":\"Invalid Access Token\"}", }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { service := publicdashboards.NewFakePublicDashboardService(t) service.On("FindPublicDashboardAndDashboardByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{}, test.DashboardResult, test.Err).Maybe() cfg := setting.NewCfg() cfg.RBACEnabled = false testServer := setupTestServer( t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser, ) response := callAPI(testServer, http.MethodGet, fmt.Sprintf("/api/public/dashboards/%s", test.AccessToken), nil, t, ) assert.Equal(t, test.ExpectedHttpResponse, response.Code) if test.Err == nil && test.FixedErrorResponse == "" { 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 if test.FixedErrorResponse != "" { require.Equal(t, test.ExpectedHttpResponse, response.Code) require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", response.Body.String()) } else { var errResp JsonErrResponse err := json.Unmarshal(response.Body.Bytes(), &errResp) require.NoError(t, err) assert.Equal(t, test.Err.Error(), errResp.Error) } }) } } // `/public/dashboards/:uid/query“ endpoint test func TestAPIQueryPublicDashboard(t *testing.T) { mockedResponse := &backend.QueryDataResponse{ Responses: map[string]backend.DataResponse{ "test": { Frames: data.Frames{ &data.Frame{ Name: "anyDataFrame", Fields: []*data.Field{ data.NewField("anyGroupName", nil, []*string{ aws.String("group_a"), aws.String("group_b"), aws.String("group_c"), }), }, }, }, Error: nil, }, }, } expectedResponse := `{ "results": { "test": { "frames": [ { "schema": { "name": "anyDataFrame", "fields": [ { "name": "anyGroupName", "type": "string", "typeInfo": { "frame": "string", "nullable": true } } ] }, "data": { "values": [ [ "group_a", "group_b", "group_c" ] ] } } ] } } }` setup := func(enabled bool) (*web.Mux, *publicdashboards.FakePublicDashboardService) { service := publicdashboards.NewFakePublicDashboardService(t) cfg := setting.NewCfg() cfg.RBACEnabled = false testServer := setupTestServer( t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards, enabled), service, nil, anonymousUser, ) return testServer, service } t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) { server, _ := setup(true) path := fmt.Sprintf("/api/public/dashboards/%s/panels/notanumber/query", validAccessToken) resp := callAPI(server, http.MethodPost, path, strings.NewReader("{}"), t) require.Equal(t, http.StatusBadRequest, resp.Code) }) t.Run("Status code is 400 when the access token is invalid", func(t *testing.T) { server, _ := setup(true) resp := callAPI(server, http.MethodPost, getValidQueryPath("SomeInvalidAccessToken"), strings.NewReader("{}"), t) require.Equal(t, http.StatusBadRequest, resp.Code) require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", resp.Body.String()) }) t.Run("Status code is 400 when the intervalMS is lesser than 0", func(t *testing.T) { server, fakeDashboardService := setup(true) fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest) resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t) require.Equal(t, http.StatusBadRequest, resp.Code) }) t.Run("Status code is 400 when the maxDataPoints is lesser than 0", func(t *testing.T) { server, fakeDashboardService := setup(true) fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest) resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), 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("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(mockedResponse, nil) resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t) require.JSONEq( t, expectedResponse, 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("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, fmt.Errorf("error")) resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t) require.Equal(t, http.StatusInternalServerError, resp.Code) }) } func getValidQueryPath(accessToken string) string { return fmt.Sprintf("/api/public/dashboards/%s/panels/2/query", accessToken) } func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) { db := db.InitTestDB(t) cacheService := datasourcesService.ProvideCacheService(localcache.ProvideService(), db) qds := buildQueryDataService(t, cacheService, nil, db) dsStore := datasourcesService.CreateStore(db, log.New("publicdashboards.test")) _ = dsStore.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 dashboardStoreService := dashboardStore.ProvideDashboardStore(db, db.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, db.Cfg)) dashboard, err := dashboardStoreService.SaveDashboard(context.Background(), saveDashboardCmd) require.NoError(t, err) // Create public dashboard savePubDashboardCmd := &SavePublicDashboardDTO{ DashboardUid: dashboard.Uid, OrgId: dashboard.OrgId, PublicDashboard: &PublicDashboard{ IsEnabled: true, }, } annotationsService := annotationstest.NewFakeAnnotationsRepo() // create public dashboard store := publicdashboardsStore.ProvideStore(db) cfg := setting.NewCfg() ac := acmock.New() cfg.RBACEnabled = false service := publicdashboardsService.ProvideService(cfg, store, qds, annotationsService, ac) pubdash, err := service.Save(context.Background(), &user.SignedInUser{}, savePubDashboardCmd) require.NoError(t, err) // setup test server server := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, db, anonymousUser, ) 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(), ) } func TestAPIGetAnnotations(t *testing.T) { testCases := []struct { Name string ExpectedHttpResponse int Annotations []AnnotationEvent ServiceError error AccessToken string From string To string ExpectedServiceCalled bool }{ { Name: "will return success when there is no error and to and from are provided", ExpectedHttpResponse: http.StatusOK, Annotations: []AnnotationEvent{{Id: 1}}, ServiceError: nil, AccessToken: validAccessToken, From: "123", To: "123", ExpectedServiceCalled: true, }, { Name: "will return 500 when service returns an error", ExpectedHttpResponse: http.StatusInternalServerError, Annotations: nil, ServiceError: errors.New("an error happened"), AccessToken: validAccessToken, From: "123", To: "123", ExpectedServiceCalled: true, }, { Name: "will return 400 when has an incorrect Access Token", ExpectedHttpResponse: http.StatusBadRequest, Annotations: nil, ServiceError: errors.New("an error happened"), AccessToken: "TooShortAccessToken", From: "123", To: "123", ExpectedServiceCalled: false, }, } for _, test := range testCases { t.Run(test.Name, func(t *testing.T) { cfg := setting.NewCfg() cfg.RBACEnabled = false service := publicdashboards.NewFakePublicDashboardService(t) if test.ExpectedServiceCalled { service.On("FindAnnotations", mock.Anything, mock.Anything, mock.AnythingOfType("string")). Return(test.Annotations, test.ServiceError).Once() } testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser) path := fmt.Sprintf("/api/public/dashboards/%s/annotations?from=%s&to=%s", test.AccessToken, test.From, test.To) response := callAPI(testServer, http.MethodGet, path, nil, t) assert.Equal(t, test.ExpectedHttpResponse, response.Code) if test.ExpectedHttpResponse == http.StatusOK { var items []AnnotationEvent err := json.Unmarshal(response.Body.Bytes(), &items) assert.NoError(t, err) assert.Equal(t, items, test.Annotations) } }) } }