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/plugins/plugins_test.go

583 lines
16 KiB

package plugins
import (
"errors"
"io"
"os"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana/pkg/services/org"
"github.com/stretchr/testify/require"
)
func Test_ReadPluginJSON(t *testing.T) {
tests := []struct {
name string
pluginJSON func(t *testing.T) io.ReadCloser
expected JSONData
err error
}{
{
name: "Valid plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
reader, err := os.Open("manager/testdata/test-app/plugin.json")
require.NoError(t, err)
return reader
},
expected: JSONData{
ID: "test-app",
Type: TypeApp,
Name: "Test App",
Info: Info{
Author: InfoLink{
Name: "Test Inc.",
URL: "http://test.com",
},
Description: "Official Grafana Test App & Dashboard bundle",
Version: "1.0.0",
Links: []InfoLink{
{Name: "Project site", URL: "http://project.com"},
{Name: "License & Terms", URL: "http://license.com"},
},
Logos: Logos{
Small: "img/logo_small.png",
Large: "img/logo_large.png",
},
Screenshots: []Screenshots{
{Path: "img/screenshot1.png", Name: "img1"},
{Path: "img/screenshot2.png", Name: "img2"},
},
Updated: "2015-02-10",
Keywords: []string{"test"},
},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "3.x.x",
Plugins: []Dependency{
{Type: "datasource", ID: "graphite", Name: "Graphite", Version: "1.0.0"},
{Type: "panel", ID: "graph", Name: "Graph", Version: "1.0.0"},
},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
Includes: []*Includes{
{Name: "Nginx Connections", Path: "dashboards/connections.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
{Name: "Nginx Memory", Path: "dashboards/memory.json", Type: "dashboard", Role: org.RoleViewer, Action: ActionAppAccess},
{Name: "Nginx Panel", Type: "panel", Role: org.RoleViewer, Action: ActionAppAccess},
{Name: "Nginx Datasource", Type: "datasource", Role: org.RoleViewer, Action: ActionAppAccess},
},
Backend: false,
},
},
{
name: "Invalid plugin JSON",
pluginJSON: func(t *testing.T) io.ReadCloser {
reader, err := os.Open("manager/testdata/invalid-plugin-json/plugin.json")
require.NoError(t, err)
return reader
},
err: ErrInvalidPluginJSON,
},
{
name: "Default value overrides",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "grafana-piechart-panel",
"name": "This will be overwritten",
"type": "panel",
"includes": [
{"type": "dashboard", "name": "Pie Charts", "path": "dashboards/demo.json"}
]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "grafana-piechart-panel",
Type: TypePanel,
Name: "Pie Chart (old)",
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
Includes: []*Includes{
{Name: "Pie Charts", Path: "dashboards/demo.json", Type: "dashboard", Role: org.RoleViewer},
},
},
},
{
name: "Phlare<>Pyroscope rebranding -- hardcoded alias",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "grafana-pyroscope-datasource",
"type": "datasource",
"aliasIDs": ["phlare"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "grafana-pyroscope-datasource",
AliasIDs: []string{"phlare"}, // Hardcoded from the parser
Type: TypeDataSource,
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaDependency: "",
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read the latest versions of extensions information (v2)",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-extensions-app",
"name": "Extensions App",
"type": "app",
"extensions": {
"addedLinks": [
{
"title": "Added link 1",
"description": "Added link 1 description",
"targets": ["grafana/dashboard/panel/menu"]
}
],
"addedComponents": [
{
"title": "Added component 1",
"description": "Added component 1 description",
"targets": ["grafana/user/profile/tab"]
}
],
"exposedComponents": [
{
"title": "Exposed component 1",
"description": "Exposed component 1 description",
"id": "myorg-extensions-app/component-1/v1"
}
],
"addedFunctions": [
{"targets": ["foo/bar"], "title":"some hook"}
],
"extensionPoints": [
{
"title": "Extension point 1",
"description": "Extension points 1 description",
"id": "myorg-extensions-app/extensions-point-1/v1"
}
]
}
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-extensions-app",
Name: "Extensions App",
Type: TypeApp,
Extensions: Extensions{
AddedLinks: []AddedLink{
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
},
AddedComponents: []AddedComponent{
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
},
ExposedComponents: []ExposedComponent{
{Id: "myorg-extensions-app/component-1/v1", Title: "Exposed component 1", Description: "Exposed component 1 description"},
},
ExtensionPoints: []ExtensionPoint{
{Id: "myorg-extensions-app/extensions-point-1/v1", Title: "Extension point 1", Description: "Extension points 1 description"},
},
AddedFunctions: []AddedFunction{
{Targets: []string{"foo/bar"}, Title: "some hook"},
},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read deprecated extensions info (v1) and parse it as v2",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-extensions-app",
"name": "Extensions App",
"type": "app",
"extensions": [
{
"extensionPointId": "grafana/dashboard/panel/menu",
"title": "Added link 1",
"description": "Added link 1 description",
"type": "link"
},
{
"extensionPointId": "grafana/dashboard/panel/menu",
"title": "Added link 2",
"description": "Added link 2 description",
"type": "link"
},
{
"extensionPointId": "grafana/user/profile/tab",
"title": "Added component 1",
"description": "Added component 1 description",
"type": "component"
}
]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-extensions-app",
Name: "Extensions App",
Type: TypeApp,
Extensions: Extensions{
AddedLinks: []AddedLink{
{Title: "Added link 1", Description: "Added link 1 description", Targets: []string{"grafana/dashboard/panel/menu"}},
{Title: "Added link 2", Description: "Added link 2 description", Targets: []string{"grafana/dashboard/panel/menu"}},
},
AddedComponents: []AddedComponent{
{Title: "Added component 1", Description: "Added component 1 description", Targets: []string{"grafana/user/profile/tab"}},
},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "works if extensions info is empty",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-extensions-app",
"name": "Extensions App",
"type": "app",
"extensions": []
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-extensions-app",
Name: "Extensions App",
Type: TypeApp,
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "works if extensions info is completely missing",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-extensions-app",
"name": "Extensions App",
"type": "app"
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-extensions-app",
Name: "Extensions App",
Type: TypeApp,
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read extensions related dependencies",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-extensions-app",
"name": "Extensions App",
"type": "app",
"dependencies": {
"grafanaDependency": "10.0.0",
"extensions": {
"exposedComponents": ["myorg-extensions-app/component-1/v1"]
}
}
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-extensions-app",
Name: "Extensions App",
Type: TypeApp,
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
GrafanaDependency: "10.0.0",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{"myorg-extensions-app/component-1/v1"},
},
},
},
},
{
name: "can read languages in a datasource plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-datasource",
"name": "Languages Datasource",
"type": "datasource",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-datasource",
Name: "Languages Datasource",
Type: TypeDataSource,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read languages in a panel plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-panel",
"name": "Languages Panel",
"type": "panel",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-panel",
Name: "Languages Panel",
Type: TypePanel,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
{
name: "can read languages in an app plugin",
pluginJSON: func(t *testing.T) io.ReadCloser {
pJSON := `{
"id": "myorg-languages-app",
"name": "Languages App",
"type": "app",
"languages": ["en-US", "pt-BR"]
}`
return io.NopCloser(strings.NewReader(pJSON))
},
expected: JSONData{
ID: "myorg-languages-app",
Name: "Languages App",
Type: TypeApp,
Languages: []string{"en-US", "pt-BR"},
Extensions: Extensions{
AddedLinks: []AddedLink{},
AddedComponents: []AddedComponent{},
AddedFunctions: []AddedFunction{},
ExposedComponents: []ExposedComponent{},
ExtensionPoints: []ExtensionPoint{},
},
Dependencies: Dependencies{
GrafanaVersion: "*",
Plugins: []Dependency{},
Extensions: ExtensionsDependencies{
ExposedComponents: []string{},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := tt.pluginJSON(t)
got, err := ReadPluginJSON(p)
// Check if the test returns the same error as expected
// (unneccary to check further if there is an error at this point)
if tt.err == nil && err != nil {
t.Errorf("Error while reading pluginJSON: %+v", err)
return
}
// Check if the test returns the same error as expected
if tt.err != nil {
require.ErrorIs(t, err, tt.err)
}
// Check if the test returns the expected pluginJSONData
if !cmp.Equal(got, tt.expected) {
t.Errorf("Unexpected pluginJSONData: %v", cmp.Diff(got, tt.expected))
}
// Should be able to close the reader
require.NoError(t, p.Close())
})
}
}
func Test_validatePluginJSON(t *testing.T) {
type args struct {
data JSONData
}
tests := []struct {
name string
args args
err error
}{
{
name: "Valid case",
args: args{
data: JSONData{
ID: "grafana-plugin-id",
Type: TypeDataSource,
},
},
},
{
name: "Invalid plugin ID",
args: args{
data: JSONData{
Type: TypePanel,
},
},
err: ErrInvalidPluginJSON,
},
{
name: "Invalid plugin type",
args: args{
data: JSONData{
ID: "grafana-plugin-id",
Type: "test",
},
},
err: ErrInvalidPluginJSON,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validatePluginJSON(tt.args.data); !errors.Is(err, tt.err) {
t.Errorf("validatePluginJSON() = %v, want %v", err, tt.err)
}
})
}
}