Add --query flag to project item-list (#12696)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Babak K. Shandiz <babakks@github.com>
This commit is contained in:
parent
3a73c39cc6
commit
8dcfd330e7
7 changed files with 719 additions and 60 deletions
|
|
@ -20,6 +20,10 @@ func (md *DisabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
|
|||
return gh.ProjectsV1Unsupported
|
||||
}
|
||||
|
||||
func (md *DisabledDetectorMock) ProjectFeatures() (ProjectFeatures, error) {
|
||||
return ProjectFeatures{}, nil
|
||||
}
|
||||
|
||||
func (md *DisabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
|
||||
return advancedIssueSearchNotSupported, nil
|
||||
}
|
||||
|
|
@ -50,6 +54,10 @@ func (md *EnabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
|
|||
return gh.ProjectsV1Supported
|
||||
}
|
||||
|
||||
func (md *EnabledDetectorMock) ProjectFeatures() (ProjectFeatures, error) {
|
||||
return allProjectFeatures, nil
|
||||
}
|
||||
|
||||
func (md *EnabledDetectorMock) SearchFeatures() (SearchFeatures, error) {
|
||||
return advancedIssueSearchNotSupported, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ type Detector interface {
|
|||
PullRequestFeatures() (PullRequestFeatures, error)
|
||||
RepositoryFeatures() (RepositoryFeatures, error)
|
||||
ProjectsV1() gh.ProjectsV1Support
|
||||
ProjectFeatures() (ProjectFeatures, error)
|
||||
SearchFeatures() (SearchFeatures, error)
|
||||
ReleaseFeatures() (ReleaseFeatures, error)
|
||||
ActionsFeatures() (ActionsFeatures, error)
|
||||
|
|
@ -58,6 +59,16 @@ var allRepositoryFeatures = RepositoryFeatures{
|
|||
AutoMerge: true,
|
||||
}
|
||||
|
||||
type ProjectFeatures struct {
|
||||
// ProjectItemQuery indicates support for the `query` argument on
|
||||
// ProjectV2.items (supported on github.com and GHES 3.20+).
|
||||
ProjectItemQuery bool
|
||||
}
|
||||
|
||||
var allProjectFeatures = ProjectFeatures{
|
||||
ProjectItemQuery: true,
|
||||
}
|
||||
|
||||
type SearchFeatures struct {
|
||||
// AdvancedIssueSearch indicates whether the host supports advanced issue
|
||||
// search via API calls.
|
||||
|
|
@ -279,6 +290,45 @@ func (d *detector) ProjectsV1() gh.ProjectsV1Support {
|
|||
return gh.ProjectsV1Unsupported
|
||||
}
|
||||
|
||||
func (d *detector) ProjectFeatures() (ProjectFeatures, error) {
|
||||
if !ghauth.IsEnterprise(d.host) {
|
||||
return allProjectFeatures, nil
|
||||
}
|
||||
|
||||
var features ProjectFeatures
|
||||
|
||||
var featureDetection struct {
|
||||
ProjectV2 struct {
|
||||
Fields []struct {
|
||||
Name string
|
||||
Args []struct {
|
||||
Name string
|
||||
}
|
||||
} `graphql:"fields(includeDeprecated: true)"`
|
||||
} `graphql:"ProjectV2: __type(name: \"ProjectV2\")"`
|
||||
}
|
||||
|
||||
gql := api.NewClientFromHTTP(d.httpClient)
|
||||
err := gql.Query(d.host, "ProjectV2_fields", &featureDetection, nil)
|
||||
if err != nil {
|
||||
return features, err
|
||||
}
|
||||
|
||||
for _, field := range featureDetection.ProjectV2.Fields {
|
||||
if field.Name == "items" {
|
||||
for _, arg := range field.Args {
|
||||
if arg.Name == "query" {
|
||||
features.ProjectItemQuery = true
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
const (
|
||||
// enterpriseAdvancedIssueSearchSupport is the minimum version of GHES that
|
||||
// supports advanced issue search and gh should use it.
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ func TestIssueFeatures(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
httpClient := &http.Client{}
|
||||
httpmock.ReplaceTripper(httpClient, reg)
|
||||
for query, resp := range tt.queryResponse {
|
||||
|
|
@ -586,6 +587,92 @@ func TestAdvancedIssueSearchSupport(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProjectFeatures(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
hostname string
|
||||
queryResponse map[string]string
|
||||
wantFeatures ProjectFeatures
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: ProjectFeatures{
|
||||
ProjectItemQuery: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: ProjectFeatures{
|
||||
ProjectItemQuery: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "GHE empty response",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query ProjectV2_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: ProjectFeatures{},
|
||||
},
|
||||
{
|
||||
name: "GHE items field without query arg",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query ProjectV2_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "ProjectV2": { "fields": [
|
||||
{"name": "items", "args": [
|
||||
{"name": "after"},
|
||||
{"name": "first"}
|
||||
]}
|
||||
] } } }
|
||||
`),
|
||||
},
|
||||
wantFeatures: ProjectFeatures{},
|
||||
},
|
||||
{
|
||||
name: "GHE items field with query arg",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query ProjectV2_fields\b`: heredoc.Doc(`
|
||||
{ "data": { "ProjectV2": { "fields": [
|
||||
{"name": "items", "args": [
|
||||
{"name": "after"},
|
||||
{"name": "first"},
|
||||
{"name": "query"}
|
||||
]}
|
||||
] } } }
|
||||
`),
|
||||
},
|
||||
wantFeatures: ProjectFeatures{
|
||||
ProjectItemQuery: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
httpClient := &http.Client{}
|
||||
httpmock.ReplaceTripper(httpClient, reg)
|
||||
for query, resp := range tt.queryResponse {
|
||||
reg.Register(httpmock.GraphQL(query), httpmock.StringResponse(resp))
|
||||
}
|
||||
detector := detector{host: tt.hostname, httpClient: httpClient}
|
||||
gotFeatures, err := detector.ProjectFeatures()
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantFeatures, gotFeatures)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseFeatures(t *testing.T) {
|
||||
withImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"},{"name":"immutable"}]}}}`
|
||||
withoutImmutableReleaseSupport := `{"data":{"Release":{"fields":[{"name":"author"},{"name":"name"}]}}}`
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ package itemlist
|
|||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
"github.com/cli/cli/v2/api"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/internal/tableprinter"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
|
|
@ -17,13 +20,15 @@ type listOpts struct {
|
|||
limit int
|
||||
owner string
|
||||
number int32
|
||||
query string
|
||||
exporter cmdutil.Exporter
|
||||
}
|
||||
|
||||
type listConfig struct {
|
||||
io *iostreams.IOStreams
|
||||
client *queries.Client
|
||||
opts listOpts
|
||||
io *iostreams.IOStreams
|
||||
client *queries.Client
|
||||
opts listOpts
|
||||
detector fd.Detector
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.Command {
|
||||
|
|
@ -31,9 +36,25 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
|
|||
listCmd := &cobra.Command{
|
||||
Short: "List the items in a project",
|
||||
Use: "item-list [<number>]",
|
||||
Long: heredoc.Doc(`
|
||||
List the items in a project.
|
||||
|
||||
If supported by the API host (github.com and GHES 3.20+), the --query option can
|
||||
be used to perform advanced search. For the full syntax, see:
|
||||
https://docs.github.com/en/issues/planning-and-tracking-with-projects/customizing-views-in-your-project/filtering-projects
|
||||
`),
|
||||
Example: heredoc.Doc(`
|
||||
# List the items in the current users's project "1"
|
||||
$ gh project item-list 1 --owner "@me"
|
||||
|
||||
# List items assigned to a specific user
|
||||
$ gh project item-list 1 --owner "@me" --query "assignee:monalisa"
|
||||
|
||||
# List open issues assigned to yourself
|
||||
$ gh project item-list 1 --owner "@me" --query "assignee:@me is:issue is:open"
|
||||
|
||||
# List items with the "bug" label that are not done
|
||||
$ gh project item-list 1 --owner "@me" --query "label:bug -status:Done"
|
||||
`),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
|
@ -60,11 +81,26 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
|
|||
if runF != nil {
|
||||
return runF(config)
|
||||
}
|
||||
|
||||
if opts.query != "" {
|
||||
httpClient, err := f.HttpClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
host, _ := cfg.Authentication().DefaultHost()
|
||||
config.detector = fd.NewDetector(api.NewCachedHTTPClient(httpClient, time.Hour*24), host)
|
||||
}
|
||||
|
||||
return runList(config)
|
||||
},
|
||||
}
|
||||
|
||||
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user.")
|
||||
listCmd.Flags().StringVar(&opts.owner, "owner", "", "Login of the owner. Use \"@me\" for the current user")
|
||||
listCmd.Flags().StringVar(&opts.query, "query", "", `Filter items using the Projects filter syntax, e.g. "assignee:octocat -status:Done"`)
|
||||
cmdutil.AddFormatFlags(listCmd, &opts.exporter)
|
||||
listCmd.Flags().IntVarP(&opts.limit, "limit", "L", queries.LimitDefault, "Maximum number of items to fetch")
|
||||
|
||||
|
|
@ -72,6 +108,16 @@ func NewCmdList(f *cmdutil.Factory, runF func(config listConfig) error) *cobra.C
|
|||
}
|
||||
|
||||
func runList(config listConfig) error {
|
||||
if config.opts.query != "" {
|
||||
features, err := config.detector.ProjectFeatures()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !features.ProjectItemQuery {
|
||||
return fmt.Errorf("the `--query` flag is not supported on this GitHub host; most likely you are targeting a version of GHES that does not yet have the query field available")
|
||||
}
|
||||
}
|
||||
|
||||
canPrompt := config.io.CanPrompt()
|
||||
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
|
||||
if err != nil {
|
||||
|
|
@ -87,7 +133,7 @@ func runList(config listConfig) error {
|
|||
config.opts.number = project.Number
|
||||
}
|
||||
|
||||
project, err := config.client.ProjectItems(owner, config.opts.number, config.opts.limit)
|
||||
project, err := config.client.ProjectItems(owner, config.opts.number, config.opts.limit, config.opts.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/MakeNowJust/heredoc"
|
||||
fd "github.com/cli/cli/v2/internal/featuredetection"
|
||||
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
|
||||
"github.com/cli/cli/v2/pkg/cmdutil"
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
|
|
@ -52,6 +53,14 @@ func TestNewCmdList(t *testing.T) {
|
|||
},
|
||||
wantsExporter: true,
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
cli: `--query "assignee:octocat"`,
|
||||
wants: listOpts{
|
||||
limit: 30,
|
||||
query: "assignee:octocat",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Setenv("GH_TOKEN", "auth-token")
|
||||
|
|
@ -83,6 +92,7 @@ func TestNewCmdList(t *testing.T) {
|
|||
|
||||
assert.Equal(t, tt.wants.number, gotOpts.number)
|
||||
assert.Equal(t, tt.wants.owner, gotOpts.owner)
|
||||
assert.Equal(t, tt.wants.query, gotOpts.query)
|
||||
assert.Equal(t, tt.wantsExporter, gotOpts.exporter != nil)
|
||||
assert.Equal(t, tt.wants.limit, gotOpts.limit)
|
||||
})
|
||||
|
|
@ -618,3 +628,109 @@ func TestRunList_JSON(t *testing.T) {
|
|||
`{"items":[{"content":{"type":"Issue","body":"","title":"an issue","number":1,"repository":"cli/go-gh","url":""},"id":"issue ID"},{"content":{"type":"PullRequest","body":"","title":"a pull request","number":2,"repository":"cli/go-gh","url":""},"id":"pull request ID"},{"content":{"type":"DraftIssue","body":"","title":"draft issue","id":"draft issue ID"},"id":"draft issue ID"}],"totalCount":3}`,
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_WithQuery(t *testing.T) {
|
||||
defer gock.Off()
|
||||
|
||||
// get user ID
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
MatchType("json").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserOrgOwner.*",
|
||||
"variables": map[string]interface{}{
|
||||
"login": "monalisa",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": "an ID",
|
||||
},
|
||||
},
|
||||
"errors": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "NOT_FOUND",
|
||||
"path": []string{"organization"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// list project items with query
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query UserProjectWithItems.*",
|
||||
"variables": map[string]interface{}{
|
||||
"firstItems": queries.LimitDefault,
|
||||
"afterItems": nil,
|
||||
"firstFields": queries.LimitMax,
|
||||
"afterFields": nil,
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"query": "assignee:octocat -status:Done",
|
||||
},
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"items": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"id": "issue ID",
|
||||
"content": map[string]interface{}{
|
||||
"__typename": "Issue",
|
||||
"title": "an issue",
|
||||
"number": 1,
|
||||
"repository": map[string]string{
|
||||
"nameWithOwner": "cli/go-gh",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := queries.NewTestClient()
|
||||
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
query: "assignee:octocat -status:Done",
|
||||
},
|
||||
client: client,
|
||||
detector: &fd.EnabledDetectorMock{},
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"Issue\tan issue\t1\tcli/go-gh\tissue ID\n",
|
||||
stdout.String())
|
||||
}
|
||||
|
||||
func TestRunList_QueryUnsupported(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
config := listConfig{
|
||||
opts: listOpts{
|
||||
number: 1,
|
||||
owner: "monalisa",
|
||||
query: "assignee:octocat",
|
||||
},
|
||||
detector: &fd.DisabledDetectorMock{},
|
||||
io: ios,
|
||||
}
|
||||
|
||||
err := runList(config)
|
||||
assert.EqualError(t, err, "the `--query` flag is not supported on this GitHub host; most likely you are targeting a version of GHES that does not yet have the query field available")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ type Project struct {
|
|||
PageInfo PageInfo
|
||||
TotalCount int
|
||||
Nodes []ProjectItem
|
||||
} `graphql:"items(first: $firstItems, after: $afterItems)"`
|
||||
} `graphql:"items(first: $firstItems, after: $afterItems, query: $query)"`
|
||||
Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"`
|
||||
Owner struct {
|
||||
TypeName string `graphql:"__typename"`
|
||||
|
|
@ -147,6 +147,81 @@ type Project struct {
|
|||
}
|
||||
}
|
||||
|
||||
type projectDTOBase struct {
|
||||
Number int32
|
||||
URL string
|
||||
ShortDescription string
|
||||
Public bool
|
||||
Closed bool
|
||||
Title string
|
||||
ID string
|
||||
Readme string
|
||||
Owner struct {
|
||||
TypeName string `graphql:"__typename"`
|
||||
User struct {
|
||||
Login string
|
||||
} `graphql:"... on User"`
|
||||
Organization struct {
|
||||
Login string
|
||||
} `graphql:"... on Organization"`
|
||||
}
|
||||
}
|
||||
|
||||
type projectDTOWithItemQuery struct {
|
||||
projectDTOBase
|
||||
Items struct {
|
||||
PageInfo PageInfo
|
||||
TotalCount int
|
||||
Nodes []ProjectItem
|
||||
} `graphql:"items(first: $firstItems, after: $afterItems, query: $query)"`
|
||||
Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"`
|
||||
}
|
||||
|
||||
type projectDTOWithoutItemQuery struct {
|
||||
projectDTOBase
|
||||
Items struct {
|
||||
PageInfo PageInfo
|
||||
TotalCount int
|
||||
Nodes []ProjectItem
|
||||
} `graphql:"items(first: $firstItems, after: $afterItems)"`
|
||||
Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"`
|
||||
}
|
||||
|
||||
func newProjectFromDTOBase(source projectDTOBase) *Project {
|
||||
project := &Project{
|
||||
Number: source.Number,
|
||||
URL: source.URL,
|
||||
ShortDescription: source.ShortDescription,
|
||||
Public: source.Public,
|
||||
Closed: source.Closed,
|
||||
Title: source.Title,
|
||||
ID: source.ID,
|
||||
Readme: source.Readme,
|
||||
}
|
||||
project.Owner.TypeName = source.Owner.TypeName
|
||||
project.Owner.User.Login = source.Owner.User.Login
|
||||
project.Owner.Organization.Login = source.Owner.Organization.Login
|
||||
return project
|
||||
}
|
||||
|
||||
func newProjectFromDTOWithItemQuery(source projectDTOWithItemQuery) *Project {
|
||||
project := newProjectFromDTOBase(source.projectDTOBase)
|
||||
project.Items.PageInfo = source.Items.PageInfo
|
||||
project.Items.TotalCount = source.Items.TotalCount
|
||||
project.Items.Nodes = source.Items.Nodes
|
||||
project.Fields = source.Fields
|
||||
return project
|
||||
}
|
||||
|
||||
func newProjectFromDTOWithoutItemQuery(source projectDTOWithoutItemQuery) *Project {
|
||||
project := newProjectFromDTOBase(source.projectDTOBase)
|
||||
project.Items.PageInfo = source.Items.PageInfo
|
||||
project.Items.TotalCount = source.Items.TotalCount
|
||||
project.Items.Nodes = source.Items.Nodes
|
||||
project.Fields = source.Fields
|
||||
return project
|
||||
}
|
||||
|
||||
func (p Project) DetailedItems() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"items": serializeProjectWithItems(&p),
|
||||
|
|
@ -508,8 +583,10 @@ func (p ProjectItem) ExportData(_ []string) map[string]interface{} {
|
|||
}
|
||||
|
||||
// ProjectItems returns the items of a project. If the OwnerType is VIEWER, no login is required.
|
||||
// If limit is 0, the default limit is used.
|
||||
func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, error) {
|
||||
// If limit is 0, the default limit is used. The queryStr parameter is passed as a server-side
|
||||
// filter to the items connection, using the same syntax as the GitHub Projects filter bar
|
||||
// (e.g. "assignee:octocat", "status:done").
|
||||
func (c *Client) ProjectItems(o *Owner, number int32, limit int, queryStr string) (*Project, error) {
|
||||
project := &Project{}
|
||||
if limit == 0 {
|
||||
limit = LimitDefault
|
||||
|
|
@ -528,20 +605,35 @@ func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, erro
|
|||
"afterFields": (*githubv4.String)(nil),
|
||||
"number": githubv4.Int(number),
|
||||
}
|
||||
if queryStr != "" {
|
||||
variables["query"] = githubv4.String(queryStr)
|
||||
}
|
||||
|
||||
var query pager[ProjectItem]
|
||||
var queryName string
|
||||
switch o.Type {
|
||||
case UserOwner:
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
query = &userOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
if queryStr == "" {
|
||||
query = &userOwnerWithItemsNoQuery{} // must be a pointer to work with graphql queries
|
||||
} else {
|
||||
query = &userOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
}
|
||||
queryName = "UserProjectWithItems"
|
||||
case OrgOwner:
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
query = &orgOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
if queryStr == "" {
|
||||
query = &orgOwnerWithItemsNoQuery{} // must be a pointer to work with graphql queries
|
||||
} else {
|
||||
query = &orgOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
}
|
||||
queryName = "OrgProjectWithItems"
|
||||
case ViewerOwner:
|
||||
query = &viewerOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
if queryStr == "" {
|
||||
query = &viewerOwnerWithItemsNoQuery{} // must be a pointer to work with graphql queries
|
||||
} else {
|
||||
query = &viewerOwnerWithItems{} // must be a pointer to work with graphql queries
|
||||
}
|
||||
queryName = "ViewerProjectWithItems"
|
||||
}
|
||||
err := c.doQueryWithProgressIndicator(queryName, query, variables)
|
||||
|
|
@ -567,6 +659,23 @@ type pager[N projectAttribute] interface {
|
|||
Project() *Project
|
||||
}
|
||||
|
||||
// userOwnerWithItemsNoQuery
|
||||
func (q userOwnerWithItemsNoQuery) HasNextPage() bool {
|
||||
return q.Owner.Project.Items.PageInfo.HasNextPage
|
||||
}
|
||||
|
||||
func (q userOwnerWithItemsNoQuery) EndCursor() string {
|
||||
return string(q.Owner.Project.Items.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
func (q userOwnerWithItemsNoQuery) Nodes() []ProjectItem {
|
||||
return q.Owner.Project.Items.Nodes
|
||||
}
|
||||
|
||||
func (q userOwnerWithItemsNoQuery) Project() *Project {
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// userOwnerWithItems
|
||||
func (q userOwnerWithItems) HasNextPage() bool {
|
||||
return q.Owner.Project.Items.PageInfo.HasNextPage
|
||||
|
|
@ -581,7 +690,7 @@ func (q userOwnerWithItems) Nodes() []ProjectItem {
|
|||
}
|
||||
|
||||
func (q userOwnerWithItems) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// orgOwnerWithItems
|
||||
|
|
@ -598,7 +707,24 @@ func (q orgOwnerWithItems) Nodes() []ProjectItem {
|
|||
}
|
||||
|
||||
func (q orgOwnerWithItems) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// orgOwnerWithItemsNoQuery
|
||||
func (q orgOwnerWithItemsNoQuery) HasNextPage() bool {
|
||||
return q.Owner.Project.Items.PageInfo.HasNextPage
|
||||
}
|
||||
|
||||
func (q orgOwnerWithItemsNoQuery) EndCursor() string {
|
||||
return string(q.Owner.Project.Items.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
func (q orgOwnerWithItemsNoQuery) Nodes() []ProjectItem {
|
||||
return q.Owner.Project.Items.Nodes
|
||||
}
|
||||
|
||||
func (q orgOwnerWithItemsNoQuery) Project() *Project {
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// viewerOwnerWithItems
|
||||
|
|
@ -615,7 +741,24 @@ func (q viewerOwnerWithItems) Nodes() []ProjectItem {
|
|||
}
|
||||
|
||||
func (q viewerOwnerWithItems) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// viewerOwnerWithItemsNoQuery
|
||||
func (q viewerOwnerWithItemsNoQuery) HasNextPage() bool {
|
||||
return q.Owner.Project.Items.PageInfo.HasNextPage
|
||||
}
|
||||
|
||||
func (q viewerOwnerWithItemsNoQuery) EndCursor() string {
|
||||
return string(q.Owner.Project.Items.PageInfo.EndCursor)
|
||||
}
|
||||
|
||||
func (q viewerOwnerWithItemsNoQuery) Nodes() []ProjectItem {
|
||||
return q.Owner.Project.Items.Nodes
|
||||
}
|
||||
|
||||
func (q viewerOwnerWithItemsNoQuery) Project() *Project {
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// userOwnerWithFields
|
||||
|
|
@ -632,7 +775,7 @@ func (q userOwnerWithFields) Nodes() []ProjectField {
|
|||
}
|
||||
|
||||
func (q userOwnerWithFields) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// orgOwnerWithFields
|
||||
|
|
@ -649,7 +792,7 @@ func (q orgOwnerWithFields) Nodes() []ProjectField {
|
|||
}
|
||||
|
||||
func (q orgOwnerWithFields) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
// viewerOwnerWithFields
|
||||
|
|
@ -666,7 +809,7 @@ func (q viewerOwnerWithFields) Nodes() []ProjectField {
|
|||
}
|
||||
|
||||
func (q viewerOwnerWithFields) Project() *Project {
|
||||
return &q.Owner.Project
|
||||
return newProjectFromDTOWithoutItemQuery(q.Owner.Project)
|
||||
}
|
||||
|
||||
type projectAttribute interface {
|
||||
|
|
@ -893,70 +1036,77 @@ type viewerLoginOrgs struct {
|
|||
}
|
||||
}
|
||||
|
||||
type ownerWithLogin struct {
|
||||
Project projectDTOWithoutItemQuery `graphql:"projectV2(number: $number)"`
|
||||
Login string
|
||||
}
|
||||
|
||||
type ownerWithProjectWithItemQuery struct {
|
||||
Project projectDTOWithItemQuery `graphql:"projectV2(number: $number)"`
|
||||
}
|
||||
|
||||
type ownerWithProjectWithoutItemQuery struct {
|
||||
Project projectDTOWithoutItemQuery `graphql:"projectV2(number: $number)"`
|
||||
}
|
||||
|
||||
// userOwner is used to query the project of a user.
|
||||
type userOwner struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
Login string
|
||||
} `graphql:"user(login: $login)"`
|
||||
Owner ownerWithLogin `graphql:"user(login: $login)"`
|
||||
}
|
||||
|
||||
// userOwnerWithItems is used to query the project of a user with its items.
|
||||
type userOwnerWithItems struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"user(login: $login)"`
|
||||
Owner ownerWithProjectWithItemQuery `graphql:"user(login: $login)"`
|
||||
}
|
||||
|
||||
// userOwnerWithItemsNoQuery is used to query the project of a user with its items, without query support.
|
||||
type userOwnerWithItemsNoQuery struct {
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"user(login: $login)"`
|
||||
}
|
||||
|
||||
// userOwnerWithFields is used to query the project of a user with its fields.
|
||||
type userOwnerWithFields struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"user(login: $login)"`
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"user(login: $login)"`
|
||||
}
|
||||
|
||||
// orgOwner is used to query the project of an organization.
|
||||
type orgOwner struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
Login string
|
||||
} `graphql:"organization(login: $login)"`
|
||||
Owner ownerWithLogin `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
// orgOwnerWithItems is used to query the project of an organization with its items.
|
||||
type orgOwnerWithItems struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"organization(login: $login)"`
|
||||
Owner ownerWithProjectWithItemQuery `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
// orgOwnerWithItemsNoQuery is used to query the project of an organization with its items, without query support.
|
||||
type orgOwnerWithItemsNoQuery struct {
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
// orgOwnerWithFields is used to query the project of an organization with its fields.
|
||||
type orgOwnerWithFields struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"organization(login: $login)"`
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"organization(login: $login)"`
|
||||
}
|
||||
|
||||
// viewerOwner is used to query the project of the viewer.
|
||||
type viewerOwner struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
Login string
|
||||
} `graphql:"viewer"`
|
||||
Owner ownerWithLogin `graphql:"viewer"`
|
||||
}
|
||||
|
||||
// viewerOwnerWithItems is used to query the project of the viewer with its items.
|
||||
type viewerOwnerWithItems struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"viewer"`
|
||||
Owner ownerWithProjectWithItemQuery `graphql:"viewer"`
|
||||
}
|
||||
|
||||
// viewerOwnerWithItemsNoQuery is used to query the project of the viewer with its items, without query support.
|
||||
type viewerOwnerWithItemsNoQuery struct {
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"viewer"`
|
||||
}
|
||||
|
||||
// viewerOwnerWithFields is used to query the project of the viewer with its fields.
|
||||
type viewerOwnerWithFields struct {
|
||||
Owner struct {
|
||||
Project Project `graphql:"projectV2(number: $number)"`
|
||||
} `graphql:"viewer"`
|
||||
Owner ownerWithProjectWithoutItemQuery `graphql:"viewer"`
|
||||
}
|
||||
|
||||
// OwnerType is the type of the owner of a project, which can be either a user or an organization. Viewer is the current user.
|
||||
|
|
@ -1062,7 +1212,7 @@ type userProjects struct {
|
|||
Projects struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []Project
|
||||
Nodes []projectDTOWithoutItemQuery
|
||||
} `graphql:"projectsV2(first: $first, after: $after)"`
|
||||
Login string
|
||||
} `graphql:"user(login: $login)"`
|
||||
|
|
@ -1074,7 +1224,7 @@ type orgProjects struct {
|
|||
Projects struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []Project
|
||||
Nodes []projectDTOWithoutItemQuery
|
||||
} `graphql:"projectsV2(first: $first, after: $after)"`
|
||||
Login string
|
||||
} `graphql:"organization(login: $login)"`
|
||||
|
|
@ -1086,7 +1236,7 @@ type viewerProjects struct {
|
|||
Projects struct {
|
||||
TotalCount int
|
||||
PageInfo PageInfo
|
||||
Nodes []Project
|
||||
Nodes []projectDTOWithoutItemQuery
|
||||
} `graphql:"projectsV2(first: $first, after: $after)"`
|
||||
Login string
|
||||
} `graphql:"viewer"`
|
||||
|
|
@ -1240,16 +1390,16 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
|
|||
var query userOwner
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
err := c.doQueryWithProgressIndicator("UserProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
return newProjectFromDTOWithoutItemQuery(query.Owner.Project), err
|
||||
} else if o.Type == OrgOwner {
|
||||
variables["login"] = githubv4.String(o.Login)
|
||||
var query orgOwner
|
||||
err := c.doQueryWithProgressIndicator("OrgProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
return newProjectFromDTOWithoutItemQuery(query.Owner.Project), err
|
||||
} else if o.Type == ViewerOwner {
|
||||
var query viewerOwner
|
||||
err := c.doQueryWithProgressIndicator("ViewerProject", &query, variables)
|
||||
return &query.Owner.Project, err
|
||||
return newProjectFromDTOWithoutItemQuery(query.Owner.Project), err
|
||||
}
|
||||
return nil, errors.New("unknown owner type")
|
||||
}
|
||||
|
|
@ -1326,7 +1476,9 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
if err := c.doQueryWithProgressIndicator("UserProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
for _, p := range query.Owner.Projects.Nodes {
|
||||
projects.Nodes = append(projects.Nodes, *newProjectFromDTOWithoutItemQuery(p))
|
||||
}
|
||||
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
|
||||
cursor = &query.Owner.Projects.PageInfo.EndCursor
|
||||
projects.TotalCount = query.Owner.Projects.TotalCount
|
||||
|
|
@ -1335,7 +1487,9 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
if err := c.doQueryWithProgressIndicator("OrgProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
for _, p := range query.Owner.Projects.Nodes {
|
||||
projects.Nodes = append(projects.Nodes, *newProjectFromDTOWithoutItemQuery(p))
|
||||
}
|
||||
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
|
||||
cursor = &query.Owner.Projects.PageInfo.EndCursor
|
||||
projects.TotalCount = query.Owner.Projects.TotalCount
|
||||
|
|
@ -1344,7 +1498,9 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Pr
|
|||
if err := c.doQueryWithProgressIndicator("ViewerProjects", &query, variables); err != nil {
|
||||
return projects, err
|
||||
}
|
||||
projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
|
||||
for _, p := range query.Owner.Projects.Nodes {
|
||||
projects.Nodes = append(projects.Nodes, *newProjectFromDTOWithoutItemQuery(p))
|
||||
}
|
||||
hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
|
||||
cursor = &query.Owner.Projects.PageInfo.EndCursor
|
||||
projects.TotalCount = query.Owner.Projects.TotalCount
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
package queries
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/h2non/gock.v1"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func TestProjectItems_DefaultLimit(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
|
@ -56,7 +66,7 @@ func TestProjectItems_DefaultLimit(t *testing.T) {
|
|||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
}
|
||||
project, err := client.ProjectItems(owner, 1, LimitMax)
|
||||
project, err := client.ProjectItems(owner, 1, LimitMax, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 3)
|
||||
}
|
||||
|
|
@ -106,7 +116,7 @@ func TestProjectItems_LowerLimit(t *testing.T) {
|
|||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
}
|
||||
project, err := client.ProjectItems(owner, 1, 2)
|
||||
project, err := client.ProjectItems(owner, 1, 2, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 2)
|
||||
}
|
||||
|
|
@ -159,11 +169,197 @@ func TestProjectItems_NoLimit(t *testing.T) {
|
|||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
}
|
||||
project, err := client.ProjectItems(owner, 1, 0)
|
||||
project, err := client.ProjectItems(owner, 1, 0, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 3)
|
||||
}
|
||||
|
||||
func TestProjectItems_WithQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
owner *Owner
|
||||
queryName string
|
||||
dataKey string
|
||||
vars map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "user owner",
|
||||
owner: &Owner{
|
||||
Type: UserOwner,
|
||||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
},
|
||||
queryName: "UserProjectWithItems",
|
||||
dataKey: "user",
|
||||
vars: map[string]interface{}{
|
||||
"firstItems": LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": LimitMax,
|
||||
"afterFields": nil,
|
||||
"login": "monalisa",
|
||||
"number": 1,
|
||||
"query": "assignee:octocat",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org owner",
|
||||
owner: &Owner{
|
||||
Type: OrgOwner,
|
||||
Login: "github",
|
||||
ID: "org ID",
|
||||
},
|
||||
queryName: "OrgProjectWithItems",
|
||||
dataKey: "organization",
|
||||
vars: map[string]interface{}{
|
||||
"firstItems": LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": LimitMax,
|
||||
"afterFields": nil,
|
||||
"login": "github",
|
||||
"number": 1,
|
||||
"query": "assignee:octocat",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "viewer owner",
|
||||
owner: &Owner{
|
||||
Type: ViewerOwner,
|
||||
ID: "viewer ID",
|
||||
},
|
||||
queryName: "ViewerProjectWithItems",
|
||||
dataKey: "viewer",
|
||||
vars: map[string]interface{}{
|
||||
"firstItems": LimitMax,
|
||||
"afterItems": nil,
|
||||
"firstFields": LimitMax,
|
||||
"afterFields": nil,
|
||||
"number": 1,
|
||||
"query": "assignee:octocat",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer gock.Off()
|
||||
gock.Observe(gock.DumpRequest)
|
||||
|
||||
gock.New("https://api.github.com").
|
||||
Post("/graphql").
|
||||
JSON(map[string]interface{}{
|
||||
"query": "query " + tt.queryName + ".*",
|
||||
"variables": tt.vars,
|
||||
}).
|
||||
Reply(200).
|
||||
JSON(map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
tt.dataKey: map[string]interface{}{
|
||||
"projectV2": map[string]interface{}{
|
||||
"items": map[string]interface{}{
|
||||
"nodes": []map[string]interface{}{
|
||||
{
|
||||
"id": "issue ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client := NewTestClient()
|
||||
project, err := client.ProjectItems(tt.owner, 1, LimitMax, "assignee:octocat")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectItems_NoQueryDoesNotUseQueryItems(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
httpClient := &http.Client{
|
||||
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, string(body), "$query")
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader(`{
|
||||
"data": {
|
||||
"user": {
|
||||
"projectV2": {
|
||||
"items": {
|
||||
"nodes": [
|
||||
{"id": "issue ID"}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
client := NewClient(httpClient, "github.com", ios)
|
||||
owner := &Owner{
|
||||
Type: UserOwner,
|
||||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
}
|
||||
project, err := client.ProjectItems(owner, 1, LimitMax, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 1)
|
||||
}
|
||||
|
||||
func TestProjects_ViewerQueryDoesNotUseQueryItems(t *testing.T) {
|
||||
ios, _, _, _ := iostreams.Test()
|
||||
httpClient := &http.Client{
|
||||
Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.NotContains(t, string(body), "$query")
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader(`{
|
||||
"data": {
|
||||
"viewer": {
|
||||
"projectsV2": {
|
||||
"totalCount": 1,
|
||||
"pageInfo": {
|
||||
"hasNextPage": false,
|
||||
"endCursor": ""
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Roadmap"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
client := NewClient(httpClient, "github.com", ios)
|
||||
projects, err := client.Projects("", ViewerOwner, 1, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, projects.Nodes, 1)
|
||||
assert.Equal(t, int32(1), projects.Nodes[0].Number)
|
||||
assert.Equal(t, "Roadmap", projects.Nodes[0].Title)
|
||||
}
|
||||
|
||||
func TestProjectFields_LowerLimit(t *testing.T) {
|
||||
|
||||
defer gock.Off()
|
||||
|
|
@ -422,7 +618,7 @@ func TestProjectItems_FieldTitle(t *testing.T) {
|
|||
Login: "monalisa",
|
||||
ID: "user ID",
|
||||
}
|
||||
project, err := client.ProjectItems(owner, 1, LimitMax)
|
||||
project, err := client.ProjectItems(owner, 1, LimitMax, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, project.Items.Nodes, 1)
|
||||
assert.Len(t, project.Items.Nodes[0].FieldValues.Nodes, 2)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue