mukan-ignite/ignite/cmd/plugin_test.go
Mukan Erkin Törük c32551b6f7
Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:24 +03:00

656 lines
17 KiB
Go

package ignitecmd
import (
"context"
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
pluginsconfig "git.cw.tr/mukan-network/mukan-ignite/ignite/config/plugins"
"git.cw.tr/mukan-network/mukan-ignite/ignite/services/plugin"
"git.cw.tr/mukan-network/mukan-ignite/ignite/services/plugin/mocks"
)
func buildRootCmd(ctx context.Context) *cobra.Command {
var (
rootCmd = &cobra.Command{
Use: "ignite",
}
scaffoldCmd = &cobra.Command{
Use: "scaffold",
}
scaffoldChainCmd = &cobra.Command{
Use: "chain",
Run: func(*cobra.Command, []string) {},
}
scaffoldModuleCmd = &cobra.Command{
Use: "module",
Run: func(*cobra.Command, []string) {},
}
)
scaffoldChainCmd.Flags().String("path", "", "the path")
scaffoldCmd.AddCommand(scaffoldChainCmd)
scaffoldCmd.AddCommand(scaffoldModuleCmd)
rootCmd.AddCommand(scaffoldCmd)
rootCmd.SetContext(ctx)
return rootCmd
}
func assertFlags(t *testing.T, expectedFlags plugin.Flags, execCmd *plugin.ExecutedCommand) {
t.Helper()
var (
have []string
expected []string
)
t.Helper()
flags, err := execCmd.NewFlags()
assert.NoError(t, err)
flags.VisitAll(func(f *pflag.Flag) {
if f.Name == "help" {
// ignore help flag
return
}
have = append(have, f.Name)
})
for _, f := range expectedFlags {
expected = append(expected, f.Name)
}
assert.Equal(t, expected, have)
}
func TestLinkPluginCmds(t *testing.T) {
t.Skip("passes locally and with act, but fails in CI")
var (
args = []string{"arg1", "arg2"}
pluginParams = map[string]string{"key": "val"}
// define a plugin with command flags
pluginWithFlags = &plugin.Command{
Use: "flaggy",
Flags: plugin.Flags{
{Name: "flag1", Type: plugin.FlagTypeString},
{Name: "flag2", Type: plugin.FlagTypeInt, DefaultValue: "0", Value: "0"},
},
}
)
// helper to assert pluginInterface.Execute() calls
expectExecute := func(t *testing.T, _ context.Context, p *mocks.PluginInterface, cmd *plugin.Command) {
t.Helper()
p.EXPECT().
Execute(
mock.Anything,
mock.MatchedBy(func(execCmd *plugin.ExecutedCommand) bool {
fmt.Println(cmd.Use == execCmd.Use, cmd.Use, execCmd.Use)
return cmd.Use == execCmd.Use
}),
mock.Anything,
).
Run(func(_ context.Context, execCmd *plugin.ExecutedCommand, _ plugin.ClientAPI) {
// Assert execCmd is populated correctly
assert.True(t, strings.HasSuffix(execCmd.Path, cmd.Use), "wrong path %s", execCmd.Path)
assert.Equal(t, args, execCmd.Args)
assertFlags(t, cmd.Flags, execCmd)
assert.Equal(t, pluginParams, execCmd.With)
}).
Return(nil)
}
tests := []struct {
name string
setup func(*testing.T, context.Context, *mocks.PluginInterface)
expectedDumpCmd string
expectedError string
}{
{
name: "ok: link foo at root",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
cmd := &plugin.Command{
Use: "foo",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Commands: []*plugin.Command{cmd}}, nil)
expectExecute(t, ctx, p, cmd)
},
expectedDumpCmd: `
ignite
foo*
scaffold
chain* --path=string
module*
`,
},
{
name: "ok: link foo at subcommand",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
cmd := &plugin.Command{
Use: "foo",
PlaceCommandUnder: "ignite scaffold",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Commands: []*plugin.Command{cmd}}, nil)
expectExecute(t, ctx, p, cmd)
},
expectedDumpCmd: `
ignite
scaffold
chain* --path=string
foo*
module*
`,
},
{
name: "ok: link foo at subcommand with incomplete PlaceCommandUnder",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
cmd := &plugin.Command{
Use: "foo",
PlaceCommandUnder: "scaffold",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Commands: []*plugin.Command{cmd}}, nil)
expectExecute(t, ctx, p, cmd)
},
expectedDumpCmd: `
ignite
scaffold
chain* --path=string
foo*
module*
`,
},
{
name: "fail: link to runnable command",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
{
Use: "foo",
PlaceCommandUnder: "ignite scaffold chain",
},
},
},
nil,
)
},
expectedError: `can't attach app command "foo" to runnable command "ignite scaffold chain"`,
},
{
name: "fail: link to unknown command",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
{
Use: "foo",
PlaceCommandUnder: "ignite unknown",
},
},
},
nil,
)
},
expectedError: `unable to find command path "ignite unknown" for app "foo"`,
},
{
name: "fail: plugin name exists in legacy commands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
{
Use: "scaffold",
},
},
},
nil,
)
},
expectedError: `app command "scaffold" already exists in Ignite's commands`,
},
{
name: "fail: plugin name with args exists in legacy commands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
{
Use: "scaffold [args]",
},
},
},
nil,
)
},
expectedError: `app command "scaffold" already exists in Ignite's commands`,
},
{
name: "fail: plugin name exists in legacy sub commands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
{
Use: "chain",
PlaceCommandUnder: "scaffold",
},
},
},
nil,
)
},
expectedError: `app command "chain" already exists in Ignite's commands`,
},
{
name: "ok: link multiple at root",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
fooCmd := &plugin.Command{
Use: "foo",
}
barCmd := &plugin.Command{
Use: "bar",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Commands: []*plugin.Command{
fooCmd, barCmd, pluginWithFlags,
},
}, nil)
expectExecute(t, ctx, p, fooCmd)
expectExecute(t, ctx, p, barCmd)
expectExecute(t, ctx, p, pluginWithFlags)
},
expectedDumpCmd: `
ignite
bar*
flaggy* --flag1=string --flag2=int
foo*
scaffold
chain* --path=string
module*
`,
},
{
name: "ok: link with subcommands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
cmd := &plugin.Command{
Use: "foo",
Commands: []*plugin.Command{
{Use: "bar"},
{Use: "baz"},
pluginWithFlags,
},
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Commands: []*plugin.Command{cmd}}, nil)
// cmd is not executed because it's not runnable, only sub-commands
// are executed.
expectExecute(t, ctx, p, cmd.Commands[0])
expectExecute(t, ctx, p, cmd.Commands[1])
expectExecute(t, ctx, p, cmd.Commands[2])
},
expectedDumpCmd: `
ignite
foo
bar*
baz*
flaggy* --flag1=string --flag2=int
scaffold
chain* --path=string
module*
`,
},
{
name: "ok: link with multiple subcommands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
cmd := &plugin.Command{
Use: "foo",
Commands: []*plugin.Command{
{Use: "bar", Commands: []*plugin.Command{{Use: "baz"}}},
{Use: "qux", Commands: []*plugin.Command{{Use: "quux"}, {Use: "corge"}}},
},
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Commands: []*plugin.Command{cmd}}, nil)
expectExecute(t, ctx, p, cmd.Commands[0].Commands[0])
expectExecute(t, ctx, p, cmd.Commands[1].Commands[0])
expectExecute(t, ctx, p, cmd.Commands[1].Commands[1])
},
expectedDumpCmd: `
ignite
foo
bar
baz*
qux
corge*
quux*
scaffold
chain* --path=string
module*
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
require := require.New(t)
assert := assert.New(t)
pi := mocks.NewPluginInterface(t)
p := &plugin.Plugin{
Plugin: pluginsconfig.Plugin{
Path: "foo",
With: pluginParams,
},
Interface: pi,
}
rootCmd := buildRootCmd(ctx)
tt.setup(t, ctx, pi)
_ = linkPlugins(ctx, rootCmd, []*plugin.Plugin{p})
if tt.expectedError != "" {
require.Error(p.Error)
require.EqualError(p.Error, tt.expectedError)
return
}
require.NoError(p.Error)
var s strings.Builder
s.WriteString("\n")
dumpCmd(rootCmd, &s, 0)
assert.Equal(tt.expectedDumpCmd, s.String())
execCmd(t, rootCmd, args)
})
}
}
// dumpCmd helps in comparing cobra.Command by writing their Use and Commands.
// Runnable commands are marked with a *.
func dumpCmd(c *cobra.Command, w io.Writer, ntabs int) {
fmt.Fprintf(w, "%s%s", strings.Repeat(" ", ntabs), c.Use)
ntabs++
if c.Runnable() {
fmt.Fprintf(w, "*")
}
c.Flags().VisitAll(func(f *pflag.Flag) {
fmt.Fprintf(w, " --%s=%s", f.Name, f.Value.Type())
})
fmt.Fprintf(w, "\n")
for _, cc := range c.Commands() {
dumpCmd(cc, w, ntabs)
}
}
func TestLinkPluginHooks(t *testing.T) {
t.Skip("passes locally and with act, but fails in CI")
var (
args = []string{"arg1", "arg2"}
pluginParams = map[string]string{"key": "val"}
ctx = context.Background()
// helper to assert pluginInterface.ExecuteHook*() calls in expected order
// (pre, then post, then cleanup)
expectExecuteHook = func(t *testing.T, p *mocks.PluginInterface, expectedFlags plugin.Flags, hooks ...*plugin.Hook) {
t.Helper()
matcher := func(hook *plugin.Hook) any {
return mock.MatchedBy(func(execHook *plugin.ExecutedHook) bool {
return hook.Name == execHook.Hook.Name &&
hook.PlaceHookOn == execHook.Hook.PlaceHookOn
})
}
asserter := func(hook *plugin.Hook) func(_ context.Context, hook *plugin.ExecutedHook, _ plugin.ClientAPI) {
return func(_ context.Context, execHook *plugin.ExecutedHook, _ plugin.ClientAPI) {
assert.True(t, strings.HasSuffix(execHook.ExecutedCommand.Path, hook.PlaceHookOn), "wrong path %q want %q", execHook.ExecutedCommand.Path, hook.PlaceHookOn)
assert.Equal(t, args, execHook.ExecutedCommand.Args)
assertFlags(t, expectedFlags, execHook.ExecutedCommand)
assert.Equal(t, pluginParams, execHook.ExecutedCommand.With)
}
}
var lastPre *mock.Call
for _, hook := range hooks {
pre := p.EXPECT().
ExecuteHookPre(ctx, matcher(hook), mock.Anything).
Run(asserter(hook)).
Return(nil).
Call
if lastPre != nil {
pre.NotBefore(lastPre)
}
lastPre = pre
}
for _, hook := range hooks {
post := p.EXPECT().
ExecuteHookPost(ctx, matcher(hook), mock.Anything).
Run(asserter(hook)).
Return(nil).
Call
cleanup := p.EXPECT().
ExecuteHookCleanUp(ctx, matcher(hook), mock.Anything).
Run(asserter(hook)).
Return(nil).
Call
post.NotBefore(lastPre)
cleanup.NotBefore(post)
}
}
)
tests := []struct {
name string
expectedError string
setup func(*testing.T, context.Context, *mocks.PluginInterface)
}{
{
name: "fail: command not runnable",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Hooks: []*plugin.Hook{
{
Name: "test-hook",
PlaceHookOn: "ignite scaffold",
},
},
},
nil,
)
},
expectedError: `can't attach app hook "test-hook" to non executable command "ignite scaffold"`,
},
{
name: "fail: command doesn't exists",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{
Hooks: []*plugin.Hook{
{
Name: "test-hook",
PlaceHookOn: "ignite chain",
},
},
},
nil,
)
},
expectedError: `unable to find command path "ignite chain" for app hook "test-hook"`,
},
{
name: "ok: single hook",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
hook := &plugin.Hook{
Name: "test-hook",
PlaceHookOn: "scaffold chain",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Hooks: []*plugin.Hook{hook}}, nil)
expectExecuteHook(t, p, plugin.Flags{{Name: "path"}}, hook)
},
},
{
name: "ok: multiple hooks on same command",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
hook1 := &plugin.Hook{
Name: "test-hook-1",
PlaceHookOn: "scaffold chain",
}
hook2 := &plugin.Hook{
Name: "test-hook-2",
PlaceHookOn: "scaffold chain",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Hooks: []*plugin.Hook{hook1, hook2}}, nil)
expectExecuteHook(t, p, plugin.Flags{{Name: "path"}}, hook1, hook2)
},
},
{
name: "ok: multiple hooks on different commands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
hookChain1 := &plugin.Hook{
Name: "test-hook-1",
PlaceHookOn: "scaffold chain",
}
hookChain2 := &plugin.Hook{
Name: "test-hook-2",
PlaceHookOn: "scaffold chain",
}
hookModule := &plugin.Hook{
Name: "test-hook-3",
PlaceHookOn: "scaffold module",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Hooks: []*plugin.Hook{hookChain1, hookChain2, hookModule}}, nil)
expectExecuteHook(t, p, plugin.Flags{{Name: "path"}}, hookChain1, hookChain2)
expectExecuteHook(t, p, nil, hookModule)
},
},
{
name: "ok: duplicate hook names on same command",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
hooks := []*plugin.Hook{
{
Name: "test-hook",
PlaceHookOn: "ignite scaffold chain",
},
{
Name: "test-hook",
PlaceHookOn: "ignite scaffold chain",
},
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Hooks: hooks}, nil)
expectExecuteHook(t, p, plugin.Flags{{Name: "path"}}, hooks...)
},
},
{
name: "ok: duplicate hook names on different commands",
setup: func(t *testing.T, ctx context.Context, p *mocks.PluginInterface) {
t.Helper()
hookChain := &plugin.Hook{
Name: "test-hook",
PlaceHookOn: "ignite scaffold chain",
}
hookModule := &plugin.Hook{
Name: "test-hook",
PlaceHookOn: "ignite scaffold module",
}
p.EXPECT().
Manifest(ctx).
Return(&plugin.Manifest{Hooks: []*plugin.Hook{hookChain, hookModule}}, nil)
expectExecuteHook(t, p, plugin.Flags{{Name: "path"}}, hookChain)
expectExecuteHook(t, p, nil, hookModule)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
require := require.New(t)
pi := mocks.NewPluginInterface(t)
p := &plugin.Plugin{
Plugin: pluginsconfig.Plugin{
Path: "foo",
With: pluginParams,
},
Interface: pi,
}
rootCmd := buildRootCmd(ctx)
tt.setup(t, ctx, pi)
_ = linkPlugins(ctx, rootCmd, []*plugin.Plugin{p})
if tt.expectedError != "" {
require.EqualError(p.Error, tt.expectedError)
return
}
require.NoError(p.Error)
execCmd(t, rootCmd, args)
})
}
}
// execCmd executes all the runnable commands contained in c.
func execCmd(t *testing.T, c *cobra.Command, args []string) {
t.Helper()
if c.Runnable() {
os.Args = strings.Fields(c.CommandPath())
os.Args = append(os.Args, args...)
err := c.Execute()
require.NoError(t, err)
return
}
for _, c := range c.Commands() {
execCmd(t, c, args)
}
}