package commits import ( "bytes" "fmt" "testing" "time" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) func TestNewCmdCommits(t *testing.T) { var trueBool = true tests := []struct { name string input string output CommitsOptions wantErr bool errMsg string }{ { name: "no arguments", input: "", wantErr: true, errMsg: "specify search keywords or flags", }, { name: "keyword arguments", input: "some search terms", output: CommitsOptions{ Query: search.Query{Keywords: []string{"some", "search", "terms"}, Kind: "commits", Limit: 30}, }, }, { name: "web flag", input: "--web", output: CommitsOptions{ Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 30}, WebMode: true, }, }, { name: "limit flag", input: "--limit 10", output: CommitsOptions{Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 10}}, }, { name: "invalid limit flag", input: "--limit 1001", wantErr: true, errMsg: "`--limit` must be between 1 and 1000", }, { name: "order flag", input: "--order asc", output: CommitsOptions{ Query: search.Query{Keywords: []string{}, Kind: "commits", Limit: 30, Order: "asc"}, }, }, { name: "invalid order flag", input: "--order invalid", wantErr: true, errMsg: "invalid argument \"invalid\" for \"--order\" flag: valid values are {asc|desc}", }, { name: "qualifier flags", input: ` --author=foo --author-date=01-01-2000 --author-email=foo@example.com --author-name=Foo --committer=bar --committer-date=01-02-2000 --committer-email=bar@example.com --committer-name=Bar --hash=aaa --merge --parent=bbb --repo=owner/repo --tree=ccc --owner=owner --visibility=public `, output: CommitsOptions{ Query: search.Query{ Keywords: []string{}, Kind: "commits", Limit: 30, Qualifiers: search.Qualifiers{ Author: "foo", AuthorDate: "01-01-2000", AuthorEmail: "foo@example.com", AuthorName: "Foo", Committer: "bar", CommitterDate: "01-02-2000", CommitterEmail: "bar@example.com", CommitterName: "Bar", Hash: "aaa", Merge: &trueBool, Parent: "bbb", Repo: []string{"owner/repo"}, Tree: "ccc", User: []string{"owner"}, Is: []string{"public"}, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ IOStreams: ios, } argv, err := shlex.Split(tt.input) assert.NoError(t, err) var gotOpts *CommitsOptions cmd := NewCmdCommits(f, func(opts *CommitsOptions) error { gotOpts = opts return nil }) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() if tt.wantErr { assert.EqualError(t, err, tt.errMsg) return } assert.NoError(t, err) assert.Equal(t, tt.output.Query, gotOpts.Query) assert.Equal(t, tt.output.WebMode, gotOpts.WebMode) }) } } func TestCommitsRun(t *testing.T) { var now = time.Date(2023, 1, 17, 12, 30, 0, 0, time.UTC) var author = search.CommitUser{Date: time.Date(2022, 12, 27, 11, 30, 0, 0, time.UTC)} var committer = search.CommitUser{Date: time.Date(2022, 12, 28, 12, 30, 0, 0, time.UTC)} var query = search.Query{ Keywords: []string{"cli"}, Kind: "commits", Limit: 30, Qualifiers: search.Qualifiers{}, } tests := []struct { errMsg string name string opts *CommitsOptions tty bool wantErr bool wantStderr string wantStdout string }{ { name: "displays results tty", opts: &CommitsOptions{ Query: query, Searcher: &search.SearcherMock{ CommitsFunc: func(query search.Query) (search.CommitsResult, error) { return search.CommitsResult{ IncompleteResults: false, Items: []search.Commit{ { Author: search.User{Login: "monalisa"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "hello"}, Repo: search.Repository{FullName: "test/cli"}, Sha: "aaaaaaaa", }, { Author: search.User{Login: "johnnytest"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "hi"}, Repo: search.Repository{FullName: "test/cliing", IsPrivate: true}, Sha: "bbbbbbbb", }, { Author: search.User{Login: "hubot"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "greetings"}, Repo: search.Repository{FullName: "cli/cli"}, Sha: "cccccccc", }, }, Total: 300, }, nil }, }, }, tty: true, wantStdout: "\nShowing 3 of 300 commits\n\nREPO SHA MESSAGE AUTHOR CREATED\ntest/cli aaaaaaaa hello monalisa about 21 days ago\ntest/cliing bbbbbbbb hi johnnytest about 21 days ago\ncli/cli cccccccc greetings hubot about 21 days ago\n", }, { name: "displays results notty", opts: &CommitsOptions{ Query: query, Searcher: &search.SearcherMock{ CommitsFunc: func(query search.Query) (search.CommitsResult, error) { return search.CommitsResult{ IncompleteResults: false, Items: []search.Commit{ { Author: search.User{Login: "monalisa"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "hello"}, Repo: search.Repository{FullName: "test/cli"}, Sha: "aaaaaaaa", }, { Author: search.User{Login: "johnnytest"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "hi"}, Repo: search.Repository{FullName: "test/cliing", IsPrivate: true}, Sha: "bbbbbbbb", }, { Author: search.User{Login: "hubot"}, Info: search.CommitInfo{Author: author, Committer: committer, Message: "greetings"}, Repo: search.Repository{FullName: "cli/cli"}, Sha: "cccccccc", }, }, Total: 300, }, nil }, }, }, wantStdout: "test/cli\taaaaaaaa\thello\tmonalisa\t2022-12-27T11:30:00Z\ntest/cliing\tbbbbbbbb\thi\tjohnnytest\t2022-12-27T11:30:00Z\ncli/cli\tcccccccc\tgreetings\thubot\t2022-12-27T11:30:00Z\n", }, { name: "displays no results", opts: &CommitsOptions{ Query: query, Searcher: &search.SearcherMock{ CommitsFunc: func(query search.Query) (search.CommitsResult, error) { return search.CommitsResult{}, nil }, }, }, wantErr: true, errMsg: "no commits matched your search", }, { name: "displays search error", opts: &CommitsOptions{ Query: query, Searcher: &search.SearcherMock{ CommitsFunc: func(query search.Query) (search.CommitsResult, error) { return search.CommitsResult{}, fmt.Errorf("error with query") }, }, }, errMsg: "error with query", wantErr: true, }, { name: "opens browser for web mode tty", opts: &CommitsOptions{ Browser: &browser.Stub{}, Query: query, Searcher: &search.SearcherMock{ URLFunc: func(query search.Query) string { return "https://github.com/search?type=commits&q=cli" }, }, WebMode: true, }, tty: true, wantStderr: "Opening https://github.com/search in your browser.\n", }, { name: "opens browser for web mode notty", opts: &CommitsOptions{ Browser: &browser.Stub{}, Query: query, Searcher: &search.SearcherMock{ URLFunc: func(query search.Query) string { return "https://github.com/search?type=commits&q=cli" }, }, WebMode: true, }, }, } for _, tt := range tests { ios, _, stdout, stderr := iostreams.Test() ios.SetStdinTTY(tt.tty) ios.SetStdoutTTY(tt.tty) ios.SetStderrTTY(tt.tty) tt.opts.IO = ios tt.opts.Now = now t.Run(tt.name, func(t *testing.T) { err := commitsRun(tt.opts) if tt.wantErr { assert.EqualError(t, err, tt.errMsg) return } else if err != nil { t.Fatalf("commitsRun unexpected error: %v", err) } assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) }) } }