Merge pull request #13316 from maxbeizer/discussion-create
feat(discussion/client): implement Create mutation with tests
This commit is contained in:
commit
41f74c4571
3 changed files with 808 additions and 6 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue