From b96a8e3c0168a3ee300c25d91879d32dc30169b6 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 27 Nov 2023 10:40:09 +0100 Subject: [PATCH 1/5] Add PR Finder test to cover projectItems --- pkg/cmd/pr/shared/finder_test.go | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index 23138ed2d..bfd82a2f2 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -10,6 +10,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/httpmock" + "github.com/stretchr/testify/require" ) func TestFind(t *testing.T) { @@ -409,6 +410,68 @@ func TestFind(t *testing.T) { wantPR: 13, wantRepo: "https://github.com/OWNER/REPO", }, + { + name: "inclduing project items", + args: args{ + selector: "", + fields: []string{"projectItems"}, + baseRepoFn: func() (ghrepo.Interface, error) { + return ghrepo.FromFullName("OWNER/REPO") + }, + branchFn: func() (string, error) { + return "blueberries", nil + }, + branchConfig: func(branch string) (c git.BranchConfig) { + c.MergeRef = "refs/pull/13/head" + return + }, + }, + httpStub: func(r *httpmock.Registry) { + r.Register( + httpmock.GraphQL(`query PullRequestByNumber\b`), + httpmock.StringResponse(`{"data":{"repository":{ + "pullRequest":{"number":13} + }}}`)) + + r.Register( + httpmock.GraphQL(`query PullRequestProjectItems\b`), + httpmock.GraphQLQuery(`{ + "data": { + "repository": { + "pullRequest": { + "projectItems": { + "nodes": [ + { + "id": "PVTI_lADOB-vozM4AVk16zgK6U50", + "project": { + "id": "PVT_kwDOB-vozM4AVk16", + "title": "Test Project" + }, + "status": { + "optionId": "47fc9ee4", + "name": "In Progress" + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": "MQ" + } + } + } + } + } + }`, + func(query string, inputs map[string]interface{}) { + require.Equal(t, float64(13), inputs["number"]) + require.Equal(t, "OWNER", inputs["owner"]) + require.Equal(t, "REPO", inputs["name"]) + }), + ) + }, + wantPR: 13, + wantRepo: "https://github.com/OWNER/REPO", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 65d63e1bc9aafbd1e98f8bb905521576f4918961 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 27 Nov 2023 10:53:19 +0100 Subject: [PATCH 2/5] Add test for ProjectV2Item query Status column --- api/queries_projects_v2_test.go | 68 +++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/api/queries_projects_v2_test.go b/api/queries_projects_v2_test.go index bf6a618ba..30d782bef 100644 --- a/api/queries_projects_v2_test.go +++ b/api/queries_projects_v2_test.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/httpmock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestUpdateProjectV2Items(t *testing.T) { @@ -185,6 +186,67 @@ func TestProjectsV2ItemsForPullRequest(t *testing.T) { }, expectError: true, }, + { + name: "retrieves project items that have status columns", + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query PullRequestProjectItems\b`), + httpmock.GraphQLQuery(`{ + "data": { + "repository": { + "pullRequest": { + "projectItems": { + "nodes": [ + { + "id": "PVTI_lADOB-vozM4AVk16zgK6U50", + "project": { + "id": "PVT_kwDOB-vozM4AVk16", + "title": "Test Project" + }, + "status": { + "optionId": "47fc9ee4", + "name": "In Progress" + } + } + ], + "pageInfo": { + "hasNextPage": false, + "endCursor": "MQ" + } + } + } + } + } + }`, + func(query string, inputs map[string]interface{}) { + require.Equal(t, float64(1), inputs["number"]) + require.Equal(t, "OWNER", inputs["owner"]) + require.Equal(t, "REPO", inputs["name"]) + }), + ) + }, + expectItems: ProjectItems{ + Nodes: []*ProjectV2Item{ + { + ID: "PVTI_lADOB-vozM4AVk16zgK6U50", + Project: struct { + ID string `json:"id"` + Title string `json:"title"` + }{ + ID: "PVT_kwDOB-vozM4AVk16", + Title: "Test Project", + }, + Status: struct { + OptionID string `json:"optionId" graphql:"... on ProjectV2ItemFieldSingleSelectValue{optionId}"` + Name string `json:"name" graphql:"... on ProjectV2ItemFieldSingleSelectValue{name}"` + }{ + OptionID: "47fc9ee4", + Name: "In Progress", + }, + }, + }, + }, + }, } for _, tt := range tests { @@ -199,11 +261,11 @@ func TestProjectsV2ItemsForPullRequest(t *testing.T) { pr := &PullRequest{Number: 1} err := ProjectsV2ItemsForPullRequest(client, repo, pr) if tt.expectError { - assert.Error(t, err) + require.Error(t, err) } else { - assert.NoError(t, err) + require.NoError(t, err) } - assert.Equal(t, tt.expectItems, pr.ProjectItems) + require.Equal(t, tt.expectItems, pr.ProjectItems) }) } } From e843349a13fe28b89d758191e49baffe6e875a35 Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Mon, 27 Nov 2023 12:25:51 +0000 Subject: [PATCH 3/5] Extract Project Status into fragment to fix unmarshal error --- api/export_pr.go | 4 +- api/export_pr_test.go | 169 ++++++++++++++++++++------------ api/queries_issue.go | 12 ++- api/queries_projects_v2_test.go | 14 +-- 4 files changed, 123 insertions(+), 76 deletions(-) diff --git a/api/export_pr.go b/api/export_pr.go index bb3310811..8c17ea510 100644 --- a/api/export_pr.go +++ b/api/export_pr.go @@ -23,7 +23,7 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} { items := make([]map[string]interface{}, 0, len(issue.ProjectItems.Nodes)) for _, n := range issue.ProjectItems.Nodes { items = append(items, map[string]interface{}{ - "status": n.Status, + "status": n.Status.StatusFragment, "title": n.Project.Title, }) } @@ -109,7 +109,7 @@ func (pr *PullRequest) ExportData(fields []string) map[string]interface{} { items := make([]map[string]interface{}, 0, len(pr.ProjectItems.Nodes)) for _, n := range pr.ProjectItems.Nodes { items = append(items, map[string]interface{}{ - "status": n.Status, + "status": n.Status.StatusFragment, "title": n.Project.Title, }) } diff --git a/api/export_pr_test.go b/api/export_pr_test.go index b7f4dcddb..50e346dee 100644 --- a/api/export_pr_test.go +++ b/api/export_pr_test.go @@ -75,38 +75,6 @@ func TestIssue_ExportData(t *testing.T) { } `), }, - { - name: "project items", - fields: []string{"projectItems"}, - inputJSON: heredoc.Doc(` - { "projectItems": { "nodes": [ - { - "id": "PVTI_id", - "project": { - "id": "PVT_id", - "title": "Some Project" - }, - "status": { - "name": "Todo", - "optionId": "abc123" - } - } - ] } } - `), - outputJSON: heredoc.Doc(` - { - "projectItems": [ - { - "status": { - "optionId": "abc123", - "name": "Todo" - }, - "title": "Some Project" - } - ] - } - `), - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -213,38 +181,6 @@ func TestPullRequest_ExportData(t *testing.T) { } `), }, - { - name: "project items", - fields: []string{"projectItems"}, - inputJSON: heredoc.Doc(` - { "projectItems": { "nodes": [ - { - "id": "PVTPR_id", - "project": { - "id": "PVT_id", - "title": "Some Project" - }, - "status": { - "name": "Todo", - "optionId": "abc123" - } - } - ] } } - `), - outputJSON: heredoc.Doc(` - { - "projectItems": [ - { - "status": { - "optionId": "abc123", - "name": "Todo" - }, - "title": "Some Project" - } - ] - } - `), - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -269,3 +205,108 @@ func TestPullRequest_ExportData(t *testing.T) { }) } } + +// The following tests exist separately from the table driven tests +// above because in the implementation code, the GraphQL request is +// performed by shurcool, and it handles the JSON decoding into the +// response struct, which is slightly different than the stdlib JSON +// decoder used by the rest of the requests when Finding an Issue or PR. +func TestIssueExportProjectItems(t *testing.T) { + issue := Issue{ + ProjectItems: ProjectItems{ + Nodes: []*ProjectV2Item{ + { + ID: "PVTI_lADOB-vozM4AVk16zgK6U50", + Project: struct { + ID string `json:"id"` + Title string `json:"title"` + }{ + ID: "PVT_kwDOB-vozM4AVk16", + Title: "Test Project", + }, + Status: Status{ + StatusFragment: struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + }{ + OptionID: "47fc9ee4", + Name: "In Progress", + }, + }, + }, + }, + }, + } + + expectedExportedJSON := heredoc.Doc(` + { + "projectItems": [ + { + "status": { + "optionId": "47fc9ee4", + "name": "In Progress" + }, + "title": "Test Project" + } + ] + } + `) + + exported := issue.ExportData([]string{"projectItems"}) + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetIndent("", "\t") + require.NoError(t, enc.Encode(exported)) + require.Equal(t, expectedExportedJSON, buf.String()) +} + +func TestPRExportProjectItems(t *testing.T) { + pr := PullRequest{ + ProjectItems: ProjectItems{ + Nodes: []*ProjectV2Item{ + { + ID: "PVTI_lADOB-vozM4AVk16zgK6U50", + Project: struct { + ID string `json:"id"` + Title string `json:"title"` + }{ + ID: "PVT_kwDOB-vozM4AVk16", + Title: "Test Project", + }, + Status: Status{ + StatusFragment: struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + }{ + OptionID: "47fc9ee4", + Name: "In Progress", + }, + }, + }, + }, + }, + } + + expectedExportedJSON := heredoc.Doc(` + { + "projectItems": [ + { + "status": { + "optionId": "47fc9ee4", + "name": "In Progress" + }, + "title": "Test Project" + } + ] + } + `) + + exported := pr.ExportData([]string{"projectItems"}) + + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetIndent("", "\t") + require.NoError(t, enc.Encode(exported)) + require.Equal(t, expectedExportedJSON, buf.String()) +} diff --git a/api/queries_issue.go b/api/queries_issue.go index fdef7783f..1871294e7 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -106,10 +106,14 @@ type ProjectV2Item struct { ID string `json:"id"` Title string `json:"title"` } - Status struct { - OptionID string `json:"optionId" graphql:"... on ProjectV2ItemFieldSingleSelectValue{optionId}"` - Name string `json:"name" graphql:"... on ProjectV2ItemFieldSingleSelectValue{name}"` - } `graphql:"status:fieldValueByName(name: \"Status\")"` + Status Status `graphql:"status:fieldValueByName(name: \"Status\")"` +} + +type Status struct { + StatusFragment struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` } func (p ProjectCards) ProjectNames() []string { diff --git a/api/queries_projects_v2_test.go b/api/queries_projects_v2_test.go index 30d782bef..8cacad921 100644 --- a/api/queries_projects_v2_test.go +++ b/api/queries_projects_v2_test.go @@ -236,12 +236,14 @@ func TestProjectsV2ItemsForPullRequest(t *testing.T) { ID: "PVT_kwDOB-vozM4AVk16", Title: "Test Project", }, - Status: struct { - OptionID string `json:"optionId" graphql:"... on ProjectV2ItemFieldSingleSelectValue{optionId}"` - Name string `json:"name" graphql:"... on ProjectV2ItemFieldSingleSelectValue{name}"` - }{ - OptionID: "47fc9ee4", - Name: "In Progress", + Status: Status{ + StatusFragment: struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + }{ + OptionID: "47fc9ee4", + Name: "In Progress", + }, }, }, }, From 7aeccdb7a08525bca2ee4d27c150d156f8e13e21 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 27 Nov 2023 15:18:13 +0100 Subject: [PATCH 4/5] Update pkg/cmd/pr/shared/finder_test.go Co-authored-by: Andy Feller --- pkg/cmd/pr/shared/finder_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/pr/shared/finder_test.go b/pkg/cmd/pr/shared/finder_test.go index bfd82a2f2..6df8d3000 100644 --- a/pkg/cmd/pr/shared/finder_test.go +++ b/pkg/cmd/pr/shared/finder_test.go @@ -411,7 +411,7 @@ func TestFind(t *testing.T) { wantRepo: "https://github.com/OWNER/REPO", }, { - name: "inclduing project items", + name: "including project items", args: args{ selector: "", fields: []string{"projectItems"}, From e775bc64b8cf4314d9e42e832e1c48e2d0cb8a02 Mon Sep 17 00:00:00 2001 From: William Martin Date: Mon, 27 Nov 2023 16:59:28 +0100 Subject: [PATCH 5/5] Fix PR and Issue list Separate the GQL and JSON decoded types that we use for queries. --- api/export_pr.go | 4 +- api/export_pr_test.go | 169 ++++++++++++-------------------- api/queries_issue.go | 20 ++-- api/queries_projects_v2.go | 67 ++++++++++++- api/queries_projects_v2_test.go | 16 +-- pkg/cmd/issue/list/list_test.go | 150 ++++++++++++++++++++++++++++ pkg/cmd/pr/list/list_test.go | 146 +++++++++++++++++++++++++++ 7 files changed, 439 insertions(+), 133 deletions(-) diff --git a/api/export_pr.go b/api/export_pr.go index 8c17ea510..bb3310811 100644 --- a/api/export_pr.go +++ b/api/export_pr.go @@ -23,7 +23,7 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} { items := make([]map[string]interface{}, 0, len(issue.ProjectItems.Nodes)) for _, n := range issue.ProjectItems.Nodes { items = append(items, map[string]interface{}{ - "status": n.Status.StatusFragment, + "status": n.Status, "title": n.Project.Title, }) } @@ -109,7 +109,7 @@ func (pr *PullRequest) ExportData(fields []string) map[string]interface{} { items := make([]map[string]interface{}, 0, len(pr.ProjectItems.Nodes)) for _, n := range pr.ProjectItems.Nodes { items = append(items, map[string]interface{}{ - "status": n.Status.StatusFragment, + "status": n.Status, "title": n.Project.Title, }) } diff --git a/api/export_pr_test.go b/api/export_pr_test.go index 50e346dee..b7f4dcddb 100644 --- a/api/export_pr_test.go +++ b/api/export_pr_test.go @@ -75,6 +75,38 @@ func TestIssue_ExportData(t *testing.T) { } `), }, + { + name: "project items", + fields: []string{"projectItems"}, + inputJSON: heredoc.Doc(` + { "projectItems": { "nodes": [ + { + "id": "PVTI_id", + "project": { + "id": "PVT_id", + "title": "Some Project" + }, + "status": { + "name": "Todo", + "optionId": "abc123" + } + } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "projectItems": [ + { + "status": { + "optionId": "abc123", + "name": "Todo" + }, + "title": "Some Project" + } + ] + } + `), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -181,6 +213,38 @@ func TestPullRequest_ExportData(t *testing.T) { } `), }, + { + name: "project items", + fields: []string{"projectItems"}, + inputJSON: heredoc.Doc(` + { "projectItems": { "nodes": [ + { + "id": "PVTPR_id", + "project": { + "id": "PVT_id", + "title": "Some Project" + }, + "status": { + "name": "Todo", + "optionId": "abc123" + } + } + ] } } + `), + outputJSON: heredoc.Doc(` + { + "projectItems": [ + { + "status": { + "optionId": "abc123", + "name": "Todo" + }, + "title": "Some Project" + } + ] + } + `), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -205,108 +269,3 @@ func TestPullRequest_ExportData(t *testing.T) { }) } } - -// The following tests exist separately from the table driven tests -// above because in the implementation code, the GraphQL request is -// performed by shurcool, and it handles the JSON decoding into the -// response struct, which is slightly different than the stdlib JSON -// decoder used by the rest of the requests when Finding an Issue or PR. -func TestIssueExportProjectItems(t *testing.T) { - issue := Issue{ - ProjectItems: ProjectItems{ - Nodes: []*ProjectV2Item{ - { - ID: "PVTI_lADOB-vozM4AVk16zgK6U50", - Project: struct { - ID string `json:"id"` - Title string `json:"title"` - }{ - ID: "PVT_kwDOB-vozM4AVk16", - Title: "Test Project", - }, - Status: Status{ - StatusFragment: struct { - OptionID string `json:"optionId"` - Name string `json:"name"` - }{ - OptionID: "47fc9ee4", - Name: "In Progress", - }, - }, - }, - }, - }, - } - - expectedExportedJSON := heredoc.Doc(` - { - "projectItems": [ - { - "status": { - "optionId": "47fc9ee4", - "name": "In Progress" - }, - "title": "Test Project" - } - ] - } - `) - - exported := issue.ExportData([]string{"projectItems"}) - - buf := bytes.Buffer{} - enc := json.NewEncoder(&buf) - enc.SetIndent("", "\t") - require.NoError(t, enc.Encode(exported)) - require.Equal(t, expectedExportedJSON, buf.String()) -} - -func TestPRExportProjectItems(t *testing.T) { - pr := PullRequest{ - ProjectItems: ProjectItems{ - Nodes: []*ProjectV2Item{ - { - ID: "PVTI_lADOB-vozM4AVk16zgK6U50", - Project: struct { - ID string `json:"id"` - Title string `json:"title"` - }{ - ID: "PVT_kwDOB-vozM4AVk16", - Title: "Test Project", - }, - Status: Status{ - StatusFragment: struct { - OptionID string `json:"optionId"` - Name string `json:"name"` - }{ - OptionID: "47fc9ee4", - Name: "In Progress", - }, - }, - }, - }, - }, - } - - expectedExportedJSON := heredoc.Doc(` - { - "projectItems": [ - { - "status": { - "optionId": "47fc9ee4", - "name": "In Progress" - }, - "title": "Test Project" - } - ] - } - `) - - exported := pr.ExportData([]string{"projectItems"}) - - buf := bytes.Buffer{} - enc := json.NewEncoder(&buf) - enc.SetIndent("", "\t") - require.NoError(t, enc.Encode(exported)) - require.Equal(t, expectedExportedJSON, buf.String()) -} diff --git a/api/queries_issue.go b/api/queries_issue.go index 1871294e7..62531e84e 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -102,18 +102,18 @@ type ProjectInfo struct { type ProjectV2Item struct { ID string `json:"id"` - Project struct { - ID string `json:"id"` - Title string `json:"title"` - } - Status Status `graphql:"status:fieldValueByName(name: \"Status\")"` + Project ProjectV2ItemProject + Status ProjectV2ItemStatus } -type Status struct { - StatusFragment struct { - OptionID string `json:"optionId"` - Name string `json:"name"` - } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` +type ProjectV2ItemProject struct { + ID string `json:"id"` + Title string `json:"title"` +} + +type ProjectV2ItemStatus struct { + OptionID string `json:"optionId"` + Name string `json:"name"` } func (p ProjectCards) ProjectNames() []string { diff --git a/api/queries_projects_v2.go b/api/queries_projects_v2.go index a602653af..958be6616 100644 --- a/api/queries_projects_v2.go +++ b/api/queries_projects_v2.go @@ -61,11 +61,27 @@ func UpdateProjectV2Items(client *Client, repo ghrepo.Interface, addProjectItems // ProjectsV2ItemsForIssue fetches all ProjectItems for an issue. func ProjectsV2ItemsForIssue(client *Client, repo ghrepo.Interface, issue *Issue) error { + type projectV2ItemStatus struct { + StatusFragment struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` + } + + type projectV2Item struct { + ID string `json:"id"` + Project struct { + ID string `json:"id"` + Title string `json:"title"` + } + Status projectV2ItemStatus `graphql:"status:fieldValueByName(name: \"Status\")"` + } + type response struct { Repository struct { Issue struct { ProjectItems struct { - Nodes []*ProjectV2Item + Nodes []*projectV2Item PageInfo struct { HasNextPage bool EndCursor string @@ -87,7 +103,20 @@ func ProjectsV2ItemsForIssue(client *Client, repo ghrepo.Interface, issue *Issue if err != nil { return err } - items.Nodes = append(items.Nodes, query.Repository.Issue.ProjectItems.Nodes...) + for _, projectItemNode := range query.Repository.Issue.ProjectItems.Nodes { + items.Nodes = append(items.Nodes, &ProjectV2Item{ + ID: projectItemNode.ID, + Project: ProjectV2ItemProject{ + ID: projectItemNode.Project.ID, + Title: projectItemNode.Project.Title, + }, + Status: ProjectV2ItemStatus{ + OptionID: projectItemNode.Status.StatusFragment.OptionID, + Name: projectItemNode.Status.StatusFragment.Name, + }, + }) + } + if !query.Repository.Issue.ProjectItems.PageInfo.HasNextPage { break } @@ -99,11 +128,27 @@ func ProjectsV2ItemsForIssue(client *Client, repo ghrepo.Interface, issue *Issue // ProjectsV2ItemsForPullRequest fetches all ProjectItems for a pull request. func ProjectsV2ItemsForPullRequest(client *Client, repo ghrepo.Interface, pr *PullRequest) error { + type projectV2ItemStatus struct { + StatusFragment struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` + } + + type projectV2Item struct { + ID string `json:"id"` + Project struct { + ID string `json:"id"` + Title string `json:"title"` + } + Status projectV2ItemStatus `graphql:"status:fieldValueByName(name: \"Status\")"` + } + type response struct { Repository struct { PullRequest struct { ProjectItems struct { - Nodes []*ProjectV2Item + Nodes []*projectV2Item PageInfo struct { HasNextPage bool EndCursor string @@ -125,7 +170,21 @@ func ProjectsV2ItemsForPullRequest(client *Client, repo ghrepo.Interface, pr *Pu if err != nil { return err } - items.Nodes = append(items.Nodes, query.Repository.PullRequest.ProjectItems.Nodes...) + + for _, projectItemNode := range query.Repository.PullRequest.ProjectItems.Nodes { + items.Nodes = append(items.Nodes, &ProjectV2Item{ + ID: projectItemNode.ID, + Project: ProjectV2ItemProject{ + ID: projectItemNode.Project.ID, + Title: projectItemNode.Project.Title, + }, + Status: ProjectV2ItemStatus{ + OptionID: projectItemNode.Status.StatusFragment.OptionID, + Name: projectItemNode.Status.StatusFragment.Name, + }, + }) + } + if !query.Repository.PullRequest.ProjectItems.PageInfo.HasNextPage { break } diff --git a/api/queries_projects_v2_test.go b/api/queries_projects_v2_test.go index 8cacad921..fae49c926 100644 --- a/api/queries_projects_v2_test.go +++ b/api/queries_projects_v2_test.go @@ -229,21 +229,13 @@ func TestProjectsV2ItemsForPullRequest(t *testing.T) { Nodes: []*ProjectV2Item{ { ID: "PVTI_lADOB-vozM4AVk16zgK6U50", - Project: struct { - ID string `json:"id"` - Title string `json:"title"` - }{ + Project: ProjectV2ItemProject{ ID: "PVT_kwDOB-vozM4AVk16", Title: "Test Project", }, - Status: Status{ - StatusFragment: struct { - OptionID string `json:"optionId"` - Name string `json:"name"` - }{ - OptionID: "47fc9ee4", - Name: "In Progress", - }, + Status: ProjectV2ItemStatus{ + OptionID: "47fc9ee4", + Name: "In Progress", }, }, }, diff --git a/pkg/cmd/issue/list/list_test.go b/pkg/cmd/issue/list/list_test.go index e816500c2..5bed620c1 100644 --- a/pkg/cmd/issue/list/list_test.go +++ b/pkg/cmd/issue/list/list_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/ghrepo" @@ -19,6 +20,7 @@ import ( "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) { @@ -456,3 +458,151 @@ func Test_issueList(t *testing.T) { }) } } + +func TestIssueList_withProjectItems(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + + reg.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(`{ + "data": { + "repository": { + "hasIssuesEnabled": true, + "issues": { + "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), + "states": []interface{}{"OPEN"}, + }, params) + })) + + client := &http.Client{Transport: reg} + issuesAndTotalCount, err := issueList( + client, + ghrepo.New("OWNER", "REPO"), + prShared.FilterOptions{ + Entity: "issue", + }, + 30, + ) + + require.NoError(t, err) + require.Len(t, issuesAndTotalCount.Issues, 1) + require.Len(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes, 1) + + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].ID, "PVTI_lAHOAA3WC84AW6WNzgJ8rnQ") + + expectedProject := api.ProjectV2ItemProject{ + ID: "PVT_kwHOAA3WC84AW6WN", + Title: "Test Public Project", + } + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].Project, expectedProject) + + expectedStatus := api.ProjectV2ItemStatus{ + OptionID: "47fc9ee4", + Name: "In Progress", + } + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].Status, expectedStatus) +} + +func TestIssueList_Search_withProjectItems(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + + reg.Register( + httpmock.GraphQL(`query IssueSearch\b`), + httpmock.GraphQLQuery(`{ + "data": { + "repository": { + "hasIssuesEnabled": true + }, + "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{}{ + "owner": "OWNER", + "repo": "REPO", + "type": "ISSUE", + "limit": float64(30), + "query": "just used to force the search API branch repo:OWNER/REPO type:issue", + }, params) + })) + + client := &http.Client{Transport: reg} + issuesAndTotalCount, err := issueList( + client, + ghrepo.New("OWNER", "REPO"), + prShared.FilterOptions{ + Entity: "issue", + Search: "just used to force the search API branch", + }, + 30, + ) + + require.NoError(t, err) + require.Len(t, issuesAndTotalCount.Issues, 1) + require.Len(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes, 1) + + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].ID, "PVTI_lAHOAA3WC84AW6WNzgJ8rl0") + + expectedProject := api.ProjectV2ItemProject{ + ID: "PVT_kwHOAA3WC84AW6WN", + Title: "Test Public Project", + } + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].Project, expectedProject) + + expectedStatus := api.ProjectV2ItemStatus{ + OptionID: "47fc9ee4", + Name: "In Progress", + } + require.Equal(t, issuesAndTotalCount.Issues[0].ProjectItems.Nodes[0].Status, expectedStatus) +} diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go index 61a8e3698..869db9c38 100644 --- a/pkg/cmd/pr/list/list_test.go +++ b/pkg/cmd/pr/list/list_test.go @@ -9,15 +9,18 @@ import ( "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) { @@ -320,3 +323,146 @@ func TestPRList_web(t *testing.T) { }) } } + +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) +}