package code import ( "bytes" "fmt" "testing" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/gh" ghmock "github.com/cli/cli/v2/internal/gh/mock" "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 TestNewCmdCode(t *testing.T) { tests := []struct { name string input string output CodeOptions wantErr bool errMsg string }{ { name: "no arguments", input: "", wantErr: true, errMsg: "specify search keywords or flags", }, { name: "keyword arguments", input: "react lifecycle", output: CodeOptions{ Query: search.Query{ Keywords: []string{"react", "lifecycle"}, Kind: "code", Limit: 30, }, }, }, { name: "web flag", input: "--web", output: CodeOptions{ Query: search.Query{ Keywords: []string{}, Kind: "code", Limit: 30, }, WebMode: true, }, }, { name: "limit flag", input: "--limit 10", output: CodeOptions{ Query: search.Query{ Keywords: []string{}, Kind: "code", Limit: 10, }, }, }, { name: "invalid limit flag", input: "--limit 1001", wantErr: true, errMsg: "`--limit` must be between 1 and 1000", }, { name: "qualifier flags", input: ` --extension=extension --filename=filename --match=path,file --language=language --repo=owner/repo --size=5 --owner=owner `, output: CodeOptions{ Query: search.Query{ Keywords: []string{}, Kind: "code", Limit: 30, Qualifiers: search.Qualifiers{ Extension: "extension", Filename: "filename", In: []string{"path", "file"}, Language: "language", Repo: []string{"owner/repo"}, Size: "5", User: []string{"owner"}, }, }, }, }, } 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 *CodeOptions cmd := NewCmdCode(f, func(opts *CodeOptions) 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 TestCodeRun(t *testing.T) { var query = search.Query{ Keywords: []string{"map"}, Kind: "code", Limit: 30, Qualifiers: search.Qualifiers{ Language: "go", Repo: []string{"cli/cli"}, }, } tests := []struct { errMsg string name string opts *CodeOptions tty bool wantErr bool wantStderr string wantStdout string wantBrowse string }{ { name: "displays results tty", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ CodeFunc: func(query search.Query) (search.CodeResult, error) { return search.CodeResult{ IncompleteResults: false, Items: []search.Code{ { Name: "context.go", Path: "context/context.go", Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ { Fragment: "\t}\n\n\tvar repos []*api.Repository\n\trepoMap := map[string]bool{}\n\n\tadd := func(r *api.Repository) {\n\t\tfn := ghrepo.FullName(r)", Matches: []search.Match{ { Text: "Map", Indices: []int{38, 41}, }, { Text: "map", Indices: []int{45, 48}, }, }, }, }, }, { Name: "pr.go", Path: "pkg/cmd/pr/pr.go", Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ { Fragment: "\t\t\t$ gh pr create --fill\n\t\t\t$ gh pr view --web\n\t\t`),\n\t\tAnnotations: map[string]string{\n\t\t\t\"help:arguments\": heredoc.Doc(`\n\t\t\t\tA pull request can be supplied as argument in any of the following formats:\n\t\t\t\t- by number, e.g. \"123\";", Matches: []search.Match{ { Text: "map", Indices: []int{68, 71}, }, }, }, }, }, }, Total: 2, }, nil }, }, }, tty: true, wantStdout: "\nShowing 2 of 2 results\n\ncli/cli context/context.go\n\trepoMap := map[string]bool{}\n\ncli/cli pkg/cmd/pr/pr.go\n\tAnnotations: map[string]string{\n", }, { name: "displays results notty", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ CodeFunc: func(query search.Query) (search.CodeResult, error) { return search.CodeResult{ IncompleteResults: false, Items: []search.Code{ { Name: "context.go", Path: "context/context.go", Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ { Fragment: "\t}\n\n\tvar repos []*api.Repository\n\trepoMap := map[string]bool{}\n\n\tadd := func(r *api.Repository) {\n\t\tfn := ghrepo.FullName(r)", Matches: []search.Match{ { Text: "Map", Indices: []int{38, 41}, }, { Text: "map", Indices: []int{45, 48}, }, }, }, }, }, { Name: "pr.go", Path: "pkg/cmd/pr/pr.go", Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ { Fragment: "\t\t\t$ gh pr create --fill\n\t\t\t$ gh pr view --web\n\t\t`),\n\t\tAnnotations: map[string]string{\n\t\t\t\"help:arguments\": heredoc.Doc(`\n\t\t\t\tA pull request can be supplied as argument in any of the following formats:\n\t\t\t\t- by number, e.g. \"123\";", Matches: []search.Match{ { Text: "map", Indices: []int{68, 71}, }, }, }, }, }, }, Total: 2, }, nil }, }, }, tty: false, wantStdout: "cli/cli:context/context.go: repoMap := map[string]bool{}\ncli/cli:pkg/cmd/pr/pr.go: Annotations: map[string]string{\n", }, { name: "displays no results", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ CodeFunc: func(query search.Query) (search.CodeResult, error) { return search.CodeResult{}, nil }, }, }, wantErr: true, errMsg: "no code results matched your search", }, { name: "displays search error", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ CodeFunc: func(query search.Query) (search.CodeResult, error) { return search.CodeResult{}, fmt.Errorf("error with query") }, }, }, wantErr: true, errMsg: "error with query", }, { name: "opens browser for web mode tty", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ URLFunc: func(query search.Query) string { return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli" }, }, WebMode: true, }, tty: true, wantStderr: "Opening https://github.com/search in your browser.\n", wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli", }, { name: "opens browser for web mode notty", opts: &CodeOptions{ Query: query, Searcher: &search.SearcherMock{ URLFunc: func(query search.Query) string { return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli" }, }, WebMode: true, }, wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli", }, { name: "converts filename and extension qualifiers for github.com web search", opts: &CodeOptions{ Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, Query: search.Query{ Keywords: []string{"map"}, Kind: "code", Limit: 30, Qualifiers: search.Qualifiers{ Filename: "testing", Extension: "go", }, }, Searcher: search.NewSearcher(nil, "github.com", &fd.DisabledDetectorMock{}), WebMode: true, }, wantBrowse: "https://github.com/search?q=map+path%3Atesting.go&type=code", }, { name: "properly handles extension with dot prefix when converting to path qualifier", opts: &CodeOptions{ Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, Query: search.Query{ Keywords: []string{"map"}, Kind: "code", Limit: 30, Qualifiers: search.Qualifiers{ Filename: "testing", Extension: ".cpp", }, }, Searcher: search.NewSearcher(nil, "github.com", &fd.DisabledDetectorMock{}), WebMode: true, }, wantBrowse: "https://github.com/search?q=map+path%3Atesting.cpp&type=code", }, { name: "does not convert filename and extension qualifiers for GHES web search", opts: &CodeOptions{ Config: func() (gh.Config, error) { cfg := &ghmock.ConfigMock{ AuthenticationFunc: func() gh.AuthConfig { authCfg := &config.AuthConfig{} authCfg.SetDefaultHost("example.com", "GH_HOST") return authCfg }, } return cfg, nil }, Query: search.Query{ Keywords: []string{"map"}, Kind: "code", Limit: 30, Qualifiers: search.Qualifiers{ Filename: "testing", Extension: "go", }, }, Searcher: search.NewSearcher(nil, "example.com", &fd.DisabledDetectorMock{}), WebMode: true, }, wantBrowse: "https://example.com/search?q=map+extension%3Ago+filename%3Atesting&type=code", }, } 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 browser := &browser.Stub{} tt.opts.Browser = browser t.Run(tt.name, func(t *testing.T) { err := codeRun(tt.opts) if tt.wantErr { assert.EqualError(t, err, tt.errMsg) return } else if err != nil { t.Fatalf("codeRun unexpected error: %v", err) } assert.Equal(t, tt.wantStdout, stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) browser.Verify(t, tt.wantBrowse) }) } }