Loading gitlab_test/utils_test.go +0 −14 Original line number Diff line number Diff line Loading @@ -275,17 +275,3 @@ func CreateTestEpicWithOptions(t *testing.T, client *gitlab.Client, gid any, opt return epic, nil } // CreateTestWorkItemWithOptions creates a work item with the provided options. func CreateTestWorkItem(t *testing.T, client *gitlab.Client, fullPath string, workItemTypeID gitlab.WorkItemTypeID, opts *gitlab.CreateWorkItemOptions) (*gitlab.WorkItem, error) { t.Helper() wi, _, err := client.WorkItems.CreateWorkItem(fullPath, workItemTypeID, opts, gitlab.WithContext(t.Context())) if err != nil { return nil, err } // TODO: install a t.Cleanup() function once DeleteWorkItem has been implemented. return wi, nil } gitlab_test/workitems_integration_test.go +22 −2 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ package gitlab_test import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" Loading @@ -10,7 +12,7 @@ import ( gitlab "gitlab.com/gitlab-org/api/client-go/v2" ) func TestCreateGetUpdateWorkItem(t *testing.T) { func TestWorkItemLifeCycle(t *testing.T) { t.Parallel() client := SetupIntegrationClient(t) Loading @@ -28,10 +30,19 @@ func TestCreateGetUpdateWorkItem(t *testing.T) { Color: gitlab.Ptr("green"), } createdWI, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) createdWI, _, err := client.WorkItems.CreateWorkItem(group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) require.NoError(t, err, "CreateWorkItem failed") require.NotNil(t, createdWI) // clean up in case test fails too early t.Cleanup(func() { _, err := client.WorkItems.DeleteWorkItem(group.FullPath, createdWI.IID, gitlab.WithContext(context.Background())) if err != nil && errors.Is(err, gitlab.ErrNotFound) { return } require.NoError(t, err, "Failed to delete test work item in cleanup") }) // THEN the work item should have the provided fields set correctly assert.Equal(t, "Integration Test Work Item", createdWI.Title, "Field: Title") assert.Equal(t, "Initial description", createdWI.Description, "Field: Description") Loading Loading @@ -80,6 +91,15 @@ func TestCreateGetUpdateWorkItem(t *testing.T) { assert.Equal(t, updatedWI.Description, finalWI.Description, "Field: Description") assert.Equal(t, deref(t, updatedWI.HealthStatus), deref(t, finalWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, updatedWI.Color), deref(t, finalWI.Color), "Field: Color") // STEP 5: Delete the work item // WHEN deleting the work item _, err = client.WorkItems.DeleteWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "DeleteWorkItem failed") // THEN the work item should no longer be retrievable _, _, err = client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.ErrorIs(t, err, gitlab.ErrNotFound) } func deref(t *testing.T, ptr *string) string { Loading testing/workitems_mock.go +44 −0 Original line number Diff line number Diff line Loading @@ -84,6 +84,50 @@ func (c *MockWorkItemsServiceInterfaceCreateWorkItemCall) DoAndReturn(f func(str return c } // DeleteWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) DeleteWorkItem(fullPath string, iid int64, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { m.ctrl.T.Helper() varargs := []any{fullPath, iid} for _, a := range options { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DeleteWorkItem", varargs...) ret0, _ := ret[0].(*gitlab.Response) ret1, _ := ret[1].(error) return ret0, ret1 } // DeleteWorkItem indicates an expected call of DeleteWorkItem. func (mr *MockWorkItemsServiceInterfaceMockRecorder) DeleteWorkItem(fullPath, iid any, options ...any) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { mr.mock.ctrl.T.Helper() varargs := append([]any{fullPath, iid}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkItem", reflect.TypeOf((*MockWorkItemsServiceInterface)(nil).DeleteWorkItem), varargs...) return &MockWorkItemsServiceInterfaceDeleteWorkItemCall{Call: call} } // MockWorkItemsServiceInterfaceDeleteWorkItemCall wrap *gomock.Call type MockWorkItemsServiceInterfaceDeleteWorkItemCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) Return(arg0 *gitlab.Response, arg1 error) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) Do(f func(string, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) DoAndReturn(f func(string, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.DoAndReturn(f) return c } // GetWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) GetWorkItem(fullPath string, iid int64, options ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error) { m.ctrl.T.Helper() Loading workitems.go +73 −16 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ type ( GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -204,6 +205,26 @@ var getWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()). } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` deleteWorkItemQuery = ` mutation DeleteWorkItem($id: WorkItemID!) { workItemDelete(input: { id: $id }) { errors } } ` ) // GetWorkItem gets a single work item. // // fullPath is the full path to either a group or project. Loading Loading @@ -810,18 +831,6 @@ var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone( } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` ) // createWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(` Loading Loading @@ -1129,6 +1138,54 @@ type workItemUpdateInputGQL struct { StatusWidget *workItemWidgetStatusInputGQL `json:"statusWidget,omitempty"` } // DeleteWorkItem deletes a single work item. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemdelete func (s *WorkItemsService) DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error) { // get the global ID gid, resp, err := s.workItemGID(fullPath, iid, options...) if err != nil { if errors.Is(err, ErrEmptyResponse) { return resp, ErrNotFound } return resp, err } mutation := GraphQLQuery{ Query: deleteWorkItemQuery, Variables: map[string]any{ "id": gid.String(), }, } var result struct { Data struct { WorkItemDelete struct { Errors []string `json:"errors"` } `json:"workItemDelete"` } GenericGraphQLErrors } resp, err = s.client.GraphQL.Do(mutation, &result, options...) if err != nil { return resp, err } if len(result.Errors) != 0 { return resp, &GraphQLResponseError{ Err: errors.New("Mutation.workItemDelete failed"), Errors: result.GenericGraphQLErrors, } } if len(result.Data.WorkItemDelete.Errors) != 0 { return resp, errors.New(strings.Join(result.Data.WorkItemDelete.Errors, ", ")) } return resp, nil } // workItemGID queries for a work item's Global ID using fullPath and iid. // Returns the Global ID string or ErrNotFound if the work item doesn't exist. // Loading @@ -1148,8 +1205,8 @@ func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...Re WorkItem struct { ID gidGQL `json:"id"` } `json:"workItem"` } } } `json:"namespace"` } `json:"data"` GenericGraphQLErrors } Loading workitems_test.go +145 −3 Original line number Diff line number Diff line Loading @@ -1250,6 +1250,147 @@ func TestUpdateWorkItem(t *testing.T) { } } func TestDeleteWorkItem(t *testing.T) { t.Parallel() tests := []struct { name string fullPath string iid int64 getIDResponse io.WriterTo deleteResponse io.WriterTo wantErr error }{ { name: "successfully deletes work item", fullPath: "test-gitlab-org/gitlab", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": { "id": "gid://gitlab/WorkItem/183771442" } } } }`), deleteResponse: strings.NewReader(`{ "data": { "workItemDelete": { "clientMutationId": null, "errors": [], "namespace": { "id": "gid://gitlab/Namespaces::ProjectNamespace/124736349" } } } }`), wantErr: nil, }, { name: "work item not found returns error", fullPath: "test-gitlab-org/gitlab", iid: 999, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": null } } }`), deleteResponse: nil, wantErr: ErrNotFound, }, { name: "namespace not found returns error", fullPath: "does/not/exist", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": null } }`), deleteResponse: nil, wantErr: ErrNotFound, }, { name: "delete mutation returns errors", fullPath: "test-gitlab-org/gitlab", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": { "id": "gid://gitlab/WorkItem/123" } } } }`), deleteResponse: strings.NewReader(`{ "data": { "workItemDelete": { "clientMutationId": null, "errors": ["Work item cannot be deleted"], "namespace": null } } }`), wantErr: errors.New("Work item cannot be deleted"), }, } schema := loadSchema(t) 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 } if err := validateSchema(schema, q); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") switch { case strings.Contains(q.Query, "GetWorkItemID"): tt.getIDResponse.WriteTo(w) case strings.Contains(q.Query, "DeleteWorkItem"): if tt.deleteResponse == nil { t.Errorf("unexpected DeleteWorkItem request: deleteResponse is nil") http.Error(w, "unexpected request", http.StatusInternalServerError) return } tt.deleteResponse.WriteTo(w) default: t.Errorf("unexpected query: %s", q.Query) http.Error(w, "unexpected query", http.StatusBadRequest) } }) _, err := client.WorkItems.DeleteWorkItem(tt.fullPath, tt.iid) if tt.wantErr != nil { require.ErrorContains(t, err, tt.wantErr.Error()) return } require.NoError(t, err) }) } } func loadSchema(t *testing.T) *graphql.Schema { t.Helper() Loading @@ -1258,8 +1399,9 @@ func loadSchema(t *testing.T) *graphql.Schema { fh, err := os.Open(filename) switch { case errors.Is(err, os.ErrNotExist): t.Logf("GraphQL schema file %q is not available", filename) return nil t.Skipf("GraphQL schema file %q is not available; generate it with: "+ "npm install -g get-graphql-schema && "+ "get-graphql-schema https://gitlab.com/api/graphql --sdl > schema/gitlab.graphql", filename) case err != nil: t.Fatalf("opening schema failed: %v", err) Loading @@ -1272,7 +1414,7 @@ func loadSchema(t *testing.T) *graphql.Schema { schema, err := graphql.ParseSchema(string(data), nil) if err != nil { t.Fatalf("parsing schema failed: %v", err) t.Fatalf("parsing schema %q failed (schema file may be corrupt): %v", filename, err) } return schema Loading Loading
gitlab_test/utils_test.go +0 −14 Original line number Diff line number Diff line Loading @@ -275,17 +275,3 @@ func CreateTestEpicWithOptions(t *testing.T, client *gitlab.Client, gid any, opt return epic, nil } // CreateTestWorkItemWithOptions creates a work item with the provided options. func CreateTestWorkItem(t *testing.T, client *gitlab.Client, fullPath string, workItemTypeID gitlab.WorkItemTypeID, opts *gitlab.CreateWorkItemOptions) (*gitlab.WorkItem, error) { t.Helper() wi, _, err := client.WorkItems.CreateWorkItem(fullPath, workItemTypeID, opts, gitlab.WithContext(t.Context())) if err != nil { return nil, err } // TODO: install a t.Cleanup() function once DeleteWorkItem has been implemented. return wi, nil }
gitlab_test/workitems_integration_test.go +22 −2 Original line number Diff line number Diff line Loading @@ -3,6 +3,8 @@ package gitlab_test import ( "context" "errors" "testing" "github.com/stretchr/testify/assert" Loading @@ -10,7 +12,7 @@ import ( gitlab "gitlab.com/gitlab-org/api/client-go/v2" ) func TestCreateGetUpdateWorkItem(t *testing.T) { func TestWorkItemLifeCycle(t *testing.T) { t.Parallel() client := SetupIntegrationClient(t) Loading @@ -28,10 +30,19 @@ func TestCreateGetUpdateWorkItem(t *testing.T) { Color: gitlab.Ptr("green"), } createdWI, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) createdWI, _, err := client.WorkItems.CreateWorkItem(group.FullPath, gitlab.WorkItemTypeEpic, &createOpt) require.NoError(t, err, "CreateWorkItem failed") require.NotNil(t, createdWI) // clean up in case test fails too early t.Cleanup(func() { _, err := client.WorkItems.DeleteWorkItem(group.FullPath, createdWI.IID, gitlab.WithContext(context.Background())) if err != nil && errors.Is(err, gitlab.ErrNotFound) { return } require.NoError(t, err, "Failed to delete test work item in cleanup") }) // THEN the work item should have the provided fields set correctly assert.Equal(t, "Integration Test Work Item", createdWI.Title, "Field: Title") assert.Equal(t, "Initial description", createdWI.Description, "Field: Description") Loading Loading @@ -80,6 +91,15 @@ func TestCreateGetUpdateWorkItem(t *testing.T) { assert.Equal(t, updatedWI.Description, finalWI.Description, "Field: Description") assert.Equal(t, deref(t, updatedWI.HealthStatus), deref(t, finalWI.HealthStatus), "Field: HealthStatus") assert.Equal(t, deref(t, updatedWI.Color), deref(t, finalWI.Color), "Field: Color") // STEP 5: Delete the work item // WHEN deleting the work item _, err = client.WorkItems.DeleteWorkItem(group.FullPath, createdWI.IID) require.NoError(t, err, "DeleteWorkItem failed") // THEN the work item should no longer be retrievable _, _, err = client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID) require.ErrorIs(t, err, gitlab.ErrNotFound) } func deref(t *testing.T, ptr *string) string { Loading
testing/workitems_mock.go +44 −0 Original line number Diff line number Diff line Loading @@ -84,6 +84,50 @@ func (c *MockWorkItemsServiceInterfaceCreateWorkItemCall) DoAndReturn(f func(str return c } // DeleteWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) DeleteWorkItem(fullPath string, iid int64, options ...gitlab.RequestOptionFunc) (*gitlab.Response, error) { m.ctrl.T.Helper() varargs := []any{fullPath, iid} for _, a := range options { varargs = append(varargs, a) } ret := m.ctrl.Call(m, "DeleteWorkItem", varargs...) ret0, _ := ret[0].(*gitlab.Response) ret1, _ := ret[1].(error) return ret0, ret1 } // DeleteWorkItem indicates an expected call of DeleteWorkItem. func (mr *MockWorkItemsServiceInterfaceMockRecorder) DeleteWorkItem(fullPath, iid any, options ...any) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { mr.mock.ctrl.T.Helper() varargs := append([]any{fullPath, iid}, options...) call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWorkItem", reflect.TypeOf((*MockWorkItemsServiceInterface)(nil).DeleteWorkItem), varargs...) return &MockWorkItemsServiceInterfaceDeleteWorkItemCall{Call: call} } // MockWorkItemsServiceInterfaceDeleteWorkItemCall wrap *gomock.Call type MockWorkItemsServiceInterfaceDeleteWorkItemCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) Return(arg0 *gitlab.Response, arg1 error) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) Do(f func(string, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn func (c *MockWorkItemsServiceInterfaceDeleteWorkItemCall) DoAndReturn(f func(string, int64, ...gitlab.RequestOptionFunc) (*gitlab.Response, error)) *MockWorkItemsServiceInterfaceDeleteWorkItemCall { c.Call = c.Call.DoAndReturn(f) return c } // GetWorkItem mocks base method. func (m *MockWorkItemsServiceInterface) GetWorkItem(fullPath string, iid int64, options ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error) { m.ctrl.T.Helper() Loading
workitems.go +73 −16 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ type ( GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error) ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error) } // WorkItemsService handles communication with the work item related methods Loading Loading @@ -204,6 +205,26 @@ var getWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()). } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` deleteWorkItemQuery = ` mutation DeleteWorkItem($id: WorkItemID!) { workItemDelete(input: { id: $id }) { errors } } ` ) // GetWorkItem gets a single work item. // // fullPath is the full path to either a group or project. Loading Loading @@ -810,18 +831,6 @@ var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone( } `)) const ( getWorkItemIDQuery = ` query GetWorkItemID($fullPath: ID!, $iid: String!) { namespace(fullPath: $fullPath) { workItem(iid: $iid) { id } } } ` ) // createWorkItemTemplate is chained from workItemTemplate so it has access to both // UserCoreBasic and WorkItem templates. var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(` Loading Loading @@ -1129,6 +1138,54 @@ type workItemUpdateInputGQL struct { StatusWidget *workItemWidgetStatusInputGQL `json:"statusWidget,omitempty"` } // DeleteWorkItem deletes a single work item. // // GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemdelete func (s *WorkItemsService) DeleteWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*Response, error) { // get the global ID gid, resp, err := s.workItemGID(fullPath, iid, options...) if err != nil { if errors.Is(err, ErrEmptyResponse) { return resp, ErrNotFound } return resp, err } mutation := GraphQLQuery{ Query: deleteWorkItemQuery, Variables: map[string]any{ "id": gid.String(), }, } var result struct { Data struct { WorkItemDelete struct { Errors []string `json:"errors"` } `json:"workItemDelete"` } GenericGraphQLErrors } resp, err = s.client.GraphQL.Do(mutation, &result, options...) if err != nil { return resp, err } if len(result.Errors) != 0 { return resp, &GraphQLResponseError{ Err: errors.New("Mutation.workItemDelete failed"), Errors: result.GenericGraphQLErrors, } } if len(result.Data.WorkItemDelete.Errors) != 0 { return resp, errors.New(strings.Join(result.Data.WorkItemDelete.Errors, ", ")) } return resp, nil } // workItemGID queries for a work item's Global ID using fullPath and iid. // Returns the Global ID string or ErrNotFound if the work item doesn't exist. // Loading @@ -1148,8 +1205,8 @@ func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...Re WorkItem struct { ID gidGQL `json:"id"` } `json:"workItem"` } } } `json:"namespace"` } `json:"data"` GenericGraphQLErrors } Loading
workitems_test.go +145 −3 Original line number Diff line number Diff line Loading @@ -1250,6 +1250,147 @@ func TestUpdateWorkItem(t *testing.T) { } } func TestDeleteWorkItem(t *testing.T) { t.Parallel() tests := []struct { name string fullPath string iid int64 getIDResponse io.WriterTo deleteResponse io.WriterTo wantErr error }{ { name: "successfully deletes work item", fullPath: "test-gitlab-org/gitlab", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": { "id": "gid://gitlab/WorkItem/183771442" } } } }`), deleteResponse: strings.NewReader(`{ "data": { "workItemDelete": { "clientMutationId": null, "errors": [], "namespace": { "id": "gid://gitlab/Namespaces::ProjectNamespace/124736349" } } } }`), wantErr: nil, }, { name: "work item not found returns error", fullPath: "test-gitlab-org/gitlab", iid: 999, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": null } } }`), deleteResponse: nil, wantErr: ErrNotFound, }, { name: "namespace not found returns error", fullPath: "does/not/exist", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": null } }`), deleteResponse: nil, wantErr: ErrNotFound, }, { name: "delete mutation returns errors", fullPath: "test-gitlab-org/gitlab", iid: 123, getIDResponse: strings.NewReader(`{ "data": { "namespace": { "workItem": { "id": "gid://gitlab/WorkItem/123" } } } }`), deleteResponse: strings.NewReader(`{ "data": { "workItemDelete": { "clientMutationId": null, "errors": ["Work item cannot be deleted"], "namespace": null } } }`), wantErr: errors.New("Work item cannot be deleted"), }, } schema := loadSchema(t) 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 } if err := validateSchema(schema, q); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") switch { case strings.Contains(q.Query, "GetWorkItemID"): tt.getIDResponse.WriteTo(w) case strings.Contains(q.Query, "DeleteWorkItem"): if tt.deleteResponse == nil { t.Errorf("unexpected DeleteWorkItem request: deleteResponse is nil") http.Error(w, "unexpected request", http.StatusInternalServerError) return } tt.deleteResponse.WriteTo(w) default: t.Errorf("unexpected query: %s", q.Query) http.Error(w, "unexpected query", http.StatusBadRequest) } }) _, err := client.WorkItems.DeleteWorkItem(tt.fullPath, tt.iid) if tt.wantErr != nil { require.ErrorContains(t, err, tt.wantErr.Error()) return } require.NoError(t, err) }) } } func loadSchema(t *testing.T) *graphql.Schema { t.Helper() Loading @@ -1258,8 +1399,9 @@ func loadSchema(t *testing.T) *graphql.Schema { fh, err := os.Open(filename) switch { case errors.Is(err, os.ErrNotExist): t.Logf("GraphQL schema file %q is not available", filename) return nil t.Skipf("GraphQL schema file %q is not available; generate it with: "+ "npm install -g get-graphql-schema && "+ "get-graphql-schema https://gitlab.com/api/graphql --sdl > schema/gitlab.graphql", filename) case err != nil: t.Fatalf("opening schema failed: %v", err) Loading @@ -1272,7 +1414,7 @@ func loadSchema(t *testing.T) *graphql.Schema { schema, err := graphql.ParseSchema(string(data), nil) if err != nil { t.Fatalf("parsing schema failed: %v", err) t.Fatalf("parsing schema %q failed (schema file may be corrupt): %v", filename, err) } return schema Loading