Files
grafana/pkg/services/dashboardimport/api/api_test.go
Alexa Vargas 62eef87208 Dashboard Library: Integrate community dashboards on Suggested Dashboards Flow (#112808)
* Extend interpolate endpoint to support community dashboard json interpolation
Added unit tests

* Implement Frontend Side
- Show tabs
- Fetch Community dashboads
- Use DashboardCard component
- Search grafana dashboard in the community tab
- Make Tabs and pagination sticky
- Adjust titles to be scoped by datasource name/type
- Add skeleton loading for community tabs and pagination
- Add dashboard details tooltip
- Bring old datasource-provisioned box back and rely on new feature toggle for community dashboards
- update i18n
- update swagger
---------

Co-authored-by: Juan Cabanas <juan.cabanas@grafana.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
2025-11-11 10:40:39 +01:00

482 lines
18 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web/webtest"
)
func TestImportDashboardAPI(t *testing.T) {
t.Run("Quota not reached, schema loader service disabled", func(t *testing.T) {
importDashboardServiceCalled := false
service := &serviceMock{
importDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
importDashboardServiceCalled = true
return nil, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
t.Run("Not signed in should return 404", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("Signed in, empty plugin id and dashboard model empty should return error", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "",
Dashboard: nil,
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
})
t.Run("Signed in, dashboard model set should call import dashboard service", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, importDashboardServiceCalled)
})
t.Run("Signed in, dashboard model set, trimdefaults enabled should not call schema loader service", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, importDashboardServiceCalled)
})
})
t.Run("Quota not reached, schema loader service enabled", func(t *testing.T) {
importDashboardServiceCalled := false
service := &serviceMock{
importDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
importDashboardServiceCalled = true
return nil, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
t.Run("Signed in, dashboard model set, trimdefaults enabled should call schema loader service", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import?trimdefaults=true", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, importDashboardServiceCalled)
})
})
t.Run("Quota reached", func(t *testing.T) {
service := &serviceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, featuremgmt.WithFeatures())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
t.Run("Signed in, dashboard model set, should return 403 forbidden/quota reached", func(t *testing.T) {
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/import", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusForbidden, resp.StatusCode)
})
})
}
func TestInterpolateDashboardFeatureFlag(t *testing.T) {
t.Run("Feature flag disabled - interpolate endpoint should return 404", func(t *testing.T) {
service := &serviceMock{}
// Create features with dashboardLibrary disabled
features := featuremgmt.WithFeatures()
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("dashboardLibrary feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
interpolateDashboardServiceCalled := false
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
interpolateDashboardServiceCalled = true
return simplejson.New(), nil
},
}
// Create features with dashboardLibrary enabled
features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "test-plugin",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, interpolateDashboardServiceCalled)
})
t.Run("suggestedDashboards feature flag enabled - interpolate endpoint should work", func(t *testing.T) {
interpolateDashboardServiceCalled := false
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
interpolateDashboardServiceCalled = true
return simplejson.New(), nil
},
}
// Create features with suggestedDashboards enabled
features := featuremgmt.WithFeatures(featuremgmt.FlagSuggestedDashboards)
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "test-plugin",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, interpolateDashboardServiceCalled)
})
}
func TestInterpolateDashboardAPI(t *testing.T) {
features := featuremgmt.WithFeatures(featuremgmt.FlagDashboardLibrary)
t.Run("Backward compatibility - plugin-based flow still works", func(t *testing.T) {
var capturedReq *dashboardimport.ImportDashboardRequest
interpolateDashboardServiceCalled := false
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
interpolateDashboardServiceCalled = true
capturedReq = req
result := simplejson.New()
result.Set("title", "Test Dashboard")
return result, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "test-plugin",
Path: "dashboards/test.json",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, interpolateDashboardServiceCalled)
require.NotNil(t, capturedReq)
require.Equal(t, "test-plugin", capturedReq.PluginId)
require.Equal(t, "dashboards/test.json", capturedReq.Path)
})
t.Run("New flow with dashboard JSON - should call service with dashboard", func(t *testing.T) {
var capturedReq *dashboardimport.ImportDashboardRequest
interpolateDashboardServiceCalled := false
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
interpolateDashboardServiceCalled = true
capturedReq = req
result := simplejson.New()
result.Set("title", "Community Dashboard")
result.Set("panels", []interface{}{})
return result, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
// Create a test dashboard with datasource that needs interpolation
testDashboard := simplejson.New()
testDashboard.Set("title", "Test Community Dashboard")
testDashboard.Set("panels", []interface{}{
map[string]interface{}{
"datasource": "${DS_PROMETHEUS}",
},
})
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: testDashboard,
Inputs: []dashboardimport.ImportDashboardInput{
{Name: "DS_PROMETHEUS", Type: "datasource", PluginId: "prometheus", Value: "my-prometheus"},
},
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, interpolateDashboardServiceCalled)
require.NotNil(t, capturedReq)
require.NotNil(t, capturedReq.Dashboard)
require.Len(t, capturedReq.Inputs, 1)
require.Equal(t, "DS_PROMETHEUS", capturedReq.Inputs[0].Name)
})
t.Run("Validation - both pluginId and dashboard missing should return error", func(t *testing.T) {
service := &serviceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "",
Dashboard: nil,
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
})
t.Run("Response should not include internal fields", func(t *testing.T) {
service := &serviceMock{
interpolateDashboardFunc: func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
result := simplejson.New()
result.Set("title", "Test Dashboard")
result.Set("__elements", map[string]interface{}{"test": "value"})
result.Set("__inputs", []interface{}{})
result.Set("__requires", []interface{}{})
result.Set("panels", []interface{}{})
return result, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
PluginId: "test-plugin",
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
UserID: 1,
Permissions: map[int64]map[string][]string{
1: {dashboards.ActionDashboardsCreate: {}},
},
})
resp, err := s.SendJSON(req)
require.NoError(t, err)
defer func() { require.NoError(t, resp.Body.Close()) }()
require.Equal(t, http.StatusOK, resp.StatusCode)
// Parse response body and verify internal fields are removed
var responseBody map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&responseBody)
require.NoError(t, err)
require.Equal(t, "Test Dashboard", responseBody["title"])
require.NotContains(t, responseBody, "__elements")
require.NotContains(t, responseBody, "__inputs")
require.NotContains(t, responseBody, "__requires")
})
t.Run("Not signed in should return 401", func(t *testing.T) {
service := &serviceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, actest.FakeAccessControl{ExpectedEvaluate: true}, features)
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
cmd := &dashboardimport.ImportDashboardRequest{
Dashboard: simplejson.New(),
}
jsonBytes, err := json.Marshal(cmd)
require.NoError(t, err)
req := s.NewPostRequest("/api/dashboards/interpolate", bytes.NewReader(jsonBytes))
resp, err := s.SendJSON(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
}
type serviceMock struct {
importDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error)
interpolateDashboardFunc func(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error)
}
func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*dashboardimport.ImportDashboardResponse, error) {
if s.importDashboardFunc != nil {
return s.importDashboardFunc(ctx, req)
}
return nil, nil
}
func (s *serviceMock) InterpolateDashboard(ctx context.Context, req *dashboardimport.ImportDashboardRequest) (*simplejson.Json, error) {
if s.interpolateDashboardFunc != nil {
return s.interpolateDashboardFunc(ctx, req)
}
return simplejson.New(), nil
}
func quotaReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) {
return true, nil
}
func quotaNotReached(c *contextmodel.ReqContext, target quota.TargetSrv) (bool, error) {
return false, nil
}