package signature

import (
	"context"
	"io/fs"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/ProtonMail/go-crypto/openpgp/clearsign"
	openpgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/grafana/grafana/pkg/plugins"
	"github.com/grafana/grafana/pkg/plugins/config"
	"github.com/grafana/grafana/pkg/plugins/manager/fakes"
	"github.com/grafana/grafana/pkg/plugins/manager/signature/statickey"
)

func provideDefaultTestService() *Signature {
	return provideTestServiceWithConfig(&config.PluginManagementCfg{})
}

func provideTestServiceWithConfig(cfg *config.PluginManagementCfg) *Signature {
	return ProvideService(cfg, statickey.New())
}

func TestReadPluginManifest(t *testing.T) {
	txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

{
  "plugin": "grafana-googlesheets-datasource",
  "version": "1.0.0-dev",
  "files": {
    "LICENSE": "7df059597099bb7dcf25d2a9aedfaf4465f72d8d",
    "README.md": "08ec6d704b6115bef57710f6d7e866c050cb50ee",
    "gfx_sheets_darwin_amd64": "1b8ae92c6e80e502bb0bf2d0ae9d7223805993ab",
    "gfx_sheets_linux_amd64": "f39e0cc7344d3186b1052e6d356eecaf54d75b49",
    "gfx_sheets_windows_amd64.exe": "c8825dfec512c1c235244f7998ee95182f9968de",
    "module.js": "aaec6f51a995b7b843b843cd14041925274d960d",
    "module.js.LICENSE.txt": "7f822fe9341af8f82ad1b0c69aba957822a377cf",
    "module.js.map": "c5a524f5c4237f6ed6a016d43cd46938efeadb45",
    "plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f"
  },
  "time": 1586817677115,
  "keyId": "7e4d0c6a708866e7"
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org

wqEEARMKAAYFAl6U6o0ACgkQfk0ManCIZuevWAIHSvcxOy1SvvL5gC+HpYyG
VbSsUvF2FsCoXUCTQflK6VdJfSPNzm8YdCdx7gNrBdly6HEs06ZaRp44F/ve
NR7DnB0CCQHO+4FlSPtXFTzNepoc+CytQyDAeOLMLmf2Tqhk2YShk+G/YlVX
74uuP5UXZxwK2YKJovdSknDIU7MhfuvvQIP/og==
=hBea
-----END PGP SIGNATURE-----`

	t.Run("valid manifest", func(t *testing.T) {
		s := provideDefaultTestService()
		manifest, err := s.readPluginManifest(context.Background(), []byte(txt))

		require.NoError(t, err)
		require.NotNil(t, manifest)
		assert.Equal(t, "grafana-googlesheets-datasource", manifest.Plugin)
		assert.Equal(t, "1.0.0-dev", manifest.Version)
		assert.Equal(t, int64(1586817677115), manifest.Time)
		assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
		expectedFiles := []string{"LICENSE", "README.md", "gfx_sheets_darwin_amd64", "gfx_sheets_linux_amd64",
			"gfx_sheets_windows_amd64.exe", "module.js", "module.js.LICENSE.txt", "module.js.map", "plugin.json",
		}
		assert.Equal(t, expectedFiles, fileList(manifest))
	})

	t.Run("invalid manifest", func(t *testing.T) {
		s := provideDefaultTestService()
		modified := strings.ReplaceAll(txt, "README.md", "xxxxxxxxxx")
		_, err := s.readPluginManifest(context.Background(), []byte(modified))
		require.Error(t, err)
	})
}

func TestReadPluginManifestV2(t *testing.T) {
	txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

{
  "manifestVersion": "2.0.0",
  "signatureType": "private",
  "signedByOrg": "willbrowne",
  "signedByOrgName": "Will Browne",
  "rootUrls": [
    "http://localhost:3000/"
  ],
  "plugin": "test",
  "version": "1.0.0",
  "time": 1605807018050,
  "keyId": "7e4d0c6a708866e7",
  "files": {
    "plugin.json": "2bb467c0bfd6c454551419efe475b8bf8573734e73c7bab52b14842adb62886f"
  }
}
-----BEGIN PGP SIGNATURE-----
Version: OpenPGP.js v4.10.1
Comment: https://openpgpjs.org

wqIEARMKAAYFAl+2q6oACgkQfk0ManCIZudmzwIJAXWz58cd/91rTXszKPnE
xbVEvERCbjKTtPBQBNQyqEvV+Ig3MuBSNOVy2SOGrMsdbS6lONgvgt4Cm+iS
wV+vYifkAgkBJtg/9DMB7/iX5O0h49CtSltcpfBFXlGqIeOwRac/yENzRzAA
khdr/tZ1PDgRxMqB/u+Vtbpl0xSxgblnrDOYMSI=
=rLIE
-----END PGP SIGNATURE-----`

	t.Run("valid manifest", func(t *testing.T) {
		s := provideDefaultTestService()
		manifest, err := s.readPluginManifest(context.Background(), []byte(txt))

		require.NoError(t, err)
		require.NotNil(t, manifest)
		assert.Equal(t, "test", manifest.Plugin)
		assert.Equal(t, "1.0.0", manifest.Version)
		assert.Equal(t, int64(1605807018050), manifest.Time)
		assert.Equal(t, "7e4d0c6a708866e7", manifest.KeyID)
		assert.Equal(t, "2.0.0", manifest.ManifestVersion)
		assert.Equal(t, plugins.SignatureTypePrivate, manifest.SignatureType)
		assert.Equal(t, "willbrowne", manifest.SignedByOrg)
		assert.Equal(t, "Will Browne", manifest.SignedByOrgName)
		assert.Equal(t, []string{"http://localhost:3000/"}, manifest.RootURLs)
		assert.Equal(t, []string{"plugin.json"}, fileList(manifest))
	})
}

func TestCalculate(t *testing.T) {
	parentDir, err := filepath.Abs("../")
	if err != nil {
		t.Errorf("could not construct absolute path of current dir")
		return
	}

	t.Run("Validate root URL against App URL for non-private plugin if is specified in manifest", func(t *testing.T) {
		tcs := []struct {
			appURL            string
			expectedSignature plugins.Signature
		}{
			{
				appURL: "https://dev.grafana.com",
				expectedSignature: plugins.Signature{
					Status:     plugins.SignatureStatusValid,
					Type:       plugins.SignatureTypeGrafana,
					SigningOrg: "Grafana Labs",
				},
			},
			{
				appURL: "https://non.matching.url.com",
				expectedSignature: plugins.Signature{
					Status: plugins.SignatureStatusInvalid,
				},
			},
		}

		for _, tc := range tcs {
			basePath := filepath.Join(parentDir, "testdata/non-pvt-with-root-url/plugin")
			s := provideTestServiceWithConfig(&config.PluginManagementCfg{GrafanaAppURL: tc.appURL})
			sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
				PluginClassFunc: func(ctx context.Context) plugins.Class {
					return plugins.ClassExternal
				},
			}, plugins.FoundPlugin{
				JSONData: plugins.JSONData{
					ID: "test-datasource",
					Info: plugins.Info{
						Version: "1.0.0",
					},
				},
				FS: mustNewStaticFSForTests(t, basePath),
			})
			require.NoError(t, err)
			require.Equal(t, tc.expectedSignature, sig)
		}
	})

	t.Run("Unsigned Chromium file should not invalidate signature for Renderer plugin running on Windows", func(t *testing.T) {
		backup := runningWindows
		t.Cleanup(func() {
			runningWindows = backup
		})

		basePath := "../testdata/renderer-added-file/plugin"

		runningWindows = true
		s := provideDefaultTestService()
		sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
			PluginClassFunc: func(ctx context.Context) plugins.Class {
				return plugins.ClassExternal
			},
		}, plugins.FoundPlugin{
			JSONData: plugins.JSONData{
				ID:   "test-renderer",
				Type: plugins.TypeRenderer,
				Info: plugins.Info{
					Version: "1.0.0",
				},
			},
			FS: mustNewStaticFSForTests(t, basePath),
		})
		require.NoError(t, err)
		require.Equal(t, plugins.Signature{
			Status:     plugins.SignatureStatusValid,
			Type:       plugins.SignatureTypeGrafana,
			SigningOrg: "Grafana Labs",
		}, sig)
	})

	t.Run("Signature verification should work with any path separator", func(t *testing.T) {
		const basePath = "../testdata/app-with-child/dist"

		platformWindows := fsPlatform{separator: '\\'}
		platformUnix := fsPlatform{separator: '/'}

		type testCase struct {
			name      string
			platform  fsPlatform
			fsFactory func() (plugins.FS, error)
		}
		var testCases []testCase
		for _, fsFactory := range []struct {
			name string
			f    func() (plugins.FS, error)
		}{
			{"local fs", func() (plugins.FS, error) {
				return plugins.NewLocalFS(basePath), nil
			}},
			{"static fs", func() (plugins.FS, error) {
				return plugins.NewStaticFS(plugins.NewLocalFS(basePath))
			}},
		} {
			testCases = append(testCases, []testCase{
				{"unix " + fsFactory.name, platformUnix, fsFactory.f},
				{"windows " + fsFactory.name, platformWindows, fsFactory.f},
			}...)
		}

		for _, tc := range testCases {
			t.Run(tc.name, func(t *testing.T) {
				// Replace toSlash for cross-platform testing
				oldToSlash := toSlash
				oldFromSlash := fromSlash
				t.Cleanup(func() {
					toSlash = oldToSlash
					fromSlash = oldFromSlash
				})
				toSlash = tc.platform.toSlashFunc()
				fromSlash = tc.platform.fromSlashFunc()

				s := provideDefaultTestService()
				pfs, err := tc.fsFactory()
				require.NoError(t, err)
				pfs, err = newPathSeparatorOverrideFS(string(tc.platform.separator), pfs)
				require.NoError(t, err)
				sig, err := s.Calculate(context.Background(), &fakes.FakePluginSource{
					PluginClassFunc: func(ctx context.Context) plugins.Class {
						return plugins.ClassExternal
					},
				}, plugins.FoundPlugin{
					JSONData: plugins.JSONData{
						ID:   "myorgid-simple-app",
						Type: plugins.TypeApp,
						Info: plugins.Info{
							Version: "%VERSION%",
						},
					},
					FS: pfs,
				})
				require.NoError(t, err)
				require.Equal(t, plugins.Signature{
					Status:     plugins.SignatureStatusValid,
					Type:       plugins.SignatureTypeGrafana,
					SigningOrg: "Grafana Labs",
				}, sig)
			})
		}
	})
}

