package edit import ( "bytes" "context" "io" "net/http" "testing" "github.com/cli/cli/v2/pkg/prompt" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewCmdEdit(t *testing.T) { tests := []struct { name string args string wantOpts EditOptions wantErr string }{ { name: "change repo description", args: "--description hello", wantOpts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), Edits: EditRepositoryInput{ Description: sp("hello"), }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStdinTTY(true) ios.SetStderrTTY(true) f := &cmdutil.Factory{ IOStreams: ios, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, HttpClient: func() (*http.Client, error) { return nil, nil }, } argv, err := shlex.Split(tt.args) assert.NoError(t, err) var gotOpts *EditOptions cmd := NewCmdEdit(f, func(opts *EditOptions) error { gotOpts = opts return nil }) cmd.Flags().BoolP("help", "x", false, "") cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() if tt.wantErr != "" { assert.EqualError(t, err, tt.wantErr) return } require.NoError(t, err) assert.Equal(t, ghrepo.FullName(tt.wantOpts.Repository), ghrepo.FullName(gotOpts.Repository)) assert.Equal(t, tt.wantOpts.Edits, gotOpts.Edits) }) } } func Test_editRun(t *testing.T) { tests := []struct { name string opts EditOptions httpStubs func(*testing.T, *httpmock.Registry) wantsStderr string wantsErr string }{ { name: "change name and description", opts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), Edits: EditRepositoryInput{ Homepage: sp("newURL"), Description: sp("hello world!"), }, }, httpStubs: func(t *testing.T, r *httpmock.Registry) { r.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, 2, len(payload)) assert.Equal(t, "newURL", payload["homepage"]) assert.Equal(t, "hello world!", payload["description"]) })) }, }, { name: "add and remove topics", opts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), AddTopics: []string{"topic1", "topic2"}, RemoveTopics: []string{"topic3"}, }, httpStubs: func(t *testing.T, r *httpmock.Registry) { r.Register( httpmock.REST("GET", "repos/OWNER/REPO/topics"), httpmock.StringResponse(`{"names":["topic2", "topic3", "go"]}`)) r.Register( httpmock.REST("PUT", "repos/OWNER/REPO/topics"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, 1, len(payload)) assert.Equal(t, []interface{}{"topic2", "go", "topic1"}, payload["names"]) })) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStdinTTY(true) ios.SetStderrTTY(true) httpReg := &httpmock.Registry{} defer httpReg.Verify(t) if tt.httpStubs != nil { tt.httpStubs(t, httpReg) } opts := &tt.opts opts.HTTPClient = &http.Client{Transport: httpReg} opts.IO = ios err := editRun(context.Background(), opts) if tt.wantsErr == "" { require.NoError(t, err) } else { assert.EqualError(t, err, tt.wantsErr) return } }) } } func Test_editRun_interactive(t *testing.T) { tests := []struct { name string opts EditOptions askStubs func(*prompt.AskStubber) httpStubs func(*testing.T, *httpmock.Registry) wantsStderr string wantsErr string }{ { name: "updates repo description", opts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), InteractiveMode: true, }, askStubs: func(as *prompt.AskStubber) { as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description"}) as.StubPrompt("Description of the repository").AnswerWith("awesome repo description") }, httpStubs: func(t *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "old description", "homePageUrl": "https://url.com", "defaultBranchRef": { "name": "main" }, "isInOrganization": false, "repositoryTopics": { "nodes": [{ "topic": { "name": "x" } }] } } } }`)) reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, "awesome repo description", payload["description"]) })) }, }, { name: "updates repo topics", opts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), InteractiveMode: true, }, askStubs: func(as *prompt.AskStubber) { as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Description", "Topics"}) as.StubPrompt("Description of the repository").AnswerWith("awesome repo description") as.StubPrompt("Add topics?(csv format)").AnswerWith("a, b,c,d ") as.StubPrompt("Remove Topics").AnswerWith([]string{"x"}) }, httpStubs: func(t *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "old description", "homePageUrl": "https://url.com", "defaultBranchRef": { "name": "main" }, "isInOrganization": false, "repositoryTopics": { "nodes": [{ "topic": { "name": "x" } }] } } } }`)) reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, "awesome repo description", payload["description"]) })) reg.Register( httpmock.REST("PUT", "repos/OWNER/REPO/topics"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, []interface{}{"a", "b", "c", "d"}, payload["names"]) })) }, }, { name: "updates repo merge options", opts: EditOptions{ Repository: ghrepo.NewWithHost("OWNER", "REPO", "github.com"), InteractiveMode: true, }, askStubs: func(as *prompt.AskStubber) { as.StubPrompt("What do you want to edit?").AnswerWith([]string{"Merge Options"}) as.StubPrompt("Allowed merge strategies").AnswerWith([]string{allowMergeCommits, allowRebaseMerge}) as.StubPrompt("Enable Auto Merge?").AnswerWith(false) as.StubPrompt("Automatically delete head branches after merging?").AnswerWith(false) }, httpStubs: func(t *testing.T, reg *httpmock.Registry) { reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "old description", "homePageUrl": "https://url.com", "defaultBranchRef": { "name": "main" }, "isInOrganization": false, "squashMergeAllowed": false, "rebaseMergeAllowed": true, "mergeCommitAllowed": true, "deleteBranchOnMerge": false, "repositoryTopics": { "nodes": [{ "topic": { "name": "x" } }] } } } }`)) reg.Register( httpmock.REST("PATCH", "repos/OWNER/REPO"), httpmock.RESTPayload(200, `{}`, func(payload map[string]interface{}) { assert.Equal(t, true, payload["allow_merge_commit"]) assert.Equal(t, false, payload["allow_squash_merge"]) assert.Equal(t, true, payload["allow_rebase_merge"]) })) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdoutTTY(true) ios.SetStdinTTY(true) ios.SetStderrTTY(true) httpReg := &httpmock.Registry{} defer httpReg.Verify(t) if tt.httpStubs != nil { tt.httpStubs(t, httpReg) } opts := &tt.opts opts.HTTPClient = &http.Client{Transport: httpReg} opts.IO = ios as := prompt.NewAskStubber(t) if tt.askStubs != nil { tt.askStubs(as) } err := editRun(context.Background(), opts) if tt.wantsErr == "" { require.NoError(t, err) } else { assert.EqualError(t, err, tt.wantsErr) return } }) } } func sp(v string) *string { return &v }