From 0c0d316b9a5536a7833653b1d6de2ae3140ff016 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Thu, 7 May 2026 09:08:51 +0100 Subject: [PATCH] test(discussion create): consolidate into TestCreateRun table test - Merge TestCreateRun_nonInteractive, TestCreateRun_tty, and related tests into a single TestCreateRun table with 11 cases - Add partial-flag cases for missing title, body, and category - Add tty blank body returns error case - Add tty does not prompt when all flags provided case Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cmd/discussion/create/create_test.go | 327 +++++++++++++---------- 1 file changed, 192 insertions(+), 135 deletions(-) diff --git a/pkg/cmd/discussion/create/create_test.go b/pkg/cmd/discussion/create/create_test.go index c840d8bf5..8aa273aae 100644 --- a/pkg/cmd/discussion/create/create_test.go +++ b/pkg/cmd/discussion/create/create_test.go @@ -15,22 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -func sampleCategories() []client.DiscussionCategory { - return []client.DiscussionCategory{ - {ID: "CAT1", Name: "General", Slug: "general"}, - {ID: "CAT2", Name: "Q&A", Slug: "q-a"}, - {ID: "CAT3", Name: "Show and tell", Slug: "show-and-tell"}, - } -} - -func sampleDiscussion() *client.Discussion { - return &client.Discussion{ - Number: 5, - Title: "My question", - URL: "https://github.com/OWNER/REPO/discussions/5", - } -} - func TestNewCmdCreate(t *testing.T) { tests := []struct { name string @@ -140,16 +124,18 @@ func TestNewCmdCreate(t *testing.T) { } } -func TestCreateRun_nonInteractive(t *testing.T) { +func TestCreateRun(t *testing.T) { tests := []struct { name string opts CreateOptions + isTTY bool + setupMock func(*client.DiscussionClientMock) + prompter *prompter.PrompterMock wantErr string wantOut string - setupMock func(*client.DiscussionClientMock) }{ { - name: "creates discussion successfully", + name: "success non-tty", opts: CreateOptions{ Title: "My question", Body: "Details", @@ -169,26 +155,26 @@ func TestCreateRun_nonInteractive(t *testing.T) { wantOut: "https://github.com/OWNER/REPO/discussions/5\n", }, { - name: "creates with label", + name: "success non-tty with label", opts: CreateOptions{ Title: "Feature request", Body: "Details", Category: "general", - Labels: []string{"enhancement"}, + Labels: []string{"enhancement", "bug"}, }, setupMock: func(m *client.DiscussionClientMock) { m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { return sampleCategories(), nil } m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { - assert.Equal(t, []string{"enhancement"}, input.Labels) + assert.Equal(t, []string{"enhancement", "bug"}, input.Labels) return sampleDiscussion(), nil } }, wantOut: "https://github.com/OWNER/REPO/discussions/5\n", }, { - name: "unknown category returns error", + name: "non-tty unknown category", opts: CreateOptions{ Title: "My question", Body: "Details", @@ -202,7 +188,7 @@ func TestCreateRun_nonInteractive(t *testing.T) { wantErr: `unknown category: "nonexistent"`, }, { - name: "ListCategories error propagates", + name: "non-tty list categories query errors", opts: CreateOptions{ Title: "My question", Body: "Details", @@ -216,7 +202,7 @@ func TestCreateRun_nonInteractive(t *testing.T) { wantErr: "fetching categories: network error", }, { - name: "Create error propagates", + name: "non-tty create mutation errors", opts: CreateOptions{ Title: "My question", Body: "Details", @@ -230,22 +216,189 @@ func TestCreateRun_nonInteractive(t *testing.T) { return nil, fmt.Errorf("mutation failed") } }, - wantErr: "creating discussion: mutation failed", + wantErr: "failed to create discussion: mutation failed", + }, + { + name: "tty prompts for all fields", + isTTY: true, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { + assert.Equal(t, "My question", input.Title) + assert.Equal(t, "CAT1", input.CategoryID) + assert.Equal(t, "Some body text", input.Body) + return sampleDiscussion(), nil + } + }, + prompter: &prompter.PrompterMock{ + InputFunc: func(prompt, defaultValue string) (string, error) { + return "My question", nil + }, + SelectFunc: func(prompt, defaultValue string, options []string) (int, error) { + assert.Equal(t, []string{"General", "Q&A", "Show and tell"}, options) + return 0, nil + }, + MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) { + assert.False(t, blankAllowed, "body editor should not allow blank input") + return "Some body text", nil + }, + }, + wantOut: "https://github.com/OWNER/REPO/discussions/5\n", + }, + { + name: "tty does not prompt when all flags provided", + isTTY: true, + opts: CreateOptions{ + Title: "My question", + Body: "Details", + Category: "Q&A", + }, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { + assert.Equal(t, "CAT2", input.CategoryID) + assert.Equal(t, "My question", input.Title) + assert.Equal(t, "Details", input.Body) + return sampleDiscussion(), nil + } + }, + wantOut: "https://github.com/OWNER/REPO/discussions/5\n", + }, + { + name: "tty partial flags prompts only for missing category", + isTTY: true, + opts: CreateOptions{ + Title: "Pre-filled title", + Body: "Pre-filled body", + }, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { + assert.Equal(t, "Pre-filled title", input.Title) + assert.Equal(t, "CAT2", input.CategoryID) + assert.Equal(t, "Pre-filled body", input.Body) + return sampleDiscussion(), nil + } + }, + prompter: &prompter.PrompterMock{ + SelectFunc: func(prompt, defaultValue string, options []string) (int, error) { + return 1, nil + }, + }, + wantOut: "https://github.com/OWNER/REPO/discussions/5\n", + }, + { + name: "tty partial flags prompts only for missing body", + isTTY: true, + opts: CreateOptions{ + Title: "Pre-filled title", + Category: "Q&A", + }, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { + assert.Equal(t, "Pre-filled title", input.Title) + assert.Equal(t, "CAT2", input.CategoryID) + assert.Equal(t, "Prompted body", input.Body) + return sampleDiscussion(), nil + } + }, + prompter: &prompter.PrompterMock{ + MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) { + return "Prompted body", nil + }, + }, + wantOut: "https://github.com/OWNER/REPO/discussions/5\n", + }, + { + name: "tty partial flags prompts only for missing title", + isTTY: true, + opts: CreateOptions{ + Body: "Pre-filled body", + Category: "General", + }, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + m.CreateFunc = func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { + assert.Equal(t, "Prompted title", input.Title) + assert.Equal(t, "CAT1", input.CategoryID) + assert.Equal(t, "Pre-filled body", input.Body) + return sampleDiscussion(), nil + } + }, + prompter: &prompter.PrompterMock{ + InputFunc: func(prompt, defaultValue string) (string, error) { + return "Prompted title", nil + }, + }, + wantOut: "https://github.com/OWNER/REPO/discussions/5\n", + }, + { + name: "tty blank title returns error", + isTTY: true, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + }, + prompter: &prompter.PrompterMock{ + InputFunc: func(prompt, defaultValue string) (string, error) { + return " ", nil + }, + }, + wantErr: "title cannot be blank", + }, + { + name: "tty blank body returns error", + isTTY: true, + opts: CreateOptions{ + Title: "Valid title", + Category: "General", + }, + setupMock: func(m *client.DiscussionClientMock) { + m.ListCategoriesFunc = func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { + return sampleCategories(), nil + } + }, + prompter: &prompter.PrompterMock{ + MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) { + return " ", nil + }, + }, + wantErr: "body cannot be blank", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, stdout, _ := iostreams.Test() - // non-interactive: no TTY + ios.SetStdoutTTY(tt.isTTY) + ios.SetStdinTTY(tt.isTTY) mockClient := &client.DiscussionClientMock{} tt.setupMock(mockClient) 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 mockClient, nil } + opts.BaseRepo = func() (ghrepo.Interface, error) { + return ghrepo.New("OWNER", "REPO"), nil + } + opts.Client = func() (client.DiscussionClient, error) { + return mockClient, nil + } + if tt.prompter != nil { + opts.Prompter = tt.prompter + } err := createRun(&opts) if tt.wantErr != "" { @@ -259,114 +412,18 @@ func TestCreateRun_nonInteractive(t *testing.T) { } } -func TestCreateRun_tty(t *testing.T) { - ios, _, stdout, stderr := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStdinTTY(true) - - mockClient := &client.DiscussionClientMock{ - ListCategoriesFunc: func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { - return sampleCategories(), nil - }, - CreateFunc: func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { - assert.Equal(t, "My question", input.Title) - assert.Equal(t, "CAT1", input.CategoryID) - assert.Equal(t, "Some body text", input.Body) - return sampleDiscussion(), nil - }, +func sampleCategories() []client.DiscussionCategory { + return []client.DiscussionCategory{ + {ID: "CAT1", Name: "General", Slug: "general"}, + {ID: "CAT2", Name: "Q&A", Slug: "q-a"}, + {ID: "CAT3", Name: "Show and tell", Slug: "show-and-tell"}, } - - pm := &prompter.PrompterMock{ - InputFunc: func(prompt, defaultValue string) (string, error) { - return "My question", nil - }, - SelectFunc: func(prompt, defaultValue string, options []string) (int, error) { - assert.Equal(t, []string{"General", "Q&A", "Show and tell"}, options) - return 0, nil - }, - MarkdownEditorFunc: func(prompt, defaultValue string, blankAllowed bool) (string, error) { - assert.False(t, blankAllowed, "body editor should not allow blank input") - return "Some body text", nil - }, - } - - opts := &CreateOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, - Client: func() (client.DiscussionClient, error) { return mockClient, nil }, - Prompter: pm, - } - - err := createRun(opts) - require.NoError(t, err) - assert.Contains(t, stderr.String(), "Created discussion #5") - assert.Equal(t, "https://github.com/OWNER/REPO/discussions/5\n", stdout.String()) } -func TestCreateRun_tty_partialFlags(t *testing.T) { - // Title and body provided, category via prompt - ios, _, stdout, stderr := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStdinTTY(true) - - mockClient := &client.DiscussionClientMock{ - ListCategoriesFunc: func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { - return sampleCategories(), nil - }, - CreateFunc: func(repo ghrepo.Interface, input client.CreateDiscussionInput) (*client.Discussion, error) { - assert.Equal(t, "Pre-filled title", input.Title) - assert.Equal(t, "CAT2", input.CategoryID) - assert.Equal(t, "Pre-filled body", input.Body) - return sampleDiscussion(), nil - }, +func sampleDiscussion() *client.Discussion { + return &client.Discussion{ + Number: 5, + Title: "My question", + URL: "https://github.com/OWNER/REPO/discussions/5", } - - pm := &prompter.PrompterMock{ - SelectFunc: func(prompt, defaultValue string, options []string) (int, error) { - return 1, nil // select Q&A - }, - } - - opts := &CreateOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, - Client: func() (client.DiscussionClient, error) { return mockClient, nil }, - Prompter: pm, - Title: "Pre-filled title", - Body: "Pre-filled body", - } - - err := createRun(opts) - require.NoError(t, err) - assert.Contains(t, stderr.String(), "Created discussion #5") - assert.Equal(t, "https://github.com/OWNER/REPO/discussions/5\n", stdout.String()) -} - -func TestCreateRun_tty_blankTitle(t *testing.T) { - ios, _, _, _ := iostreams.Test() - ios.SetStdoutTTY(true) - ios.SetStdinTTY(true) - - mockClient := &client.DiscussionClientMock{ - ListCategoriesFunc: func(repo ghrepo.Interface) ([]client.DiscussionCategory, error) { - return sampleCategories(), nil - }, - } - - pm := &prompter.PrompterMock{ - InputFunc: func(prompt, defaultValue string) (string, error) { - return " ", nil // whitespace only - }, - } - - opts := &CreateOptions{ - IO: ios, - BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, - Client: func() (client.DiscussionClient, error) { return mockClient, nil }, - Prompter: pm, - } - - err := createRun(opts) - require.Error(t, err) - assert.Contains(t, err.Error(), "title cannot be blank") }