cli/pkg/cmd/discussion/view/view_test.go
Babak K. Shandiz 52f219a5ac
test(discussion view): consolidate view run tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-29 13:05:21 +01:00

1155 lines
34 KiB
Go

package view
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/discussion/client"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/jsonfieldstest"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestJSONFields(t *testing.T) {
jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdView, []string{
"id",
"number",
"title",
"body",
"url",
"closed",
"state",
"stateReason",
"author",
"category",
"labels",
"answered",
"answerChosenAt",
"answerChosenBy",
"comments",
"reactionGroups",
"createdAt",
"updatedAt",
"closedAt",
"locked",
})
}
func TestNewCmdView(t *testing.T) {
tests := []struct {
name string
args string
wantErr string
wantOpts ViewOptions
wantRepo string
}{
{
name: "number argument",
args: "123",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Limit: 30,
Order: "newest",
},
},
{
name: "hash number argument",
args: "'#456'",
wantOpts: ViewOptions{
DiscussionNumber: 456,
Limit: 30,
Order: "newest",
},
},
{
name: "URL argument",
args: "https://github.com/OTHER/REPO/discussions/789",
wantOpts: ViewOptions{
DiscussionNumber: 789,
Limit: 30,
Order: "newest",
},
wantRepo: "OTHER/REPO",
},
{
name: "invalid argument",
args: "not-a-number",
wantErr: "invalid discussion argument",
},
{
name: "no arguments",
args: "",
wantErr: "accepts 1 arg(s), received 0",
},
{
name: "web flag",
args: "123 --web",
wantOpts: ViewOptions{
DiscussionNumber: 123,
WebMode: true,
Limit: 30,
Order: "newest",
},
},
{
name: "comments flag",
args: "123 --comments",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Comments: true,
Limit: 30,
Order: "newest",
},
},
{
name: "comments with limit",
args: "123 --comments --limit 10",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Comments: true,
Limit: 10,
Order: "newest",
},
},
{
name: "comments with after",
args: "123 --comments --after CURSOR_ABC",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Comments: true,
Limit: 30,
After: "CURSOR_ABC",
Order: "newest",
},
},
{
name: "comments with order oldest",
args: "123 --comments --order oldest",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Comments: true,
Limit: 30,
Order: "oldest",
},
},
{
name: "replies flag",
args: "123 --replies DC_abc",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Replies: "DC_abc",
Limit: 30,
Order: "newest",
},
},
{
name: "replies with limit",
args: "123 --replies DC_abc --limit 10",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Replies: "DC_abc",
Limit: 10,
Order: "newest",
},
},
{
name: "replies with after",
args: "123 --replies DC_abc --after CURSOR",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Replies: "DC_abc",
Limit: 30,
After: "CURSOR",
Order: "newest",
},
},
{
name: "replies with order oldest",
args: "123 --replies DC_abc --order oldest",
wantOpts: ViewOptions{
DiscussionNumber: 123,
Replies: "DC_abc",
Limit: 30,
Order: "oldest",
},
},
{
name: "replies with comments is mutually exclusive",
args: "123 --replies DC_abc --comments",
wantErr: "specify only one of --comments, --replies, or --web",
},
{
name: "replies with web is mutually exclusive",
args: "123 --replies DC_abc --web",
wantErr: "specify only one of --comments, --replies, or --web",
},
{
name: "comments with web is mutually exclusive",
args: "123 --comments --web",
wantErr: "specify only one of --comments, --replies, or --web",
},
{
name: "order requires comments or replies",
args: "123 --order newest",
wantErr: "--order requires --comments or --replies",
},
{
name: "limit requires comments or replies",
args: "123 --limit 5",
wantErr: "--limit requires --comments or --replies",
},
{
name: "after requires comments or replies",
args: "123 --after CURSOR",
wantErr: "--after requires --comments or --replies",
},
{
name: "invalid limit zero",
args: "123 --comments --limit 0",
wantErr: "invalid limit",
},
{
name: "invalid limit negative",
args: "123 --comments --limit -5",
wantErr: "invalid limit",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{}
ios, _, _, _ := iostreams.Test()
f.IOStreams = ios
f.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.New("OWNER", "REPO"), nil
}
f.Browser = &browser.Stub{}
var gotOpts *ViewOptions
cmd := NewCmdView(f, func(opts *ViewOptions) error {
gotOpts = opts
return nil
})
argv, err := shlex.Split(tt.args)
require.NoError(t, err)
cmd.SetArgs(argv)
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantErr != "" {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
return
}
require.NoError(t, err)
repo, err := gotOpts.BaseRepo()
require.NoError(t, err)
if tt.wantRepo != "" {
assert.Equal(t, tt.wantRepo, ghrepo.FullName(repo))
}
assert.Equal(t, tt.wantOpts.DiscussionNumber, gotOpts.DiscussionNumber)
assert.Equal(t, tt.wantOpts.WebMode, gotOpts.WebMode)
assert.Equal(t, tt.wantOpts.Comments, gotOpts.Comments)
assert.Equal(t, tt.wantOpts.Replies, gotOpts.Replies)
assert.Equal(t, tt.wantOpts.Limit, gotOpts.Limit)
assert.Equal(t, tt.wantOpts.After, gotOpts.After)
assert.Equal(t, tt.wantOpts.Order, gotOpts.Order)
})
}
}
func TestViewRun(t *testing.T) {
fixedNow := func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) }
tests := []struct {
name string
tty bool
clientStub func(*testing.T, *client.DiscussionClientMock)
opts ViewOptions
wantStdout string
wantStderr string
wantBrowser string
}{
{
name: "tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetByNumberFunc = func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
return exampleAnswerableDiscussion(), nil
}
},
wantStdout: heredoc.Doc(`
an interesting question #123
Open · Q&A · Asked by monalisa · about 1 hour ago · 3 comments
Labels: help-wanted
about my interesting question
👍 5 • 🚀 2
View this discussion on GitHub: https://github.com/OWNER/REPO/discussions/123
`),
},
{
name: "nontty",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetByNumberFunc = func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
return exampleAnswerableDiscussion(), nil
}
},
wantStdout: heredoc.Doc(`
title: an interesting question
state: OPEN
category: Q&A
author: monalisa
labels: help-wanted
comments: 3
number: 123
url: https://github.com/OWNER/REPO/discussions/123
--
about my interesting question
`),
},
{
name: "web",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetByNumberFunc = func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
return exampleAnswerableDiscussion(), nil
}
},
opts: ViewOptions{
WebMode: true,
},
wantStderr: "Opening https://github.com/OWNER/REPO/discussions/123 in your browser.\n",
wantBrowser: "https://github.com/OWNER/REPO/discussions/123",
},
{
name: "not answerable tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetByNumberFunc = func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
return exampleUnanswerableDiscussion(), nil
}
},
wantStdout: heredoc.Doc(`
a cool discussion #123
Open · General · Started by monalisa · about 1 hour ago · 3 comments
Labels: help-wanted
about my cool idea
👍 5 • 🚀 2
View this discussion on GitHub: https://github.com/OWNER/REPO/discussions/123
`),
},
{
name: "comments tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 30, commentLimit)
assert.Equal(t, "", after)
assert.Equal(t, false, newest)
return exampleDiscussionWithComments(), nil
}
},
opts: ViewOptions{
Comments: true,
Order: "oldest",
},
wantStdout: heredoc.Doc(`
an interesting question #123
Open · Q&A · Asked by monalisa · about 1 hour ago · 2 comments
Labels: help-wanted
about my interesting question
👍 5 • 🚀 2
Comments
octocat commented less than a minute ago ✓ Answer
This is a comment
👍 3
hubot commented less than a minute ago
Thanks!
And 4 more replies
monalisa commented less than a minute ago
Another comment
View this discussion on GitHub: https://github.com/OWNER/REPO/discussions/123
`),
},
{
name: "comments nontty",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 30, commentLimit)
assert.Equal(t, "", after)
assert.Equal(t, false, newest)
return exampleDiscussionWithComments(), nil
}
},
opts: ViewOptions{
Comments: true,
Order: "oldest",
},
wantStdout: heredoc.Doc(`
title: an interesting question
state: OPEN
category: Q&A
author: monalisa
labels: help-wanted
comments: 2
number: 123
url: https://github.com/OWNER/REPO/discussions/123
--
about my interesting question
comment: octocat 2025-03-02T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-1 answer
--
This is a comment
comment: hubot 2025-03-02T01:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-2
--
Thanks!
comment: monalisa 2025-03-03T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-3
--
Another comment
`),
},
{
name: "comments pagination tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
d := exampleDiscussionWithComments()
d.Comments.NextCursor = "NEXT_CURSOR_123"
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 10, commentLimit)
assert.Equal(t, "CURSOR_ABC", after)
assert.Equal(t, false, newest)
return d, nil
}
},
opts: ViewOptions{
Comments: true,
Limit: 10,
After: "CURSOR_ABC",
Order: "oldest",
},
wantStdout: heredoc.Doc(`
an interesting question #123
Open · Q&A · Asked by monalisa · about 1 hour ago · 2 comments
Labels: help-wanted
about my interesting question
👍 5 • 🚀 2
Comments
octocat commented less than a minute ago ✓ Answer
This is a comment
👍 3
hubot commented less than a minute ago
Thanks!
And 4 more replies
monalisa commented less than a minute ago
Another comment
To see more comments, pass: --after NEXT_CURSOR_123
View this discussion on GitHub: https://github.com/OWNER/REPO/discussions/123
`),
},
{
name: "comments pagination nontty",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
d := exampleDiscussionWithComments()
d.Comments.NextCursor = "NEXT_CURSOR_456"
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 30, commentLimit)
assert.Equal(t, "", after)
assert.Equal(t, false, newest)
return d, nil
}
},
opts: ViewOptions{
Comments: true,
Order: "oldest",
},
wantStdout: heredoc.Doc(`
title: an interesting question
state: OPEN
category: Q&A
author: monalisa
labels: help-wanted
comments: 2
next: NEXT_CURSOR_456
number: 123
url: https://github.com/OWNER/REPO/discussions/123
--
about my interesting question
comment: octocat 2025-03-02T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-1 answer
--
This is a comment
comment: hubot 2025-03-02T01:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-2
--
Thanks!
comment: monalisa 2025-03-03T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-3
--
Another comment
`),
},
{
name: "json without comments field",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetByNumberFunc = func(repo ghrepo.Interface, number int) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
return exampleAnswerableDiscussion(), nil
}
},
opts: ViewOptions{
Exporter: jsonExporter("title", "url"),
},
wantStdout: compactJSON(heredoc.Doc(`
{
"title": "an interesting question",
"url": "https://github.com/OWNER/REPO/discussions/123"
}
`)),
},
{
name: "json with comments field",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 30, commentLimit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
return exampleDiscussionWithComments(), nil
}
},
opts: ViewOptions{
Exporter: jsonExporter("comments"),
},
wantStdout: compactJSON(heredoc.Doc(`
{
"comments": {
"nodes": [
{
"author": {"id": "", "login": "octocat", "name": ""},
"body": "This is a comment",
"createdAt": "2025-03-02T00:00:00Z",
"id": "C_1",
"isAnswer": true,
"reactionGroups": [
{"content": "THUMBS_UP", "totalCount": 3}
],
"replies": {
"nodes": [
{
"author": {"id": "", "login": "hubot", "name": ""},
"body": "Thanks!",
"createdAt": "2025-03-02T01:00:00Z",
"id": "C_1_R1",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2"
}
],
"totalCount": 5
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1"
},
{
"author": {"id": "", "login": "monalisa", "name": ""},
"body": "Another comment",
"createdAt": "2025-03-03T00:00:00Z",
"id": "C_2",
"isAnswer": false,
"reactionGroups": [],
"replies": {
"nodes": [],
"totalCount": 0
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3"
}
],
"totalCount": 2
}
}
`)),
},
{
name: "json with comments field pagination",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetWithCommentsFunc = func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, 30, commentLimit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
d := exampleDiscussionWithComments()
d.Comments.NextCursor = "NEXT_COM_CUR"
return d, nil
}
},
opts: ViewOptions{
Exporter: jsonExporter("comments"),
},
wantStdout: compactJSON(heredoc.Doc(`
{
"comments": {
"next": "NEXT_COM_CUR",
"nodes": [
{
"author": {"id": "", "login": "octocat", "name": ""},
"body": "This is a comment",
"createdAt": "2025-03-02T00:00:00Z",
"id": "C_1",
"isAnswer": true,
"reactionGroups": [
{"content": "THUMBS_UP", "totalCount": 3}
],
"replies": {
"nodes": [
{
"author": {"id": "", "login": "hubot", "name": ""},
"body": "Thanks!",
"createdAt": "2025-03-02T01:00:00Z",
"id": "C_1_R1",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2"
}
],
"totalCount": 5
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1"
},
{
"author": {"id": "", "login": "monalisa", "name": ""},
"body": "Another comment",
"createdAt": "2025-03-03T00:00:00Z",
"id": "C_2",
"isAnswer": false,
"reactionGroups": [],
"replies": {
"nodes": [],
"totalCount": 0
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3"
}
],
"totalCount": 2
}
}
`)),
},
{
name: "replies tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
return exampleDiscussionWithReplies(""), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
},
wantStdout: heredoc.Doc(`
octocat commented less than a minute ago ✓ Answer
This is the parent comment
👍 3
hubot commented less than a minute ago
First reply
monalisa commented less than a minute ago
Second reply
`),
},
{
name: "replies pagination tty",
tty: true,
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
return exampleDiscussionWithReplies("NEXT_CUR"), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
},
wantStdout: heredoc.Doc(`
octocat commented less than a minute ago ✓ Answer
This is the parent comment
👍 3
hubot commented less than a minute ago
First reply
monalisa commented less than a minute ago
Second reply
To see more replies, pass: --after NEXT_CUR
`),
},
{
name: "replies nontty",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, false, newest)
return exampleDiscussionWithReplies(""), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
Order: "oldest",
},
wantStdout: heredoc.Doc(`
comment: octocat 2025-03-02T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-1 answer
replies: 2
--
This is the parent comment
comment: hubot 2025-03-02T01:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-2
--
First reply
comment: monalisa 2025-03-02T02:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-3
--
Second reply
`),
},
{
name: "replies pagination nontty",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, false, newest)
return exampleDiscussionWithReplies("NEXT_CUR_456"), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
Order: "oldest",
},
wantStdout: heredoc.Doc(`
comment: octocat 2025-03-02T00:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-1 answer
replies: 2
next: NEXT_CUR_456
--
This is the parent comment
comment: hubot 2025-03-02T01:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-2
--
First reply
comment: monalisa 2025-03-02T02:00:00Z https://github.com/OWNER/REPO/discussions/123#discussioncomment-3
--
Second reply
`),
},
{
name: "replies json",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
return exampleDiscussionWithReplies(""), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
Exporter: jsonExporter("comments"),
},
wantStdout: compactJSON(heredoc.Doc(`
{
"comments": {
"nodes": [
{
"author": {"id": "", "login": "octocat", "name": ""},
"body": "This is the parent comment",
"createdAt": "2025-03-02T00:00:00Z",
"id": "DC_abc",
"isAnswer": true,
"reactionGroups": [
{"content": "THUMBS_UP", "totalCount": 3}
],
"replies": {
"nodes": [
{
"author": {"id": "", "login": "hubot", "name": ""},
"body": "First reply",
"createdAt": "2025-03-02T01:00:00Z",
"id": "R1",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2"
},
{
"author": {"id": "", "login": "monalisa", "name": ""},
"body": "Second reply",
"createdAt": "2025-03-02T02:00:00Z",
"id": "R2",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3"
}
],
"totalCount": 2
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1"
}
],
"totalCount": 1
}
}
`)),
},
{
name: "replies json pagination",
clientStub: func(t *testing.T, m *client.DiscussionClientMock) {
m.GetCommentRepliesFunc = func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) {
assert.Equal(t, "OWNER/REPO", ghrepo.FullName(repo))
assert.Equal(t, 123, number)
assert.Equal(t, "DC_abc", commentID)
assert.Equal(t, 30, limit)
assert.Equal(t, "", after)
assert.Equal(t, true, newest)
return exampleDiscussionWithReplies("NEXT_REP_CUR"), nil
}
},
opts: ViewOptions{
Replies: "DC_abc",
Exporter: jsonExporter("comments"),
},
wantStdout: compactJSON(heredoc.Doc(`
{
"comments": {
"nodes": [
{
"author": {"id": "", "login": "octocat", "name": ""},
"body": "This is the parent comment",
"createdAt": "2025-03-02T00:00:00Z",
"id": "DC_abc",
"isAnswer": true,
"reactionGroups": [
{"content": "THUMBS_UP", "totalCount": 3}
],
"replies": {
"next": "NEXT_REP_CUR",
"nodes": [
{
"author": {"id": "", "login": "hubot", "name": ""},
"body": "First reply",
"createdAt": "2025-03-02T01:00:00Z",
"id": "R1",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2"
},
{
"author": {"id": "", "login": "monalisa", "name": ""},
"body": "Second reply",
"createdAt": "2025-03-02T02:00:00Z",
"id": "R2",
"isAnswer": false,
"reactionGroups": [],
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3"
}
],
"totalCount": 2
},
"upvoteCount": 0,
"url": "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1"
}
],
"totalCount": 1
}
}
`)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
mock := &client.DiscussionClientMock{}
tt.clientStub(t, mock)
b := &browser.Stub{}
opts := tt.opts
opts.IO = ios
opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }
opts.Client = func() (client.DiscussionClient, error) { return mock, nil }
opts.Browser = b
opts.DiscussionNumber = 123
opts.Now = fixedNow
if opts.Limit == 0 {
opts.Limit = 30
}
if opts.Order == "" {
opts.Order = "newest"
}
err := viewRun(&opts)
require.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
if tt.wantBrowser != "" {
b.Verify(t, tt.wantBrowser)
}
})
}
}
func exampleDiscussionWithComments() *client.Discussion {
d := exampleAnswerableDiscussion()
d.Comments = client.DiscussionCommentList{
TotalCount: 2,
Comments: []client.DiscussionComment{
{
ID: "C_1",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1",
Author: client.DiscussionActor{Login: "octocat"},
Body: "This is a comment",
CreatedAt: time.Date(2025, 3, 2, 0, 0, 0, 0, time.UTC),
IsAnswer: true,
ReactionGroups: []client.ReactionGroup{
{Content: "THUMBS_UP", TotalCount: 3},
},
Replies: client.DiscussionCommentList{
TotalCount: 5,
Comments: []client.DiscussionComment{
{
ID: "C_1_R1",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2",
Author: client.DiscussionActor{Login: "hubot"},
Body: "Thanks!",
CreatedAt: time.Date(2025, 3, 2, 1, 0, 0, 0, time.UTC),
},
},
},
},
{
ID: "C_2",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3",
Author: client.DiscussionActor{Login: "monalisa"},
Body: "Another comment",
CreatedAt: time.Date(2025, 3, 3, 0, 0, 0, 0, time.UTC),
},
},
}
return d
}
func exampleDiscussionWithReplies(nextCursor string) *client.Discussion {
d := exampleAnswerableDiscussion()
d.Comments = client.DiscussionCommentList{
TotalCount: 1,
Comments: []client.DiscussionComment{
{
ID: "DC_abc",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-1",
Author: client.DiscussionActor{Login: "octocat"},
Body: "This is the parent comment",
CreatedAt: time.Date(2025, 3, 2, 0, 0, 0, 0, time.UTC),
IsAnswer: true,
ReactionGroups: []client.ReactionGroup{
{Content: "THUMBS_UP", TotalCount: 3},
},
Replies: client.DiscussionCommentList{
TotalCount: 2,
NextCursor: nextCursor,
Comments: []client.DiscussionComment{
{
ID: "R1",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-2",
Author: client.DiscussionActor{Login: "hubot"},
Body: "First reply",
CreatedAt: time.Date(2025, 3, 2, 1, 0, 0, 0, time.UTC),
},
{
ID: "R2",
URL: "https://github.com/OWNER/REPO/discussions/123#discussioncomment-3",
Author: client.DiscussionActor{Login: "monalisa"},
Body: "Second reply",
CreatedAt: time.Date(2025, 3, 2, 2, 0, 0, 0, time.UTC),
},
},
},
},
},
}
return d
}
func exampleAnswerableDiscussion() *client.Discussion {
return &client.Discussion{
ID: "D_123",
Number: 123,
Title: "an interesting question",
Body: "about my interesting question",
URL: "https://github.com/OWNER/REPO/discussions/123",
Closed: false,
Author: client.DiscussionActor{Login: "monalisa"},
Category: client.DiscussionCategory{
Name: "Q&A", Slug: "q-a", IsAnswerable: true,
},
Labels: []client.DiscussionLabel{{Name: "help-wanted", Color: "0075ca"}},
Answered: false,
Comments: client.DiscussionCommentList{TotalCount: 3},
ReactionGroups: []client.ReactionGroup{
{Content: "THUMBS_UP", TotalCount: 5},
{Content: "ROCKET", TotalCount: 2},
},
CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
}
}
func exampleUnanswerableDiscussion() *client.Discussion {
return &client.Discussion{
ID: "D_123",
Number: 123,
Title: "a cool discussion",
Body: "about my cool idea",
URL: "https://github.com/OWNER/REPO/discussions/123",
Closed: false,
Author: client.DiscussionActor{Login: "monalisa"},
Category: client.DiscussionCategory{
Name: "General", Slug: "general", IsAnswerable: false,
},
Labels: []client.DiscussionLabel{{Name: "help-wanted", Color: "0075ca"}},
Answered: false,
Comments: client.DiscussionCommentList{TotalCount: 3},
ReactionGroups: []client.ReactionGroup{
{Content: "THUMBS_UP", TotalCount: 5},
{Content: "ROCKET", TotalCount: 2},
},
CreatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2025, 3, 1, 0, 0, 0, 0, time.UTC),
}
}
func compactJSON(s string) string {
var buf bytes.Buffer
if err := json.Compact(&buf, []byte(s)); err != nil {
panic(fmt.Sprintf("compactJSON: %v", err))
}
return buf.String() + "\n"
}
func jsonExporter(fields ...string) cmdutil.Exporter {
e := cmdutil.NewJSONExporter()
e.SetFields(fields)
return e
}