Loading workitems.go +152 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,8 @@ import ( "fmt" "regexp" "strconv" "strings" "text/template" "time" ) Loading @@ -13,6 +15,7 @@ type ( WorkItemsServiceInterface interface { GetWorkItemByID(gid any, options ...RequestOptionFunc) (*WorkItem, *Response, error) GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -207,6 +210,155 @@ query ($fullPath: ID!, $iid: String) { return wiQL.unwrap(), resp, nil } /* workItems( search: String in: [IssuableSearchableField!] ids: [WorkItemID!] authorUsername: String confidential: Boolean assigneeUsernames: [String!] assigneeWildcardId: AssigneeWildcardId labelName: [String!] milestoneTitle: [String!] milestoneWildcardId: MilestoneWildcardId myReactionEmoji: String iids: [String!] state: IssuableState types: [IssueType!] createdBefore: Time createdAfter: Time updatedBefore: Time updatedAfter: Time dueBefore: Time dueAfter: Time closedBefore: Time closedAfter: Time subscribed: SubscriptionStatus not: NegatedWorkItemFilterInput or: UnionedWorkItemFilterInput parentIds: [WorkItemID!] releaseTag: [String!] releaseTagWildcardId: ReleaseTagWildcardId crmContactId: String crmOrganizationId: String iid: String sort: WorkItemSort = CREATED_DESC verificationStatusWidget: VerificationStatusFilterInput healthStatusFilter: HealthStatusFilter weight: String weightWildcardId: WeightWildcardId iterationId: [ID] iterationWildcardId: IterationWildcardId iterationCadenceId: [IterationsCadenceID!] includeAncestors: Boolean = false includeDescendants: Boolean = false timeframe: Timeframe after: String before: String first: Int last: Int ): WorkItemConnection */ // ListWorkItemsOptions represents the available ListWorkItems() options. // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#queryworkitems type ListWorkItemsOptions struct { State *string AuthorUsername *string } var workItemFieldTypes = map[string]string{ "state": "IssuableState", "authorUsername": "String", } var listWorkItemsTemplate = template.Must(template.New("ListWorkItems").Parse(` query ListWorkItems($fullPath: ID!{{ range .Fields }}, ${{ .Name }}: {{ .Type }}{{ end }}) { namespace(fullPath: $fullPath) { workItems({{ range $i, $f := .Fields }}{{ if ne $i 0 }}, {{ end }}{{ $f.Name }}: ${{ $f.Name }}{{ end }}) { nodes { id iid title } } } } `)) // ListWorkItems lists workitems in a given namespace (group or project). func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) { type fieldGQL struct { Name string Type string } var ( queryFields []fieldGQL queryVariables = map[string]any{ "fullPath": fullPath, } ) if opt != nil { if opt.State != nil { queryFields = append(queryFields, fieldGQL{"state", workItemFieldTypes["state"]}) queryVariables["state"] = opt.State } if opt.AuthorUsername != nil { queryFields = append(queryFields, fieldGQL{"authorUsername", workItemFieldTypes["authorUsername"]}) queryVariables["authorUsername"] = opt.AuthorUsername } } var queryBuilder strings.Builder if err := listWorkItemsTemplate.Execute(&queryBuilder, map[string]any{"Fields": queryFields}); err != nil { return nil, nil, err } query := GraphQLQuery{ Query: queryBuilder.String(), Variables: queryVariables, } var result struct { Data struct { Namespace struct { WorkItems struct { Nodes []workItemGQL `json:"nodes"` } `json:"workItems"` } `json:"namespace"` } GenericGraphQLErrors } resp, err := s.client.GraphQL.Do(query, &result, options...) if err != nil { return nil, resp, err } if len(result.Errors) != 0 { return nil, resp, &GraphQLResponseError{ Err: errors.New("GraphQL query failed"), Errors: result.GenericGraphQLErrors, } } var ret []*WorkItem for _, wi := range result.Data.Namespace.WorkItems.Nodes { ret = append(ret, wi.unwrap()) } return ret, resp, nil } // workItemGQL represents the JSON structure returned by the GraphQL query. // It is used to parse the response and convert it to the more user-friendly WorkItem type. type workItemGQL struct { Loading workitems_test.go +166 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ package gitlab import ( "bytes" _ "embed" "encoding/json" "fmt" "io" "net/http" "strings" Loading Loading @@ -279,3 +281,167 @@ func TestGetWorkItemByID(t *testing.T) { }) } } func TestListWorkItems(t *testing.T) { t.Parallel() tests := []struct { name string fullPath string opt *ListWorkItemsOptions response io.WriterTo wantQuerySubstr []string want []*WorkItem wantErr error }{ { name: "successful query with authorUsername", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [ { "id": "gid://gitlab/WorkItem/181297786", "iid": "39", "title": "Phase 6: Rollout to Additional Services" }, { "id": "gid://gitlab/WorkItem/181297779", "iid": "38", "title": "Phase 5: Dedicated Integration" } ] } } }, "correlationId": "9c88d56b0061dfef-IAD" } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $authorUsername: String)`, `workItems(authorUsername: $authorUsername) {`, }, want: []*WorkItem{ { ID: 181297786, IID: 39, Title: "Phase 6: Rollout to Additional Services", }, { ID: 181297779, IID: 38, Title: "Phase 5: Dedicated Integration", }, }, }, { name: "successful response with work item", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ State: Ptr("opened"), AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [ { "id": "gid://gitlab/WorkItem/181297786", "iid": "39", "title": "Phase 6: Rollout to Additional Services" } ] } } }, "correlationId": "9c88d56b0061dfef-IAD" } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $state: IssuableState, $authorUsername: String)`, `workItems(state: $state, authorUsername: $authorUsername) {`, }, want: []*WorkItem{ { ID: 181297786, IID: 39, Title: "Phase 6: Rollout to Additional Services", }, }, }, { name: "empty response is not an error", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ State: Ptr("opened"), AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [] } } } } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $state: IssuableState, $authorUsername: String)`, `workItems(state: $state, authorUsername: $authorUsername) {`, }, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mux, client := setup(t) mux.HandleFunc("/api/graphql", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() testMethod(t, r, http.MethodPost) var q GraphQLQuery if err := json.NewDecoder(r.Body).Decode(&q); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } for _, ss := range tt.wantQuerySubstr { if !strings.Contains(q.Query, ss) { http.Error(w, fmt.Sprintf("want substring %q, got query %q", ss, q.Query), http.StatusBadRequest) return } } w.Header().Set("Content-Type", "application/json") tt.response.WriteTo(w) }) got, _, err := client.WorkItems.ListWorkItems(tt.fullPath, tt.opt) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) assert.Nil(t, got) return } require.NoError(t, err) assert.Equal(t, tt.want, got) }) } } Loading
workitems.go +152 −0 Original line number Diff line number Diff line Loading @@ -6,6 +6,8 @@ import ( "fmt" "regexp" "strconv" "strings" "text/template" "time" ) Loading @@ -13,6 +15,7 @@ type ( WorkItemsServiceInterface interface { GetWorkItemByID(gid any, options ...RequestOptionFunc) (*WorkItem, *Response, error) GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -207,6 +210,155 @@ query ($fullPath: ID!, $iid: String) { return wiQL.unwrap(), resp, nil } /* workItems( search: String in: [IssuableSearchableField!] ids: [WorkItemID!] authorUsername: String confidential: Boolean assigneeUsernames: [String!] assigneeWildcardId: AssigneeWildcardId labelName: [String!] milestoneTitle: [String!] milestoneWildcardId: MilestoneWildcardId myReactionEmoji: String iids: [String!] state: IssuableState types: [IssueType!] createdBefore: Time createdAfter: Time updatedBefore: Time updatedAfter: Time dueBefore: Time dueAfter: Time closedBefore: Time closedAfter: Time subscribed: SubscriptionStatus not: NegatedWorkItemFilterInput or: UnionedWorkItemFilterInput parentIds: [WorkItemID!] releaseTag: [String!] releaseTagWildcardId: ReleaseTagWildcardId crmContactId: String crmOrganizationId: String iid: String sort: WorkItemSort = CREATED_DESC verificationStatusWidget: VerificationStatusFilterInput healthStatusFilter: HealthStatusFilter weight: String weightWildcardId: WeightWildcardId iterationId: [ID] iterationWildcardId: IterationWildcardId iterationCadenceId: [IterationsCadenceID!] includeAncestors: Boolean = false includeDescendants: Boolean = false timeframe: Timeframe after: String before: String first: Int last: Int ): WorkItemConnection */ // ListWorkItemsOptions represents the available ListWorkItems() options. // // GitLab API docs: // https://docs.gitlab.com/ee/api/graphql/reference/#queryworkitems type ListWorkItemsOptions struct { State *string AuthorUsername *string } var workItemFieldTypes = map[string]string{ "state": "IssuableState", "authorUsername": "String", } var listWorkItemsTemplate = template.Must(template.New("ListWorkItems").Parse(` query ListWorkItems($fullPath: ID!{{ range .Fields }}, ${{ .Name }}: {{ .Type }}{{ end }}) { namespace(fullPath: $fullPath) { workItems({{ range $i, $f := .Fields }}{{ if ne $i 0 }}, {{ end }}{{ $f.Name }}: ${{ $f.Name }}{{ end }}) { nodes { id iid title } } } } `)) // ListWorkItems lists workitems in a given namespace (group or project). func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) { type fieldGQL struct { Name string Type string } var ( queryFields []fieldGQL queryVariables = map[string]any{ "fullPath": fullPath, } ) if opt != nil { if opt.State != nil { queryFields = append(queryFields, fieldGQL{"state", workItemFieldTypes["state"]}) queryVariables["state"] = opt.State } if opt.AuthorUsername != nil { queryFields = append(queryFields, fieldGQL{"authorUsername", workItemFieldTypes["authorUsername"]}) queryVariables["authorUsername"] = opt.AuthorUsername } } var queryBuilder strings.Builder if err := listWorkItemsTemplate.Execute(&queryBuilder, map[string]any{"Fields": queryFields}); err != nil { return nil, nil, err } query := GraphQLQuery{ Query: queryBuilder.String(), Variables: queryVariables, } var result struct { Data struct { Namespace struct { WorkItems struct { Nodes []workItemGQL `json:"nodes"` } `json:"workItems"` } `json:"namespace"` } GenericGraphQLErrors } resp, err := s.client.GraphQL.Do(query, &result, options...) if err != nil { return nil, resp, err } if len(result.Errors) != 0 { return nil, resp, &GraphQLResponseError{ Err: errors.New("GraphQL query failed"), Errors: result.GenericGraphQLErrors, } } var ret []*WorkItem for _, wi := range result.Data.Namespace.WorkItems.Nodes { ret = append(ret, wi.unwrap()) } return ret, resp, nil } // workItemGQL represents the JSON structure returned by the GraphQL query. // It is used to parse the response and convert it to the more user-friendly WorkItem type. type workItemGQL struct { Loading
workitems_test.go +166 −0 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ package gitlab import ( "bytes" _ "embed" "encoding/json" "fmt" "io" "net/http" "strings" Loading Loading @@ -279,3 +281,167 @@ func TestGetWorkItemByID(t *testing.T) { }) } } func TestListWorkItems(t *testing.T) { t.Parallel() tests := []struct { name string fullPath string opt *ListWorkItemsOptions response io.WriterTo wantQuerySubstr []string want []*WorkItem wantErr error }{ { name: "successful query with authorUsername", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [ { "id": "gid://gitlab/WorkItem/181297786", "iid": "39", "title": "Phase 6: Rollout to Additional Services" }, { "id": "gid://gitlab/WorkItem/181297779", "iid": "38", "title": "Phase 5: Dedicated Integration" } ] } } }, "correlationId": "9c88d56b0061dfef-IAD" } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $authorUsername: String)`, `workItems(authorUsername: $authorUsername) {`, }, want: []*WorkItem{ { ID: 181297786, IID: 39, Title: "Phase 6: Rollout to Additional Services", }, { ID: 181297779, IID: 38, Title: "Phase 5: Dedicated Integration", }, }, }, { name: "successful response with work item", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ State: Ptr("opened"), AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [ { "id": "gid://gitlab/WorkItem/181297786", "iid": "39", "title": "Phase 6: Rollout to Additional Services" } ] } } }, "correlationId": "9c88d56b0061dfef-IAD" } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $state: IssuableState, $authorUsername: String)`, `workItems(state: $state, authorUsername: $authorUsername) {`, }, want: []*WorkItem{ { ID: 181297786, IID: 39, Title: "Phase 6: Rollout to Additional Services", }, }, }, { name: "empty response is not an error", fullPath: "gitlab-com/gl-infra/platform/runway/team", opt: &ListWorkItemsOptions{ State: Ptr("opened"), AuthorUsername: Ptr("fforster"), }, response: strings.NewReader(` { "data": { "namespace": { "workItems": { "nodes": [] } } } } `), wantQuerySubstr: []string{ `query ListWorkItems($fullPath: ID!, $state: IssuableState, $authorUsername: String)`, `workItems(state: $state, authorUsername: $authorUsername) {`, }, want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mux, client := setup(t) mux.HandleFunc("/api/graphql", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() testMethod(t, r, http.MethodPost) var q GraphQLQuery if err := json.NewDecoder(r.Body).Decode(&q); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } for _, ss := range tt.wantQuerySubstr { if !strings.Contains(q.Query, ss) { http.Error(w, fmt.Sprintf("want substring %q, got query %q", ss, q.Query), http.StatusBadRequest) return } } w.Header().Set("Content-Type", "application/json") tt.response.WriteTo(w) }) got, _, err := client.WorkItems.ListWorkItems(tt.fullPath, tt.opt) if tt.wantErr != nil { require.ErrorIs(t, err, tt.wantErr) assert.Nil(t, got) return } require.NoError(t, err) assert.Equal(t, tt.want, got) }) } }