type fsPlatform struct {
	separator rune
}

// toSlashFunc returns a new function that acts as filepath.ToSlash but for the specified os-separator.
// This can be used to test filepath.ToSlash-dependant code cross-platform.
func (p fsPlatform) toSlashFunc() func(string) string {
	return func(path string) string {
		if p.separator == '/' {
			return path
		}
		return strings.ReplaceAll(path, string(p.separator), "/")
	}
}

// fromSlashFunc returns a new function that acts as filepath.FromSlash but for the specified os-separator.
// This can be used to test filepath.FromSlash-dependant code cross-platform.
func (p fsPlatform) fromSlashFunc() func(string) string {
	return func(path string) string {
		if p.separator == '/' {
			return path
		}
		return strings.ReplaceAll(path, "/", string(p.separator))
	}
}

func TestFsPlatform(t *testing.T) {
	t.Run("unix", func(t *testing.T) {
		toSlashUnix := fsPlatform{'/'}.toSlashFunc()
		require.Equal(t, "folder", toSlashUnix("folder"))
		require.Equal(t, "/folder", toSlashUnix("/folder"))
		require.Equal(t, "/folder/file", toSlashUnix("/folder/file"))
		require.Equal(t, "/folder/other\\file", toSlashUnix("/folder/other\\file"))
	})

	t.Run("windows", func(t *testing.T) {
		toSlashWindows := fsPlatform{'\\'}.toSlashFunc()
		require.Equal(t, "folder", toSlashWindows("folder"))
		require.Equal(t, "C:/folder", toSlashWindows("C:\\folder"))
		require.Equal(t, "folder/file.exe", toSlashWindows("folder\\file.exe"))
	})
}

