mukan-ignite/ignite/services/plugin/plugin_test.go
Mukan Erkin Törük 26b204bd04
Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
feat: fork Ignite CLI v29 as Mukan Ignite — remove cosmos-sdk restrictions
2026-05-11 03:31:37 +03:00

611 lines
17 KiB
Go

package plugin
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
hplugin "github.com/hashicorp/go-plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
pluginsconfig "github.com/ignite/cli/v29/ignite/config/plugins"
"github.com/ignite/cli/v29/ignite/pkg/cliui"
"github.com/ignite/cli/v29/ignite/pkg/errors"
"github.com/ignite/cli/v29/ignite/pkg/gocmd"
"github.com/ignite/cli/v29/ignite/pkg/gomodule"
)
func TestNewPlugin(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
tests := []struct {
name string
pluginCfg pluginsconfig.Plugin
expectedPlugin Plugin
}{
{
name: "fail: empty path",
expectedPlugin: Plugin{
Error: errors.Errorf(`missing app property "path"`),
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "fail: local plugin doesnt exists",
pluginCfg: pluginsconfig.Plugin{Path: "/xxx/yyy/app"},
expectedPlugin: Plugin{
Error: errors.Errorf(`local app path "/xxx/yyy/app" not found`),
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "fail: local plugin is not a directory",
pluginCfg: pluginsconfig.Plugin{Path: path.Join(wd, "testdata/fakebin")},
expectedPlugin: Plugin{
Error: errors.Errorf(fmt.Sprintf("local app path %q is not a directory", path.Join(wd, "testdata/fakebin"))),
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: local plugin",
pluginCfg: pluginsconfig.Plugin{Path: path.Join(wd, "testdata")},
expectedPlugin: Plugin{
srcPath: path.Join(wd, "testdata"),
name: "testdata",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "fail: remote plugin with only domain",
pluginCfg: pluginsconfig.Plugin{Path: "github.com"},
expectedPlugin: Plugin{
Error: errors.Errorf(`app path "github.com" is not a valid repository URL`),
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "fail: remote plugin with incomplete URL",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite"},
expectedPlugin: Plugin{
Error: errors.Errorf(`app path "github.com/ignite" is not a valid repository URL`),
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote app",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app",
reference: "",
srcPath: ".ignite/apps/github.com/ignite/app",
name: "app",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote plugin with @ref",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app@develop"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app@develop",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app-develop",
reference: "develop",
srcPath: ".ignite/apps/github.com/ignite/app-develop",
name: "app",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote plugin with @ref containing slash",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app@package/v1.0.0"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app@package/v1.0.0",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app-package-v1.0.0",
reference: "package/v1.0.0",
srcPath: ".ignite/apps/github.com/ignite/app-package-v1.0.0",
name: "app",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote plugin with subpath",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app/plugin1"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app",
reference: "",
srcPath: ".ignite/apps/github.com/ignite/app/plugin1",
name: "plugin1",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote plugin with subpath and @ref",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app/plugin1@develop"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app@develop",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app-develop",
reference: "develop",
srcPath: ".ignite/apps/github.com/ignite/app-develop/plugin1",
name: "plugin1",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
{
name: "ok: remote plugin with subpath and @ref containing slash",
pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/app/plugin1@package/v1.0.0"},
expectedPlugin: Plugin{
repoPath: "github.com/ignite/app@package/v1.0.0",
cloneURL: "https://github.com/ignite/app",
cloneDir: ".ignite/apps/github.com/ignite/app-package-v1.0.0",
reference: "package/v1.0.0",
srcPath: ".ignite/apps/github.com/ignite/app-package-v1.0.0/plugin1",
name: "plugin1",
stdout: os.Stdout,
stderr: os.Stderr,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.expectedPlugin.Plugin = tt.pluginCfg
p := newPlugin(".ignite/apps", tt.pluginCfg)
assertPlugin(t, tt.expectedPlugin, *p)
})
}
}
// Helper to make a local git repository with gofile committed.
// Returns the repo directory and the git.Repository.
func makeGitRepo(t *testing.T, name string) (string, *git.Repository) {
t.Helper()
require := require.New(t)
repoDir := t.TempDir()
scaffoldPlugin(t, repoDir, "github.com/ignite/"+name, false)
repo, err := git.PlainInit(repoDir, false)
require.NoError(err)
w, err := repo.Worktree()
require.NoError(err)
_, err = w.Add(".")
require.NoError(err)
_, err = w.Commit("msg", &git.CommitOptions{
Author: &object.Signature{
Name: "bob",
Email: "bob@example.com",
When: time.Now(),
},
})
require.NoError(err)
return repoDir, repo
}
type TestClientAPI struct{ ClientAPI }
func (TestClientAPI) GetChainInfo(context.Context) (*ChainInfo, error) {
return &ChainInfo{}, nil
}
func (TestClientAPI) GetIgniteInfo(context.Context) (*IgniteInfo, error) {
return &IgniteInfo{}, nil
}
func TestPluginLoad(t *testing.T) {
wd, err := os.Getwd()
require.NoError(t, err)
clientAPI := &TestClientAPI{}
tests := []struct {
name string
buildPlugin func(t *testing.T) Plugin
expectedError string
}{
{
name: "fail: plugin is already in error",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
return Plugin{
Error: errors.New("oups"),
}
},
expectedError: `oups`,
},
{
name: "fail: no go files in srcPath",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
return Plugin{
srcPath: path.Join(wd, "testdata"),
name: "testdata",
}
},
expectedError: `no Go files in`,
},
{
name: "ok: from local",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
path := scaffoldPlugin(t, t.TempDir(), "github.com/foo/bar", false)
return Plugin{
srcPath: path,
name: "bar",
}
},
},
{
name: "ok: from git repo",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
repoDir, _ := makeGitRepo(t, "remote")
cloneDir := t.TempDir()
return Plugin{
cloneURL: repoDir,
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "remote"),
name: "remote",
}
},
},
{
name: "fail: git repo doesnt exists",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
cloneDir := t.TempDir()
return Plugin{
repoPath: "/xxxx/yyyy",
cloneURL: "/xxxx/yyyy",
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "app"),
}
},
expectedError: `cloning "/xxxx/yyyy": repository not found`,
},
{
name: "ok: from git repo with tag",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
repoDir, repo := makeGitRepo(t, "remote-tag")
h, err := repo.Head()
require.NoError(t, err)
_, err = repo.CreateTag("v1", h.Hash(), &git.CreateTagOptions{
Tagger: &object.Signature{Name: "me"},
Message: "v1",
})
require.NoError(t, err)
cloneDir := t.TempDir()
return Plugin{
cloneURL: repoDir,
reference: "v1",
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "remote-tag"),
name: "remote-tag",
}
},
},
{
name: "ok: from git repo with branch",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
repoDir, repo := makeGitRepo(t, "remote-branch")
w, err := repo.Worktree()
require.NoError(t, err)
err = w.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName("branch1"),
Create: true,
})
require.NoError(t, err)
cloneDir := t.TempDir()
return Plugin{
cloneURL: repoDir,
reference: "branch1",
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "remote-branch"),
name: "remote-branch",
}
},
},
{
name: "ok: from git repo with hash",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
repoDir, repo := makeGitRepo(t, "remote-hash")
h, err := repo.Head()
require.NoError(t, err)
cloneDir := t.TempDir()
return Plugin{
cloneURL: repoDir,
reference: h.Hash().String(),
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "remote-hash"),
name: "remote-hash",
}
},
},
{
name: "fail: git ref not found",
buildPlugin: func(t *testing.T) Plugin {
t.Helper()
repoDir, _ := makeGitRepo(t, "remote-no-ref")
cloneDir := t.TempDir()
return Plugin{
cloneURL: repoDir,
reference: "doesnt_exists",
cloneDir: cloneDir,
srcPath: path.Join(cloneDir, "remote-no-ref"),
name: "remote-no-ref",
}
},
expectedError: `cloning ".*": reference not found`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
require := require.New(t)
assert := assert.New(t)
p := tt.buildPlugin(t)
defer p.KillClient()
p.load(context.Background())
if tt.expectedError != "" {
require.Error(p.Error, "expected error %q", tt.expectedError)
require.Regexp(tt.expectedError, p.Error.Error())
return
}
require.NoError(p.Error)
require.NotNil(p.Interface)
manifest, err := p.Interface.Manifest(ctx)
require.NoError(err)
assert.Equal(p.name, manifest.Name)
assert.NoError(p.Interface.Execute(ctx, &ExecutedCommand{OsArgs: []string{"ignite", p.name, "hello"}}, clientAPI))
assert.NoError(p.Interface.ExecuteHookPre(ctx, &ExecutedHook{}, clientAPI))
assert.NoError(p.Interface.ExecuteHookPost(ctx, &ExecutedHook{}, clientAPI))
assert.NoError(p.Interface.ExecuteHookCleanUp(ctx, &ExecutedHook{}, clientAPI))
})
}
}
func TestPluginLoadSharedHost(t *testing.T) {
tests := []struct {
name string
instances int
sharesHost bool
}{
{
name: "ok: from local sharedhost is on 1 instance",
instances: 1,
sharesHost: true,
},
{
name: "ok: from local sharedhost is on 2 instances",
instances: 2,
sharesHost: true,
},
{
name: "ok: from local sharedhost is on 4 instances",
instances: 4,
sharesHost: true,
},
{
name: "ok: from local sharedhost is off 4 instances",
instances: 4,
sharesHost: false,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var (
require = require.New(t)
assert = assert.New(t)
// scaffold an unique plugin for all instances
path = scaffoldPlugin(t, t.TempDir(),
fmt.Sprintf("github.com/foo/bar-%d", i), tt.sharesHost)
plugins []*Plugin
)
// Load one plugin per instance
for i := 0; i < tt.instances; i++ {
p := Plugin{
Plugin: pluginsconfig.Plugin{Path: path},
srcPath: path,
name: filepath.Base(path),
}
p.load(context.Background())
require.NoError(p.Error)
plugins = append(plugins, &p)
}
// Ensure all plugins are killed at the end of test case
defer func() {
for i := len(plugins) - 1; i >= 0; i-- {
plugins[i].KillClient()
if tt.sharesHost && i > 0 {
assert.False(plugins[i].client.Exited(), "non host app can't kill host app")
assert.True(checkConfCache(plugins[i].Path), "non host app doesn't remove config cache when killed")
} else {
assert.True(plugins[i].client.Exited(), "app should be killed")
}
assert.False(plugins[i].isHost, "killed plugins are no longer host")
}
assert.False(checkConfCache(plugins[0].Path), "once host is killed the cache should be cleared")
}()
var hostConf *hplugin.ReattachConfig
for i := 0; i < len(plugins); i++ {
if tt.sharesHost {
assert.True(checkConfCache(plugins[i].Path), "sharedHost must have a cache entry")
if i == 0 {
// first plugin is the host
assert.True(plugins[i].isHost, "first app is the host")
// Assert reattach config has been saved
hostConf = plugins[i].client.ReattachConfig()
ref, err := readConfigCache(plugins[i].Path)
if assert.NoError(err) {
assert.Equal(hostConf, &ref, "wrong cache entry for app host")
}
} else {
// plugins after first aren't host
assert.False(plugins[i].isHost, "app %d can't be host", i)
assert.Equal(hostConf, plugins[i].client.ReattachConfig(), "ReattachConfig different from host app")
}
} else {
assert.False(plugins[i].isHost, "app %d can't be host if sharedHost is disabled", i)
assert.False(checkConfCache(plugins[i].Path), "app %d can't have a cache entry if sharedHost is disabled", i)
}
}
})
}
}
func TestPluginClean(t *testing.T) {
tests := []struct {
name string
plugin *Plugin
expectRemove bool
}{
{
name: "dont clean local app",
plugin: &Plugin{
Plugin: pluginsconfig.Plugin{Path: "/local"},
},
},
{
name: "dont clean plugin with errors",
plugin: &Plugin{Error: errors.New("oups")},
},
{
name: "ok",
plugin: &Plugin{
cloneURL: "https://github.com/ignite/app",
},
expectRemove: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmp, err := os.MkdirTemp("", "cloneDir")
require.NoError(t, err)
tt.plugin.cloneDir = tmp
err = tt.plugin.clean()
require.NoError(t, err)
if tt.expectRemove {
_, err := os.Stat(tmp)
assert.True(t, os.IsNotExist(err), "cloneDir not removed")
}
})
}
}
func TestPluginOutdatedBinary(t *testing.T) {
t.Run("returns true when source and binary mtimes are equal", func(t *testing.T) {
tmp := t.TempDir()
srcFile := filepath.Join(tmp, "main.go")
binFile := filepath.Join(tmp, "app.ign")
require.NoError(t, os.WriteFile(srcFile, []byte("package main\n"), 0o644))
require.NoError(t, os.WriteFile(binFile, []byte("binary"), 0o755))
equalTime := time.Now().Add(-time.Minute).Truncate(time.Second)
require.NoError(t, os.Chtimes(srcFile, equalTime, equalTime))
require.NoError(t, os.Chtimes(binFile, equalTime, equalTime))
p := Plugin{
srcPath: tmp,
name: "app",
}
require.True(t, p.outdatedBinary())
})
}
// scaffoldPlugin runs Scaffold and updates the go.mod so it uses the
// current ignite/cli sources.
func scaffoldPlugin(t *testing.T, dir, name string, sharedHost bool) string {
t.Helper()
require := require.New(t)
session := cliui.New(cliui.WithoutUserInteraction(true))
path, err := Scaffold(context.Background(), session, dir, name, sharedHost)
require.NoError(err)
// We want the scaffolded plugin to use the current version of ignite/cli,
// for that we need to update the plugin go.mod and add a replace to target
// current ignite/cli
gomod, err := gomodule.ParseAt(path)
require.NoError(err)
// use GOMOD env to get current directory module path
modpath, err := gocmd.Env(gocmd.EnvGOMOD)
require.NoError(err)
modpath = filepath.Dir(modpath)
err = gomod.AddReplace("github.com/ignite/cli/v29", "", modpath, "")
require.NoError(err)
// Save go.mod
data, err := gomod.Format()
require.NoError(err)
err = os.WriteFile(filepath.Join(path, "go.mod"), data, 0o644)
require.NoError(err)
return path
}
func assertPlugin(t *testing.T, want, have Plugin) {
t.Helper()
if want.Error != nil {
require.Errorf(t, have.Error, "expected error %q", want.Error)
assert.Regexp(t, want.Error.Error(), have.Error.Error())
} else {
require.NoErrorf(t, have.Error, "expected no error, got %v", have.Error)
}
// Errors aren't comparable with assert.Equal, because of the different stacks
want.Error = nil
have.Error = nil
assert.Equal(t, want, have)
}