diff --git a/github/event_types.go b/github/event_types.go index ba175ac72cd..4e8dc55bd10 100644 --- a/github/event_types.go +++ b/github/event_types.go @@ -1150,15 +1150,18 @@ type FieldValue struct { // ProjectV2Item represents an item belonging to a project. type ProjectV2Item struct { - ID *int64 `json:"id,omitempty"` - NodeID *string `json:"node_id,omitempty"` - ProjectNodeID *string `json:"project_node_id,omitempty"` - ContentNodeID *string `json:"content_node_id,omitempty"` - ContentType *string `json:"content_type,omitempty"` - Creator *User `json:"creator,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` - ArchivedAt *Timestamp `json:"archived_at,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + ProjectNodeID *string `json:"project_node_id,omitempty"` + ContentNodeID *string `json:"content_node_id,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + ContentType *string `json:"content_type,omitempty"` + Creator *User `json:"creator,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ArchivedAt *Timestamp `json:"archived_at,omitempty"` + ItemURL *string `json:"item_url,omitempty"` + Fields []*ProjectV2Field `json:"fields,omitempty"` } // PublicEvent is triggered when a private repository is open sourced. diff --git a/github/github-accessors.go b/github/github-accessors.go index 6e2bbaf93bd..7844b5b36d0 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -14078,6 +14078,38 @@ func (l *ListOrganizations) GetTotalCount() int { return *l.TotalCount } +// GetQuery returns the Query field if it's non-nil, zero value otherwise. +func (l *ListProjectsOptions) GetQuery() string { + if l == nil || l.Query == nil { + return "" + } + return *l.Query +} + +// GetAfter returns the After field if it's non-nil, zero value otherwise. +func (l *ListProjectsPaginationOptions) GetAfter() string { + if l == nil || l.After == nil { + return "" + } + return *l.After +} + +// GetBefore returns the Before field if it's non-nil, zero value otherwise. +func (l *ListProjectsPaginationOptions) GetBefore() string { + if l == nil || l.Before == nil { + return "" + } + return *l.Before +} + +// GetPerPage returns the PerPage field if it's non-nil, zero value otherwise. +func (l *ListProjectsPaginationOptions) GetPerPage() int { + if l == nil || l.PerPage == nil { + return 0 + } + return *l.PerPage +} + // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. func (l *ListRepositories) GetTotalCount() int { if l == nil || l.TotalCount == nil { @@ -19118,6 +19150,14 @@ func (p *ProjectV2Event) GetSender() *User { return p.Sender } +// GetConfiguration returns the Configuration field. +func (p *ProjectV2Field) GetConfiguration() *ProjectV2FieldConfiguration { + if p == nil { + return nil + } + return p.Configuration +} + // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Field) GetCreatedAt() Timestamp { if p == nil || p.CreatedAt == nil { @@ -19126,6 +19166,14 @@ func (p *ProjectV2Field) GetCreatedAt() Timestamp { return *p.CreatedAt } +// GetDataType returns the DataType field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetDataType() string { + if p == nil || p.DataType == nil { + return "" + } + return *p.DataType +} + // GetID returns the ID field if it's non-nil, zero value otherwise. func (p *ProjectV2Field) GetID() int64 { if p == nil || p.ID == nil { @@ -19134,6 +19182,30 @@ func (p *ProjectV2Field) GetID() int64 { return *p.ID } +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetNodeID() string { + if p == nil || p.NodeID == nil { + return "" + } + return *p.NodeID +} + +// GetProjectURL returns the ProjectURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2Field) GetProjectURL() string { + if p == nil || p.ProjectURL == nil { + return "" + } + return *p.ProjectURL +} + // GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Field) GetUpdatedAt() Timestamp { if p == nil || p.UpdatedAt == nil { @@ -19142,6 +19214,86 @@ func (p *ProjectV2Field) GetUpdatedAt() Timestamp { return *p.UpdatedAt } +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldConfiguration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetStartDay returns the StartDay field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldConfiguration) GetStartDay() int { + if p == nil || p.StartDay == nil { + return 0 + } + return *p.StartDay +} + +// GetDuration returns the Duration field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIteration) GetDuration() int { + if p == nil || p.Duration == nil { + return 0 + } + return *p.Duration +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIteration) GetID() string { + if p == nil || p.ID == nil { + return "" + } + return *p.ID +} + +// GetStartDate returns the StartDate field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIteration) GetStartDate() string { + if p == nil || p.StartDate == nil { + return "" + } + return *p.StartDate +} + +// GetTitle returns the Title field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldIteration) GetTitle() string { + if p == nil || p.Title == nil { + return "" + } + return *p.Title +} + +// GetColor returns the Color field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldOption) GetColor() string { + if p == nil || p.Color == nil { + return "" + } + return *p.Color +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldOption) GetDescription() string { + if p == nil || p.Description == nil { + return "" + } + return *p.Description +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldOption) GetID() string { + if p == nil || p.ID == nil { + return "" + } + return *p.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (p *ProjectV2FieldOption) GetName() string { + if p == nil || p.Name == nil { + return "" + } + return *p.Name +} + // GetArchivedAt returns the ArchivedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetArchivedAt() Timestamp { if p == nil || p.ArchivedAt == nil { @@ -19190,6 +19342,14 @@ func (p *ProjectV2Item) GetID() int64 { return *p.ID } +// GetItemURL returns the ItemURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2Item) GetItemURL() string { + if p == nil || p.ItemURL == nil { + return "" + } + return *p.ItemURL +} + // GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetNodeID() string { if p == nil || p.NodeID == nil { @@ -19206,6 +19366,14 @@ func (p *ProjectV2Item) GetProjectNodeID() string { return *p.ProjectNodeID } +// GetProjectURL returns the ProjectURL field if it's non-nil, zero value otherwise. +func (p *ProjectV2Item) GetProjectURL() string { + if p == nil || p.ProjectURL == nil { + return "" + } + return *p.ProjectURL +} + // GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. func (p *ProjectV2Item) GetUpdatedAt() Timestamp { if p == nil || p.UpdatedAt == nil { @@ -28966,6 +29134,14 @@ func (u *UpdateOrganizationPrivateRegistry) GetVisibility() *PrivateRegistryVisi return u.Visibility } +// GetArchived returns the Archived field if it's non-nil, zero value otherwise. +func (u *UpdateProjectItemOptions) GetArchived() bool { + if u == nil || u.Archived == nil { + return false + } + return *u.Archived +} + // GetForce returns the Force field if it's non-nil, zero value otherwise. func (u *UpdateRef) GetForce() bool { if u == nil || u.Force == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 32d44331180..c6ece7ee50a 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -18288,6 +18288,50 @@ func TestListOrganizations_GetTotalCount(tt *testing.T) { l.GetTotalCount() } +func TestListProjectsOptions_GetQuery(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProjectsOptions{Query: &zeroValue} + l.GetQuery() + l = &ListProjectsOptions{} + l.GetQuery() + l = nil + l.GetQuery() +} + +func TestListProjectsPaginationOptions_GetAfter(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProjectsPaginationOptions{After: &zeroValue} + l.GetAfter() + l = &ListProjectsPaginationOptions{} + l.GetAfter() + l = nil + l.GetAfter() +} + +func TestListProjectsPaginationOptions_GetBefore(tt *testing.T) { + tt.Parallel() + var zeroValue string + l := &ListProjectsPaginationOptions{Before: &zeroValue} + l.GetBefore() + l = &ListProjectsPaginationOptions{} + l.GetBefore() + l = nil + l.GetBefore() +} + +func TestListProjectsPaginationOptions_GetPerPage(tt *testing.T) { + tt.Parallel() + var zeroValue int + l := &ListProjectsPaginationOptions{PerPage: &zeroValue} + l.GetPerPage() + l = &ListProjectsPaginationOptions{} + l.GetPerPage() + l = nil + l.GetPerPage() +} + func TestListRepositories_GetTotalCount(tt *testing.T) { tt.Parallel() var zeroValue int @@ -24825,6 +24869,14 @@ func TestProjectV2Event_GetSender(tt *testing.T) { p.GetSender() } +func TestProjectV2Field_GetConfiguration(tt *testing.T) { + tt.Parallel() + p := &ProjectV2Field{} + p.GetConfiguration() + p = nil + p.GetConfiguration() +} + func TestProjectV2Field_GetCreatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -24836,6 +24888,17 @@ func TestProjectV2Field_GetCreatedAt(tt *testing.T) { p.GetCreatedAt() } +func TestProjectV2Field_GetDataType(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Field{DataType: &zeroValue} + p.GetDataType() + p = &ProjectV2Field{} + p.GetDataType() + p = nil + p.GetDataType() +} + func TestProjectV2Field_GetID(tt *testing.T) { tt.Parallel() var zeroValue int64 @@ -24847,6 +24910,39 @@ func TestProjectV2Field_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2Field_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Field{Name: &zeroValue} + p.GetName() + p = &ProjectV2Field{} + p.GetName() + p = nil + p.GetName() +} + +func TestProjectV2Field_GetNodeID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Field{NodeID: &zeroValue} + p.GetNodeID() + p = &ProjectV2Field{} + p.GetNodeID() + p = nil + p.GetNodeID() +} + +func TestProjectV2Field_GetProjectURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Field{ProjectURL: &zeroValue} + p.GetProjectURL() + p = &ProjectV2Field{} + p.GetProjectURL() + p = nil + p.GetProjectURL() +} + func TestProjectV2Field_GetUpdatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -24858,6 +24954,116 @@ func TestProjectV2Field_GetUpdatedAt(tt *testing.T) { p.GetUpdatedAt() } +func TestProjectV2FieldConfiguration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldConfiguration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldConfiguration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldConfiguration_GetStartDay(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldConfiguration{StartDay: &zeroValue} + p.GetStartDay() + p = &ProjectV2FieldConfiguration{} + p.GetStartDay() + p = nil + p.GetStartDay() +} + +func TestProjectV2FieldIteration_GetDuration(tt *testing.T) { + tt.Parallel() + var zeroValue int + p := &ProjectV2FieldIteration{Duration: &zeroValue} + p.GetDuration() + p = &ProjectV2FieldIteration{} + p.GetDuration() + p = nil + p.GetDuration() +} + +func TestProjectV2FieldIteration_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIteration{ID: &zeroValue} + p.GetID() + p = &ProjectV2FieldIteration{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2FieldIteration_GetStartDate(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIteration{StartDate: &zeroValue} + p.GetStartDate() + p = &ProjectV2FieldIteration{} + p.GetStartDate() + p = nil + p.GetStartDate() +} + +func TestProjectV2FieldIteration_GetTitle(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldIteration{Title: &zeroValue} + p.GetTitle() + p = &ProjectV2FieldIteration{} + p.GetTitle() + p = nil + p.GetTitle() +} + +func TestProjectV2FieldOption_GetColor(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldOption{Color: &zeroValue} + p.GetColor() + p = &ProjectV2FieldOption{} + p.GetColor() + p = nil + p.GetColor() +} + +func TestProjectV2FieldOption_GetDescription(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldOption{Description: &zeroValue} + p.GetDescription() + p = &ProjectV2FieldOption{} + p.GetDescription() + p = nil + p.GetDescription() +} + +func TestProjectV2FieldOption_GetID(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldOption{ID: &zeroValue} + p.GetID() + p = &ProjectV2FieldOption{} + p.GetID() + p = nil + p.GetID() +} + +func TestProjectV2FieldOption_GetName(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2FieldOption{Name: &zeroValue} + p.GetName() + p = &ProjectV2FieldOption{} + p.GetName() + p = nil + p.GetName() +} + func TestProjectV2Item_GetArchivedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -24921,6 +25127,17 @@ func TestProjectV2Item_GetID(tt *testing.T) { p.GetID() } +func TestProjectV2Item_GetItemURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Item{ItemURL: &zeroValue} + p.GetItemURL() + p = &ProjectV2Item{} + p.GetItemURL() + p = nil + p.GetItemURL() +} + func TestProjectV2Item_GetNodeID(tt *testing.T) { tt.Parallel() var zeroValue string @@ -24943,6 +25160,17 @@ func TestProjectV2Item_GetProjectNodeID(tt *testing.T) { p.GetProjectNodeID() } +func TestProjectV2Item_GetProjectURL(tt *testing.T) { + tt.Parallel() + var zeroValue string + p := &ProjectV2Item{ProjectURL: &zeroValue} + p.GetProjectURL() + p = &ProjectV2Item{} + p.GetProjectURL() + p = nil + p.GetProjectURL() +} + func TestProjectV2Item_GetUpdatedAt(tt *testing.T) { tt.Parallel() var zeroValue Timestamp @@ -37313,6 +37541,17 @@ func TestUpdateOrganizationPrivateRegistry_GetVisibility(tt *testing.T) { u.GetVisibility() } +func TestUpdateProjectItemOptions_GetArchived(tt *testing.T) { + tt.Parallel() + var zeroValue bool + u := &UpdateProjectItemOptions{Archived: &zeroValue} + u.GetArchived() + u = &UpdateProjectItemOptions{} + u.GetArchived() + u = nil + u.GetArchived() +} + func TestUpdateRef_GetForce(tt *testing.T) { tt.Parallel() var zeroValue bool diff --git a/github/projects.go b/github/projects.go index 9f562fee865..1ccadaeaae5 100644 --- a/github/projects.go +++ b/github/projects.go @@ -57,13 +57,13 @@ func (p ProjectV2) String() string { return Stringify(p) } // per page (max 100 per GitHub API docs). type ListProjectsPaginationOptions struct { // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. - Before string `url:"before,omitempty"` + Before *string `url:"before,omitempty"` // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor. - After string `url:"after,omitempty"` + After *string `url:"after,omitempty"` // For paginated result sets, the number of results to include per page. - PerPage int `url:"per_page,omitempty"` + PerPage *int `url:"per_page,omitempty"` } // ListProjectsOptions specifies optional parameters to list projects for user / organization. @@ -71,7 +71,7 @@ type ListProjectsOptions struct { ListProjectsPaginationOptions // Q is an optional query string to limit results to projects of the specified type. - Query string `url:"q,omitempty"` + Query *string `url:"q,omitempty"` } // ProjectV2FieldOption represents an option for a project field of type single_select or multi_select. @@ -79,13 +79,31 @@ type ListProjectsOptions struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2FieldOption struct { - ID string `json:"id,omitempty"` - // The display name of the option. - Name string `json:"name,omitempty"` - // The color associated with this option (e.g., "blue", "red"). - Color string `json:"color,omitempty"` - // An optional description for this option. - Description string `json:"description,omitempty"` + ID *string `json:"id,omitempty"` // The unique identifier for this option. + Name *string `json:"name,omitempty"` // The display name of the option. + Color *string `json:"color,omitempty"` // The color associated with this option (e.g., "blue", "red"). + Description *string `json:"description,omitempty"` // An optional description for this option. +} + +// ProjectV2FieldIteration represents an iteration within a project field of type iteration. +// It defines a specific time-bound period that can be associated with project items. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields +type ProjectV2FieldIteration struct { + ID *string `json:"id,omitempty"` // The unique identifier for the iteration. + Title *string `json:"title,omitempty"` // The title of the iteration. + StartDate *string `json:"start_date,omitempty"` // The start date of the iteration in ISO 8601 format. + Duration *int `json:"duration,omitempty"` // The duration of the iteration in seconds. +} + +// ProjectV2FieldConfiguration represents the configuration for a project field of type iteration. +// It defines settings such as duration and start day for iterations within the project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields +type ProjectV2FieldConfiguration struct { + Duration *int `json:"duration,omitempty"` // The duration of the iteration field in seconds. + StartDay *int `json:"start_day,omitempty"` // The start day for the iteration. + Iterations []*ProjectV2FieldIteration `json:"iterations,omitempty"` // The list of iterations associated with the configuration. } // ProjectV2Field represents a field in a GitHub Projects V2 project. @@ -93,22 +111,23 @@ type ProjectV2FieldOption struct { // // GitHub API docs: https://docs.github.com/rest/projects/fields type ProjectV2Field struct { - ID *int64 `json:"id,omitempty"` - NodeID string `json:"node_id,omitempty"` - Name string `json:"name,omitempty"` - DataType string `json:"dataType,omitempty"` - URL string `json:"url,omitempty"` - Options []*any `json:"options,omitempty"` - CreatedAt *Timestamp `json:"created_at,omitempty"` - UpdatedAt *Timestamp `json:"updated_at,omitempty"` + ID *int64 `json:"id,omitempty"` + NodeID *string `json:"node_id,omitempty"` + Name *string `json:"name,omitempty"` + DataType *string `json:"data_type,omitempty"` + ProjectURL *string `json:"project_url,omitempty"` + Options []*ProjectV2FieldOption `json:"options,omitempty"` + Configuration *ProjectV2FieldConfiguration `json:"configuration,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } -// ListProjectsForOrg lists Projects V2 for an organization. +// ListOrganizationProjects lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-organization // //meta:operation GET /orgs/{org}/projectsV2 -func (s *ProjectsService) ListProjectsForOrg(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { +func (s *ProjectsService) ListOrganizationProjects(ctx context.Context, org string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2", org) u, err := addOptions(u, opts) if err != nil { @@ -128,12 +147,12 @@ func (s *ProjectsService) ListProjectsForOrg(ctx context.Context, org string, op return projects, resp, nil } -// GetProjectForOrg gets a Projects V2 project for an organization by ID. +// GetOrganizationProject gets a Projects V2 project for an organization by ID. // // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-organization // //meta:operation GET /orgs/{org}/projectsV2/{project_number} -func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, projectNumber int) (*ProjectV2, *Response, error) { +func (s *ProjectsService) GetOrganizationProject(ctx context.Context, org string, projectNumber int) (*ProjectV2, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2/%v", org, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { @@ -148,12 +167,12 @@ func (s *ProjectsService) GetProjectForOrg(ctx context.Context, org string, proj return project, resp, nil } -// ListProjectsForUser lists Projects V2 for a user. +// ListUserProjects lists Projects V2 for a user. // // GitHub API docs: https://docs.github.com/rest/projects/projects#list-projects-for-user // //meta:operation GET /users/{username}/projectsV2 -func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { +func (s *ProjectsService) ListUserProjects(ctx context.Context, username string, opts *ListProjectsOptions) ([]*ProjectV2, *Response, error) { u := fmt.Sprintf("users/%v/projectsV2", username) u, err := addOptions(u, opts) if err != nil { @@ -172,12 +191,12 @@ func (s *ProjectsService) ListProjectsForUser(ctx context.Context, username stri return projects, resp, nil } -// GetProjectForUser gets a Projects V2 project for a user by ID. +// GetUserProject gets a Projects V2 project for a user by ID. // // GitHub API docs: https://docs.github.com/rest/projects/projects#get-project-for-user // //meta:operation GET /users/{username}/projectsV2/{project_number} -func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string, projectNumber int) (*ProjectV2, *Response, error) { +func (s *ProjectsService) GetUserProject(ctx context.Context, username string, projectNumber int) (*ProjectV2, *Response, error) { u := fmt.Sprintf("users/%v/projectsV2/%v", username, projectNumber) req, err := s.client.NewRequest("GET", u, nil) if err != nil { @@ -192,12 +211,12 @@ func (s *ProjectsService) GetProjectForUser(ctx context.Context, username string return project, resp, nil } -// ListProjectFieldsForOrg lists Projects V2 for an organization. +// ListOrganizationProjectFields lists Projects V2 for an organization. // // GitHub API docs: https://docs.github.com/rest/projects/fields#list-project-fields-for-organization // //meta:operation GET /orgs/{org}/projectsV2/{project_number}/fields -func (s *ProjectsService) ListProjectFieldsForOrg(ctx context.Context, org string, projectNumber int, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { +func (s *ProjectsService) ListOrganizationProjectFields(ctx context.Context, org string, projectNumber int, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields", org, projectNumber) u, err := addOptions(u, opts) if err != nil { @@ -216,3 +235,293 @@ func (s *ProjectsService) ListProjectFieldsForOrg(ctx context.Context, org strin } return fields, resp, nil } + +// ListUserProjectFields lists Projects V2 for a user. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields#list-project-fields-for-user +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/fields +func (s *ProjectsService) ListUserProjectFields(ctx context.Context, user string, projectNumber int, opts *ListProjectsOptions) ([]*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/fields", user, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var fields []*ProjectV2Field + resp, err := s.client.Do(ctx, req, &fields) + if err != nil { + return nil, resp, err + } + return fields, resp, nil +} + +// GetOrganizationProjectField gets a single project field from an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields#get-project-field-for-organization +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/fields/{field_id} +func (s *ProjectsService) GetOrganizationProjectField(ctx context.Context, org string, projectNumber int, fieldID int64) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/fields/%v", org, projectNumber, fieldID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + field := new(ProjectV2Field) + resp, err := s.client.Do(ctx, req, field) + if err != nil { + return nil, resp, err + } + return field, resp, nil +} + +// GetUserProjectField gets a single project field from a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/fields#get-project-field-for-user +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/fields/{field_id} +func (s *ProjectsService) GetUserProjectField(ctx context.Context, user string, projectNumber int, fieldID int64) (*ProjectV2Field, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/fields/%v", user, projectNumber, fieldID) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + field := new(ProjectV2Field) + resp, err := s.client.Do(ctx, req, field) + if err != nil { + return nil, resp, err + } + return field, resp, nil +} + +// ListProjectItemsOptions specifies optional parameters when listing project items. +// Note: Pagination uses before/after cursor-style pagination similar to ListProjectsOptions. +// "Fields" can be used to restrict which field values are returned (by their numeric IDs). +type ListProjectItemsOptions struct { + // Embed ListProjectsOptions to reuse pagination and query parameters. + ListProjectsOptions + // Fields restricts which field values are returned by numeric field IDs. + Fields []int64 `url:"fields,omitempty,comma"` +} + +// GetProjectItemOptions specifies optional parameters when getting a project item. +type GetProjectItemOptions struct { + // Fields restricts which field values are returned by numeric field IDs. + Fields []int64 `url:"fields,omitempty,comma"` +} + +// AddProjectItemOptions represents the payload to add an item (issue or pull request) +// to a project. The Type must be either "Issue" or "PullRequest" (as per API docs) and +// ID is the numerical ID of that issue or pull request. +type AddProjectItemOptions struct { + Type string `json:"type,omitempty"` + ID int64 `json:"id,omitempty"` +} + +// UpdateProjectItemOptions represents fields that can be modified for a project item. +// Currently the REST API allows archiving/unarchiving an item (archived boolean). +// This struct can be expanded in the future as the API grows. +type UpdateProjectItemOptions struct { + // Archived indicates whether the item should be archived (true) or unarchived (false). + Archived *bool `json:"archived,omitempty"` + // Fields allows updating field values for the item. Each entry supplies a field ID and a value. + Fields []*ProjectV2Field `json:"fields,omitempty"` +} + +// ListOrganizationProjectItems lists items for an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#list-items-for-an-organization-owned-project +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/items +func (s *ProjectsService) ListOrganizationProjectItems(ctx context.Context, org string, projectNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items", org, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var items []*ProjectV2Item + resp, err := s.client.Do(ctx, req, &items) + if err != nil { + return nil, resp, err + } + return items, resp, nil +} + +// AddOrganizationProjectItem adds an issue or pull request item to an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#add-item-to-organization-owned-project +// +//meta:operation POST /orgs/{org}/projectsV2/{project_number}/items +func (s *ProjectsService) AddOrganizationProjectItem(ctx context.Context, org string, projectNumber int, opts *AddProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items", org, projectNumber) + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// GetOrganizationProjectItem gets a single item from an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#get-an-item-for-an-organization-owned-project +// +//meta:operation GET /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) GetOrganizationProjectItem(ctx context.Context, org string, projectNumber int, itemID int64, opts *GetProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("GET", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// UpdateOrganizationProjectItem updates an item in an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#update-project-item-for-organization +// +//meta:operation PATCH /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) UpdateOrganizationProjectItem(ctx context.Context, org string, projectNumber int, itemID int64, opts *UpdateProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// DeleteOrganizationProjectItem deletes an item from an organization owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#delete-project-item-for-organization +// +//meta:operation DELETE /orgs/{org}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) DeleteOrganizationProjectItem(ctx context.Context, org string, projectNumber int, itemID int64) (*Response, error) { + u := fmt.Sprintf("orgs/%v/projectsV2/%v/items/%v", org, projectNumber, itemID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} + +// ListUserProjectItems lists items for a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#list-items-for-a-user-owned-project +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/items +func (s *ProjectsService) ListUserProjectItems(ctx context.Context, username string, projectNumber int, opts *ListProjectItemsOptions) ([]*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items", username, projectNumber) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + var items []*ProjectV2Item + resp, err := s.client.Do(ctx, req, &items) + if err != nil { + return nil, resp, err + } + return items, resp, nil +} + +// AddUserProjectItem adds an issue or pull request item to a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#add-item-to-user-owned-project +// +//meta:operation POST /users/{username}/projectsV2/{project_number}/items +func (s *ProjectsService) AddUserProjectItem(ctx context.Context, username string, projectNumber int, opts *AddProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items", username, projectNumber) + req, err := s.client.NewRequest("POST", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// GetUserProjectItem gets a single item from a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#get-an-item-for-a-user-owned-project +// +//meta:operation GET /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) GetUserProjectItem(ctx context.Context, username string, projectNumber int, itemID int64, opts *GetProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("GET", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// UpdateUserProjectItem updates an item in a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#update-project-item-for-user +// +//meta:operation PATCH /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) UpdateUserProjectItem(ctx context.Context, username string, projectNumber int, itemID int64, opts *UpdateProjectItemOptions) (*ProjectV2Item, *Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("PATCH", u, opts) + if err != nil { + return nil, nil, err + } + item := new(ProjectV2Item) + resp, err := s.client.Do(ctx, req, item) + if err != nil { + return nil, resp, err + } + return item, resp, nil +} + +// DeleteUserProjectItem deletes an item from a user owned project. +// +// GitHub API docs: https://docs.github.com/rest/projects/items#delete-project-item-for-user +// +//meta:operation DELETE /users/{username}/projectsV2/{project_number}/items/{item_id} +func (s *ProjectsService) DeleteUserProjectItem(ctx context.Context, username string, projectNumber int, itemID int64) (*Response, error) { + u := fmt.Sprintf("users/%v/projectsV2/%v/items/%v", username, projectNumber, itemID) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + return s.client.Do(ctx, req, nil) +} diff --git a/github/projects_test.go b/github/projects_test.go index a5f71c0d675..4bb656b9e4b 100644 --- a/github/projects_test.go +++ b/github/projects_test.go @@ -8,11 +8,12 @@ package github import ( "context" "fmt" + "io" "net/http" "testing" ) -func TestProjectsService_ListProjectsForOrg(t *testing.T) { +func TestProjectsService_ListOrganizationProjects(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -29,24 +30,24 @@ func TestProjectsService_ListProjectsForOrg(t *testing.T) { fmt.Fprint(w, `[{"id":1,"title":"T1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) - opts := &ListProjectsOptions{Query: "alpha", ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1"}} + opts := &ListProjectsOptions{Query: Ptr("alpha"), ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr("2"), Before: Ptr("1")}} ctx := t.Context() - projects, _, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) + projects, _, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) if err != nil { - t.Fatalf("Projects.ListProjectsForOrg returned error: %v", err) + t.Fatalf("Projects.ListOrganizationProjects returned error: %v", err) } if len(projects) != 1 || projects[0].GetID() != 1 || projects[0].GetTitle() != "T1" { - t.Fatalf("Projects.ListProjectsForOrg returned %+v", projects) + t.Fatalf("Projects.ListOrganizationProjects returned %+v", projects) } - const methodName = "ListProjectsForOrg" + const methodName = "ListOrganizationProjects" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectsForOrg(ctx, "\n", opts) + _, _, err = client.Projects.ListOrganizationProjects(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) + got, resp, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -55,12 +56,12 @@ func TestProjectsService_ListProjectsForOrg(t *testing.T) { // still allow both set (no validation enforced) – ensure it does not error ctxBypass := context.WithValue(t.Context(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectsForOrg(ctxBypass, "o", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { + if _, _, err = client.Projects.ListOrganizationProjects(ctxBypass, "o", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("b"), After: Ptr("a")}}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } -func TestProjectsService_GetProjectForOrg(t *testing.T) { +func TestProjectsService_GetOrganizationProject(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -70,17 +71,17 @@ func TestProjectsService_GetProjectForOrg(t *testing.T) { }) ctx := t.Context() - project, _, err := client.Projects.GetProjectForOrg(ctx, "o", 1) + project, _, err := client.Projects.GetOrganizationProject(ctx, "o", 1) if err != nil { - t.Fatalf("Projects.GetProjectForOrg returned error: %v", err) + t.Fatalf("Projects.GetOrganizationProject returned error: %v", err) } if project.GetID() != 1 || project.GetTitle() != "OrgProj" { - t.Fatalf("Projects.GetProjectForOrg returned %+v", project) + t.Fatalf("Projects.GetOrganizationProject returned %+v", project) } - const methodName = "GetProjectForOrg" + const methodName = "GetOrganizationProject" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.GetProjectForOrg(ctx, "o", 1) + got, resp, err := client.Projects.GetOrganizationProject(ctx, "o", 1) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -104,25 +105,25 @@ func TestProjectsService_ListUserProjects(t *testing.T) { fmt.Fprint(w, `[{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) }) - opts := &ListProjectsOptions{Query: "beta", ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "1", After: "2", PerPage: 2}} + opts := &ListProjectsOptions{Query: Ptr("beta"), ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("1"), After: Ptr("2"), PerPage: Ptr(2)}} ctx := t.Context() var ctxBypass context.Context - projects, _, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + projects, _, err := client.Projects.ListUserProjects(ctx, "u", opts) if err != nil { - t.Fatalf("Projects.ListProjectsForUser returned error: %v", err) + t.Fatalf("Projects.ListUserProjects returned error: %v", err) } if len(projects) != 1 || projects[0].GetID() != 2 || projects[0].GetTitle() != "UProj" { - t.Fatalf("Projects.ListProjectsForUser returned %+v", projects) + t.Fatalf("Projects.ListUserProjects returned %+v", projects) } - const methodName = "ListProjectsForUser" + const methodName = "ListUserProjects" testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectsForUser(ctx, "\n", opts) + _, _, err = client.Projects.ListUserProjects(ctx, "\n", opts) return err }) testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + got, resp, err := client.Projects.ListUserProjects(ctx, "u", opts) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -131,32 +132,32 @@ func TestProjectsService_ListUserProjects(t *testing.T) { // still allow both set (no validation enforced) – ensure it does not error ctxBypass = context.WithValue(t.Context(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectsForUser(ctxBypass, "u", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { + if _, _, err = client.Projects.ListUserProjects(ctxBypass, "u", &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("b"), After: Ptr("a")}}); err != nil { t.Fatalf("unexpected error when both before/after set: %v", err) } } -func TestProjectsService_GetProjectForUser(t *testing.T) { +func TestProjectsService_GetUserProject(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - mux.HandleFunc("/users/u/projectsV2/2", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/users/u/projectsV2/3", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{"id":2,"title":"UProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) + fmt.Fprint(w, `{"id":3,"title":"UserProj","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}`) }) ctx := t.Context() - project, _, err := client.Projects.GetProjectForUser(ctx, "u", 2) + project, _, err := client.Projects.GetUserProject(ctx, "u", 3) if err != nil { - t.Fatalf("Projects.GetProjectForUser returned error: %v", err) + t.Fatalf("Projects.GetUserProject returned error: %v", err) } - if project.GetID() != 2 || project.GetTitle() != "UProj" { - t.Fatalf("Projects.GetProjectForUser returned %+v", project) + if project.GetID() != 3 || project.GetTitle() != "UserProj" { + t.Fatalf("Projects.GetUserProject returned %+v", project) } - const methodName = "GetProjectForUser" + const methodName = "GetUserProject" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.GetProjectForUser(ctx, "u", 2) + got, resp, err := client.Projects.GetUserProject(ctx, "u", 3) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } @@ -164,80 +165,247 @@ func TestProjectsService_GetProjectForUser(t *testing.T) { }) } -func TestProjectsService_ListProjectsForOrg_pagination(t *testing.T) { +func TestProjectsService_ListOrganizationProjectFields(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - // First page returns a Link header with rel="next" containing an after cursor (after=cursor2) - mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") q := r.URL.Query() - after := q.Get("after") - before := q.Get("before") - if after == "" && before == "" { - // first request - w.Header().Set("Link", "; rel=\"next\"") - fmt.Fprint(w, `[{"id":1,"title":"P1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + if q.Get("before") == "b" && q.Get("after") == "a" { // bypass scenario + fmt.Fprint(w, `[]`) return } - if after == "cursor2" { - // second request simulates a previous link - w.Header().Set("Link", "; rel=\"prev\"") - fmt.Fprint(w, `[{"id":2,"title":"P2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) - return + testFormValues(t, r, values{"after": "2", "before": "1", "q": "text"}) + fmt.Fprint(w, `[ + { + "id": 1, + "node_id": "node_1", + "name": "Status", + "data_type": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + {"id": "1", "name": "Todo", "color": "blue", "description": "Tasks to be done"}, + {"id": "2", "name": "In Progress", "color": "yellow"} + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }, + { + "id": 2, + "node_id": "node_2", + "name": "Priority", + "data_type": "text", + "url": "https://api.github.com/projects/1/fields/field2", + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" } - // unexpected state - http.Error(w, "unexpected query", http.StatusBadRequest) + ]`) }) + opts := &ListProjectsOptions{Query: Ptr("text"), ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr("2"), Before: Ptr("1")}} ctx := t.Context() - first, resp, err := client.Projects.ListProjectsForOrg(ctx, "o", nil) + fields, _, err := client.Projects.ListOrganizationProjectFields(ctx, "o", 1, opts) if err != nil { - t.Fatalf("first page error: %v", err) + t.Fatalf("Projects.ListOrganizationProjectFields returned error: %v", err) } - if len(first) != 1 || first[0].GetID() != 1 { - t.Fatalf("unexpected first page %+v", first) + if len(fields) != 2 { + t.Fatalf("Projects.ListOrganizationProjectFields returned %d fields, want 2", len(fields)) } - if resp.After != "cursor2" { - t.Fatalf("expected resp.After=cursor2 got %q", resp.After) + if fields[0].ID == nil || *fields[0].ID != 1 || fields[1].ID == nil || *fields[1].ID != 2 { + t.Fatalf("unexpected field IDs: %+v", fields) } - // Use resp.After as opts.After for next page - opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} - second, resp2, err := client.Projects.ListProjectsForOrg(ctx, "o", opts) + const methodName = "ListOrganizationProjectFields" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListOrganizationProjectFields(ctx, "\n", 1, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListOrganizationProjectFields(ctx, "o", 1, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + ctxBypass := context.WithValue(ctx, BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListOrganizationProjectFields(ctxBypass, "o", 1, &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("b"), After: Ptr("a")}}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } +} + +func TestProjectsService_ListUserProjectFields(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/users/u/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { // bypass scenario + fmt.Fprint(w, `[]`) + return + } + testFormValues(t, r, values{"after": "2", "before": "1", "q": "text"}) + fmt.Fprint(w, `[ + { + "id": 1, + "node_id": "node_1", + "name": "Status", + "data_type": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + {"id": "1", "name": "Todo", "color": "blue", "description": "Tasks to be done"}, + {"id": "2", "name": "In Progress", "color": "yellow"} + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }, + { + "id": 2, + "node_id": "node_2", + "name": "Priority", + "data_type": "text", + "url": "https://api.github.com/projects/1/fields/field2", + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + } + ]`) + }) + + opts := &ListProjectsOptions{Query: Ptr("text"), ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr("2"), Before: Ptr("1")}} + ctx := t.Context() + fields, _, err := client.Projects.ListUserProjectFields(ctx, "u", 1, opts) if err != nil { - t.Fatalf("second page error: %v", err) + t.Fatalf("Projects.ListUserProjectFields returned error: %v", err) } - if len(second) != 1 || second[0].GetID() != 2 { - t.Fatalf("unexpected second page %+v", second) + if len(fields) != 2 { + t.Fatalf("Projects.ListUserProjectFields returned %d fields, want 2", len(fields)) } - if resp2.Before != "cursor2" { - t.Fatalf("expected resp2.Before=cursor2 got %q", resp2.Before) + if fields[0].ID == nil || *fields[0].ID != 1 || fields[1].ID == nil || *fields[1].ID != 2 { + t.Fatalf("unexpected field IDs: %+v", fields) } + + const methodName = "ListUserProjectFields" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListUserProjectFields(ctx, "\n", 1, opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListUserProjectFields(ctx, "u", 1, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + ctxBypass := context.WithValue(ctx, BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListUserProjectFields(ctxBypass, "u", 1, &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("b"), After: Ptr("a")}}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } +} + +func TestProjectsService_GetOrganizationProjectField(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/fields/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, ` + { + "id": 1, + "node_id": "node_1", + "name": "Status", + "data_type": "single_select", + "url": "https://api.github.com/projects/1/fields/field1", + "options": [ + {"id": "1", "name": "Todo", "color": "blue", "description": "Tasks to be done"}, + {"id": "2", "name": "In Progress", "color": "yellow"} + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }`) + }) + + ctx := t.Context() + field, _, err := client.Projects.GetOrganizationProjectField(ctx, "o", 1, 1) + if err != nil { + t.Fatalf("Projects.GetOrganizationProjectField returned error: %v", err) + } + if field == nil || field.ID == nil || *field.ID != 1 { + t.Fatalf("unexpected field: %+v", field) + } + + const methodName = "GetOrganizationProjectField" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetOrganizationProjectField(ctx, "o", 1, 1) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) } -func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { +func TestProjectsService_GetUserProjectField(t *testing.T) { t.Parallel() client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/1/fields/3", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, ` + { + "id": 3, + "node_id": "node_3", + "name": "Status", + "data_type": "single_select", + "url": "https://api.github.com/projects/1/fields/field3", + "options": [ + {"id": "1", "name": "Done", "color": "red", "description": "Done task"}, + {"id": "2", "name": "In Progress", "color": "yellow"} + ], + "created_at": "2011-01-02T15:04:05Z", + "updated_at": "2012-01-02T15:04:05Z" + }`) + }) + + ctx := t.Context() + field, _, err := client.Projects.GetUserProjectField(ctx, "u", 1, 3) + if err != nil { + t.Fatalf("Projects.GetUserProjectField returned error: %v", err) + } + if field == nil || field.ID == nil || *field.ID != 3 { + t.Fatalf("unexpected field: %+v", field) + } + + const methodName = "GetUserProjectField" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetUserProjectField(ctx, "u", 1, 3) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_ListUserProjects_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() after := q.Get("after") before := q.Get("before") - if after == "" && before == "" { // first page + if after == "" && before == "" { w.Header().Set("Link", "; rel=\"next\"") fmt.Fprint(w, `[{"id":10,"title":"UP1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } - if after == "ucursor2" { // second page provides prev + if after == "ucursor2" { w.Header().Set("Link", "; rel=\"prev\"") fmt.Fprint(w, `[{"id":11,"title":"UP2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } http.Error(w, "unexpected query", http.StatusBadRequest) }) - ctx := t.Context() - first, resp, err := client.Projects.ListProjectsForUser(ctx, "u", nil) + first, resp, err := client.Projects.ListUserProjects(ctx, "u", nil) if err != nil { t.Fatalf("first page error: %v", err) } @@ -248,8 +416,8 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { t.Fatalf("expected resp.After=ucursor2 got %q", resp.After) } - opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} - second, resp2, err := client.Projects.ListProjectsForUser(ctx, "u", opts) + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr(resp.After)}} + second, resp2, err := client.Projects.ListUserProjects(ctx, "u", opts) if err != nil { t.Fatalf("second page error: %v", err) } @@ -261,122 +429,30 @@ func TestProjectsService_ListProjectsForUser_pagination(t *testing.T) { } } -func TestProjectsService_ListProjectFieldsForOrg(t *testing.T) { +func TestProjectsService_ListUserProjects_error(t *testing.T) { t.Parallel() client, mux, _ := setup(t) - - mux.HandleFunc("/orgs/o/projectsV2/1/fields", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/users/u/projectsV2", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - q := r.URL.Query() - if q.Get("before") == "b" && q.Get("after") == "a" { - fmt.Fprint(w, `[]`) - return - } - testFormValues(t, r, values{"q": "text", "after": "2", "before": "1"}) - fmt.Fprint(w, `[ - { - "id": 1, - "node_id": "node_1", - "name": "Status", - "dataType": "single_select", - "url": "https://api.github.com/projects/1/fields/field1", - "options": [ - { - "id": "option1", - "name": "Todo", - "color": "blue", - "description": "Tasks to be done" - }, - { - "id": "option2", - "name": "In Progress", - "color": "yellow" - } - ], - "created_at": "2011-01-02T15:04:05Z", - "updated_at": "2012-01-02T15:04:05Z" - }, - { - "id": 2, - "node_id": "node_2", - "name": "Priority", - "dataType": "text", - "url": "https://api.github.com/projects/1/fields/field2", - "created_at": "2011-01-02T15:04:05Z", - "updated_at": "2012-01-02T15:04:05Z" - } - ]`) + fmt.Fprint(w, `[]`) }) - - opts := &ListProjectsOptions{Query: "text", ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: "2", Before: "1"}} ctx := t.Context() - fields, _, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) - if err != nil { - t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v", err) - } - - if len(fields) != 2 { - t.Fatalf("Projects.ListProjectFieldsForOrg returned %v fields, want 2", len(fields)) - } - - field1 := fields[0] - if field1.ID == nil || *field1.ID != 1 || field1.Name != "Status" || field1.DataType != "single_select" { - t.Errorf("First field: got ID=%v, Name=%v, DataType=%v; want 1, Status, single_select", field1.ID, field1.Name, field1.DataType) - } - if len(field1.Options) != 2 { - t.Errorf("First field options: got %v, want 2", len(field1.Options)) - } else { - getName := func(o *any) string { - if o == nil || *o == nil { - return "" - } - switch v := (*o).(type) { - case map[string]any: - if n, ok := v["name"].(string); ok { - return n - } - default: - // fall back to fmt for debug; reflection can be added if needed. - } - return "" - } - name0, name1 := getName(field1.Options[0]), getName(field1.Options[1]) - if name0 != "Todo" || name1 != "In Progress" { - t.Errorf("First field option names: got %q, %q; want Todo, In Progress", name0, name1) - } - } - - // Validate second field (without options) - field2 := fields[1] - if field2.ID == nil || *field2.ID != 2 || field2.Name != "Priority" || field2.DataType != "text" { - t.Errorf("Second field: got ID=%v, Name=%v, DataType=%v; want 2, Priority, text", field2.ID, field2.Name, field2.DataType) - } - if len(field2.Options) != 0 { - t.Errorf("Second field options: got %v, want 0", len(field2.Options)) - } - - const methodName = "ListProjectFieldsForOrg" - testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, "\n", 1, opts) - return err - }) - + const methodName = "ListUserProjects" testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) + got, resp, err := client.Projects.ListUserProjects(ctx, "u", nil) if got != nil { t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) } return resp, err }) - - // still allow both set (no validation enforced) – ensure it does not error - ctxBypass := context.WithValue(t.Context(), BypassRateLimitCheck, true) - if _, _, err = client.Projects.ListProjectFieldsForOrg(ctxBypass, "o", 1, &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: "b", After: "a"}}); err != nil { - t.Fatalf("unexpected error when both before/after set: %v", err) - } + // bad options (bad username) should error + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListUserProjects(ctx, "\n", nil) + return err + }) } -func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { +func TestProjectsService_ListOrganizationProjectFields_pagination(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -388,13 +464,13 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { if after == "" && before == "" { // first request w.Header().Set("Link", "; rel=\"next\"") - fmt.Fprint(w, `[{"id":1,"name":"Status","dataType":"single_select","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + fmt.Fprint(w, `[{"id":1,"name":"Status","data_type":"single_select","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } if after == "cursor2" { // second request simulates a previous link w.Header().Set("Link", "; rel=\"prev\"") - fmt.Fprint(w, `[{"id":2,"name":"Priority","dataType":"text","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + fmt.Fprint(w, `[{"id":2,"name":"Priority","data_type":"text","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) return } // unexpected state @@ -402,7 +478,7 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { }) ctx := t.Context() - first, resp, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, nil) + first, resp, err := client.Projects.ListOrganizationProjectFields(ctx, "o", 1, nil) if err != nil { t.Fatalf("first page error: %v", err) } @@ -413,8 +489,8 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { t.Fatalf("expected resp.After=cursor2 got %q", resp.After) } - opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: resp.After}} - second, resp2, err := client.Projects.ListProjectFieldsForOrg(ctx, "o", 1, opts) + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr(resp.After)}} + second, resp2, err := client.Projects.ListOrganizationProjectFields(ctx, "o", 1, opts) if err != nil { t.Fatalf("second page error: %v", err) } @@ -426,6 +502,53 @@ func TestProjectsService_ListProjectFieldsForOrg_pagination(t *testing.T) { } } +func TestProjectsService_ListOrganizationProjects_pagination(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + after := q.Get("after") + before := q.Get("before") + if after == "" && before == "" { + w.Header().Set("Link", "; rel=\"next\"") + fmt.Fprint(w, `[{"id":20,"title":"OP1","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + if after == "ocursor2" { + w.Header().Set("Link", "; rel=\"prev\"") + fmt.Fprint(w, `[{"id":21,"title":"OP2","created_at":"2011-01-02T15:04:05Z","updated_at":"2012-01-02T15:04:05Z"}]`) + return + } + http.Error(w, "unexpected query", http.StatusBadRequest) + }) + + ctx := t.Context() + first, resp, err := client.Projects.ListOrganizationProjects(ctx, "o", nil) + if err != nil { + t.Fatalf("first page error: %v", err) + } + if len(first) != 1 || first[0].GetID() != 20 { + t.Fatalf("unexpected first page %+v", first) + } + if resp.After != "ocursor2" { + t.Fatalf("expected resp.After=ocursor2 got %q", resp.After) + } + + opts := &ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr(resp.After)}} + second, resp2, err := client.Projects.ListOrganizationProjects(ctx, "o", opts) + if err != nil { + t.Fatalf("second page error: %v", err) + } + if len(second) != 1 || second[0].GetID() != 21 { + t.Fatalf("unexpected second page %+v", second) + } + if resp2.Before != "ocursor2" { + t.Fatalf("expected resp2.Before=ocursor2 got %q", resp2.Before) + } +} + +// Marshal test ensures V2 fields marshal correctly. func TestProjectV2_Marshal(t *testing.T) { t.Parallel() testJSONMarshal(t, &ProjectV2{}, "{}") @@ -451,48 +574,537 @@ func TestProjectV2_Marshal(t *testing.T) { testJSONMarshal(t, p, want) } +// Marshal test ensures V2 field structures marshal correctly. func TestProjectV2Field_Marshal(t *testing.T) { t.Parallel() - testJSONMarshal(t, &ProjectV2Field{}, "{}") // empty struct - testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") // option struct still individually testable - - type optStruct struct { - Color string `json:"color,omitempty"` - Description string `json:"description,omitempty"` - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - } - optVal := &optStruct{Color: "blue", Description: "Tasks to be done", ID: "option1", Name: "Todo"} - var optAny any = optVal + testJSONMarshal(t, &ProjectV2Field{}, "{}") + testJSONMarshal(t, &ProjectV2FieldOption{}, "{}") field := &ProjectV2Field{ - ID: Ptr(int64(1)), - NodeID: "node_1", - Name: "Status", - DataType: "single_select", - URL: "https://api.github.com/projects/1/fields/field1", - Options: []*any{&optAny}, + ID: Ptr(int64(2)), + NodeID: Ptr("node_1"), + Name: Ptr("Status"), + DataType: Ptr("single_select"), + ProjectURL: Ptr("https://api.github.com/projects/67890"), + Options: []*ProjectV2FieldOption{ + { + ID: Ptr("1"), + Name: Ptr("Todo"), + Color: Ptr("blue"), + Description: Ptr("Tasks to be done"), + }, + }, CreatedAt: &Timestamp{referenceTime}, UpdatedAt: &Timestamp{referenceTime}, } want := `{ - "id": 1, - "node_id": "node_1", - "name": "Status", - "dataType": "single_select", - "url": "https://api.github.com/projects/1/fields/field1", - "options": [ - { - "id": "option1", - "name": "Todo", - "color": "blue", - "description": "Tasks to be done" - } - ], - "created_at": ` + referenceTimeStr + `, - "updated_at": ` + referenceTimeStr + ` - }` + "id": 2, + "node_id": "node_1", + "name": "Status", + "data_type": "single_select", + "project_url": "https://api.github.com/projects/67890", + "options": [ + { + "id": "1", + "name": "Todo", + "color": "blue", + "description": "Tasks to be done" + } + ], + "created_at": ` + referenceTimeStr + `, + "updated_at": ` + referenceTimeStr + ` + }` testJSONMarshal(t, field, want) } + +// Marshal test ensures ProjectV2FieldConfiguration marshals correctly. +func TestProjectV2FieldConfiguration_Marshal(t *testing.T) { + t.Parallel() + testJSONMarshal(t, &ProjectV2FieldConfiguration{}, "{}") + testJSONMarshal(t, &ProjectV2FieldIteration{}, "{}") + + // Test a field with configuration (iteration field) + fieldWithConfiguration := &ProjectV2Field{ + ID: Ptr(int64(3)), + NodeID: Ptr("node_3"), + Name: Ptr("Sprint"), + DataType: Ptr("iteration"), + ProjectURL: Ptr("https://api.github.com/projects/67890"), + Configuration: &ProjectV2FieldConfiguration{ + Duration: Ptr(1209600), // 2 weeks in seconds + StartDay: Ptr(1), // Monday + Iterations: []*ProjectV2FieldIteration{ + { + ID: Ptr("iter_1"), + Title: Ptr("Sprint 1"), + StartDate: Ptr("2025-01-06"), + Duration: Ptr(1209600), + }, + { + ID: Ptr("iter_2"), + Title: Ptr("Sprint 2"), + StartDate: Ptr("2025-01-20"), + Duration: Ptr(1209600), + }, + }, + }, + CreatedAt: &Timestamp{referenceTime}, + UpdatedAt: &Timestamp{referenceTime}, + } + + want := `{ + "id": 3, + "node_id": "node_3", + "name": "Sprint", + "data_type": "iteration", + "project_url": "https://api.github.com/projects/67890", + "configuration": { + "duration": 1209600, + "start_day": 1, + "iterations": [ + { + "id": "iter_1", + "title": "Sprint 1", + "start_date": "2025-01-06", + "duration": 1209600 + }, + { + "id": "iter_2", + "title": "Sprint 2", + "start_date": "2025-01-20", + "duration": 1209600 + } + ] + }, + "created_at": ` + referenceTimeStr + `, + "updated_at": ` + referenceTimeStr + ` + }` + + testJSONMarshal(t, fieldWithConfiguration, want) + + // Test just the configuration struct by itself + config := &ProjectV2FieldConfiguration{ + Duration: Ptr(604800), // 1 week in seconds + StartDay: Ptr(0), // Sunday + Iterations: []*ProjectV2FieldIteration{ + { + ID: Ptr("config_iter_1"), + Title: Ptr("Week 1"), + StartDate: Ptr("2025-01-01"), + Duration: Ptr(604800), + }, + }, + } + + configWant := `{ + "duration": 604800, + "start_day": 0, + "iterations": [ + { + "id": "config_iter_1", + "title": "Week 1", + "start_date": "2025-01-01", + "duration": 604800 + } + ] + }` + + testJSONMarshal(t, config, configWant) + + // Test iteration struct by itself + iteration := &ProjectV2FieldIteration{ + ID: Ptr("single_iter"), + Title: Ptr("Test Iteration"), + StartDate: Ptr("2025-02-01"), + Duration: Ptr(1209600), + } + + iterationWant := `{ + "id": "single_iter", + "title": "Test Iteration", + "start_date": "2025-02-01", + "duration": 1209600 + }` + + testJSONMarshal(t, iteration, iterationWant) +} + +func TestProjectsService_ListOrganizationProjectItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("before") == "b" && q.Get("after") == "a" { // bypass scenario + fmt.Fprint(w, `[]`) + return + } + testFormValues(t, r, values{"after": "2", "before": "1", "per_page": "50", "fields": "10,11", "q": "status:open"}) + fmt.Fprint(w, `[{"id":17,"node_id":"PVTI_node"}]`) + }) + + opts := &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{After: Ptr("2"), Before: Ptr("1"), PerPage: Ptr(50)}, Query: Ptr("status:open")}, Fields: []int64{10, 11}} + ctx := t.Context() + items, _, err := client.Projects.ListOrganizationProjectItems(ctx, "o", 1, opts) + if err != nil { + t.Fatalf("Projects.ListOrganizationProjectItems returned error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 17 { + t.Fatalf("Projects.ListOrganizationProjectItems returned %+v", items) + } + + const methodName = "ListOrganizationProjectItems" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListOrganizationProjectItems(ctx, "\n", 1, opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListOrganizationProjectItems(ctx, "o", 1, opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + + ctxBypass := context.WithValue(ctx, BypassRateLimitCheck, true) + if _, _, err = client.Projects.ListOrganizationProjectItems(ctxBypass, "o", 1, &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{Before: Ptr("b"), After: Ptr("a")}}}); err != nil { + t.Fatalf("unexpected error when both before/after set: %v", err) + } +} + +func TestProjectsService_AddOrganizationProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"type":"Issue","id":99}`+"\n" { // encoder adds newline + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":99,"node_id":"PVTI_new"}`) + }) + + ctx := t.Context() + item, _, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 99}) + if err != nil { + t.Fatalf("Projects.AddOrganizationProjectItem returned error: %v", err) + } + if item.GetID() != 99 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_AddProjectItemForOrg_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusCreated) + fmt.Fprint(w, `{"id":1}`) + }) + ctx := t.Context() + const methodName = "AddOrganizationProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddOrganizationProjectItem(ctx, "o", 1, &AddProjectItemOptions{Type: "Issue", ID: 1}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_GetOrganizationProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":17,"node_id":"PVTI_node"}`) + }) + ctx := t.Context() + opts := &GetProjectItemOptions{} + item, _, err := client.Projects.GetOrganizationProjectItem(ctx, "o", 1, 17, opts) + if err != nil { + t.Fatalf("GetOrganizationProjectItem error: %v", err) + } + if item.GetID() != 17 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_GetOrganizationProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":17}`) + }) + ctx := t.Context() + const methodName = "GetOrganizationProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetOrganizationProjectItem(ctx, "o", 1, 17, &GetProjectItemOptions{}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_UpdateOrganizationProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"archived":true}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":17}`) + }) + archived := true + ctx := t.Context() + item, _, err := client.Projects.UpdateOrganizationProjectItem(ctx, "o", 1, 17, &UpdateProjectItemOptions{Archived: &archived}) + if err != nil { + t.Fatalf("UpdateOrganizationProjectItem error: %v", err) + } + if item.GetID() != 17 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_UpdateOrganizationProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + fmt.Fprint(w, `{"id":17}`) + }) + archived := true + ctx := t.Context() + const methodName = "UpdateProjectItemForOrg" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.UpdateOrganizationProjectItem(ctx, "o", 1, 17, &UpdateProjectItemOptions{Archived: &archived}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_DeleteOrganizationProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := t.Context() + if _, err := client.Projects.DeleteOrganizationProjectItem(ctx, "o", 1, 17); err != nil { + t.Fatalf("DeleteOrganizationProjectItem error: %v", err) + } +} + +func TestProjectsService_DeleteOrganizationProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/orgs/o/projectsV2/1/items/17", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := t.Context() + const methodName = "DeleteOrganizationProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Projects.DeleteOrganizationProjectItem(ctx, "o", 1, 17) + }) +} + +func TestProjectsService_ListUserProjectItems(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "20", "q": "type:issue"}) + fmt.Fprint(w, `[{"id":7,"node_id":"PVTI_user"}]`) + }) + ctx := t.Context() + items, _, err := client.Projects.ListUserProjectItems(ctx, "u", 2, &ListProjectItemsOptions{ListProjectsOptions: ListProjectsOptions{ListProjectsPaginationOptions: ListProjectsPaginationOptions{PerPage: Ptr(20)}, Query: Ptr("type:issue")}}) + if err != nil { + t.Fatalf("ListUserProjectItems error: %v", err) + } + if len(items) != 1 || items[0].GetID() != 7 { + t.Fatalf("unexpected items: %+v", items) + } +} + +func TestProjectsService_ListUserProjectItems_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[]`) + }) + ctx := t.Context() + const methodName = "ListUserProjectItems" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.ListUserProjectItems(ctx, "u", 2, nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Projects.ListUserProjectItems(ctx, "\n", 2, nil) + return err + }) +} + +func TestProjectsService_AddUserProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"type":"PullRequest","id":123}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":123,"node_id":"PVTI_new_user"}`) + }) + ctx := t.Context() + item, _, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "PullRequest", ID: 123}) + if err != nil { + t.Fatalf("AddUserProjectItem error: %v", err) + } + if item.GetID() != 123 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_AddUserProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{"id":5}`) + }) + ctx := t.Context() + const methodName = "AddUserProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.AddUserProjectItem(ctx, "u", 2, &AddProjectItemOptions{Type: "Issue", ID: 5}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_GetUserProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":55,"node_id":"PVTI_user_item"}`) + }) + ctx := t.Context() + opts := &GetProjectItemOptions{} + item, _, err := client.Projects.GetUserProjectItem(ctx, "u", 2, 55, opts) + if err != nil { + t.Fatalf("GetUserProjectItem error: %v", err) + } + if item.GetID() != 55 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_GetUserProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":55}`) + }) + ctx := t.Context() + const methodName = "GetUserProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.GetUserProjectItem(ctx, "u", 2, 55, &GetProjectItemOptions{}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_UpdateUserProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + b, _ := io.ReadAll(r.Body) + body := string(b) + if body != `{"archived":false}`+"\n" { + t.Fatalf("unexpected body: %s", body) + } + fmt.Fprint(w, `{"id":55}`) + }) + archived := false + ctx := t.Context() + item, _, err := client.Projects.UpdateUserProjectItem(ctx, "u", 2, 55, &UpdateProjectItemOptions{Archived: &archived}) + if err != nil { + t.Fatalf("UpdateUserProjectItem error: %v", err) + } + if item.GetID() != 55 { + t.Fatalf("unexpected item: %+v", item) + } +} + +func TestProjectsService_UpdateUserProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + fmt.Fprint(w, `{"id":55}`) + }) + archived := false + ctx := t.Context() + const methodName = "UpdateUserProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Projects.UpdateUserProjectItem(ctx, "u", 2, 55, &UpdateProjectItemOptions{Archived: &archived}) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestProjectsService_DeleteUserProjectItem(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := t.Context() + if _, err := client.Projects.DeleteUserProjectItem(ctx, "u", 2, 55); err != nil { + t.Fatalf("DeleteUserProjectItem error: %v", err) + } +} + +func TestProjectsService_DeleteUserProjectItem_error(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + mux.HandleFunc("/users/u/projectsV2/2/items/55", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + ctx := t.Context() + const methodName = "DeleteUserProjectItem" + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Projects.DeleteUserProjectItem(ctx, "u", 2, 55) + }) +} diff --git a/test/integration/projects_test.go b/test/integration/projects_test.go index f51af52691f..1357fcca816 100644 --- a/test/integration/projects_test.go +++ b/test/integration/projects_test.go @@ -41,10 +41,10 @@ func TestProjectsV2_Org(t *testing.T) { opts := &github.ListProjectsOptions{} // List projects for org; pick the first available project we can read. - projects, _, err := client.Projects.ListProjectsForOrg(ctx, org, opts) + projects, _, err := client.Projects.ListOrganizationProjects(ctx, org, opts) if err != nil { // If listing itself fails, abort this test. - t.Fatalf("Projects.ListProjectsForOrg returned error: %v", err) + t.Fatalf("Projects.ListOrganizationProjects returned error: %v", err) } if len(projects) == 0 { t.Skipf("no Projects V2 found for org %s", org) @@ -56,19 +56,19 @@ func TestProjectsV2_Org(t *testing.T) { projectNumber := *project.Number // Re-fetch via Get to exercise endpoint explicitly. - proj, _, err := client.Projects.GetProjectForOrg(ctx, org, projectNumber) + proj, _, err := client.Projects.GetOrganizationProject(ctx, org, projectNumber) if err != nil { // Permission mismatch? Skip CRUD while still reporting failure would make the test fail; // we want correctness so treat as fatal here. - t.Fatalf("Projects.GetProjectForOrg returned error: %v", err) + t.Fatalf("Projects.GetOrganizationProject returned error: %v", err) } if proj.Number == nil || *proj.Number != projectNumber { - t.Fatalf("GetProjectForOrg returned unexpected project number: got %+v want %d", proj.Number, projectNumber) + t.Fatalf("GetOrganizationProject returned unexpected project number: got %+v want %d", proj.Number, projectNumber) } - _, _, err = client.Projects.ListProjectFieldsForOrg(ctx, org, projectNumber, nil) + _, _, err = client.Projects.ListOrganizationProjectFields(ctx, org, projectNumber, nil) if err != nil { - t.Fatalf("Projects.ListProjectFieldsForOrg returned error: %v. Fields listing might require extra permissions", err) + t.Fatalf("Projects.ListOrganizationProjectFields returned error: %v. Fields listing might require extra permissions", err) } } @@ -81,9 +81,9 @@ func TestProjectsV2_User(t *testing.T) { ctx := t.Context() opts := &github.ListProjectsOptions{} - projects, _, err := client.Projects.ListProjectsForUser(ctx, user, opts) + projects, _, err := client.Projects.ListUserProjects(ctx, user, opts) if err != nil { - t.Fatalf("Projects.ListProjectsForUser returned error: %v. This indicates API or permission issue", err) + t.Fatalf("Projects.ListUserProjects returned error: %v. This indicates API or permission issue", err) } if len(projects) == 0 { t.Skipf("no Projects V2 found for user %s", user) @@ -93,12 +93,12 @@ func TestProjectsV2_User(t *testing.T) { t.Skip("selected user project has nil Number field") } - proj, _, err := client.Projects.GetProjectForUser(ctx, user, *project.Number) + proj, _, err := client.Projects.GetUserProject(ctx, user, *project.Number) if err != nil { // can't fetch specific project; treat as fatal - t.Fatalf("Projects.GetProjectForUser returned error: %v", err) + t.Fatalf("Projects.GetUserProject returned error: %v", err) } if proj.Number == nil || *proj.Number != *project.Number { - t.Fatalf("GetProjectForUser returned unexpected project number: got %+v want %d", proj.Number, *project.Number) + t.Fatalf("GetUserProject returned unexpected project number: got %+v want %d", proj.Number, *project.Number) } }