// fsPathSeparatorFiles embeds a plugins.FS and overrides the Files() behaviour so all the returned elements
// have the specified path separator. This can be used to test Files() behaviour cross-platform.
type fsPathSeparatorFiles struct {
	plugins.FS

	separator string
}

// newPathSeparatorOverrideFS returns a new fsPathSeparatorFiles. Sep is the separator that will be used ONLY for
// the elements returned by Files().
func newPathSeparatorOverrideFS(sep string, ufs plugins.FS) (fsPathSeparatorFiles, error) {
	return fsPathSeparatorFiles{
		FS:        ufs,
		separator: sep,
	}, nil
}

// Files returns LocalFS.Files(), but all path separators for the current platform (filepath.Separator)
// are replaced with f.separator.
func (f fsPathSeparatorFiles) Files() ([]string, error) {
	files, err := f.FS.Files()
	if err != nil {
		return nil, err
	}
	const osSepStr = string(filepath.Separator)
	for i := 0; i < len(files); i++ {
		files[i] = strings.ReplaceAll(files[i], osSepStr, f.separator)
	}
	return files, nil
}

func (f fsPathSeparatorFiles) Rel(base string) (string, error) {
	return filepath.Rel(f.Base(), strings.ReplaceAll(base, f.separator, string(filepath.Separator)))
}

