diff --git a/pkg/cmd/discussion/client/client_impl.go b/pkg/cmd/discussion/client/client_impl.go index 88aa7f170..78108a233 100644 --- a/pkg/cmd/discussion/client/client_impl.go +++ b/pkg/cmd/discussion/client/client_impl.go @@ -775,8 +775,188 @@ func (c *discussionClient) ListCategories(repo ghrepo.Interface) ([]DiscussionCa return categories, nil } -func (c *discussionClient) Create(_ ghrepo.Interface, _ CreateDiscussionInput) (*Discussion, error) { - return nil, fmt.Errorf("not implemented") +// repositoryMeta holds the node ID and feature flags fetched for a repository. +type repositoryMeta struct { + ID string + HasDiscussionsEnabled bool +} + +// getRepositoryMeta fetches the node ID and discussion-enabled flag for a repository. +func (c *discussionClient) getRepositoryMeta(repo ghrepo.Interface) (*repositoryMeta, error) { + var query struct { + Repository struct { + ID string + HasDiscussionsEnabled bool + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + if err := c.gql.Query(repo.RepoHost(), "RepositoryMeta", &query, variables); err != nil { + return nil, err + } + + return &repositoryMeta{ + ID: query.Repository.ID, + HasDiscussionsEnabled: query.Repository.HasDiscussionsEnabled, + }, nil +} + +// resolveLabels fetches all labels for a repository and matches the requested names +// case-insensitively. Returns an error if any requested label name is not found. +func (c *discussionClient) resolveLabels(repo ghrepo.Interface, labelNames []string) ([]DiscussionLabel, error) { + if len(labelNames) == 0 { + return nil, nil + } + + var query struct { + Repository struct { + Labels struct { + Nodes []struct { + ID string + Name string + Color string + } + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"labels(first: 100, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + } + + wanted := make(map[string]bool, len(labelNames)) + for _, n := range labelNames { + wanted[strings.ToLower(n)] = true + } + + found := make(map[string]DiscussionLabel, len(labelNames)) + for { + if err := c.gql.Query(repo.RepoHost(), "RepositoryLabels", &query, variables); err != nil { + return nil, err + } + for _, n := range query.Repository.Labels.Nodes { + if wanted[strings.ToLower(n.Name)] { + found[strings.ToLower(n.Name)] = DiscussionLabel{ID: n.ID, Name: n.Name, Color: n.Color} + } + } + if len(found) == len(wanted) { + break + } + if !query.Repository.Labels.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.Labels.PageInfo.EndCursor) + } + + if len(found) != len(wanted) { + var missing []string + for _, name := range labelNames { + if _, ok := found[strings.ToLower(name)]; !ok { + missing = append(missing, name) + } + } + return nil, fmt.Errorf("labels not found: %s", strings.Join(missing, ", ")) + } + + result := make([]DiscussionLabel, 0, len(labelNames)) + for _, name := range labelNames { + result = append(result, found[strings.ToLower(name)]) + } + return result, nil +} + +// addLabelsToDiscussion applies labels to a discussion via the addLabelsToLabelable mutation. +func (c *discussionClient) addLabelsToDiscussion(repo ghrepo.Interface, discussionID string, labelIDs []string) error { + ids := make([]githubv4.ID, len(labelIDs)) + for i, id := range labelIDs { + ids[i] = githubv4.ID(id) + } + + var mutation struct { + AddLabelsToLabelable struct { + Typename string `graphql:"__typename"` + } `graphql:"addLabelsToLabelable(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.AddLabelsToLabelableInput{ + LabelableID: githubv4.ID(discussionID), + LabelIDs: ids, + }, + } + + return c.gql.Mutate(repo.RepoHost(), "AddLabelsToDiscussion", &mutation, variables) +} + +func (c *discussionClient) Create(repo ghrepo.Interface, input CreateDiscussionInput) (*Discussion, error) { + meta, err := c.getRepositoryMeta(repo) + if err != nil { + return nil, err + } + if !meta.HasDiscussionsEnabled { + return nil, fmt.Errorf("the '%s/%s' repository has discussions disabled", repo.RepoOwner(), repo.RepoName()) + } + + var mutation struct { + CreateDiscussion struct { + Discussion struct { + discussionListNode + Comments struct { + TotalCount int + } + } + } `graphql:"createDiscussion(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.CreateDiscussionInput{ + RepositoryID: githubv4.ID(meta.ID), + CategoryID: githubv4.ID(input.CategoryID), + Title: githubv4.String(input.Title), + Body: githubv4.String(input.Body), + }, + } + + if err := c.gql.Mutate(repo.RepoHost(), "CreateDiscussion", &mutation, variables); err != nil { + return nil, err + } + + d := mapDiscussionFromListNode(mutation.CreateDiscussion.Discussion.discussionListNode) + d.Comments = DiscussionCommentList{TotalCount: mutation.CreateDiscussion.Discussion.Comments.TotalCount} + + for _, rg := range mutation.CreateDiscussion.Discussion.ReactionGroups { + d.ReactionGroups = append(d.ReactionGroups, ReactionGroup{ + Content: rg.Content, + TotalCount: rg.Users.TotalCount, + }) + } + + if len(input.Labels) > 0 { + resolvedLabels, err := c.resolveLabels(repo, input.Labels) + if err != nil { + return nil, err + } + labelIDs := make([]string, len(resolvedLabels)) + for i, l := range resolvedLabels { + labelIDs[i] = l.ID + } + if err := c.addLabelsToDiscussion(repo, d.ID, labelIDs); err != nil { + return nil, err + } + d.Labels = resolvedLabels + } + + return &d, nil } func (c *discussionClient) Update(_ ghrepo.Interface, _ UpdateDiscussionInput) (*Discussion, error) { diff --git a/pkg/cmd/discussion/client/client_impl_test.go b/pkg/cmd/discussion/client/client_impl_test.go index 16c0813a2..8ca5f52b2 100644 --- a/pkg/cmd/discussion/client/client_impl_test.go +++ b/pkg/cmd/discussion/client/client_impl_test.go @@ -2475,3 +2475,625 @@ func TestGetCommentReplies(t *testing.T) { }) } } + +func repoMetaResp(id string, discussionsEnabled bool) string { + return fmt.Sprintf(`{ + "data": { + "repository": { + "id": %q, + "hasDiscussionsEnabled": %t + } + } + }`, id, discussionsEnabled) +} + +func TestCreate(t *testing.T) { + repo := ghrepo.New("OWNER", "REPO") + + tests := []struct { + name string + input CreateDiscussionInput + httpStubs func(*testing.T, *httpmock.Registry) + wantErr string + assertDisc *Discussion + }{ + { + name: "maps all fields", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "New Discussion", + Body: "Discussion body", + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQLMutationMatcher(`mutation CreateDiscussion\b`, func(input map[string]interface{}) bool { + assert.Equal(t, "R_1", input["repositoryId"]) + assert.Equal(t, "CAT_1", input["categoryId"]) + assert.Equal(t, "New Discussion", input["title"]) + assert.Equal(t, "Discussion body", input["body"]) + return true + }), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "New Discussion", + "body": "Discussion body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 0}}], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + }, + assertDisc: &Discussion{ + ID: "D_new", + Number: 99, + Title: "New Discussion", + Body: "Discussion body", + URL: "https://github.com/OWNER/REPO/discussions/99", + Author: DiscussionActor{ID: "U1", Login: "alice", Name: "Alice"}, + Category: DiscussionCategory{ + ID: "CAT_1", + Name: "General", + Slug: "general", + Emoji: ":speech_balloon:", + }, + Labels: []DiscussionLabel{}, + ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 0}}, + CreatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "discussions disabled", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "Test", + Body: "Body", + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", false)), + ) + }, + wantErr: "has discussions disabled", + }, + { + name: "repo not found", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "Test", + Body: "Body", + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": null + }, + "errors": [ + { + "type": "NOT_FOUND", + "path": ["repository"], + "message": "Could not resolve to a Repository with the name 'OWNER/REPO'." + } + ] + } + `)), + ) + }, + wantErr: "Could not resolve to a Repository with the name 'OWNER/REPO'.", + }, + { + name: "mutation error", + input: CreateDiscussionInput{ + CategoryID: "BAD_CAT", + Title: "Test", + Body: "Body", + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": null + }, + "errors": [ + { + "type": "NOT_FOUND", + "message": "Could not resolve to a node with the global id of 'BAD_CAT'." + } + ] + } + `)), + ) + }, + wantErr: "Could not resolve to a node with the global id of 'BAD_CAT'.", + }, + { + name: "paginates labels across multiple pages", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "New Discussion", + Body: "Discussion body", + Labels: []string{"bug", "enhancement"}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "New Discussion", + "body": "Discussion body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [ + {"id": "L_bug", "name": "bug", "color": "d73a4a"} + ], + "pageInfo": {"hasNextPage": true, "endCursor": "LABEL_CUR_1"} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [ + {"id": "L_enh", "name": "enhancement", "color": "a2eeef"} + ], + "pageInfo": {"hasNextPage": false, "endCursor": ""} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`mutation AddLabelsToDiscussion\b`), + httpmock.StringResponse(`{"data":{"addLabelsToLabelable":{"__typename":"Discussion"}}}`), + ) + }, + assertDisc: &Discussion{ + ID: "D_new", + Number: 99, + Title: "New Discussion", + Body: "Discussion body", + URL: "https://github.com/OWNER/REPO/discussions/99", + Author: DiscussionActor{ID: "U1", Login: "alice", Name: "Alice"}, + Category: DiscussionCategory{ + ID: "CAT_1", + Name: "General", + Slug: "general", + Emoji: ":speech_balloon:", + }, + Labels: []DiscussionLabel{ + {ID: "L_bug", Name: "bug", Color: "d73a4a"}, + {ID: "L_enh", Name: "enhancement", Color: "a2eeef"}, + }, + CreatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "stops paginating labels when all found", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "New Discussion", + Body: "Discussion body", + Labels: []string{"bug", "enhancement"}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "New Discussion", + "body": "Discussion body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + // Register a single page that returns both labels but claims more pages exist. + // The code should stop paginating once all wanted labels are found. + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [ + {"id": "L_bug", "name": "bug", "color": "d73a4a"}, + {"id": "L_enh", "name": "enhancement", "color": "a2eeef"} + ], + "pageInfo": {"hasNextPage": true, "endCursor": "LABEL_CUR_999"} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`mutation AddLabelsToDiscussion\b`), + httpmock.StringResponse(`{"data":{"addLabelsToLabelable":{"__typename":"Discussion"}}}`), + ) + }, + assertDisc: &Discussion{ + ID: "D_new", + Number: 99, + Title: "New Discussion", + Body: "Discussion body", + URL: "https://github.com/OWNER/REPO/discussions/99", + Author: DiscussionActor{ID: "U1", Login: "alice", Name: "Alice"}, + Category: DiscussionCategory{ + ID: "CAT_1", + Name: "General", + Slug: "general", + Emoji: ":speech_balloon:", + }, + Labels: []DiscussionLabel{ + {ID: "L_bug", Name: "bug", Color: "d73a4a"}, + {ID: "L_enh", Name: "enhancement", Color: "a2eeef"}, + }, + CreatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "creates discussion with labels", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "New Discussion", + Body: "Discussion body", + Labels: []string{"bug", "enhancement"}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "New Discussion", + "body": "Discussion body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 0}}], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [ + {"id": "L_bug", "name": "bug", "color": "d73a4a"}, + {"id": "L_enh", "name": "enhancement", "color": "a2eeef"} + ], + "pageInfo": {"hasNextPage": false, "endCursor": ""} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQLMutationMatcher(`mutation AddLabelsToDiscussion\b`, func(input map[string]interface{}) bool { + assert.Equal(t, "D_new", input["labelableId"]) + labelIDs, ok := input["labelIds"].([]interface{}) + assert.True(t, ok) + assert.Equal(t, []interface{}{"L_bug", "L_enh"}, labelIDs) + return true + }), + httpmock.StringResponse(`{"data":{"addLabelsToLabelable":{"__typename":"Discussion"}}}`), + ) + }, + assertDisc: &Discussion{ + ID: "D_new", + Number: 99, + Title: "New Discussion", + Body: "Discussion body", + URL: "https://github.com/OWNER/REPO/discussions/99", + Author: DiscussionActor{ID: "U1", Login: "alice", Name: "Alice"}, + Category: DiscussionCategory{ + ID: "CAT_1", + Name: "General", + Slug: "general", + Emoji: ":speech_balloon:", + }, + Labels: []DiscussionLabel{ + {ID: "L_bug", Name: "bug", Color: "d73a4a"}, + {ID: "L_enh", Name: "enhancement", Color: "a2eeef"}, + }, + ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 0}}, + CreatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "label not found returns error", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "Test", + Body: "Body", + Labels: []string{"nonexistent", "also-missing"}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "Test", + "body": "Body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [], + "pageInfo": {"hasNextPage": false, "endCursor": ""} + } + } + } + } + `)), + ) + }, + wantErr: `labels not found: nonexistent, also-missing`, + }, + { + name: "add labels mutation failure returns error", + input: CreateDiscussionInput{ + CategoryID: "CAT_1", + Title: "Test", + Body: "Body", + Labels: []string{"bug"}, + }, + httpStubs: func(t *testing.T, reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMeta\b`), + httpmock.StringResponse(repoMetaResp("R_1", true)), + ) + reg.Register( + httpmock.GraphQL(`mutation CreateDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "createDiscussion": { + "discussion": { + "id": "D_new", + "number": 99, + "title": "Test", + "body": "Body", + "url": "https://github.com/OWNER/REPO/discussions/99", + "closed": false, + "stateReason": "", + "isAnswered": false, + "answerChosenAt": "0001-01-01T00:00:00Z", + "author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"}, + "category": {"id": "CAT_1", "name": "General", "slug": "general", "emoji": ":speech_balloon:", "isAnswerable": false}, + "answerChosenBy": null, + "labels": {"nodes": []}, + "reactionGroups": [], + "createdAt": "2025-06-01T00:00:00Z", + "updatedAt": "2025-06-01T00:00:00Z", + "closedAt": "0001-01-01T00:00:00Z", + "locked": false, + "comments": {"totalCount": 0} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`query RepositoryLabels\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": { + "repository": { + "labels": { + "nodes": [ + {"id": "L_bug", "name": "bug", "color": "d73a4a"} + ], + "pageInfo": {"hasNextPage": false, "endCursor": ""} + } + } + } + } + `)), + ) + reg.Register( + httpmock.GraphQL(`mutation AddLabelsToDiscussion\b`), + httpmock.StringResponse(heredoc.Doc(` + { + "data": null, + "errors": [{"message": "could not apply labels"}] + } + `)), + ) + }, + wantErr: "could not apply labels", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reg := &httpmock.Registry{} + defer reg.Verify(t) + + if tt.httpStubs != nil { + tt.httpStubs(t, reg) + } + + c := newTestDiscussionClient(reg) + d, err := c.Create(repo, tt.input) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + require.NoError(t, err) + require.NotNil(t, d) + require.NotNil(t, tt.assertDisc, "assertDisc must be set for non-error cases") + assert.Equal(t, tt.assertDisc, d) + }) + } +} diff --git a/pkg/cmd/discussion/client/types.go b/pkg/cmd/discussion/client/types.go index 1eaab5329..d725ee92c 100644 --- a/pkg/cmd/discussion/client/types.go +++ b/pkg/cmd/discussion/client/types.go @@ -327,10 +327,10 @@ type SearchFilters struct { // CreateDiscussionInput holds the parameters for creating a discussion. type CreateDiscussionInput struct { - RepositoryID string - CategoryID string - Title string - Body string + CategoryID string + Title string + Body string + Labels []string } // UpdateDiscussionInput holds optional parameters for updating a discussion.