package view import ( "bytes" "fmt" "net/http" "testing" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/run" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/jsonfieldstest" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) func TestJSONFields(t *testing.T) { jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdView, []string{ "archivedAt", "assignableUsers", "codeOfConduct", "contactLinks", "createdAt", "defaultBranchRef", "deleteBranchOnMerge", "description", "diskUsage", "forkCount", "fundingLinks", "hasDiscussionsEnabled", "hasIssuesEnabled", "hasProjectsEnabled", "hasWikiEnabled", "homepageUrl", "id", "isArchived", "isBlankIssuesEnabled", "isEmpty", "isFork", "isInOrganization", "isMirror", "isPrivate", "isSecurityPolicyEnabled", "isTemplate", "isUserConfigurationRepository", "issueTemplates", "issues", "labels", "languages", "latestRelease", "licenseInfo", "mentionableUsers", "mergeCommitAllowed", "milestones", "mirrorUrl", "name", "nameWithOwner", "openGraphImageUrl", "owner", "parent", "primaryLanguage", "projects", "projectsV2", "pullRequestTemplates", "pullRequests", "pushedAt", "rebaseMergeAllowed", "repositoryTopics", "securityPolicyUrl", "sshUrl", "squashMergeAllowed", "stargazerCount", "templateRepository", "updatedAt", "url", "usesCustomOpenGraphImage", "viewerCanAdminister", "viewerDefaultCommitEmail", "viewerDefaultMergeMethod", "viewerHasStarred", "viewerPermission", "viewerPossibleCommitEmails", "viewerSubscription", "visibility", "watchers", }) } func TestNewCmdView(t *testing.T) { tests := []struct { name string cli string wants ViewOptions wantsErr bool }{ { name: "no args", cli: "", wants: ViewOptions{ RepoArg: "", Web: false, }, }, { name: "sets repo arg", cli: "some/repo", wants: ViewOptions{ RepoArg: "some/repo", Web: false, }, }, { name: "sets web", cli: "-w", wants: ViewOptions{ RepoArg: "", Web: true, }, }, { name: "sets branch", cli: "-b feat/awesome", wants: ViewOptions{ RepoArg: "", Branch: "feat/awesome", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { io, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ IOStreams: io, } // THOUGHT: this seems ripe for cmdutil. It's almost identical to the set up for the same test // in gist create. argv, err := shlex.Split(tt.cli) assert.NoError(t, err) var gotOpts *ViewOptions cmd := NewCmdView(f, func(opts *ViewOptions) error { gotOpts = opts return nil }) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() if tt.wantsErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Equal(t, tt.wants.Web, gotOpts.Web) assert.Equal(t, tt.wants.Branch, gotOpts.Branch) assert.Equal(t, tt.wants.RepoArg, gotOpts.RepoArg) }) } } func Test_RepoView_Web(t *testing.T) { tests := []struct { name string stdoutTTY bool wantStderr string wantBrowse string }{ { name: "tty", stdoutTTY: true, wantStderr: "Opening https://github.com/OWNER/REPO in your browser.\n", wantBrowse: "https://github.com/OWNER/REPO", }, { name: "nontty", stdoutTTY: false, wantStderr: "", wantBrowse: "https://github.com/OWNER/REPO", }, } for _, tt := range tests { reg := &httpmock.Registry{} reg.StubRepoInfoResponse("OWNER", "REPO", "main") browser := &browser.Stub{} opts := &ViewOptions{ Web: true, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, Browser: browser, } io, _, stdout, stderr := iostreams.Test() opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) _, teardown := run.Stub() defer teardown(t) if err := viewRun(opts); err != nil { t.Errorf("viewRun() error = %v", err) } assert.Equal(t, "", stdout.String()) assert.Equal(t, tt.wantStderr, stderr.String()) reg.Verify(t) browser.Verify(t, tt.wantBrowse) }) } } func Test_ViewRun(t *testing.T) { tests := []struct { name string opts *ViewOptions repoName string stdoutTTY bool wantOut string wantStderr string wantErr bool }{ { name: "nontty", wantOut: heredoc.Doc(` name: OWNER/REPO description: social distancing -- # truly cool readme check it out `), }, { name: "url arg", repoName: "jill/valentine", opts: &ViewOptions{ RepoArg: "https://github.com/jill/valentine", }, stdoutTTY: true, wantOut: heredoc.Doc(` jill/valentine social distancing # truly cool readme check it out View this repository on GitHub: https://github.com/jill/valentine `), }, { name: "name arg", repoName: "jill/valentine", opts: &ViewOptions{ RepoArg: "jill/valentine", }, stdoutTTY: true, wantOut: heredoc.Doc(` jill/valentine social distancing # truly cool readme check it out View this repository on GitHub: https://github.com/jill/valentine `), }, { name: "branch arg", opts: &ViewOptions{ Branch: "feat/awesome", }, stdoutTTY: true, wantOut: heredoc.Doc(` OWNER/REPO social distancing # truly cool readme check it out View this repository on GitHub: https://github.com/OWNER/REPO/tree/feat%2Fawesome `), }, { name: "no args", stdoutTTY: true, wantOut: heredoc.Doc(` OWNER/REPO social distancing # truly cool readme check it out View this repository on GitHub: https://github.com/OWNER/REPO `), }, } for _, tt := range tests { if tt.opts == nil { tt.opts = &ViewOptions{} } if tt.repoName == "" { tt.repoName = "OWNER/REPO" } tt.opts.BaseRepo = func() (ghrepo.Interface, error) { repo, _ := ghrepo.FromFullName(tt.repoName) return repo, nil } reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "social distancing" } } }`)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/readme", tt.repoName)), httpmock.StringResponse(` { "name": "readme.md", "content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`)) tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } io, _, stdout, stderr := iostreams.Test() tt.opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) if err := viewRun(tt.opts); (err != nil) != tt.wantErr { t.Errorf("viewRun() error = %v, wantErr %v", err, tt.wantErr) } assert.Equal(t, tt.wantStderr, stderr.String()) assert.Equal(t, tt.wantOut, stdout.String()) reg.Verify(t) }) } } func Test_ViewRun_NonMarkdownReadme(t *testing.T) { tests := []struct { name string stdoutTTY bool wantOut string }{ { name: "tty", wantOut: heredoc.Doc(` OWNER/REPO social distancing # truly cool readme check it out View this repository on GitHub: https://github.com/OWNER/REPO `), stdoutTTY: true, }, { name: "nontty", wantOut: heredoc.Doc(` name: OWNER/REPO description: social distancing -- # truly cool readme check it out `), }, } for _, tt := range tests { reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "social distancing" } } }`)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/readme"), httpmock.StringResponse(` { "name": "readme.org", "content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`)) opts := &ViewOptions{ HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, } io, _, stdout, stderr := iostreams.Test() opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) if err := viewRun(opts); err != nil { t.Errorf("viewRun() error = %v", err) } assert.Equal(t, tt.wantOut, stdout.String()) assert.Equal(t, "", stderr.String()) reg.Verify(t) }) } } func Test_ViewRun_NoReadme(t *testing.T) { tests := []struct { name string stdoutTTY bool wantOut string }{ { name: "tty", wantOut: heredoc.Doc(` OWNER/REPO social distancing This repository does not have a README View this repository on GitHub: https://github.com/OWNER/REPO `), stdoutTTY: true, }, { name: "nontty", wantOut: heredoc.Doc(` name: OWNER/REPO description: social distancing `), }, } for _, tt := range tests { reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "social distancing" } } }`)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/readme"), httpmock.StatusStringResponse(404, `{}`)) opts := &ViewOptions{ HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, } io, _, stdout, stderr := iostreams.Test() opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) if err := viewRun(opts); err != nil { t.Errorf("viewRun() error = %v", err) } assert.Equal(t, tt.wantOut, stdout.String()) assert.Equal(t, "", stderr.String()) reg.Verify(t) }) } } func Test_ViewRun_NoDescription(t *testing.T) { tests := []struct { name string stdoutTTY bool wantOut string }{ { name: "tty", wantOut: heredoc.Doc(` OWNER/REPO No description provided # truly cool readme check it out View this repository on GitHub: https://github.com/OWNER/REPO `), stdoutTTY: true, }, { name: "nontty", wantOut: heredoc.Doc(` name: OWNER/REPO description: -- # truly cool readme check it out `), }, } for _, tt := range tests { reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "" } } }`)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/readme"), httpmock.StringResponse(` { "name": "readme.org", "content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`)) opts := &ViewOptions{ HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, } io, _, stdout, stderr := iostreams.Test() opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) if err := viewRun(opts); err != nil { t.Errorf("viewRun() error = %v", err) } assert.Equal(t, tt.wantOut, stdout.String()) assert.Equal(t, "", stderr.String()) reg.Verify(t) }) } } func Test_ViewRun_WithoutUsername(t *testing.T) { reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(` { "data": { "viewer": { "login": "OWNER" }}}`)) reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "social distancing" } } }`)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/readme"), httpmock.StringResponse(` { "name": "readme.md", "content": "IyB0cnVseSBjb29sIHJlYWRtZSBjaGVjayBpdCBvdXQ="}`)) io, _, stdout, stderr := iostreams.Test() io.SetStdoutTTY(false) opts := &ViewOptions{ RepoArg: "REPO", HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, IO: io, Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil }, } if err := viewRun(opts); err != nil { t.Errorf("viewRun() error = %v", err) } assert.Equal(t, heredoc.Doc(` name: OWNER/REPO description: social distancing -- # truly cool readme check it out `), stdout.String()) assert.Equal(t, "", stderr.String()) reg.Verify(t) } func Test_ViewRun_HandlesSpecialCharacters(t *testing.T) { tests := []struct { name string opts *ViewOptions repoName string stdoutTTY bool wantOut string wantStderr string wantErr bool }{ { name: "nontty", wantOut: heredoc.Doc(` name: OWNER/REPO description: Some basic special characters " & / < > ' -- # < is always > than & ' and " `), }, { name: "no args", stdoutTTY: true, wantOut: heredoc.Doc(` OWNER/REPO Some basic special characters " & / < > ' # < is always > than & ' and " View this repository on GitHub: https://github.com/OWNER/REPO `), }, } for _, tt := range tests { if tt.opts == nil { tt.opts = &ViewOptions{} } if tt.repoName == "" { tt.repoName = "OWNER/REPO" } tt.opts.BaseRepo = func() (ghrepo.Interface, error) { repo, _ := ghrepo.FromFullName(tt.repoName) return repo, nil } reg := &httpmock.Registry{} reg.Register( httpmock.GraphQL(`query RepositoryInfo\b`), httpmock.StringResponse(` { "data": { "repository": { "description": "Some basic special characters \" & / < > '" } } }`)) reg.Register( httpmock.REST("GET", fmt.Sprintf("repos/%s/readme", tt.repoName)), httpmock.StringResponse(` { "name": "readme.md", "content": "IyA8IGlzIGFsd2F5cyA+IHRoYW4gJiAnIGFuZCAi"}`)) tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } io, _, stdout, stderr := iostreams.Test() tt.opts.IO = io t.Run(tt.name, func(t *testing.T) { io.SetStdoutTTY(tt.stdoutTTY) if err := viewRun(tt.opts); (err != nil) != tt.wantErr { t.Errorf("viewRun() error = %v, wantErr %v", err, tt.wantErr) } assert.Equal(t, tt.wantStderr, stderr.String()) assert.Equal(t, tt.wantOut, stdout.String()) reg.Verify(t) }) } } func Test_viewRun_json(t *testing.T) { io, _, stdout, stderr := iostreams.Test() io.SetStdoutTTY(false) reg := &httpmock.Registry{} defer reg.Verify(t) reg.StubRepoInfoResponse("OWNER", "REPO", "main") opts := &ViewOptions{ IO: io, HttpClient: func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }, BaseRepo: func() (ghrepo.Interface, error) { return ghrepo.New("OWNER", "REPO"), nil }, Exporter: &testExporter{ fields: []string{"name", "defaultBranchRef"}, }, } _, teardown := run.Stub() defer teardown(t) err := viewRun(opts) assert.NoError(t, err) assert.Equal(t, heredoc.Doc(` name: REPO defaultBranchRef: main `), stdout.String()) assert.Equal(t, "", stderr.String()) } type testExporter struct { fields []string } func (e *testExporter) Fields() []string { return e.fields } func (e *testExporter) Write(io *iostreams.IOStreams, data interface{}) error { r := data.(*api.Repository) fmt.Fprintf(io.Out, "name: %s\n", r.Name) fmt.Fprintf(io.Out, "defaultBranchRef: %s\n", r.DefaultBranchRef.Name) return nil }