func (f fsPathSeparatorFiles) Open(name string) (fs.File, error) {
	return f.FS.Open(strings.ReplaceAll(name, f.separator, string(filepath.Separator)))
}

func TestFSPathSeparatorFiles(t *testing.T) {
	for _, tc := range []struct {
		name string
		sep  string
	}{
		{"unix", "/"},
		{"windows", "\\"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			pfs, err := newPathSeparatorOverrideFS(
				tc.sep, plugins.NewInMemoryFS(
					map[string][]byte{"a": nil, strings.Join([]string{"a", "b", "c"}, tc.sep): nil},
				),
			)
			require.NoError(t, err)
			files, err := pfs.Files()
			require.NoError(t, err)
			exp := []string{"a", strings.Join([]string{"a", "b", "c"}, tc.sep)}
			sort.Strings(files)
			sort.Strings(exp)
			require.Equal(t, exp, files)
		})
	}
}

func fileList(manifest *PluginManifest) []string {
	keys := make([]string, 0, len(manifest.Files))
	for k := range manifest.Files {
		keys = append(keys, k)
	}

	sort.Strings(keys)

	return keys
}

func Test_urlMatch_privateGlob(t *testing.T) {
	type args struct {
		specs  []string
		target string
	}
	tests := []struct {
		name        string
		args        args
		shouldMatch bool
	}{
		{
			name: "Support single wildcard matching single subdomain",
			args: args{
				specs:  []string{"https://*.example.com"},
				target: "https://test.example.com",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support single wildcard matching multiple subdomains",
			args: args{
				specs:  []string{"https://*.example.com"},
				target: "https://more.test.example.com",
			},
			shouldMatch: false,
		},
		{
			name: "Support multiple wildcards matching multiple subdomains",
			args: args{
				specs:  []string{"https://**.example.com"},
				target: "https://test.example.com",
			},
			shouldMatch: true,
		},
		{
			name: "Support multiple wildcards matching multiple subdomains",
			args: args{
				specs:  []string{"https://**.example.com"},
				target: "https://more.test.example.com",
			},
			shouldMatch: true,
		},
		{
			name: "Support single wildcard matching single paths",
			args: args{
				specs:  []string{"https://www.example.com/*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support single wildcard matching multiple paths",
			args: args{
				specs:  []string{"https://www.example.com/*"},
				target: "https://www.example.com/other/grafana",
			},
			shouldMatch: false,
		},
		{
			name: "Support double wildcard matching multiple paths",
			args: args{
				specs:  []string{"https://www.example.com/**"},
				target: "https://www.example.com/other/grafana",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support subdomain mismatch",
			args: args{
				specs:  []string{"https://www.test.example.com/grafana/docs"},
				target: "https://www.dev.example.com/grafana/docs",
			},
			shouldMatch: false,
		},
		{
			name: "Support single wildcard matching single path",
			args: args{
				specs:  []string{"https://www.example.com/grafana*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support single wildcard matching different path prefix",
			args: args{
				specs:  []string{"https://www.example.com/grafana*"},
				target: "https://www.example.com/somethingelse",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support path mismatch",
			args: args{
				specs:  []string{"https://example.com/grafana"},
				target: "https://example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Support both domain and path wildcards",
			args: args{
				specs:  []string{"https://*.example.com/*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support wildcards without TLDs",
			args: args{
				specs:  []string{"https://example.*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Support exact match",
			args: args{
				specs:  []string{"https://example.com/test"},
				target: "https://example.com/test",
			},
			shouldMatch: true,
		},
		{
			name: "Does not support scheme mismatch",
			args: args{
				specs:  []string{"https://test.example.com/grafana"},
				target: "http://test.example.com/grafana",
			},
			shouldMatch: false,
		},
		{
			name: "Support trailing slash in spec",
			args: args{
				specs:  []string{"https://example.com/"},
				target: "https://example.com",
			},
			shouldMatch: true,
		},
		{
			name: "Support trailing slash in target",
			args: args{
				specs:  []string{"https://example.com"},
				target: "https://example.com/",
			},
			shouldMatch: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := urlMatch(tt.args.specs, tt.args.target, plugins.SignatureTypePrivateGlob)
			require.NoError(t, err)
			require.Equal(t, tt.shouldMatch, got)
		})
	}
}

func Test_urlMatch_private(t *testing.T) {
	type args struct {
		specs  []string
		target string
	}
	tests := []struct {
		name        string
		args        args
		shouldMatch bool
	}{
		{
			name: "Support exact match",
			args: args{
				specs:  []string{"https://example.com/test"},
				target: "https://example.com/test",
			},
			shouldMatch: true,
		},
		{
			name: "Support trailing slash in spec",
			args: args{
				specs:  []string{"https://example.com/test/"},
				target: "https://example.com/test",
			},
			shouldMatch: true,
		},
		{
			name: "Support trailing slash in target",
			args: args{
				specs:  []string{"https://example.com/test"},
				target: "https://example.com/test/",
			},
			shouldMatch: true,
		},
		{
			name: "Do not support single wildcard matching single subdomain",
			args: args{
				specs:  []string{"https://*.example.com"},
				target: "https://test.example.com",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support multiple wildcards matching multiple subdomains",
			args: args{
				specs:  []string{"https://**.example.com"},
				target: "https://more.test.example.com",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support single wildcard matching single paths",
			args: args{
				specs:  []string{"https://www.example.com/*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support double wildcard matching multiple paths",
			args: args{
				specs:  []string{"https://www.example.com/**"},
				target: "https://www.example.com/other/grafana",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support subdomain mismatch",
			args: args{
				specs:  []string{"https://www.test.example.com/grafana/docs"},
				target: "https://www.dev.example.com/grafana/docs",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support path mismatch",
			args: args{
				specs:  []string{"https://example.com/grafana"},
				target: "https://example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support both domain and path wildcards",
			args: args{
				specs:  []string{"https://*.example.com/*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support wildcards without TLDs",
			args: args{
				specs:  []string{"https://example.*"},
				target: "https://www.example.com/grafana1",
			},
			shouldMatch: false,
		},
		{
			name: "Do not support scheme mismatch",
			args: args{
				specs:  []string{"https://test.example.com/grafana"},
				target: "http://test.example.com/grafana",
			},
			shouldMatch: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := urlMatch(tt.args.specs, tt.args.target, plugins.SignatureTypePrivate)
			require.NoError(t, err)
			require.Equal(t, tt.shouldMatch, got)
		})
	}
}

func Test_validateManifest(t *testing.T) {
	tcs := []struct {
		name        string
		manifest    *PluginManifest
		expectedErr string
	}{
		{
			name:        "Empty plugin field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.Plugin = "" }),
			expectedErr: "valid manifest field plugin is required",
		},
		{
			name:        "Empty keyId field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.KeyID = "" }),
			expectedErr: "valid manifest field keyId is required",
		},
		{
			name:        "Empty signedByOrg field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrg = "" }),
			expectedErr: "valid manifest field signedByOrg is required",
		},
		{
			name:        "Empty signedByOrgName field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.SignedByOrgName = "" }),
			expectedErr: "valid manifest field SignedByOrgName is required",
		},
		{
			name:        "Empty signatureType field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "" }),
			expectedErr: "valid manifest field signatureType is required",
		},
		{
			name:        "Invalid signatureType field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.SignatureType = "invalidSignatureType" }),
			expectedErr: "valid manifest field signatureType is required",
		},
		{
			name:        "Empty files field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.Files = map[string]string{} }),
			expectedErr: "valid manifest field files is required",
		},
		{
			name:        "Empty time field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.Time = 0 }),
			expectedErr: "valid manifest field time is required",
		},
		{
			name:        "Empty version field",
			manifest:    createV2Manifest(t, func(m *PluginManifest) { m.Version = "" }),
			expectedErr: "valid manifest field version is required",
		},
	}
	for _, tc := range tcs {
		t.Run(tc.name, func(t *testing.T) {
			s := provideDefaultTestService()
			err := s.validateManifest(context.Background(), *tc.manifest, nil)
			require.Errorf(t, err, tc.expectedErr)
		})
	}
}

func createV2Manifest(t *testing.T, cbs ...func(*PluginManifest)) *PluginManifest {
	t.Helper()

	m := &PluginManifest{
		Plugin:  "grafana-test-app",
		Version: "2.5.3",
		KeyID:   "7e4d0c6a708866e7",
		Time:    1586817677115,
		Files: map[string]string{
			"plugin.json": "55556b845e91935cc48fae3aa67baf0f22694c3f",
		},
		ManifestVersion: "2.0.0",
		SignatureType:   plugins.SignatureTypeGrafana,
		SignedByOrg:     "grafana",
		SignedByOrgName: "grafana",
	}

	for _, cb := range cbs {
		cb(m)
	}

	return m
}

func mustNewStaticFSForTests(t *testing.T, dir string) plugins.FS {
	sfs, err := plugins.NewStaticFS(plugins.NewLocalFS(dir))
	require.NoError(t, err)
	return sfs
}

type revokedKeyProvider struct{}

func (p *revokedKeyProvider) GetPublicKey(ctx context.Context, keyID string) (string, error) {
	// dummy revoked key created locally
	const publicKeyText = `-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBGRkuFABDAC4DHqoOXDLOcv9YnGk44yFdP+5keYzO3d97f14iK8tyw7o9Umo
FP6mJIkl1P1YDNM4ZRSj1TYowakg5eTYePyYPfmvvKYjmanXinmbFHmfiRdlM4LJ
HbQH5AGH2cAAfACybPGwXtHQrOXRrmF+cPTi/KAshc5ynzIJneEbsp7YleOPuG+P
CVT5GwdGrhfrxNBuEJo1+fuVTY2Ddc4hwT+pk63nIwnqdbYRxhiN3jfpvUXVp0W5
5awNtjH+uSL3tNMfXzu+DxXusGoZa8lpOgHo6ss3QzW5U79J7ulwNID3GoQCBipq
hfoQTZidLB9BhQceWTcXurgcxdk3C5Mk90wupHWcCj9/WV0KFFYkgTY1eF60m14a
CfD2PYkEJaozu5MsUxon+VNH6bbKu3XFmpRvLxfp26whW+cZU6f5YicLVjF5ZOlk
YdtyR/H7p7NJeHNjy9/uCZdVatMsAtOB+kWyDwG6BT53sZjesR1SgfIIz+n7/118
JQDtJUz2+Js6jFUAEQEAAYkBtgQgAQoAIBYhBALnEdechVpRjsMvBbiT5aNg2RGg
BQJkZLoKAh0AAAoJELiT5aNg2RGgoWEL/RnFu3djKWyYdA4XAIg5nkaplADMxECK
0sAyPnYAA1Q+nSMZMqcm5+vzQGi/RoQlrJpRu+8yuW4rpPTC0IfKi/QNT3hVHmrr
NPVqAtde0BMSYKfOfM88BGDqXlKnIMUrMLDhHWNi7zdk9tClxEBDUBd22SFyeIfk
XbAuGJa64JZbwD0m5WR93Lxb+9YX4ZRZY9GG7Sgs2roAmGEzJfX8OurhKBz+p7TO
fo5V7jYyV2+iOGo7zpFobp/80A7mIV/SWJSluV7B3F11ZRRIgAZKTkXsua8MKEwx
tF0pp+0pd8SUt2Q4mUivVewAEaFWZG2sw/i1KLJj0PH7gX6hN2fToIGRR1/lswOC
N9we9DBD23DukqibkHss4fm7oTDto3jYL2mclrQ/WYSFXRF4vrhZTNmTPU3OULcK
5agGzsF0RuQoTGX3buUnBIDNBfvJY5A056urc2ur2Ik43PRYaSW6dcvnGu12AWWk
Vi5ALKSsP8/e1LujD9p8ZvN5YhYGBaLbmrQHYW5kcmVzNYkB1AQTAQoAPhYhBALn
EdechVpRjsMvBbiT5aNg2RGgBQJkZLhQAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJELiT5aNg2RGgAv0L/jH2gN4GDcBDF6J84uIIUW+lbFSsH6co
arTgkE5tQgyv3XlJU8bM8wNZWvlLpRvPD9Zq+NQzpMpAVV/0GPnt9oApxlQMHlzT
R65A/Ryb6viieUqtDfQ/w+GLds7R7AL09dRMC9X5GIt9f1NYD48AYhFZNiERG7Ra
qAeWU2hQ+LqZVKIFqNmLCtn1ZRlz169UNEoht37VgSE593trctgYaINt2C6bmgDX
rDXD06MQyYmHcGb1wCGp3t0zjGvWq0Db5UEdKm2BTwovD5+kpiIow8DlMqirYfqI
sTf/DbhErxqJyMujxtID6GeBvhO6U5QL+7JmoA50pmaPWuOoOEcracAU+G4d/H6A
bCF45s5ek6P5IfcVLmKNNcVTHcfy50c/VazLbJcR6bmrN+4iFX2mXiWFT50COWvk
/LPnmwRUtsEJy+76hd/AZhhYQZa7kB9pPiKTkejGTKJz4t6n79kKRF274OOs2DG/
xg3L8tEs8mPG/XkiVbeV3wIdhH8EKJV057kBjQRkZLhQAQwAvHj/HxYoC65XiyhC
fpZlPhOn7bqLbvHKSsRJN63Z1IIARX5hbKEPnBNf1OljVpt5AYgAEKTrcE4Hca3q
5UBXDqQGYHoHO0PM8kGGd5mA1RPmZRmjzQKmve4+GlE8yk4TeUjIs0YxRaGbW0lk
mpwLqUEo3axRKfVvTpPMzEKgA5gZ9yrCA6LZ3blgaIt3kz5rbANHTwBR/Czh0omU
A6gUOcTtlk/LtoStLGxXR/bs4Kdle4H3lBUvpvWp2ecV7ws5XAtQgZLSDbQp5/ib
ugenkY231QTu0H0jD8z3PE6oUfrC7Q/S9kHE43VU/ZAasrxu84FNwBoDhbuQ0L5x
0SkoHrBT2B28g43EYAr8BZhtkLighx9a5hv8FdJ8C+4zB05iJt/PzWAlEE3v72x5
G2G+n1wfESiRCxd9VKvggS6AjQukZCwr7Sam+8l2iRGnUDa/ONuAu+L05IKOLnVo
ksZf/ewo0Wfp4X7tSKp31AGvibEerEKjlKlG82+1xnBAu1DlABEBAAGJAbYEGAEK
ACAWIQQC5xHXnIVaUY7DLwW4k+WjYNkRoAUCZGS4UAIbDAAKCRC4k+WjYNkRoJ4u
C/wLAdNUcXNGZTOrmXpAeqiNxUJnd9kRExKagD/gHjcq9lpUFr7O+t2Br5pEooOH
kfwQFTFGCAbsQ7eRO22KtJ8/e9nZYtsSv2iGo6lruhN18PNuLdAnqE0b/a+5JYPD
4tqsnecsiFCLuOGQ65zVYfyXA6waKZ/cODPmMQZQA5J19e+rmPW7nYl1sg4+QHMB
XcorhKraroJ5IICzf4/EJAQ9BSmty4AHwMOBP+Az1g4++ptbxE3tE9KAeg6mmymZ
PpoRYv4VOKDjEaJEro41747qITazAVchXa0Vj8QyrJJ1cPCUm8tBEDtq0tQ9hpDP
IyCRWKL8TwklDYl6UNQ1f7/WFx3VjJ951kS8y3xD6wZje7VuG/FByuJl53pOOTu9
wftPi+ooA/LI6kzJ9v7mEWtvr0zewKAXlYw0QTZh+jj89w5AXad3+zikbKh8JnXi
EOCpaqfJr6Ar/8Q8bY2/whOG0HCQPCSnKYfSJxf/NZ0w45QuAreoiEoiQiNsLbrQ
pHo=
=RnzB
-----END PGP PUBLIC KEY BLOCK-----
`
	return publicKeyText, nil
}

func Test_VerifyRevokedKey(t *testing.T) {
	s := ProvideService(&config.PluginManagementCfg{}, &revokedKeyProvider{})
	m := createV2Manifest(t)
	txt := `-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

{
  "manifestVersion": "2.0.0",
  "signatureType": "private",
  "signedByOrg": "andresmartinez",
  "signedByOrgName": "andresmartinez",
  "rootUrls": [
    "https://example.com/grafana"
  ],
  "plugin": "myorg-withbackend-app",
  "version": "1.0.0",
  "time": 1684235356873,
  "keyId": "CE6E5BFAC57F4B66",
  "files": {
    "CHANGELOG.md": "ba613d6f914b27dce9ace4d8c0cb074273c9eb6c536d8e7ac24c5ce6ae941fd0",
    "gpx_app-myorg-withbackend-app_darwin_amd64": "d85169cc9c3cacf71f2b570478dc14d9db1985f97d0f6c93b9cccfe1cbb7a87b",
    "img/logo.svg": "1defc6f7e585c67657bcfd8fddc599ee7dfa82f8674413f49fa274c2cd453ec6",
    "plugin.json": "f74b561db5b079c610cc0025602cb489c8108b18db76f2190e690501f7f5b8c6",
    "go_plugin_build_manifest": "9a85c38b762566db00468a39335eab9bf14d8a86bdf5de774feae8f5c1936d8d",
    "standalone.txt": "da5961ac3dc4ffe6bd26c6b5633e37bef1815cf4beee2dea769188a1295e956c",
    "gpx_app-myorg-withbackend-app_linux_amd64": "4ed9ea3a13700dc36ce2918d07d6b3d2e04e2174966b8da219890f57192b36e6",
    "gpx_app-myorg-withbackend-app_linux_arm": "c9e6f2c8b3e0216d46299442ab1e32a9f4ece35b806e499bc027e8de51e13f64",
    "LICENSE": "c5accbbd8546e94c34aed24afe689a617627d18eed5a6c48277e48db57c23851",
    "README.md": "20268419628cb55aa86394d01012be936f70f1131cee6fcf4008aded5164b7f0",
    "gpx_app-myorg-withbackend-app_windows_amd64.exe": "4106704de494fc6a82733961b119c2393b066fc6f9a32822ab8f1dbacdabe057",
    "module.js.map": "9cdc252e92c0408fb39326af592720223640a1a7f1a68fea23ac3aea056bf938",
    "gpx_app-myorg-withbackend-app_darwin_arm64": "ae8f2966f1100a5f32af1a87f6edda0aacec615246043211926b071136909784",
    "gpx_app-myorg-withbackend-app_linux_arm64": "1ea731dbe23049e790cb51c09b18983a65819f5b3b52c2f8d787293c6f362887",
    "pid.txt": "edf0f8e95323881a533842f79e808584d4f584831ab53b1887c31e683e2fd5a5",
    "module.js": "061e1da2918204c34f38fcb717f1e27dacd64e5cdca64f281a7f4ebb2baf33c1"
  }
}
-----BEGIN PGP SIGNATURE-----

iQGzBAEBCgAdFiEEAucR15yFWlGOwy8FuJPlo2DZEaAFAmRkuWAACgkQuJPlo2DZ
EaAILgv/YElEpqelrqga3usjPsA9EAX4f7eOUv2mG9gg5UISpEc8KXwJ3htXd/3+
W2ij9KehkLsFsCTQs+oMIqJjsJAqA1nCwYEOIJlcDcuobMMMSqdJMG6xZOq3dTX4
O2DEcybtHHRD3C59ks1wmwNdI8B5T5SCxDXGmBo4kBh1h3t7sSlE3MoSTIfE3csR
GyB05ONtLpMwc+xlyuuSjcxcNAYcbCl8ts29sj3EsXSD/Vnh06Rhex2XmVtLqog9
4ygC0fBSpWONSPW31yckdae2L+ZncJVO5LZ2gdM/+69a+v5f/TTauaM7Ts3PYUBf
FtJEv/KHDaxR+SwxDgu7iMoiKntGaozyG1oZvlsSYnzs0Y9a6fHoxJT2zNCIotP2
NPprfbHTrYiDkTN3LBksKcYLU7VO8Z1/VriGfhbgDAP3s1zwSmx4tiXdwZLmdJbC
6jDpIJL3mDBMlVxNe/cCgYGIA90YpqQe/B0X3sJwUcUNSQrvZJpgoz8k04AuKuxh
syrhBXja
=cQxQ
-----END PGP SIGNATURE-----
`
	block, _ := clearsign.Decode([]byte(txt))
	require.NotNil(t, block, "failed to decode block")
	err := s.validateManifest(context.Background(), *m, block)
	require.Error(t, err)
	require.ErrorIs(t, err, openpgpErrors.ErrKeyRevoked)
}
