diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go index c9e2c6971..e54a3fb17 100644 --- a/pkg/cmd/project/shared/queries/queries.go +++ b/pkg/cmd/project/shared/queries/queries.go @@ -29,16 +29,31 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream } } -func NewTestClient() *Client { +// TestClientOpt is a test option for the test client. +type TestClientOpt func(*Client) + +// WithPrompter is a test option to set the prompter for the test client. +func WithPrompter(p iprompter) TestClientOpt { + return func(c *Client) { + c.prompter = p + } +} + +func NewTestClient(opts ...TestClientOpt) *Client { apiClient := &hostScopedClient{ hostname: "github.com", Client: api.NewClientFromHTTP(http.DefaultClient), } - return &Client{ + c := &Client{ apiClient: apiClient, spinner: false, prompter: nil, } + + for _, o := range opts { + o(c) + } + return c } type iprompter interface { diff --git a/pkg/cmd/project/view/view.go b/pkg/cmd/project/view/view.go index ce54e1f09..3d94b5af3 100644 --- a/pkg/cmd/project/view/view.go +++ b/pkg/cmd/project/view/view.go @@ -82,18 +82,6 @@ func NewCmdView(f *cmdutil.Factory, runF func(config viewConfig) error) *cobra.C } func runView(config viewConfig) error { - if config.opts.web { - url, err := buildURL(config) - if err != nil { - return err - } - - if err := config.URLOpener(url); err != nil { - return err - } - return nil - } - canPrompt := config.io.CanPrompt() owner, err := config.client.NewOwner(canPrompt, config.opts.owner) if err != nil { @@ -105,6 +93,10 @@ func runView(config viewConfig) error { return err } + if config.opts.web { + return config.URLOpener(project.URL) + } + if config.opts.exporter != nil { return config.opts.exporter.Write(config.io, *project) } @@ -112,31 +104,6 @@ func runView(config viewConfig) error { return printResults(config, project) } -// TODO: support non-github.com hostnames -func buildURL(config viewConfig) (string, error) { - var url string - if config.opts.owner == "@me" { - owner, err := config.client.ViewerLoginName() - if err != nil { - return "", err - } - url = fmt.Sprintf("https://github.com/users/%s/projects/%d", owner, config.opts.number) - } else { - _, ownerType, err := config.client.OwnerIDAndType(config.opts.owner) - if err != nil { - return "", err - } - - if ownerType == queries.UserOwner { - url = fmt.Sprintf("https://github.com/users/%s/projects/%d", config.opts.owner, config.opts.number) - } else { - url = fmt.Sprintf("https://github.com/orgs/%s/projects/%d", config.opts.owner, config.opts.number) - } - } - - return url, nil -} - func printResults(config viewConfig, project *queries.Project) error { var sb strings.Builder sb.WriteString("# Title\n") diff --git a/pkg/cmd/project/view/view_test.go b/pkg/cmd/project/view/view_test.go index 5ac599d12..557c9732f 100644 --- a/pkg/cmd/project/view/view_test.go +++ b/pkg/cmd/project/view/view_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -90,35 +91,6 @@ func TestNewCmdview(t *testing.T) { } } -func TestBuildURLViewer(t *testing.T) { - defer gock.Off() - - gock.New("https://api.github.com"). - Post("/graphql"). - Reply(200). - JSON(` - {"data": - {"viewer": - { - "login":"theviewer" - } - } - } - `) - - client := queries.NewTestClient() - - url, err := buildURL(viewConfig{ - opts: viewOpts{ - number: 1, - owner: "@me", - }, - client: client, - }) - assert.NoError(t, err) - assert.Equal(t, "https://github.com/users/theviewer/projects/1", url) -} - func TestRunView_User(t *testing.T) { defer gock.Off() @@ -223,6 +195,7 @@ func TestRunView_Viewer(t *testing.T) { "items": { "totalCount": 10 }, + "url":"https://github.com/orgs/github/projects/8", "readme": null, "fields": { "nodes": [ @@ -294,6 +267,7 @@ func TestRunView_Org(t *testing.T) { "items": { "totalCount": 10 }, + "url":"https://github.com/orgs/github/projects/8", "readme": null, "fields": { "nodes": [ @@ -361,10 +335,11 @@ func TestRunViewWeb_User(t *testing.T) { { "login":"monalisa", "projectV2": { - "number": 1, + "number": 8, "items": { "totalCount": 10 }, + "url":"https://github.com/users/monalisa/projects/8", "readme": null, "fields": { "nodes": [ @@ -381,6 +356,7 @@ func TestRunViewWeb_User(t *testing.T) { client := queries.NewTestClient() buf := bytes.Buffer{} + ios, _, _, _ := iostreams.Test() config := viewConfig{ opts: viewOpts{ owner: "monalisa", @@ -392,6 +368,7 @@ func TestRunViewWeb_User(t *testing.T) { return nil }, client: client, + io: ios, } err := runView(config) @@ -436,10 +413,11 @@ func TestRunViewWeb_Org(t *testing.T) { { "login":"github", "projectV2": { - "number": 1, + "number": 8, "items": { "totalCount": 10 }, + "url": "https://github.com/orgs/github/projects/8", "readme": null, "fields": { "nodes": [ @@ -456,6 +434,7 @@ func TestRunViewWeb_Org(t *testing.T) { client := queries.NewTestClient() buf := bytes.Buffer{} + ios, _, _, _ := iostreams.Test() config := viewConfig{ opts: viewOpts{ owner: "github", @@ -467,6 +446,7 @@ func TestRunViewWeb_Org(t *testing.T) { return nil }, client: client, + io: ios, } err := runView(config) @@ -496,18 +476,24 @@ func TestRunViewWeb_Me(t *testing.T) { gock.New("https://api.github.com"). Post("/graphql"). + MatchType("json"). + JSON(map[string]interface{}{ + "query": "query ViewerProject.*", + "variables": map[string]interface{}{"afterFields": nil, "afterItems": nil, "firstFields": 100, "firstItems": 0, "number": 8}, + }). Reply(200). JSON(` {"data": - {"user": + {"viewer": { "login":"github", "projectV2": { - "number": 1, + "number": 8, "items": { "totalCount": 10 }, "readme": null, + "url": "https://github.com/users/theviewer/projects/8", "fields": { "nodes": [ { @@ -523,6 +509,7 @@ func TestRunViewWeb_Me(t *testing.T) { client := queries.NewTestClient() buf := bytes.Buffer{} + ios, _, _, _ := iostreams.Test() config := viewConfig{ opts: viewOpts{ owner: "@me", @@ -534,9 +521,229 @@ func TestRunViewWeb_Me(t *testing.T) { return nil }, client: client, + io: ios, } err := runView(config) assert.NoError(t, err) assert.Equal(t, "https://github.com/users/theviewer/projects/8", buf.String()) } + +func TestRunViewWeb_TTY(t *testing.T) { + tests := []struct { + name string + setup func(*viewOpts, *testing.T) func() + promptStubs func(*prompter.PrompterMock) + gqlStubs func(*testing.T) + expectedBrowse string + tty bool + }{ + { + name: "Org project --web with tty", + tty: true, + setup: func(vo *viewOpts, t *testing.T) func() { + return func() { + vo.web = true + } + }, + gqlStubs: func(t *testing.T) { + gock.New("https://api.github.com"). + Post("/graphql"). + MatchType("json"). + JSON(map[string]interface{}{ + "query": "query ViewerLoginAndOrgs.*", + "variables": map[string]interface{}{ + "after": nil, + }, + }). + Reply(200). + JSON(map[string]interface{}{ + "data": map[string]interface{}{ + "viewer": map[string]interface{}{ + "id": "monalisa-ID", + "login": "monalisa", + "organizations": map[string]interface{}{ + "nodes": []interface{}{ + map[string]interface{}{ + "login": "github", + "viewerCanCreateProjects": true, + }, + }, + }, + }, + }, + }) + + gock.New("https://api.github.com"). + Post("/graphql"). + MatchType("json"). + JSON(map[string]interface{}{ + "query": "query OrgProjects.*", + "variables": map[string]interface{}{ + "after": nil, + "afterFields": nil, + "afterItems": nil, + "first": 30, + "firstFields": 100, + "firstItems": 0, + "login": "github", + }, + }). + Reply(200). + JSON(map[string]interface{}{ + "data": map[string]interface{}{ + "organization": map[string]interface{}{ + "login": "github", + "projectsV2": map[string]interface{}{ + "nodes": []interface{}{ + map[string]interface{}{ + "id": "a-project-ID", + "title": "Get it done!", + "url": "https://github.com/orgs/github/projects/1", + "number": 1, + }, + }, + }, + }, + }, + }) + }, + promptStubs: func(pm *prompter.PrompterMock) { + pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { + switch prompt { + case "Which owner would you like to use?": + return prompter.IndexFor(opts, "github") + case "Which project would you like to use?": + return prompter.IndexFor(opts, "Get it done! (#1)") + default: + return -1, prompter.NoSuchPromptErr(prompt) + } + } + }, + expectedBrowse: "https://github.com/orgs/github/projects/1", + }, + { + name: "User project --web with tty", + tty: true, + setup: func(vo *viewOpts, t *testing.T) func() { + return func() { + vo.web = true + } + }, + gqlStubs: func(t *testing.T) { + gock.New("https://api.github.com"). + Post("/graphql"). + MatchType("json"). + JSON(map[string]interface{}{ + "query": "query ViewerLoginAndOrgs.*", + "variables": map[string]interface{}{ + "after": nil, + }, + }). + Reply(200). + JSON(map[string]interface{}{ + "data": map[string]interface{}{ + "viewer": map[string]interface{}{ + "id": "monalisa-ID", + "login": "monalisa", + "organizations": map[string]interface{}{ + "nodes": []interface{}{ + map[string]interface{}{ + "login": "github", + "viewerCanCreateProjects": true, + }, + }, + }, + }, + }, + }) + + gock.New("https://api.github.com"). + Post("/graphql"). + MatchType("json"). + JSON(map[string]interface{}{ + "query": "query ViewerProjects.*", + "variables": map[string]interface{}{ + "after": nil, "afterFields": nil, "afterItems": nil, "first": 30, "firstFields": 100, "firstItems": 0, + }, + }). + Reply(200). + JSON(map[string]interface{}{ + "data": map[string]interface{}{ + "viewer": map[string]interface{}{ + "login": "monalisa", + "projectsV2": map[string]interface{}{ + "totalCount": 1, + "nodes": []interface{}{ + map[string]interface{}{ + "id": "a-project-ID", + "number": 1, + "title": "@monalia's first project", + "url": "https://github.com/users/monalisa/projects/1", + }, + }, + }, + }, + }, + }) + }, + promptStubs: func(pm *prompter.PrompterMock) { + pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) { + switch prompt { + case "Which owner would you like to use?": + return prompter.IndexFor(opts, "monalisa") + case "Which project would you like to use?": + return prompter.IndexFor(opts, "@monalia's first project (#1)") + default: + return -1, prompter.NoSuchPromptErr(prompt) + } + } + }, + expectedBrowse: "https://github.com/users/monalisa/projects/1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer gock.Off() + gock.Observe(gock.DumpRequest) + + if tt.gqlStubs != nil { + tt.gqlStubs(t) + } + + pm := &prompter.PrompterMock{} + if tt.promptStubs != nil { + tt.promptStubs(pm) + } + + opts := viewOpts{} + if tt.setup != nil { + tt.setup(&opts, t)() + } + + ios, _, _, _ := iostreams.Test() + ios.SetStdoutTTY(tt.tty) + ios.SetStdinTTY(tt.tty) + ios.SetStderrTTY(tt.tty) + + buf := bytes.Buffer{} + + client := queries.NewTestClient(queries.WithPrompter(pm)) + + config := viewConfig{ + opts: opts, + URLOpener: func(url string) error { + _, err := buf.WriteString(url) + return err + }, + client: client, + io: ios, + } + + err := runView(config) + assert.NoError(t, err) + assert.Equal(t, tt.expectedBrowse, buf.String()) + }) + } +}