package imports import ( "bytes" "fmt" "io" "os" "path/filepath" "strings" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/pkg/cmd/alias/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewCmdImport(t *testing.T) { tests := []struct { name string cli string tty bool wants ImportOptions wantsError string }{ { name: "no filename and stdin tty", cli: "", tty: true, wants: ImportOptions{ Filename: "", OverwriteExisting: false, }, wantsError: "no filename passed and nothing on STDIN", }, { name: "no filename and stdin is not tty", cli: "", tty: false, wants: ImportOptions{ Filename: "-", OverwriteExisting: false, }, }, { name: "stdin arg", cli: "-", wants: ImportOptions{ Filename: "-", OverwriteExisting: false, }, }, { name: "multiple filenames", cli: "aliases1 aliases2", wants: ImportOptions{ Filename: "aliases1 aliases2", OverwriteExisting: false, }, wantsError: "too many arguments", }, { name: "clobber flag", cli: "aliases --clobber", wants: ImportOptions{ Filename: "aliases", OverwriteExisting: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdinTTY(tt.tty) f := &cmdutil.Factory{IOStreams: ios} argv, err := shlex.Split(tt.cli) assert.NoError(t, err) var gotOpts *ImportOptions cmd := NewCmdImport(f, func(opts *ImportOptions) error { gotOpts = opts return nil }) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() if tt.wantsError != "" { assert.EqualError(t, err, tt.wantsError) return } assert.NoError(t, err) assert.Equal(t, tt.wants.Filename, gotOpts.Filename) assert.Equal(t, tt.wants.OverwriteExisting, gotOpts.OverwriteExisting) }) } } func TestImportRun(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "aliases.yml") importFileMsg := fmt.Sprintf(`- Importing aliases from file %q`, tmpFile) importStdinMsg := "- Importing aliases from standard input" tests := []struct { name string opts *ImportOptions stdin string fileContents string initConfig string aliasCommands []*cobra.Command wantConfig string wantStderr string }{ { name: "with no existing aliases", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: false, }, fileContents: heredoc.Doc(` co: pr checkout igrep: '!gh issue list --label="$1" | grep "$2"' `), wantConfig: heredoc.Doc(` aliases: co: pr checkout igrep: '!gh issue list --label="$1" | grep "$2"' `), wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias igrep\n\n", }, { name: "with existing aliases", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: false, }, fileContents: heredoc.Doc(` users: |- api graphql -F name="$1" -f query=' query ($name: String!) { user(login: $name) { name } }' co: pr checkout `), initConfig: heredoc.Doc(` aliases: igrep: '!gh issue list --label="$1" | grep "$2"' editor: vim `), aliasCommands: []*cobra.Command{ {Use: "igrep"}, }, wantConfig: heredoc.Doc(` aliases: igrep: '!gh issue list --label="$1" | grep "$2"' co: pr checkout users: |- api graphql -F name="$1" -f query=' query ($name: String!) { user(login: $name) { name } }' editor: vim `), wantStderr: importFileMsg + "\n✓ Added alias co\n✓ Added alias users\n\n", }, { name: "from stdin", opts: &ImportOptions{ Filename: "-", OverwriteExisting: false, }, stdin: heredoc.Doc(` co: pr checkout features: |- issue list --label=enhancement igrep: '!gh issue list --label="$1" | grep "$2"' `), wantConfig: heredoc.Doc(` aliases: co: pr checkout features: |- issue list --label=enhancement igrep: '!gh issue list --label="$1" | grep "$2"' `), wantStderr: importStdinMsg + "\n✓ Added alias co\n✓ Added alias features\n✓ Added alias igrep\n\n", }, { name: "already taken aliases", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: false, }, fileContents: heredoc.Doc(` co: pr checkout -R cool/repo igrep: '!gh issue list --label="$1" | grep "$2"' `), initConfig: heredoc.Doc(` aliases: co: pr checkout editor: vim `), aliasCommands: []*cobra.Command{ {Use: "co"}, }, wantConfig: heredoc.Doc(` aliases: co: pr checkout igrep: '!gh issue list --label="$1" | grep "$2"' editor: vim `), wantStderr: importFileMsg + "\nX Could not import alias co: name already taken\n✓ Added alias igrep\n\n", }, { name: "override aliases", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: true, }, fileContents: heredoc.Doc(` co: pr checkout -R cool/repo igrep: '!gh issue list --label="$1" | grep "$2"' `), initConfig: heredoc.Doc(` aliases: co: pr checkout editor: vim `), aliasCommands: []*cobra.Command{ {Use: "co"}, }, wantConfig: heredoc.Doc(` aliases: co: pr checkout -R cool/repo igrep: '!gh issue list --label="$1" | grep "$2"' editor: vim `), wantStderr: importFileMsg + "\n! Changed alias co\n✓ Added alias igrep\n\n", }, { name: "alias is a gh command", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: false, }, fileContents: heredoc.Doc(` pr: pr checkout issue: issue list api: api graphql `), wantStderr: strings.Join( []string{ importFileMsg, "X Could not import alias api: already a gh command or extension", "X Could not import alias issue: already a gh command or extension", "X Could not import alias pr: already a gh command or extension\n\n", }, "\n", ), }, { name: "invalid expansion", opts: &ImportOptions{ Filename: tmpFile, OverwriteExisting: false, }, fileContents: heredoc.Doc(` alias1: alias2: ps checkout `), wantStderr: strings.Join( []string{ importFileMsg, "X Could not import alias alias1: expansion does not correspond to a gh command, extension, or alias", "X Could not import alias alias2: expansion does not correspond to a gh command, extension, or alias\n\n", }, "\n", ), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.fileContents != "" { err := os.WriteFile(tmpFile, []byte(tt.fileContents), 0600) require.NoError(t, err) } ios, stdin, _, stderr := iostreams.Test() ios.SetStdinTTY(true) ios.SetStdoutTTY(true) ios.SetStderrTTY(true) tt.opts.IO = ios readConfigs := config.StubWriteConfig(t) cfg := config.NewFromString(tt.initConfig) tt.opts.Config = func() (gh.Config, error) { return cfg, nil } // Create fake command structure for testing. rootCmd := &cobra.Command{} prCmd := &cobra.Command{Use: "pr"} prCmd.AddCommand(&cobra.Command{Use: "checkout"}) prCmd.AddCommand(&cobra.Command{Use: "status"}) rootCmd.AddCommand(prCmd) issueCmd := &cobra.Command{Use: "issue"} issueCmd.AddCommand(&cobra.Command{Use: "list"}) rootCmd.AddCommand(issueCmd) apiCmd := &cobra.Command{Use: "api"} apiCmd.AddCommand(&cobra.Command{Use: "graphql"}) rootCmd.AddCommand(apiCmd) for _, cmd := range tt.aliasCommands { rootCmd.AddCommand(cmd) } tt.opts.validAliasName = shared.ValidAliasNameFunc(rootCmd) tt.opts.validAliasExpansion = shared.ValidAliasExpansionFunc(rootCmd) if tt.stdin != "" { stdin.WriteString(tt.stdin) } err := importRun(tt.opts) require.NoError(t, err) configOut := bytes.Buffer{} readConfigs(&configOut, io.Discard) assert.Equal(t, tt.wantStderr, stderr.String()) assert.Equal(t, tt.wantConfig, configOut.String()) }) } }