package list import ( "bytes" "io" "net/http" "strings" "testing" "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/run" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/test" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) { ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(isTTY) ios.SetStdinTTY(isTTY) ios.SetStderrTTY(isTTY) browser := &browser.Stub{} factory := &cmdutil.Factory{ IOStreams: ios, Browser: browser, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: rt}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, } fakeNow := func() time.Time { return time.Date(2022, time.August, 24, 23, 50, 0, 0, time.UTC) } cmd := NewCmdList(factory, func(opts *ListOptions) error { opts.Now = fakeNow return listRun(opts) }) argv, err := shlex.Split(cli) if err != nil { return nil, err } cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() return &test.CmdOut{ OutBuf: stdout, ErrBuf: stderr, BrowsedURL: browser.BrowsedURL(), }, err } func initFakeHTTP() *httpmock.Registry { return &httpmock.Registry{} } func TestPRList(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) output, err := runCommand(http, true, "") if err != nil { t.Fatal(err) } assert.Equal(t, heredoc.Doc(` Showing 3 of 3 open pull requests in OWNER/REPO ID TITLE BRANCH CREATED AT #32 New feature feature about 3 hours ago #29 Fixed bad bug hubot:bug-fix about 1 month ago #28 Improve documentation docs about 2 years ago `), output.String()) assert.Equal(t, ``, output.Stderr()) } func TestPRList_nontty(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register(httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prList.json")) output, err := runCommand(http, false, "") if err != nil { t.Fatal(err) } assert.Equal(t, "", output.Stderr()) assert.Equal(t, `32 New feature feature DRAFT 2022-08-24T20:01:12Z 29 Fixed bad bug hubot:bug-fix OPEN 2022-07-20T19:01:12Z 28 Improve documentation docs MERGED 2020-01-26T19:01:12Z `, output.String()) } func TestPRList_filtering(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestList\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, []interface{}{"OPEN", "CLOSED", "MERGED"}, params["state"].([]interface{})) })) output, err := runCommand(http, true, `-s all`) assert.Error(t, err) assert.Equal(t, "", output.String()) assert.Equal(t, "", output.Stderr()) } func TestPRList_filteringRemoveDuplicate(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestList\b`), httpmock.FileResponse("./fixtures/prListWithDuplicates.json")) output, err := runCommand(http, true, "") if err != nil { t.Fatal(err) } out := output.String() idx := strings.Index(out, "New feature") if idx < 0 { t.Fatalf("text %q not found in %q", "New feature", out) } assert.Equal(t, idx, strings.LastIndex(out, "New feature")) } func TestPRList_filteringClosed(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestList\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, []interface{}{"CLOSED", "MERGED"}, params["state"].([]interface{})) })) _, err := runCommand(http, true, `-s closed`) assert.Error(t, err) } func TestPRList_filteringHeadBranch(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestList\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, interface{}("bug-fix"), params["headBranch"]) })) _, err := runCommand(http, true, `-H bug-fix`) assert.Error(t, err) } func TestPRList_filteringAssignee(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestSearch\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, `assignee:hubot base:develop is:merged label:"needs tests" repo:OWNER/REPO type:pr`, params["q"].(string)) })) _, err := runCommand(http, true, `-s merged -l "needs tests" -a hubot -B develop`) assert.Error(t, err) } func TestPRList_filteringDraft(t *testing.T) { tests := []struct { name string cli string expectedQuery string }{ { name: "draft", cli: "--draft", expectedQuery: `draft:true repo:OWNER/REPO state:open type:pr`, }, { name: "non-draft", cli: "--draft=false", expectedQuery: `draft:false repo:OWNER/REPO state:open type:pr`, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestSearch\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, test.expectedQuery, params["q"].(string)) })) _, err := runCommand(http, true, test.cli) assert.Error(t, err) }) } } func TestPRList_filteringAuthor(t *testing.T) { tests := []struct { name string cli string expectedQuery string }{ { name: "author @me", cli: `--author "@me"`, expectedQuery: `author:@me repo:OWNER/REPO state:open type:pr`, }, { name: "author user", cli: `--author "monalisa"`, expectedQuery: `author:monalisa repo:OWNER/REPO state:open type:pr`, }, { name: "app author", cli: `--author "app/dependabot"`, expectedQuery: `author:app/dependabot repo:OWNER/REPO state:open type:pr`, }, { name: "app author with app option", cli: `--app "dependabot"`, expectedQuery: `author:app/dependabot repo:OWNER/REPO state:open type:pr`, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) http.Register( httpmock.GraphQL(`query PullRequestSearch\b`), httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) { assert.Equal(t, test.expectedQuery, params["q"].(string)) })) _, err := runCommand(http, true, test.cli) assert.Error(t, err) }) } } func TestPRList_withInvalidLimitFlag(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) _, err := runCommand(http, true, `--limit=0`) assert.EqualError(t, err, "invalid value for --limit: 0") } func TestPRList_web(t *testing.T) { tests := []struct { name string cli string expectedBrowserURL string }{ { name: "filters", cli: "-a peter -l bug -l docs -L 10 -s merged -B trunk", expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=assignee%3Apeter+base%3Atrunk+is%3Amerged+label%3Abug+label%3Adocs+type%3Apr", }, { name: "draft", cli: "--draft=true", expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=draft%3Atrue+state%3Aopen+type%3Apr", }, { name: "non-draft", cli: "--draft=0", expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=draft%3Afalse+state%3Aopen+type%3Apr", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { http := initFakeHTTP() defer http.Verify(t) _, cmdTeardown := run.Stub() defer cmdTeardown(t) output, err := runCommand(http, true, "--web "+test.cli) if err != nil { t.Errorf("error running command `pr list` with `--web` flag: %v", err) } assert.Equal(t, "", output.String()) assert.Equal(t, "Opening github.com/OWNER/REPO/pulls in your browser.\n", output.Stderr()) assert.Equal(t, test.expectedBrowserURL, output.BrowsedURL) }) } } func TestPRList_withProjectItems(t *testing.T) { reg := &httpmock.Registry{} defer reg.Verify(t) reg.Register( httpmock.GraphQL(`query PullRequestList\b`), httpmock.GraphQLQuery(`{ "data": { "repository": { "pullRequests": { "totalCount": 1, "nodes": [ { "projectItems": { "nodes": [ { "id": "PVTI_lAHOAA3WC84AW6WNzgJ8rnQ", "project": { "id": "PVT_kwHOAA3WC84AW6WN", "title": "Test Public Project" }, "status": { "optionId": "47fc9ee4", "name": "In Progress" } } ], "totalCount": 1 } } ] } } } }`, func(_ string, params map[string]interface{}) { require.Equal(t, map[string]interface{}{ "owner": "OWNER", "repo": "REPO", "limit": float64(30), "state": []interface{}{"OPEN"}, }, params) })) client := &http.Client{Transport: reg} prsAndTotalCount, err := listPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ Entity: "pr", State: "open", }, 30, ) require.NoError(t, err) require.Len(t, prsAndTotalCount.PullRequests, 1) require.Len(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes, 1) require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].ID, "PVTI_lAHOAA3WC84AW6WNzgJ8rnQ") expectedProject := api.ProjectV2ItemProject{ ID: "PVT_kwHOAA3WC84AW6WN", Title: "Test Public Project", } require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].Project, expectedProject) expectedStatus := api.ProjectV2ItemStatus{ OptionID: "47fc9ee4", Name: "In Progress", } require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].Status, expectedStatus) } func TestPRList_Search_withProjectItems(t *testing.T) { reg := &httpmock.Registry{} defer reg.Verify(t) reg.Register( httpmock.GraphQL(`query PullRequestSearch\b`), httpmock.GraphQLQuery(`{ "data": { "search": { "issueCount": 1, "nodes": [ { "projectItems": { "nodes": [ { "id": "PVTI_lAHOAA3WC84AW6WNzgJ8rl0", "project": { "id": "PVT_kwHOAA3WC84AW6WN", "title": "Test Public Project" }, "status": { "optionId": "47fc9ee4", "name": "In Progress" } } ], "totalCount": 1 } } ] } } }`, func(_ string, params map[string]interface{}) { require.Equal(t, map[string]interface{}{ "limit": float64(30), "q": "just used to force the search API branch repo:OWNER/REPO state:open type:pr", }, params) })) client := &http.Client{Transport: reg} prsAndTotalCount, err := listPullRequests( client, ghrepo.New("OWNER", "REPO"), prShared.FilterOptions{ Entity: "pr", State: "open", Search: "just used to force the search API branch", }, 30, ) require.NoError(t, err) require.Len(t, prsAndTotalCount.PullRequests, 1) require.Len(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes, 1) require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].ID, "PVTI_lAHOAA3WC84AW6WNzgJ8rl0") expectedProject := api.ProjectV2ItemProject{ ID: "PVT_kwHOAA3WC84AW6WN", Title: "Test Public Project", } require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].Project, expectedProject) expectedStatus := api.ProjectV2ItemStatus{ OptionID: "47fc9ee4", Name: "In Progress", } require.Equal(t, prsAndTotalCount.PullRequests[0].ProjectItems.Nodes[0].Status, expectedStatus) }