- Add "paginates labels across multiple pages" case (two pages, one label each) - Add "stops paginating labels when all found" case (early break verified via reg.Verify) - Update "creates discussion with labels" to two labels with variable assertions - Update "label not found" to verify all missing labels reported at once Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
3099 lines
91 KiB
Go
3099 lines
91 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/pkg/httpmock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestDiscussionClient(reg *httpmock.Registry) DiscussionClient {
|
|
httpClient := &http.Client{}
|
|
httpmock.ReplaceTripper(httpClient, reg)
|
|
return NewDiscussionClient(httpClient)
|
|
}
|
|
|
|
// minimalNode returns a minimal JSON discussion node with the given id and title.
|
|
func minimalNode(id, title string) string {
|
|
return heredoc.Docf(`
|
|
{
|
|
"id": %q,
|
|
"number": 1,
|
|
"title": %q,
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {
|
|
"__typename": "User",
|
|
"login": "alice"
|
|
},
|
|
"category": {
|
|
"id": "C1",
|
|
"name": "General",
|
|
"slug": "general",
|
|
"emoji": "",
|
|
"isAnswerable": false
|
|
},
|
|
"answerChosenBy": null,
|
|
"labels": {
|
|
"nodes": []
|
|
},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
`, id, title)
|
|
}
|
|
|
|
// minimalNodes returns count comma-separated minimal JSON discussion nodes.
|
|
func minimalNodes(count int) string {
|
|
nodes := make([]string, count)
|
|
for i := range nodes {
|
|
nodes[i] = minimalNode(fmt.Sprintf("D%d", i+1), fmt.Sprintf("Discussion %d", i+1))
|
|
}
|
|
return strings.Join(nodes, ",")
|
|
}
|
|
|
|
// listResp builds a mock repository.discussions JSON response.
|
|
func listResp(hasNext bool, cursor string, total int, nodes string) string {
|
|
return heredoc.Docf(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussions": {
|
|
"totalCount": %d,
|
|
"pageInfo": {
|
|
"hasNextPage": %t,
|
|
"endCursor": %q
|
|
},
|
|
"nodes": [%s]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`, total, hasNext, cursor, nodes)
|
|
}
|
|
|
|
// searchResp builds a mock search JSON response.
|
|
func searchResp(hasNext bool, cursor string, count int, nodes string) string {
|
|
return heredoc.Docf(`
|
|
{
|
|
"data": {
|
|
"search": {
|
|
"discussionCount": %d,
|
|
"pageInfo": {
|
|
"hasNextPage": %t,
|
|
"endCursor": %q
|
|
},
|
|
"nodes": [%s]
|
|
}
|
|
}
|
|
}
|
|
`, count, hasNext, cursor, nodes)
|
|
}
|
|
|
|
func TestList(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
richNode := heredoc.Doc(`
|
|
{
|
|
"id": "D_rich1",
|
|
"number": 42,
|
|
"title": "Rich discussion",
|
|
"body": "body text here",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42",
|
|
"closed": true,
|
|
"stateReason": "RESOLVED",
|
|
"isAnswered": true,
|
|
"answerChosenAt": "2024-06-01T12:00:00Z",
|
|
"author": {
|
|
"__typename": "User",
|
|
"login": "alice",
|
|
"id": "U1",
|
|
"name": "Alice"
|
|
},
|
|
"category": {
|
|
"id": "C1",
|
|
"name": "Q&A",
|
|
"slug": "q-a",
|
|
"emoji": ":question:",
|
|
"isAnswerable": true
|
|
},
|
|
"answerChosenBy": {
|
|
"__typename": "User",
|
|
"login": "bob",
|
|
"id": "U2",
|
|
"name": "Bob"
|
|
},
|
|
"labels": {
|
|
"nodes": [
|
|
{"id": "L1", "name": "bug", "color": "d73a4a"},
|
|
{"id": "L2", "name": "enhancement", "color": "a2eeef"}
|
|
]
|
|
},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-06-02T00:00:00Z",
|
|
"closedAt": "2024-06-01T00:00:00Z",
|
|
"locked": true
|
|
}
|
|
`)
|
|
|
|
emptyResp := listResp(false, "", 0, "")
|
|
disabledResp := heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": false,
|
|
"discussions": {
|
|
"totalCount": 0,
|
|
"pageInfo": {
|
|
"hasNextPage": false,
|
|
"endCursor": null
|
|
},
|
|
"nodes": []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
|
|
tests := []struct {
|
|
name string
|
|
filters ListFilters
|
|
after string
|
|
limit int
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
wantTotal int
|
|
wantLen int
|
|
wantCursor string
|
|
wantTitles []string
|
|
wantSingleDisc *Discussion
|
|
}{
|
|
{
|
|
name: "maps all fields",
|
|
limit: 10,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(false, "", 1, richNode)),
|
|
)
|
|
},
|
|
wantTotal: 1,
|
|
wantLen: 1,
|
|
wantSingleDisc: &Discussion{
|
|
ID: "D_rich1",
|
|
Number: 42,
|
|
Title: "Rich discussion",
|
|
Body: "body text here",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42",
|
|
Closed: true,
|
|
StateReason: "RESOLVED",
|
|
Author: DiscussionActor{
|
|
ID: "U1",
|
|
Login: "alice",
|
|
Name: "Alice",
|
|
},
|
|
Category: DiscussionCategory{
|
|
ID: "C1",
|
|
Name: "Q&A",
|
|
Slug: "q-a",
|
|
Emoji: ":question:",
|
|
IsAnswerable: true,
|
|
},
|
|
Labels: []DiscussionLabel{
|
|
{ID: "L1", Name: "bug", Color: "d73a4a"},
|
|
{ID: "L2", Name: "enhancement", Color: "a2eeef"},
|
|
},
|
|
Answered: true,
|
|
AnswerChosenAt: time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
AnswerChosenBy: &DiscussionActor{
|
|
ID: "U2",
|
|
Login: "bob",
|
|
Name: "Bob",
|
|
},
|
|
Comments: DiscussionCommentList{},
|
|
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2024, 6, 2, 0, 0, 0, 0, time.UTC),
|
|
ClosedAt: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
Locked: true,
|
|
},
|
|
},
|
|
{
|
|
name: "empty list",
|
|
limit: 10,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(emptyResp),
|
|
)
|
|
},
|
|
wantTotal: 0,
|
|
wantLen: 0,
|
|
},
|
|
{
|
|
name: "discussions disabled",
|
|
limit: 10,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(disabledResp),
|
|
)
|
|
},
|
|
wantErr: "discussions disabled",
|
|
},
|
|
{
|
|
name: "limit zero",
|
|
limit: 0,
|
|
wantErr: "limit argument must be positive",
|
|
},
|
|
{
|
|
name: "invalid orderBy",
|
|
limit: 10,
|
|
filters: ListFilters{OrderBy: "invalid"},
|
|
wantErr: "unknown order-by field",
|
|
},
|
|
{
|
|
name: "invalid direction",
|
|
limit: 10,
|
|
filters: ListFilters{Direction: "sideways"},
|
|
wantErr: "unknown order direction",
|
|
},
|
|
{
|
|
name: "invalid state",
|
|
limit: 10,
|
|
filters: ListFilters{State: new("merged")},
|
|
wantErr: "unknown state filter",
|
|
},
|
|
{
|
|
name: "with after cursor",
|
|
limit: 10,
|
|
after: "someCursor",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, "someCursor", vars["after"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "open state filter",
|
|
limit: 10,
|
|
filters: ListFilters{State: new(FilterStateOpen)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, []interface{}{"OPEN"}, vars["states"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "closed state filter",
|
|
limit: 10,
|
|
filters: ListFilters{State: new(FilterStateClosed)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, []interface{}{"CLOSED"}, vars["states"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "answered filter",
|
|
limit: 10,
|
|
filters: ListFilters{Answered: new(true)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, true, vars["answered"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "unanswered filter",
|
|
limit: 10,
|
|
filters: ListFilters{Answered: new(false)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, false, vars["answered"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "category ID filter",
|
|
limit: 10,
|
|
filters: ListFilters{CategoryID: "CAT123"},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, "CAT123", vars["categoryId"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "order by created asc",
|
|
limit: 10,
|
|
filters: ListFilters{OrderBy: OrderByCreated, Direction: OrderDirectionAsc},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
orderBy, ok := vars["orderBy"].(map[string]interface{})
|
|
require.True(t, ok, "orderBy should be a map")
|
|
assert.Equal(t, "CREATED_AT", orderBy["field"])
|
|
assert.Equal(t, "ASC", orderBy["direction"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "order by updated desc",
|
|
limit: 10,
|
|
filters: ListFilters{OrderBy: OrderByUpdated, Direction: OrderDirectionDesc},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
orderBy, ok := vars["orderBy"].(map[string]interface{})
|
|
require.True(t, ok, "orderBy should be a map")
|
|
assert.Equal(t, "UPDATED_AT", orderBy["field"])
|
|
assert.Equal(t, "DESC", orderBy["direction"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
// Bot actors have no name; ID comes from the Bot.ID field.
|
|
name: "bot actor",
|
|
limit: 10,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(false, "", 1, heredoc.Doc(`
|
|
{
|
|
"id": "D_bot",
|
|
"number": 1,
|
|
"title": "Bot post",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {
|
|
"__typename": "Bot",
|
|
"login": "gh-bot",
|
|
"id": "bot-node-id"
|
|
},
|
|
"category": {
|
|
"id": "C1",
|
|
"name": "General",
|
|
"slug": "general",
|
|
"emoji": "",
|
|
"isAnswerable": false
|
|
},
|
|
"answerChosenBy": null,
|
|
"labels": {
|
|
"nodes": []
|
|
},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
`))),
|
|
)
|
|
},
|
|
wantLen: 1,
|
|
wantTotal: 1,
|
|
wantSingleDisc: &Discussion{
|
|
ID: "D_bot",
|
|
Number: 1,
|
|
Title: "Bot post",
|
|
Author: DiscussionActor{ID: "bot-node-id", Login: "gh-bot", Name: ""},
|
|
Category: DiscussionCategory{ID: "C1", Name: "General", Slug: "general"},
|
|
Labels: []DiscussionLabel{},
|
|
Comments: DiscussionCommentList{},
|
|
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
},
|
|
{
|
|
// When limit > 100, the first page requests 100 and the second page
|
|
// requests the remainder, exercising the per-iteration first variable.
|
|
name: "limit greater than 100",
|
|
limit: 101,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(listResp(true, "pg2cursor", 101, minimalNodes(100)), func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, float64(100), vars["first"])
|
|
}),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.GraphQLQuery(listResp(false, "", 101, minimalNode("D101", "Discussion 101")), func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, float64(1), vars["first"])
|
|
}),
|
|
)
|
|
},
|
|
wantLen: 101,
|
|
wantTotal: 101,
|
|
},
|
|
{
|
|
// When the page has more items than requested, NextCursor is set.
|
|
name: "pagination sets next cursor",
|
|
limit: 1,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(true, "cursor42", 5, minimalNode("D1", "Discussion 1"))),
|
|
)
|
|
},
|
|
wantLen: 1,
|
|
wantTotal: 5,
|
|
wantCursor: "cursor42",
|
|
},
|
|
{
|
|
// Two pages are fetched when limit exceeds the first page's results.
|
|
name: "pagination fetches multiple pages",
|
|
limit: 2,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(true, "cursor1", 2, minimalNode("D1", "First"))),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(false, "", 2, minimalNode("D2", "Second"))),
|
|
)
|
|
},
|
|
wantLen: 2,
|
|
wantTotal: 2,
|
|
wantTitles: []string{"First", "Second"},
|
|
},
|
|
{
|
|
name: "exact fit does not overfetch",
|
|
limit: 1,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionList\b`),
|
|
httpmock.StringResponse(listResp(false, "", 1, minimalNode("D1", "Only one"))),
|
|
)
|
|
},
|
|
wantLen: 1,
|
|
wantTotal: 1,
|
|
wantTitles: []string{"Only one"},
|
|
},
|
|
}
|
|
|
|
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)
|
|
result, err := c.List(repo, tt.filters, tt.after, tt.limit)
|
|
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.Equal(t, tt.wantTotal, result.TotalCount)
|
|
assert.Len(t, result.Discussions, tt.wantLen)
|
|
assert.Equal(t, tt.wantCursor, result.NextCursor)
|
|
|
|
for i, title := range tt.wantTitles {
|
|
assert.Equal(t, title, result.Discussions[i].Title)
|
|
}
|
|
|
|
if tt.wantSingleDisc != nil {
|
|
require.NotEmpty(t, result.Discussions)
|
|
assert.Equal(t, *tt.wantSingleDisc, result.Discussions[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSearch(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
richNode := heredoc.Doc(`
|
|
{
|
|
"id": "D_rich1",
|
|
"number": 42,
|
|
"title": "Rich search result",
|
|
"body": "body text here",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42",
|
|
"closed": true,
|
|
"stateReason": "RESOLVED",
|
|
"isAnswered": true,
|
|
"answerChosenAt": "2024-06-01T12:00:00Z",
|
|
"author": {
|
|
"__typename": "User",
|
|
"login": "alice",
|
|
"id": "U1",
|
|
"name": "Alice"
|
|
},
|
|
"category": {
|
|
"id": "C1",
|
|
"name": "Q&A",
|
|
"slug": "q-a",
|
|
"emoji": ":question:",
|
|
"isAnswerable": true
|
|
},
|
|
"answerChosenBy": {
|
|
"__typename": "User",
|
|
"login": "bob",
|
|
"id": "U2",
|
|
"name": "Bob"
|
|
},
|
|
"labels": {
|
|
"nodes": [
|
|
{"id": "L1", "name": "bug", "color": "d73a4a"}
|
|
]
|
|
},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-06-02T00:00:00Z",
|
|
"closedAt": "2024-06-01T00:00:00Z",
|
|
"locked": true
|
|
}
|
|
`)
|
|
|
|
emptyResp := searchResp(false, "", 0, "")
|
|
|
|
tests := []struct {
|
|
name string
|
|
filters SearchFilters
|
|
after string
|
|
limit int
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
wantTotal int
|
|
wantLen int
|
|
wantCursor string
|
|
wantTitles []string
|
|
wantSingleDisc *Discussion
|
|
}{
|
|
{
|
|
name: "maps all fields",
|
|
limit: 10,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.StringResponse(searchResp(false, "", 1, richNode)),
|
|
)
|
|
},
|
|
wantTotal: 1,
|
|
wantLen: 1,
|
|
wantSingleDisc: &Discussion{
|
|
ID: "D_rich1",
|
|
Number: 42,
|
|
Title: "Rich search result",
|
|
Body: "body text here",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42",
|
|
Closed: true,
|
|
StateReason: "RESOLVED",
|
|
Author: DiscussionActor{
|
|
ID: "U1",
|
|
Login: "alice",
|
|
Name: "Alice",
|
|
},
|
|
Category: DiscussionCategory{
|
|
ID: "C1",
|
|
Name: "Q&A",
|
|
Slug: "q-a",
|
|
Emoji: ":question:",
|
|
IsAnswerable: true,
|
|
},
|
|
Labels: []DiscussionLabel{
|
|
{ID: "L1", Name: "bug", Color: "d73a4a"},
|
|
},
|
|
Answered: true,
|
|
AnswerChosenAt: time.Date(2024, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
AnswerChosenBy: &DiscussionActor{
|
|
ID: "U2",
|
|
Login: "bob",
|
|
Name: "Bob",
|
|
},
|
|
Comments: DiscussionCommentList{},
|
|
CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2024, 6, 2, 0, 0, 0, 0, time.UTC),
|
|
ClosedAt: time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
Locked: true,
|
|
},
|
|
},
|
|
{
|
|
name: "limit zero",
|
|
limit: 0,
|
|
wantErr: "limit argument must be positive",
|
|
},
|
|
{
|
|
name: "invalid orderBy",
|
|
limit: 10,
|
|
filters: SearchFilters{OrderBy: "bogus"},
|
|
wantErr: "unknown order-by field",
|
|
},
|
|
{
|
|
name: "invalid direction",
|
|
limit: 10,
|
|
filters: SearchFilters{Direction: "sideways"},
|
|
wantErr: "unknown order direction",
|
|
},
|
|
{
|
|
name: "invalid state",
|
|
limit: 10,
|
|
filters: SearchFilters{State: new("merged")},
|
|
wantErr: "unknown state filter",
|
|
},
|
|
{
|
|
name: "with after cursor",
|
|
limit: 10,
|
|
after: "someCursor",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, "someCursor", vars["after"])
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "open state filter",
|
|
limit: 10,
|
|
filters: SearchFilters{State: new(FilterStateOpen)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "is:open")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "closed state filter",
|
|
limit: 10,
|
|
filters: SearchFilters{State: new(FilterStateClosed)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "is:closed")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "answered filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Answered: new(true)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "is:answered")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "unanswered filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Answered: new(false)},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "is:unanswered")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "author filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Author: "alice"},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), `author:"alice"`)
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "labels filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Labels: []string{"bug", "enhancement"}},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
q := vars["query"].(string)
|
|
assert.Contains(t, q, `label:"bug"`)
|
|
assert.Contains(t, q, `label:"enhancement"`)
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "category filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Category: "Q&A"},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), `category:"Q&A"`)
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "keywords filter",
|
|
limit: 10,
|
|
filters: SearchFilters{Keywords: "some keyword"},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "some keyword")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "order by created asc",
|
|
limit: 10,
|
|
filters: SearchFilters{OrderBy: OrderByCreated, Direction: OrderDirectionAsc},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "sort:created-asc")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
name: "order by updated desc",
|
|
limit: 10,
|
|
filters: SearchFilters{OrderBy: OrderByUpdated, Direction: OrderDirectionDesc},
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(emptyResp, func(_ string, vars map[string]interface{}) {
|
|
assert.Contains(t, vars["query"].(string), "sort:updated-desc")
|
|
}),
|
|
)
|
|
},
|
|
},
|
|
{
|
|
// When limit > 100, the first page requests 100 and the second page
|
|
// requests the remainder, exercising the per-iteration first variable.
|
|
name: "limit greater than 100",
|
|
limit: 101,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(searchResp(true, "pg2cursor", 101, minimalNodes(100)), func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, float64(100), vars["first"])
|
|
}),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.GraphQLQuery(searchResp(false, "", 101, minimalNode("D101", "Discussion 101")), func(_ string, vars map[string]interface{}) {
|
|
assert.Equal(t, float64(1), vars["first"])
|
|
}),
|
|
)
|
|
},
|
|
wantLen: 101,
|
|
wantTotal: 101,
|
|
},
|
|
{
|
|
// When the page has more items than requested, NextCursor is set.
|
|
name: "pagination sets next cursor",
|
|
limit: 1,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.StringResponse(searchResp(true, "searchCursor42", 5, minimalNode("D1", "Discussion 1"))),
|
|
)
|
|
},
|
|
wantLen: 1,
|
|
wantTotal: 5,
|
|
wantCursor: "searchCursor42",
|
|
},
|
|
{
|
|
// Two pages are fetched when limit exceeds the first page's results.
|
|
name: "pagination fetches multiple pages",
|
|
limit: 2,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.StringResponse(searchResp(true, "searchCursor1", 2, minimalNode("D1", "First"))),
|
|
)
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.StringResponse(searchResp(false, "", 2, minimalNode("D2", "Second"))),
|
|
)
|
|
},
|
|
wantLen: 2,
|
|
wantTotal: 2,
|
|
wantTitles: []string{"First", "Second"},
|
|
},
|
|
{
|
|
name: "exact fit does not overfetch",
|
|
limit: 1,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionListSearch\b`),
|
|
httpmock.StringResponse(searchResp(false, "", 1, minimalNode("D1", "Only one"))),
|
|
)
|
|
},
|
|
wantLen: 1,
|
|
wantTotal: 1,
|
|
wantTitles: []string{"Only one"},
|
|
},
|
|
}
|
|
|
|
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)
|
|
result, err := c.Search(repo, tt.filters, tt.after, tt.limit)
|
|
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
assert.Equal(t, tt.wantTotal, result.TotalCount)
|
|
assert.Len(t, result.Discussions, tt.wantLen)
|
|
assert.Equal(t, tt.wantCursor, result.NextCursor)
|
|
|
|
for i, title := range tt.wantTitles {
|
|
assert.Equal(t, title, result.Discussions[i].Title)
|
|
}
|
|
|
|
if tt.wantSingleDisc != nil {
|
|
require.NotEmpty(t, result.Discussions)
|
|
assert.Equal(t, *tt.wantSingleDisc, result.Discussions[0])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListCategories(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
tests := []struct {
|
|
name string
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
wantCats []DiscussionCategory
|
|
}{
|
|
{
|
|
name: "maps all fields",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCategoryList\b`),
|
|
httpmock.StringResponse(`{"data":{"repository":{
|
|
"hasDiscussionsEnabled":true,
|
|
"discussionCategories":{"nodes":[
|
|
{"id":"C1","name":"General","slug":"general","emoji":":speech_balloon:","isAnswerable":false},
|
|
{"id":"C2","name":"Q&A","slug":"q-a","emoji":":question:","isAnswerable":true}
|
|
]}
|
|
}}}`),
|
|
)
|
|
},
|
|
wantCats: []DiscussionCategory{
|
|
{ID: "C1", Name: "General", Slug: "general", Emoji: ":speech_balloon:", IsAnswerable: false},
|
|
{ID: "C2", Name: "Q&A", Slug: "q-a", Emoji: ":question:", IsAnswerable: true},
|
|
},
|
|
},
|
|
{
|
|
name: "discussions disabled",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCategoryList\b`),
|
|
httpmock.StringResponse(`{"data":{"repository":{
|
|
"hasDiscussionsEnabled":false,
|
|
"discussionCategories":{"nodes":[]}
|
|
}}}`),
|
|
)
|
|
},
|
|
wantErr: "discussions disabled",
|
|
},
|
|
}
|
|
|
|
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)
|
|
categories, err := c.ListCategories(repo)
|
|
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Len(t, categories, len(tt.wantCats))
|
|
for i, want := range tt.wantCats {
|
|
assert.Equal(t, want, categories[i])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetByNumber(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
tests := []struct {
|
|
name string
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
assertDisc *Discussion
|
|
}{
|
|
{
|
|
name: "maps all fields",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionMinimal\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 42,
|
|
"title": "Test Discussion",
|
|
"body": "This is a test",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42",
|
|
"closed": true,
|
|
"stateReason": "RESOLVED",
|
|
"isAnswered": true,
|
|
"answerChosenAt": "2025-06-01T12:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice", "id": "U1", "name": "Alice"},
|
|
"category": {"id": "C1", "name": "Q&A", "slug": "q-a", "emoji": ":question:", "isAnswerable": true},
|
|
"answerChosenBy": {"__typename": "User", "login": "bob", "id": "U2", "name": "Bob"},
|
|
"labels": {"nodes": [{"id": "L1", "name": "bug", "color": "d73a4a"}]},
|
|
"reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 3}}],
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-02T00:00:00Z",
|
|
"closedAt": "2025-06-01T00:00:00Z",
|
|
"locked": true,
|
|
"comments": {"totalCount": 5}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: &Discussion{
|
|
ID: "D_1",
|
|
Number: 42,
|
|
Title: "Test Discussion",
|
|
Body: "This is a test",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42",
|
|
Closed: true,
|
|
StateReason: "RESOLVED",
|
|
Author: DiscussionActor{ID: "U1", Login: "alice", Name: "Alice"},
|
|
Category: DiscussionCategory{
|
|
ID: "C1",
|
|
Name: "Q&A",
|
|
Slug: "q-a",
|
|
Emoji: ":question:",
|
|
IsAnswerable: true,
|
|
},
|
|
Labels: []DiscussionLabel{{ID: "L1", Name: "bug", Color: "d73a4a"}},
|
|
Answered: true,
|
|
AnswerChosenAt: time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
AnswerChosenBy: &DiscussionActor{ID: "U2", Login: "bob", Name: "Bob"},
|
|
ReactionGroups: []ReactionGroup{
|
|
{Content: "THUMBS_UP", TotalCount: 3},
|
|
},
|
|
Comments: DiscussionCommentList{TotalCount: 5},
|
|
CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
|
|
ClosedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
Locked: true,
|
|
},
|
|
},
|
|
{
|
|
name: "discussions disabled",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionMinimal\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": false,
|
|
"discussion": null
|
|
}
|
|
},
|
|
"errors": [
|
|
{
|
|
"type": "NOT_FOUND",
|
|
"path": ["repository", "discussion"],
|
|
"message": "Could not resolve to a Discussion with the number of 42."
|
|
}
|
|
]
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
wantErr: "Could not resolve to a Discussion with the number of 42.",
|
|
},
|
|
{
|
|
name: "repo not found",
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionMinimal\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'.",
|
|
},
|
|
}
|
|
|
|
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.GetByNumber(repo, 42)
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetWithComments(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
tests := []struct {
|
|
name string
|
|
limit int
|
|
after string
|
|
newest bool
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
assertDisc func(*testing.T, *Discussion)
|
|
}{
|
|
{
|
|
name: "maps comments with replies",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 42,
|
|
"title": "Test Discussion",
|
|
"body": "Discussion body",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42",
|
|
"closed": true,
|
|
"stateReason": "RESOLVED",
|
|
"isAnswered": true,
|
|
"answerChosenAt": "2025-06-01T12:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice", "id": "U_alice", "name": "Alice"},
|
|
"category": {"id": "CAT1", "name": "Q&A", "slug": "q-a", "emoji": ":question:", "isAnswerable": true},
|
|
"answerChosenBy": {"__typename": "User", "login": "bob", "id": "U_bob", "name": "Bob"},
|
|
"labels": {"nodes": [{"id": "L1", "name": "bug", "color": "d73a4a"}]},
|
|
"reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 3}}],
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-02T00:00:00Z",
|
|
"closedAt": "2025-06-01T00:00:00Z",
|
|
"locked": true,
|
|
"comments": {
|
|
"totalCount": 1,
|
|
"pageInfo": {"endCursor": "COM_CUR", "hasNextPage": true, "startCursor": "COM_START", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "C1",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42#comment-1",
|
|
"author": {"__typename": "User", "login": "octocat", "id": "U_octocat", "name": "Octocat"},
|
|
"body": "Main comment",
|
|
"createdAt": "2025-03-01T00:00:00Z",
|
|
"isAnswer": true,
|
|
"upvoteCount": 5,
|
|
"reactionGroups": [{"content": "HEART", "users": {"totalCount": 2}}],
|
|
"replies": {
|
|
"totalCount": 1,
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42#reply-1",
|
|
"author": {"__typename": "User", "login": "hubot", "id": "U_hubot", "name": "Hubot"},
|
|
"body": "Thanks!",
|
|
"createdAt": "2025-04-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 1,
|
|
"reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 1}}]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
assert.Equal(t, Discussion{
|
|
ID: "D_1",
|
|
Number: 42,
|
|
Title: "Test Discussion",
|
|
Body: "Discussion body",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42",
|
|
Closed: true,
|
|
StateReason: "RESOLVED",
|
|
Author: DiscussionActor{ID: "U_alice", Login: "alice", Name: "Alice"},
|
|
Category: DiscussionCategory{
|
|
ID: "CAT1",
|
|
Name: "Q&A",
|
|
Slug: "q-a",
|
|
Emoji: ":question:",
|
|
IsAnswerable: true,
|
|
},
|
|
Labels: []DiscussionLabel{{ID: "L1", Name: "bug", Color: "d73a4a"}},
|
|
Answered: true,
|
|
AnswerChosenAt: time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
AnswerChosenBy: &DiscussionActor{ID: "U_bob", Login: "bob", Name: "Bob"},
|
|
ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 3}},
|
|
CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
|
|
ClosedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
Locked: true,
|
|
Comments: DiscussionCommentList{
|
|
TotalCount: 1,
|
|
NextCursor: "COM_CUR",
|
|
Direction: DiscussionCommentListDirectionForward,
|
|
Comments: []DiscussionComment{
|
|
{
|
|
ID: "C1",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42#comment-1",
|
|
Author: DiscussionActor{ID: "U_octocat", Login: "octocat", Name: "Octocat"},
|
|
Body: "Main comment",
|
|
CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
|
|
IsAnswer: true,
|
|
UpvoteCount: 5,
|
|
ReactionGroups: []ReactionGroup{{Content: "HEART", TotalCount: 2}},
|
|
Replies: DiscussionCommentList{
|
|
TotalCount: 1,
|
|
Direction: DiscussionCommentListDirectionBackward,
|
|
Comments: []DiscussionComment{
|
|
{
|
|
ID: "R1",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42#reply-1",
|
|
Author: DiscussionActor{ID: "U_hubot", Login: "hubot", Name: "Hubot"},
|
|
Body: "Thanks!",
|
|
CreatedAt: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC),
|
|
UpvoteCount: 1,
|
|
ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 1}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, *d)
|
|
},
|
|
},
|
|
{
|
|
name: "pagination forward",
|
|
limit: 5,
|
|
after: "CUR_A",
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 3,
|
|
"pageInfo": {"endCursor": "CUR_B", "hasNextPage": true, "startCursor": "", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "C1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Hello",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 1)
|
|
assert.Equal(t, 3, comments.TotalCount)
|
|
assert.Equal(t, "CUR_A", comments.Cursor)
|
|
assert.Equal(t, "CUR_B", comments.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, comments.Direction)
|
|
},
|
|
},
|
|
{
|
|
name: "pagination backward newest",
|
|
limit: 5,
|
|
after: "CUR_X",
|
|
newest: true,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 5,
|
|
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "CUR_Y", "hasPreviousPage": true},
|
|
"nodes": [
|
|
{
|
|
"id": "C1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "First",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
},
|
|
{
|
|
"id": "C2",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Second",
|
|
"createdAt": "2025-01-02T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 2)
|
|
assert.Equal(t, 5, comments.TotalCount)
|
|
assert.Equal(t, "CUR_X", comments.Cursor)
|
|
assert.Equal(t, "CUR_Y", comments.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionBackward, comments.Direction)
|
|
assert.Equal(t, "C2", comments.Comments[0].ID, "newest mode should reverse comments")
|
|
assert.Equal(t, "C1", comments.Comments[1].ID)
|
|
},
|
|
},
|
|
{
|
|
name: "no more pages",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 1,
|
|
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "C1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Only one",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 1)
|
|
assert.Equal(t, 1, comments.TotalCount)
|
|
assert.Equal(t, "", comments.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, comments.Direction)
|
|
},
|
|
},
|
|
{
|
|
name: "discussions disabled",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": false,
|
|
"discussion": null
|
|
}
|
|
},
|
|
"errors": [
|
|
{
|
|
"type": "NOT_FOUND",
|
|
"path": ["repository", "discussion"],
|
|
"message": "Could not resolve to a Discussion with the number of 1."
|
|
}
|
|
]
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
wantErr: "Could not resolve to a Discussion",
|
|
},
|
|
{
|
|
name: "repo not found",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\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: "empty comments",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 0,
|
|
"pageInfo": {"endCursor": null, "hasNextPage": false, "startCursor": null, "hasPreviousPage": false},
|
|
"nodes": []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 0)
|
|
assert.Equal(t, 0, comments.TotalCount)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, comments.Direction)
|
|
},
|
|
},
|
|
{
|
|
name: "first page newest reverses comments",
|
|
limit: 5,
|
|
newest: true,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 8,
|
|
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "CUR_START", "hasPreviousPage": true},
|
|
"nodes": [
|
|
{
|
|
"id": "C4",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Fourth",
|
|
"createdAt": "2025-01-04T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
},
|
|
{
|
|
"id": "C5",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Fifth",
|
|
"createdAt": "2025-01-05T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {"totalCount": 0, "nodes": []}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 2)
|
|
assert.Equal(t, 8, comments.TotalCount)
|
|
assert.Equal(t, "", comments.Cursor)
|
|
assert.Equal(t, "CUR_START", comments.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionBackward, comments.Direction)
|
|
assert.Equal(t, "C5", comments.Comments[0].ID, "newest mode should reverse comments")
|
|
assert.Equal(t, "C4", comments.Comments[1].ID)
|
|
},
|
|
},
|
|
{
|
|
name: "multiple replies on comment",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionWithComments\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false,
|
|
"comments": {
|
|
"totalCount": 1,
|
|
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "C1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Parent",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 3,
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "First reply",
|
|
"createdAt": "2025-01-02T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
},
|
|
{
|
|
"id": "R2",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "carol"},
|
|
"body": "Second reply",
|
|
"createdAt": "2025-01-03T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
},
|
|
{
|
|
"id": "R3",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "dave"},
|
|
"body": "Third reply",
|
|
"createdAt": "2025-01-04T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
comments := d.Comments
|
|
assert.Len(t, comments.Comments, 1)
|
|
assert.Equal(t, 1, comments.TotalCount)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, comments.Direction)
|
|
c := comments.Comments[0]
|
|
require.Len(t, c.Replies.Comments, 3)
|
|
assert.Equal(t, 3, c.Replies.TotalCount)
|
|
assert.Equal(t, "R1", c.Replies.Comments[0].ID)
|
|
assert.Equal(t, "R2", c.Replies.Comments[1].ID)
|
|
assert.Equal(t, "R3", c.Replies.Comments[2].ID)
|
|
},
|
|
},
|
|
}
|
|
|
|
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.GetWithComments(repo, 1, tt.limit, tt.after, tt.newest)
|
|
|
|
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")
|
|
tt.assertDisc(t, d)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCommentReplies(t *testing.T) {
|
|
repo := ghrepo.New("OWNER", "REPO")
|
|
|
|
tests := []struct {
|
|
name string
|
|
commentID string
|
|
limit int
|
|
after string
|
|
newest bool
|
|
httpStubs func(*testing.T, *httpmock.Registry)
|
|
wantErr string
|
|
assertDisc func(*testing.T, *Discussion)
|
|
}{
|
|
{
|
|
name: "maps all fields",
|
|
commentID: "DC_abc",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 42,
|
|
"title": "Test Discussion",
|
|
"body": "Discussion body",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42",
|
|
"closed": true,
|
|
"stateReason": "RESOLVED",
|
|
"isAnswered": true,
|
|
"answerChosenAt": "2025-06-01T12:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice", "id": "U_alice", "name": "Alice"},
|
|
"category": {"id": "CAT1", "name": "Q&A", "slug": "q-a", "emoji": ":question:", "isAnswerable": true},
|
|
"answerChosenBy": {"__typename": "User", "login": "bob", "id": "U_bob", "name": "Bob"},
|
|
"labels": {"nodes": [{"id": "L1", "name": "bug", "color": "d73a4a"}]},
|
|
"reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 3}}],
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"updatedAt": "2025-01-02T00:00:00Z",
|
|
"closedAt": "2025-06-01T00:00:00Z",
|
|
"locked": true
|
|
}
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42#discussioncomment-1",
|
|
"author": {"__typename": "User", "login": "octocat", "id": "U_octocat", "name": "Octocat"},
|
|
"body": "Top-level comment",
|
|
"createdAt": "2025-03-01T00:00:00Z",
|
|
"isAnswer": true,
|
|
"upvoteCount": 5,
|
|
"reactionGroups": [{"content": "HEART", "users": {"totalCount": 2}}],
|
|
"replies": {
|
|
"totalCount": 1,
|
|
"pageInfo": {"endCursor": "REP_CUR", "hasNextPage": true, "startCursor": "REP_START", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "https://github.com/OWNER/REPO/discussions/42#discussioncomment-2",
|
|
"author": {"__typename": "User", "login": "hubot", "id": "U_hubot", "name": "Hubot"},
|
|
"body": "A reply",
|
|
"createdAt": "2025-04-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 1,
|
|
"reactionGroups": [{"content": "THUMBS_UP", "users": {"totalCount": 1}}]
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
assert.Equal(t, Discussion{
|
|
ID: "D_1",
|
|
Number: 42,
|
|
Title: "Test Discussion",
|
|
Body: "Discussion body",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42",
|
|
Closed: true,
|
|
StateReason: "RESOLVED",
|
|
Author: DiscussionActor{ID: "U_alice", Login: "alice", Name: "Alice"},
|
|
Category: DiscussionCategory{
|
|
ID: "CAT1",
|
|
Name: "Q&A",
|
|
Slug: "q-a",
|
|
Emoji: ":question:",
|
|
IsAnswerable: true,
|
|
},
|
|
Labels: []DiscussionLabel{{ID: "L1", Name: "bug", Color: "d73a4a"}},
|
|
Answered: true,
|
|
AnswerChosenAt: time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC),
|
|
AnswerChosenBy: &DiscussionActor{ID: "U_bob", Login: "bob", Name: "Bob"},
|
|
ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 3}},
|
|
CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
UpdatedAt: time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC),
|
|
ClosedAt: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
|
|
Locked: true,
|
|
Comments: DiscussionCommentList{
|
|
TotalCount: 1,
|
|
Comments: []DiscussionComment{
|
|
{
|
|
ID: "DC_abc",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42#discussioncomment-1",
|
|
Author: DiscussionActor{ID: "U_octocat", Login: "octocat", Name: "Octocat"},
|
|
Body: "Top-level comment",
|
|
CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
|
|
IsAnswer: true,
|
|
UpvoteCount: 5,
|
|
ReactionGroups: []ReactionGroup{{Content: "HEART", TotalCount: 2}},
|
|
Replies: DiscussionCommentList{
|
|
TotalCount: 1,
|
|
NextCursor: "REP_CUR",
|
|
Direction: DiscussionCommentListDirectionForward,
|
|
Comments: []DiscussionComment{
|
|
{
|
|
ID: "R1",
|
|
URL: "https://github.com/OWNER/REPO/discussions/42#discussioncomment-2",
|
|
Author: DiscussionActor{ID: "U_hubot", Login: "hubot", Name: "Hubot"},
|
|
Body: "A reply",
|
|
CreatedAt: time.Date(2025, 4, 1, 0, 0, 0, 0, time.UTC),
|
|
UpvoteCount: 1,
|
|
ReactionGroups: []ReactionGroup{{Content: "THUMBS_UP", TotalCount: 1}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, *d)
|
|
},
|
|
},
|
|
{
|
|
name: "pagination forward oldest",
|
|
commentID: "DC_abc",
|
|
limit: 5,
|
|
after: "CUR_A",
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Comment",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 3,
|
|
"pageInfo": {"endCursor": "CUR_B", "hasNextPage": true, "startCursor": "CUR_A", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Reply 1",
|
|
"createdAt": "2025-02-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
},
|
|
{
|
|
"id": "R2",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "carol"},
|
|
"body": "Reply 2",
|
|
"createdAt": "2025-03-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
replies := d.Comments.Comments[0].Replies
|
|
assert.Len(t, replies.Comments, 2)
|
|
assert.Equal(t, 3, replies.TotalCount)
|
|
assert.Equal(t, "CUR_A", replies.Cursor)
|
|
assert.Equal(t, "CUR_B", replies.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, replies.Direction)
|
|
assert.Equal(t, "R1", replies.Comments[0].ID, "forward mode should preserve chronological order")
|
|
assert.Equal(t, "R2", replies.Comments[1].ID)
|
|
},
|
|
},
|
|
{
|
|
name: "pagination backward newest reverses replies",
|
|
commentID: "DC_abc",
|
|
limit: 5,
|
|
after: "CUR_X",
|
|
newest: true,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Comment",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 5,
|
|
"pageInfo": {"endCursor": "CUR_END", "hasNextPage": false, "startCursor": "CUR_Y", "hasPreviousPage": true},
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Older",
|
|
"createdAt": "2025-02-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
},
|
|
{
|
|
"id": "R2",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "carol"},
|
|
"body": "Newer",
|
|
"createdAt": "2025-03-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
replies := d.Comments.Comments[0].Replies
|
|
assert.Len(t, replies.Comments, 2)
|
|
assert.Equal(t, 5, replies.TotalCount)
|
|
assert.Equal(t, "CUR_X", replies.Cursor)
|
|
assert.Equal(t, "CUR_Y", replies.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionBackward, replies.Direction)
|
|
assert.Equal(t, "R2", replies.Comments[0].ID, "newest mode should reverse replies")
|
|
assert.Equal(t, "R1", replies.Comments[1].ID)
|
|
},
|
|
},
|
|
{
|
|
name: "first page newest reverses replies",
|
|
commentID: "DC_abc",
|
|
limit: 5,
|
|
newest: true,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Comment",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 3,
|
|
"pageInfo": {"endCursor": "", "hasNextPage": false, "startCursor": "CUR_START", "hasPreviousPage": true},
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Older",
|
|
"createdAt": "2025-02-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
},
|
|
{
|
|
"id": "R2",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "carol"},
|
|
"body": "Newer",
|
|
"createdAt": "2025-03-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
replies := d.Comments.Comments[0].Replies
|
|
assert.Len(t, replies.Comments, 2)
|
|
assert.Equal(t, 3, replies.TotalCount)
|
|
assert.Equal(t, "", replies.Cursor)
|
|
assert.Equal(t, "CUR_START", replies.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionBackward, replies.Direction)
|
|
assert.Equal(t, "R2", replies.Comments[0].ID, "newest mode should reverse replies")
|
|
assert.Equal(t, "R1", replies.Comments[1].ID)
|
|
},
|
|
},
|
|
{
|
|
name: "no more pages",
|
|
commentID: "DC_abc",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Comment",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 1,
|
|
"pageInfo": {"endCursor": "CUR_ONLY", "hasNextPage": false, "startCursor": "CUR_ONLY", "hasPreviousPage": false},
|
|
"nodes": [
|
|
{
|
|
"id": "R1",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "bob"},
|
|
"body": "Only reply",
|
|
"createdAt": "2025-02-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": []
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
assertDisc: func(t *testing.T, d *Discussion) {
|
|
replies := d.Comments.Comments[0].Replies
|
|
assert.Len(t, replies.Comments, 1)
|
|
assert.Equal(t, 1, replies.TotalCount)
|
|
assert.Equal(t, "", replies.NextCursor)
|
|
assert.Equal(t, DiscussionCommentListDirectionForward, replies.Direction)
|
|
},
|
|
},
|
|
{
|
|
name: "discussions disabled",
|
|
commentID: "DC_abc",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": false,
|
|
"discussion": null
|
|
},
|
|
"node": {
|
|
"id": "DC_abc",
|
|
"url": "",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"body": "Comment",
|
|
"createdAt": "2025-01-01T00:00:00Z",
|
|
"isAnswer": false,
|
|
"upvoteCount": 0,
|
|
"reactionGroups": [],
|
|
"replies": {
|
|
"totalCount": 0,
|
|
"pageInfo": {"endCursor": null, "hasNextPage": false, "startCursor": null, "hasPreviousPage": false},
|
|
"nodes": []
|
|
}
|
|
}
|
|
},
|
|
"errors": [
|
|
{
|
|
"type": "NOT_FOUND",
|
|
"path": ["repository", "discussion"],
|
|
"message": "Could not resolve to a Discussion with the number of 1."
|
|
}
|
|
]
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
wantErr: "Could not resolve to a Discussion",
|
|
},
|
|
{
|
|
name: "repo not found",
|
|
commentID: "DC_abc",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": null,
|
|
"node": 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",
|
|
},
|
|
{
|
|
name: "reply node not found",
|
|
commentID: "DC_invalid",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": null
|
|
},
|
|
"errors": [
|
|
{
|
|
"type": "NOT_FOUND",
|
|
"path": ["node"],
|
|
"message": "Could not resolve to a node with the global id of 'DC_invalid'"
|
|
}
|
|
]
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
wantErr: "Could not resolve to a node",
|
|
},
|
|
{
|
|
name: "node is not a discussion comment",
|
|
commentID: "I_notacomment",
|
|
limit: 10,
|
|
newest: false,
|
|
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
|
|
reg.Register(
|
|
httpmock.GraphQL(`query DiscussionCommentReplies\b`),
|
|
httpmock.StringResponse(heredoc.Doc(`
|
|
{
|
|
"data": {
|
|
"repository": {
|
|
"hasDiscussionsEnabled": true,
|
|
"discussion": {
|
|
"id": "D_1",
|
|
"number": 1,
|
|
"title": "Test",
|
|
"body": "",
|
|
"url": "",
|
|
"closed": false,
|
|
"stateReason": "",
|
|
"isAnswered": false,
|
|
"answerChosenAt": "0001-01-01T00:00:00Z",
|
|
"author": {"__typename": "User", "login": "alice"},
|
|
"category": {"id": "C1", "name": "General", "slug": "general", "emoji": "", "isAnswerable": false},
|
|
"answerChosenBy": null,
|
|
"labels": {"nodes": []},
|
|
"reactionGroups": [],
|
|
"createdAt": "2024-01-01T00:00:00Z",
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"closedAt": "0001-01-01T00:00:00Z",
|
|
"locked": false
|
|
}
|
|
},
|
|
"node": {}
|
|
}
|
|
}
|
|
`)),
|
|
)
|
|
},
|
|
wantErr: "node I_notacomment is not a discussion comment",
|
|
},
|
|
}
|
|
|
|
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.GetCommentReplies(repo, 1, tt.commentID, tt.limit, tt.after, tt.newest)
|
|
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, d)
|
|
require.Len(t, d.Comments.Comments, 1, "GetCommentReplies should return exactly one comment")
|
|
require.NotNil(t, tt.assertDisc, "assertDisc must be set for non-error cases")
|
|
tt.assertDisc(t, d)
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|