From 52f219a5aca05384e76bc243768214022248cbb3 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Wed, 29 Apr 2026 13:05:21 +0100 Subject: [PATCH] test(discussion view): consolidate view run tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/discussion/view/view_test.go | 1798 ++++++++++++-------------- 1 file changed, 823 insertions(+), 975 deletions(-) diff --git a/pkg/cmd/discussion/view/view_test.go b/pkg/cmd/discussion/view/view_test.go index 71897a173..0c975f3c2 100644 --- a/pkg/cmd/discussion/view/view_test.go +++ b/pkg/cmd/discussion/view/view_test.go @@ -2,41 +2,46 @@ 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 testDiscussion() *client.Discussion { - return &client.Discussion{ - ID: "D_123", - Number: 123, - Title: "How to authenticate with SSO?", - Body: "I need help with SSO authentication.", - 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 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) { @@ -264,257 +269,750 @@ func TestNewCmdView(t *testing.T) { } } -func TestViewRun_tty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) +func TestViewRun(t *testing.T) { + fixedNow := func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) } - d := testDiscussion() - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil + 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 + } + } + `)), }, } - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) }, + 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) + } + }) } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "How to authenticate with SSO?") - assert.Contains(t, out, "#123") - assert.Contains(t, out, "Q&A") - assert.Contains(t, out, "Asked by") - assert.Contains(t, out, "monalisa") - assert.Contains(t, out, "3 comments") - assert.Contains(t, out, "help-wanted") - assert.Contains(t, out, "View this discussion on GitHub") } -func TestViewRun_nontty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussion() - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "title:\tHow to authenticate with SSO?") - assert.Contains(t, out, "state:\tOPEN") - assert.Contains(t, out, "category:\tQ&A") - assert.Contains(t, out, "author:\tmonalisa") - assert.Contains(t, out, "labels:\thelp-wanted") - assert.Contains(t, out, "number:\t123") - assert.Contains(t, out, "--") - assert.Contains(t, out, "I need help with SSO authentication.") -} - -func TestViewRun_json(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - exporter := cmdutil.NewJSONExporter() - exporter.SetFields(discussionFields) - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Limit: 30, - Order: "newest", - Exporter: exporter, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, `"title"`) - assert.Contains(t, out, `"number"`) - assert.Contains(t, out, "How to authenticate with SSO?") -} - -func TestViewRun_web(t *testing.T) { - ios, _, _, stderr := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - b := &browser.Stub{} - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Browser: b, - DiscussionNumber: 123, - WebMode: true, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - b.Verify(t, "https://github.com/OWNER/REPO/discussions/123") - assert.Contains(t, stderr.String(), "Opening") -} - -func TestViewRun_urlArg(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussion() - d.URL = "https://github.com/OTHER/REPO/discussions/42" - d.Number = 42 - - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - assert.Equal(t, "OTHER", repo.RepoOwner()) - assert.Equal(t, "REPO", repo.RepoName()) - assert.Equal(t, 42, number) - return d, nil - }, - } - - f := &cmdutil.Factory{} - 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 - opts.Client = func() (client.DiscussionClient, error) { - return mock, nil - } - return viewRun(opts) - }) - - cmd.SetArgs([]string{"https://github.com/OTHER/REPO/discussions/42"}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - require.NoError(t, err) - assert.Equal(t, 42, gotOpts.DiscussionNumber) - - out := stdout.String() - assert.Contains(t, out, "number:\t42") -} - -func TestViewRun_answerable(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - d := testDiscussion() - d.Category.IsAnswerable = true - - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - assert.Contains(t, stdout.String(), "Asked by") -} - -func TestViewRun_notAnswerable(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - d := testDiscussion() - d.Category.Name = "General" - d.Category.IsAnswerable = false - - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Now: func() time.Time { return time.Date(2025, 3, 1, 1, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "Started by") - assert.NotContains(t, out, "Asked by") -} - -func testDiscussionWithComments() *client.Discussion { - d := testDiscussion() +func exampleDiscussionWithComments() *client.Discussion { + d := exampleAnswerableDiscussion() d.Comments = client.DiscussionCommentList{ TotalCount: 2, Comments: []client.DiscussionComment{ @@ -553,558 +1051,8 @@ func testDiscussionWithComments() *client.Discussion { return d } -func TestViewRun_comments_tty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - d := testDiscussionWithComments() - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - assert.Equal(t, 30, commentLimit) - assert.Equal(t, false, newest) - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Now: func() time.Time { return time.Date(2025, 3, 4, 0, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "Comments") - assert.Contains(t, out, "octocat") - assert.Contains(t, out, "โœ“ Answer") - assert.Contains(t, out, "This is a comment") - assert.Contains(t, out, "hubot") - assert.Contains(t, out, "Thanks!") - assert.Contains(t, out, "And 4 more replies") - assert.Contains(t, out, "monalisa") - assert.Contains(t, out, "Another comment") -} - -func TestViewRun_comments_nontty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "comment:\toctocat\t") - assert.Contains(t, out, "answer") - assert.Contains(t, out, "This is a comment") - assert.Contains(t, out, "comment:\thubot\t") - assert.Contains(t, out, "comment:\tmonalisa\t") -} - -func TestViewRun_comments_json(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - exporter := cmdutil.NewJSONExporter() - exporter.SetFields(discussionFields) - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Exporter: exporter, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, `"totalCount"`) - assert.Contains(t, out, `"isAnswer":true`) - assert.Contains(t, out, `"octocat"`) -} - -func TestNewCmdView_orderWithoutComments(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{} - - cmd := NewCmdView(f, func(opts *ViewOptions) error { - return nil - }) - - cmd.SetArgs([]string{"123", "--order", "newest"}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "--order requires --comments") -} - -func TestViewRun_noComments_usesGetByNumber(t *testing.T) { - ios, _, _, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussion() - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: false, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - assert.Equal(t, 1, len(mock.GetByNumberCalls())) - assert.Equal(t, 0, len(mock.GetWithCommentsCalls())) -} - -func TestNewCmdView_limitWithoutComments(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{} - - cmd := NewCmdView(f, func(opts *ViewOptions) error { - return nil - }) - - cmd.SetArgs([]string{"123", "--limit", "10"}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "--limit requires --comments") -} - -func TestNewCmdView_afterWithoutComments(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{} - - cmd := NewCmdView(f, func(opts *ViewOptions) error { - return nil - }) - - cmd.SetArgs([]string{"123", "--after", "CURSOR_ABC"}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "--after requires --comments") -} - -func TestNewCmdView_invalidLimit(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{} - - cmd := NewCmdView(f, func(opts *ViewOptions) error { - return nil - }) - - cmd.SetArgs([]string{"123", "--comments", "--limit", "0"}) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid limit") -} - -func TestViewRun_commentsWithPagination_tty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - d := testDiscussionWithComments() - d.Comments.NextCursor = "NEXT_CURSOR_123" - - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - assert.Equal(t, 10, commentLimit) - assert.Equal(t, "CURSOR_ABC", after) - assert.Equal(t, false, newest) - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 10, - After: "CURSOR_ABC", - Order: "oldest", - Now: func() time.Time { return time.Date(2025, 3, 4, 0, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "To see more comments, pass: --after NEXT_CURSOR_123") -} - -func TestViewRun_commentsWithPagination_nontty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - d.Comments.NextCursor = "NEXT_CURSOR_456" - - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, "next:\tNEXT_CURSOR_456") -} - -func TestViewRun_commentsWithPagination_json(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - d.Comments.Cursor = "PREV_CURSOR" - d.Comments.NextCursor = "NEXT_CURSOR_789" - - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - exporter := cmdutil.NewJSONExporter() - exporter.SetFields(discussionFields) - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Exporter: exporter, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.Contains(t, out, `"cursor":"PREV_CURSOR"`) - assert.Contains(t, out, `"next":"NEXT_CURSOR_789"`) -} - -func TestViewRun_noPaginationCursor_tty(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStderrTTY(true) - - d := testDiscussionWithComments() - - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: true, - Limit: 30, - Order: "oldest", - Now: func() time.Time { return time.Date(2025, 3, 4, 0, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - assert.NotContains(t, out, "--after") -} - -func TestViewRun_jsonComments_usesGetWithComments(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussionWithComments() - mock := &client.DiscussionClientMock{ - GetWithCommentsFunc: func(repo ghrepo.Interface, number int, commentLimit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - exporter := cmdutil.NewJSONExporter() - exporter.SetFields([]string{"comments"}) - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: false, - Limit: 30, - Order: "newest", - Exporter: exporter, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - // --json comments should use GetWithComments even without --comments flag - assert.Equal(t, 0, len(mock.GetByNumberCalls())) - assert.Equal(t, 1, len(mock.GetWithCommentsCalls())) - - out := stdout.String() - assert.Contains(t, out, `"totalCount"`) - assert.Contains(t, out, `"octocat"`) -} - -func TestViewRun_jsonWithoutComments_usesGetByNumber(t *testing.T) { - ios, _, _, _ := iostreams.Test() - ios.SetStdoutTTY(false) - - d := testDiscussion() - mock := &client.DiscussionClientMock{ - GetByNumberFunc: func(repo ghrepo.Interface, number int) (*client.Discussion, error) { - return d, nil - }, - } - - exporter := cmdutil.NewJSONExporter() - exporter.SetFields([]string{"title", "number"}) - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Comments: false, - Exporter: exporter, - Now: time.Now, - } - - err := viewRun(opts) - require.NoError(t, err) - - // --json title,number should NOT fetch comments - assert.Equal(t, 1, len(mock.GetByNumberCalls())) - assert.Equal(t, 0, len(mock.GetWithCommentsCalls())) -} - -// --------------------------------------------------------------------------- -// --replies flag validation -// --------------------------------------------------------------------------- - -func TestNewCmdView_repliesFlags(t *testing.T) { - tests := []struct { - name string - args []string - wantErr string - }{ - { - name: "replies with comments is mutually exclusive", - args: []string{"123", "--replies", "DC_abc", "--comments"}, - wantErr: "specify only one of --comments, --replies, or --web", - }, - { - name: "replies with web is mutually exclusive", - args: []string{"123", "--replies", "DC_abc", "--web"}, - wantErr: "specify only one of --comments, --replies, or --web", - }, - { - name: "order requires comments or replies", - args: []string{"123", "--order", "newest"}, - wantErr: "--order requires --comments or --replies", - }, - { - name: "limit requires comments or replies", - args: []string{"123", "--limit", "5"}, - wantErr: "--limit requires --comments or --replies", - }, - { - name: "after requires comments or replies", - args: []string{"123", "--after", "CURSOR"}, - wantErr: "--after requires --comments or --replies", - }, - { - name: "order works with replies", - args: []string{"123", "--replies", "DC_abc", "--order", "oldest"}, - }, - { - name: "limit works with replies", - args: []string{"123", "--replies", "DC_abc", "--limit", "10"}, - }, - { - name: "after works with replies", - args: []string{"123", "--replies", "DC_abc", "--after", "CURSOR"}, - }, - } - - 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{} - - cmd := NewCmdView(f, func(opts *ViewOptions) error { - return nil - }) - - cmd.SetArgs(tt.args) - cmd.SetOut(&bytes.Buffer{}) - cmd.SetErr(&bytes.Buffer{}) - - err := cmd.Execute() - if tt.wantErr != "" { - require.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) - return - } - require.NoError(t, err) - }) - } -} - -// --------------------------------------------------------------------------- -// --replies viewRun tests (table-driven) -// --------------------------------------------------------------------------- - -func testDiscussionWithReplies(nextCursor string) *client.Discussion { - d := testDiscussion() +func exampleDiscussionWithReplies(nextCursor string) *client.Discussion { + d := exampleAnswerableDiscussion() d.Comments = client.DiscussionCommentList{ TotalCount: 1, Comments: []client.DiscussionComment{ @@ -1144,164 +1092,64 @@ func testDiscussionWithReplies(nextCursor string) *client.Discussion { return d } -func TestViewRun_replies(t *testing.T) { - tests := []struct { - name string - tty bool - replies string - limit int - after string - order string - exporter cmdutil.Exporter - nextCursor string - wantContains []string - wantExcludes []string - wantClient func(*testing.T, *client.DiscussionClientMock) - }{ - { - name: "tty renders comment and replies", - tty: true, - replies: "DC_abc", - limit: 30, - order: "newest", - wantContains: []string{ - "octocat", - "This is the parent comment", - "โœ“ Answer", - "hubot", - "First reply", - "monalisa", - "Second reply", - }, +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, }, - { - name: "tty shows pagination hint", - tty: true, - replies: "DC_abc", - limit: 30, - order: "newest", - nextCursor: "NEXT_CUR", - wantContains: []string{ - "--after NEXT_CUR", - }, + 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}, }, - { - name: "tty no pagination hint when no next cursor", - tty: true, - replies: "DC_abc", - limit: 30, - order: "newest", - wantExcludes: []string{ - "--after", - }, - }, - { - name: "nontty raw output", - tty: false, - replies: "DC_abc", - limit: 30, - order: "oldest", - wantContains: []string{ - "comment:\toctocat\t", - "answer", - "replies:\t2", - "This is the parent comment", - "hubot", - "First reply", - }, - }, - { - name: "nontty shows next cursor", - tty: false, - replies: "DC_abc", - limit: 30, - order: "oldest", - nextCursor: "NEXT_CUR_456", - wantContains: []string{ - "next:\tNEXT_CUR_456", - }, - }, - { - name: "json output", - tty: false, - replies: "DC_abc", - limit: 30, - order: "newest", - exporter: func() cmdutil.Exporter { - e := cmdutil.NewJSONExporter() - e.SetFields(discussionFields) - return e - }(), - wantContains: []string{ - `"totalCount"`, - `"isAnswer":true`, - `"octocat"`, - }, - }, - { - name: "routes to GetCommentReplies only", - tty: false, - replies: "DC_abc", - limit: 10, - after: "CUR_A", - order: "oldest", - wantClient: func(t *testing.T, mock *client.DiscussionClientMock) { - require.Equal(t, 1, len(mock.GetCommentRepliesCalls())) - assert.Equal(t, 0, len(mock.GetByNumberCalls())) - assert.Equal(t, 0, len(mock.GetWithCommentsCalls())) - - call := mock.GetCommentRepliesCalls()[0] - assert.Equal(t, "DC_abc", call.CommentID) - assert.Equal(t, 10, call.Limit) - assert.Equal(t, "CUR_A", call.After) - assert.Equal(t, false, call.Newest) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ios, _, stdout, _ := iostreams.Test() - ios.SetStdoutTTY(tt.tty) - ios.SetStderrTTY(tt.tty) - - d := testDiscussionWithReplies(tt.nextCursor) - mock := &client.DiscussionClientMock{ - GetCommentRepliesFunc: func(repo ghrepo.Interface, number int, commentID string, limit int, after string, newest bool) (*client.Discussion, error) { - return d, nil - }, - } - - opts := &ViewOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { - return ghrepo.New("OWNER", "REPO"), nil - }, - Client: func() (client.DiscussionClient, error) { - return mock, nil - }, - DiscussionNumber: 123, - Replies: tt.replies, - Limit: tt.limit, - After: tt.after, - Order: tt.order, - Exporter: tt.exporter, - Now: func() time.Time { return time.Date(2025, 3, 4, 0, 0, 0, 0, time.UTC) }, - } - - err := viewRun(opts) - require.NoError(t, err) - - out := stdout.String() - for _, s := range tt.wantContains { - assert.Contains(t, out, s) - } - for _, s := range tt.wantExcludes { - assert.NotContains(t, out, s) - } - if tt.wantClient != nil { - tt.wantClient(t, mock) - } - }) + 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 +}