|
|
@ -12,7 +12,6 @@ import ( |
|
|
|
"strings" |
|
|
|
"strings" |
|
|
|
"testing" |
|
|
|
"testing" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert" |
|
|
|
|
|
|
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/stretchr/testify/require" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend" |
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend" |
|
|
@ -135,13 +134,13 @@ func Test_PluginsInstallAndUninstall_AccessControl(t *testing.T) { |
|
|
|
t.Run(testName("Install", tc), func(t *testing.T) { |
|
|
|
t.Run(testName("Install", tc), func(t *testing.T) { |
|
|
|
input := strings.NewReader("{ \"version\": \"1.0.2\" }") |
|
|
|
input := strings.NewReader("{ \"version\": \"1.0.2\" }") |
|
|
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/install", input, t) |
|
|
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/install", input, t) |
|
|
|
assert.Equal(t, tc.expectedCode, response.Code) |
|
|
|
require.Equal(t, tc.expectedCode, response.Code) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
t.Run(testName("Uninstall", tc), func(t *testing.T) { |
|
|
|
t.Run(testName("Uninstall", tc), func(t *testing.T) { |
|
|
|
input := strings.NewReader("{ }") |
|
|
|
input := strings.NewReader("{ }") |
|
|
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/uninstall", input, t) |
|
|
|
response := callAPI(sc.server, http.MethodPost, "/api/plugins/test/uninstall", input, t) |
|
|
|
assert.Equal(t, tc.expectedCode, response.Code) |
|
|
|
require.Equal(t, tc.expectedCode, response.Code) |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -155,56 +154,45 @@ func Test_GetPluginAssets(t *testing.T) { |
|
|
|
require.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
t.Cleanup(func() { |
|
|
|
t.Cleanup(func() { |
|
|
|
err := os.RemoveAll(tmpFile.Name()) |
|
|
|
err := os.RemoveAll(tmpFile.Name()) |
|
|
|
assert.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
err = os.RemoveAll(tmpFileInParentDir.Name()) |
|
|
|
err = os.RemoveAll(tmpFileInParentDir.Name()) |
|
|
|
assert.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
}) |
|
|
|
}) |
|
|
|
expectedBody := "Plugin test" |
|
|
|
expectedBody := "Plugin test" |
|
|
|
_, err = tmpFile.WriteString(expectedBody) |
|
|
|
_, err = tmpFile.WriteString(expectedBody) |
|
|
|
assert.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
|
|
requestedFile := filepath.Clean(tmpFile.Name()) |
|
|
|
requestedFile := filepath.Clean(tmpFile.Name()) |
|
|
|
|
|
|
|
|
|
|
|
t.Run("Given a request for an existing plugin file that is listed as a signature covered file", func(t *testing.T) { |
|
|
|
t.Run("Given a request for an existing plugin file", func(t *testing.T) { |
|
|
|
p := plugins.PluginDTO{ |
|
|
|
p := &plugins.Plugin{ |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
ID: pluginID, |
|
|
|
ID: pluginID, |
|
|
|
}, |
|
|
|
}, |
|
|
|
PluginDir: pluginDir, |
|
|
|
PluginDir: pluginDir, |
|
|
|
SignedFiles: map[string]struct{}{ |
|
|
|
|
|
|
|
requestedFile: {}, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
PluginList: []plugins.PluginDTO{p.ToDTO()}, |
|
|
|
} |
|
|
|
} |
|
|
|
l := &logtest.Fake{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, |
|
|
|
func(sc *scenarioContext) { |
|
|
|
func(sc *scenarioContext) { |
|
|
|
callGetPluginAsset(sc) |
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 200, sc.resp.Code) |
|
|
|
require.Equal(t, 200, sc.resp.Code) |
|
|
|
assert.Equal(t, expectedBody, sc.resp.Body.String()) |
|
|
|
require.Equal(t, expectedBody, sc.resp.Body.String()) |
|
|
|
assert.Zero(t, l.WarnLogs.Calls) |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
t.Run("Given a request for a relative path", func(t *testing.T) { |
|
|
|
t.Run("Given a request for a relative path", func(t *testing.T) { |
|
|
|
p := plugins.PluginDTO{ |
|
|
|
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir) |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
|
|
|
|
ID: pluginID, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
PluginDir: pluginDir, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
} |
|
|
|
} |
|
|
|
l := &logtest.Fake{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name()) |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, tmpFileInParentDir.Name()) |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, |
|
|
|
func(sc *scenarioContext) { |
|
|
|
func(sc *scenarioContext) { |
|
|
|
callGetPluginAsset(sc) |
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
@ -212,44 +200,15 @@ func Test_GetPluginAssets(t *testing.T) { |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
t.Run("Given a request for an existing plugin file that is not listed as a signature covered file", func(t *testing.T) { |
|
|
|
|
|
|
|
p := plugins.PluginDTO{ |
|
|
|
|
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
|
|
|
|
ID: pluginID, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
PluginDir: pluginDir, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
|
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
l := &logtest.Fake{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
|
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
|
|
|
|
func(sc *scenarioContext) { |
|
|
|
|
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 200, sc.resp.Code) |
|
|
|
|
|
|
|
assert.Equal(t, expectedBody, sc.resp.Body.String()) |
|
|
|
|
|
|
|
assert.Zero(t, l.WarnLogs.Calls) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) { |
|
|
|
t.Run("Given a request for an non-existing plugin file", func(t *testing.T) { |
|
|
|
p := plugins.PluginDTO{ |
|
|
|
p := createPluginDTO(plugins.JSONData{ID: pluginID}, plugins.External, pluginDir) |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
|
|
|
|
ID: pluginID, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
PluginDir: pluginDir, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
service := &plugins.FakePluginStore{ |
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
PluginList: []plugins.PluginDTO{p}, |
|
|
|
} |
|
|
|
} |
|
|
|
l := &logtest.Fake{} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requestedFile := "nonExistent" |
|
|
|
requestedFile := "nonExistent" |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, |
|
|
|
func(sc *scenarioContext) { |
|
|
|
func(sc *scenarioContext) { |
|
|
|
callGetPluginAsset(sc) |
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
@ -257,8 +216,7 @@ func Test_GetPluginAssets(t *testing.T) { |
|
|
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson) |
|
|
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson) |
|
|
|
require.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
require.Equal(t, 404, sc.resp.Code) |
|
|
|
require.Equal(t, 404, sc.resp.Code) |
|
|
|
assert.Equal(t, "Plugin file not found", respJson["message"]) |
|
|
|
require.Equal(t, "Plugin file not found", respJson["message"]) |
|
|
|
assert.Zero(t, l.WarnLogs.Calls) |
|
|
|
|
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
@ -270,16 +228,16 @@ func Test_GetPluginAssets(t *testing.T) { |
|
|
|
|
|
|
|
|
|
|
|
requestedFile := "nonExistent" |
|
|
|
requestedFile := "nonExistent" |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, |
|
|
|
func(sc *scenarioContext) { |
|
|
|
func(sc *scenarioContext) { |
|
|
|
callGetPluginAsset(sc) |
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
|
var respJson map[string]interface{} |
|
|
|
var respJson map[string]interface{} |
|
|
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson) |
|
|
|
err := json.NewDecoder(sc.resp.Body).Decode(&respJson) |
|
|
|
require.NoError(t, err) |
|
|
|
require.NoError(t, err) |
|
|
|
assert.Equal(t, 404, sc.resp.Code) |
|
|
|
require.Equal(t, 404, sc.resp.Code) |
|
|
|
assert.Equal(t, "Plugin not found", respJson["message"]) |
|
|
|
require.Equal(t, "Plugin not found", respJson["message"]) |
|
|
|
assert.Zero(t, l.WarnLogs.Calls) |
|
|
|
require.Zero(t, l.WarnLogs.Calls) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
@ -295,13 +253,13 @@ func Test_GetPluginAssets(t *testing.T) { |
|
|
|
l := &logtest.Fake{} |
|
|
|
l := &logtest.Fake{} |
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
url := fmt.Sprintf("/public/plugins/%s/%s", pluginID, requestedFile) |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, l, |
|
|
|
pluginAssetScenario(t, "When calling GET on", url, "/public/plugins/:pluginId/*", service, |
|
|
|
func(sc *scenarioContext) { |
|
|
|
func(sc *scenarioContext) { |
|
|
|
callGetPluginAsset(sc) |
|
|
|
callGetPluginAsset(sc) |
|
|
|
|
|
|
|
|
|
|
|
require.Equal(t, 200, sc.resp.Code) |
|
|
|
require.Equal(t, 200, sc.resp.Code) |
|
|
|
assert.Equal(t, expectedBody, sc.resp.Body.String()) |
|
|
|
require.Equal(t, expectedBody, sc.resp.Body.String()) |
|
|
|
assert.Zero(t, l.WarnLogs.Calls) |
|
|
|
require.Zero(t, l.WarnLogs.Calls) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
@ -348,7 +306,7 @@ func TestMakePluginResourceRequestSetCookieNotPresent(t *testing.T) { |
|
|
|
break |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
assert.Empty(t, resp.Header().Values("Set-Cookie"), "Set-Cookie header should not be present") |
|
|
|
require.Empty(t, resp.Header().Values("Set-Cookie"), "Set-Cookie header should not be present") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) { |
|
|
|
func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) { |
|
|
@ -382,8 +340,8 @@ func TestMakePluginResourceRequestContentTypeUnique(t *testing.T) { |
|
|
|
break |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
assert.Len(t, resp.Header().Values("Content-Type"), 1, "should have 1 Content-Type header") |
|
|
|
require.Len(t, resp.Header().Values("Content-Type"), 1, "should have 1 Content-Type header") |
|
|
|
assert.Len(t, resp.Header().Values("x-another"), 1, "should have 1 X-Another header") |
|
|
|
require.Len(t, resp.Header().Values("x-another"), 1, "should have 1 X-Another header") |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -417,12 +375,11 @@ func callGetPluginAsset(sc *scenarioContext) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store, |
|
|
|
func pluginAssetScenario(t *testing.T, desc string, url string, urlPattern string, pluginStore plugins.Store, |
|
|
|
logger log.Logger, fn scenarioFunc) { |
|
|
|
fn scenarioFunc) { |
|
|
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { |
|
|
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { |
|
|
|
hs := HTTPServer{ |
|
|
|
hs := HTTPServer{ |
|
|
|
Cfg: setting.NewCfg(), |
|
|
|
Cfg: setting.NewCfg(), |
|
|
|
pluginStore: pluginStore, |
|
|
|
pluginStore: pluginStore, |
|
|
|
log: logger, |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sc := setupScenarioContext(t, url) |
|
|
|
sc := setupScenarioContext(t, url) |
|
|
@ -478,42 +435,40 @@ func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryData |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func Test_PluginsList_AccessControl(t *testing.T) { |
|
|
|
func Test_PluginsList_AccessControl(t *testing.T) { |
|
|
|
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{ |
|
|
|
p1 := &plugins.Plugin{ |
|
|
|
{ |
|
|
|
PluginDir: "/grafana/plugins/test-app/dist", |
|
|
|
PluginDir: "/grafana/plugins/test-app/dist", |
|
|
|
Class: plugins.External, |
|
|
|
Class: "external", |
|
|
|
DefaultNavURL: "/plugins/test-app/page/test", |
|
|
|
DefaultNavURL: "/plugins/test-app/page/test", |
|
|
|
Signature: plugins.SignatureUnsigned, |
|
|
|
Pinned: false, |
|
|
|
Module: "plugins/test-app/module", |
|
|
|
Signature: "unsigned", |
|
|
|
BaseURL: "public/plugins/test-app", |
|
|
|
Module: "plugins/test-app/module", |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
BaseURL: "public/plugins/test-app", |
|
|
|
ID: "test-app", |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
Type: plugins.App, |
|
|
|
ID: "test-app", |
|
|
|
Name: "test-app", |
|
|
|
Type: "app", |
|
|
|
Info: plugins.Info{ |
|
|
|
Name: "test-app", |
|
|
|
Version: "1.0.0", |
|
|
|
Info: plugins.Info{ |
|
|
|
|
|
|
|
Version: "1.0.0", |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
{ |
|
|
|
} |
|
|
|
PluginDir: "/grafana/public/app/plugins/datasource/mysql", |
|
|
|
p2 := &plugins.Plugin{ |
|
|
|
Class: "core", |
|
|
|
PluginDir: "/grafana/public/app/plugins/datasource/mysql", |
|
|
|
Pinned: false, |
|
|
|
Class: plugins.Core, |
|
|
|
Signature: "internal", |
|
|
|
Pinned: false, |
|
|
|
Module: "app/plugins/datasource/mysql/module", |
|
|
|
Signature: plugins.SignatureInternal, |
|
|
|
BaseURL: "public/app/plugins/datasource/mysql", |
|
|
|
Module: "app/plugins/datasource/mysql/module", |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
BaseURL: "public/app/plugins/datasource/mysql", |
|
|
|
ID: "mysql", |
|
|
|
JSONData: plugins.JSONData{ |
|
|
|
Type: "datasource", |
|
|
|
ID: "mysql", |
|
|
|
Name: "MySQL", |
|
|
|
Type: plugins.DataSource, |
|
|
|
Info: plugins.Info{ |
|
|
|
Name: "MySQL", |
|
|
|
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"}, |
|
|
|
Info: plugins.Info{ |
|
|
|
Description: "Data source for MySQL databases", |
|
|
|
Author: plugins.InfoLink{Name: "Grafana Labs", URL: "https://grafana.com"}, |
|
|
|
}, |
|
|
|
Description: "Data source for MySQL databases", |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}} |
|
|
|
} |
|
|
|
|
|
|
|
pluginStore := plugins.FakePluginStore{PluginList: []plugins.PluginDTO{p1.ToDTO(), p2.ToDTO()}} |
|
|
|
|
|
|
|
|
|
|
|
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{ |
|
|
|
pluginSettings := pluginsettings.FakePluginSettings{Plugins: map[string]*pluginsettings.DTO{ |
|
|
|
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true}, |
|
|
|
"test-app": {ID: 0, OrgID: 1, PluginID: "test-app", PluginVersion: "1.0.0", Enabled: true}, |
|
|
@ -574,3 +529,12 @@ func Test_PluginsList_AccessControl(t *testing.T) { |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func createPluginDTO(jd plugins.JSONData, class plugins.Class, pluginDir string) plugins.PluginDTO { |
|
|
|
|
|
|
|
p := &plugins.Plugin{ |
|
|
|
|
|
|
|
JSONData: jd, |
|
|
|
|
|
|
|
Class: class, |
|
|
|
|
|
|
|
PluginDir: pluginDir, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return p.ToDTO() |
|
|
|
|
|
|
|
} |
|
|
|