github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/issue/view/view_test.go (about)

     1  package view
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/cli/cli/internal/config"
    12  	"github.com/cli/cli/internal/ghrepo"
    13  	"github.com/cli/cli/internal/run"
    14  	"github.com/cli/cli/pkg/cmdutil"
    15  	"github.com/cli/cli/pkg/httpmock"
    16  	"github.com/cli/cli/pkg/iostreams"
    17  	"github.com/cli/cli/test"
    18  	"github.com/google/shlex"
    19  	"github.com/stretchr/testify/assert"
    20  )
    21  
    22  func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
    23  	io, _, stdout, stderr := iostreams.Test()
    24  	io.SetStdoutTTY(isTTY)
    25  	io.SetStdinTTY(isTTY)
    26  	io.SetStderrTTY(isTTY)
    27  
    28  	factory := &cmdutil.Factory{
    29  		IOStreams: io,
    30  		HttpClient: func() (*http.Client, error) {
    31  			return &http.Client{Transport: rt}, nil
    32  		},
    33  		Config: func() (config.Config, error) {
    34  			return config.NewBlankConfig(), nil
    35  		},
    36  		BaseRepo: func() (ghrepo.Interface, error) {
    37  			return ghrepo.New("OWNER", "REPO"), nil
    38  		},
    39  	}
    40  
    41  	cmd := NewCmdView(factory, nil)
    42  
    43  	argv, err := shlex.Split(cli)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	cmd.SetArgs(argv)
    48  
    49  	cmd.SetIn(&bytes.Buffer{})
    50  	cmd.SetOut(ioutil.Discard)
    51  	cmd.SetErr(ioutil.Discard)
    52  
    53  	_, err = cmd.ExecuteC()
    54  	return &test.CmdOut{
    55  		OutBuf: stdout,
    56  		ErrBuf: stderr,
    57  	}, err
    58  }
    59  
    60  func TestIssueView_web(t *testing.T) {
    61  	io, _, stdout, stderr := iostreams.Test()
    62  	io.SetStdoutTTY(true)
    63  	io.SetStderrTTY(true)
    64  	browser := &cmdutil.TestBrowser{}
    65  
    66  	reg := &httpmock.Registry{}
    67  	defer reg.Verify(t)
    68  
    69  	reg.Register(
    70  		httpmock.GraphQL(`query IssueByNumber\b`),
    71  		httpmock.StringResponse(`
    72  			{ "data": { "repository": { "hasIssuesEnabled": true, "issue": {
    73  				"number": 123,
    74  				"url": "https://github.com/OWNER/REPO/issues/123"
    75  			} } } }
    76  		`))
    77  
    78  	_, cmdTeardown := run.Stub()
    79  	defer cmdTeardown(t)
    80  
    81  	err := viewRun(&ViewOptions{
    82  		IO:      io,
    83  		Browser: browser,
    84  		HttpClient: func() (*http.Client, error) {
    85  			return &http.Client{Transport: reg}, nil
    86  		},
    87  		BaseRepo: func() (ghrepo.Interface, error) {
    88  			return ghrepo.New("OWNER", "REPO"), nil
    89  		},
    90  		WebMode:     true,
    91  		SelectorArg: "123",
    92  	})
    93  	if err != nil {
    94  		t.Errorf("error running command `issue view`: %v", err)
    95  	}
    96  
    97  	assert.Equal(t, "", stdout.String())
    98  	assert.Equal(t, "Opening github.com/OWNER/REPO/issues/123 in your browser.\n", stderr.String())
    99  	browser.Verify(t, "https://github.com/OWNER/REPO/issues/123")
   100  }
   101  
   102  func TestIssueView_nontty_Preview(t *testing.T) {
   103  	tests := map[string]struct {
   104  		fixture         string
   105  		expectedOutputs []string
   106  	}{
   107  		"Open issue without metadata": {
   108  			fixture: "./fixtures/issueView_preview.json",
   109  			expectedOutputs: []string{
   110  				`title:\tix of coins`,
   111  				`state:\tOPEN`,
   112  				`comments:\t9`,
   113  				`author:\tmarseilles`,
   114  				`assignees:`,
   115  				`number:\t123\n`,
   116  				`\*\*bold story\*\*`,
   117  			},
   118  		},
   119  		"Open issue with metadata": {
   120  			fixture: "./fixtures/issueView_previewWithMetadata.json",
   121  			expectedOutputs: []string{
   122  				`title:\tix of coins`,
   123  				`assignees:\tmarseilles, monaco`,
   124  				`author:\tmarseilles`,
   125  				`state:\tOPEN`,
   126  				`comments:\t9`,
   127  				`labels:\tone, two, three, four, five`,
   128  				`projects:\tProject 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
   129  				`milestone:\tuluru\n`,
   130  				`number:\t123\n`,
   131  				`\*\*bold story\*\*`,
   132  			},
   133  		},
   134  		"Open issue with empty body": {
   135  			fixture: "./fixtures/issueView_previewWithEmptyBody.json",
   136  			expectedOutputs: []string{
   137  				`title:\tix of coins`,
   138  				`state:\tOPEN`,
   139  				`author:\tmarseilles`,
   140  				`labels:\ttarot`,
   141  				`number:\t123\n`,
   142  			},
   143  		},
   144  		"Closed issue": {
   145  			fixture: "./fixtures/issueView_previewClosedState.json",
   146  			expectedOutputs: []string{
   147  				`title:\tix of coins`,
   148  				`state:\tCLOSED`,
   149  				`\*\*bold story\*\*`,
   150  				`author:\tmarseilles`,
   151  				`labels:\ttarot`,
   152  				`number:\t123\n`,
   153  				`\*\*bold story\*\*`,
   154  			},
   155  		},
   156  	}
   157  	for name, tc := range tests {
   158  		t.Run(name, func(t *testing.T) {
   159  			http := &httpmock.Registry{}
   160  			defer http.Verify(t)
   161  
   162  			http.Register(httpmock.GraphQL(`query IssueByNumber\b`), httpmock.FileResponse(tc.fixture))
   163  
   164  			output, err := runCommand(http, false, "123")
   165  			if err != nil {
   166  				t.Errorf("error running `issue view`: %v", err)
   167  			}
   168  
   169  			assert.Equal(t, "", output.Stderr())
   170  
   171  			//nolint:staticcheck // prefer exact matchers over ExpectLines
   172  			test.ExpectLines(t, output.String(), tc.expectedOutputs...)
   173  		})
   174  	}
   175  }
   176  
   177  func TestIssueView_tty_Preview(t *testing.T) {
   178  	tests := map[string]struct {
   179  		fixture         string
   180  		expectedOutputs []string
   181  	}{
   182  		"Open issue without metadata": {
   183  			fixture: "./fixtures/issueView_preview.json",
   184  			expectedOutputs: []string{
   185  				`ix of coins #123`,
   186  				`Open.*marseilles opened about 9 years ago.*9 comments`,
   187  				`bold story`,
   188  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   189  			},
   190  		},
   191  		"Open issue with metadata": {
   192  			fixture: "./fixtures/issueView_previewWithMetadata.json",
   193  			expectedOutputs: []string{
   194  				`ix of coins #123`,
   195  				`Open.*marseilles opened about 9 years ago.*9 comments`,
   196  				`8 \x{1f615} • 7 \x{1f440} • 6 \x{2764}\x{fe0f} • 5 \x{1f389} • 4 \x{1f604} • 3 \x{1f680} • 2 \x{1f44e} • 1 \x{1f44d}`,
   197  				`Assignees:.*marseilles, monaco\n`,
   198  				`Labels:.*one, two, three, four, five\n`,
   199  				`Projects:.*Project 1 \(column A\), Project 2 \(column B\), Project 3 \(column C\), Project 4 \(Awaiting triage\)\n`,
   200  				`Milestone:.*uluru\n`,
   201  				`bold story`,
   202  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   203  			},
   204  		},
   205  		"Open issue with empty body": {
   206  			fixture: "./fixtures/issueView_previewWithEmptyBody.json",
   207  			expectedOutputs: []string{
   208  				`ix of coins #123`,
   209  				`Open.*marseilles opened about 9 years ago.*9 comments`,
   210  				`No description provided`,
   211  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   212  			},
   213  		},
   214  		"Closed issue": {
   215  			fixture: "./fixtures/issueView_previewClosedState.json",
   216  			expectedOutputs: []string{
   217  				`ix of coins #123`,
   218  				`Closed.*marseilles opened about 9 years ago.*9 comments`,
   219  				`bold story`,
   220  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   221  			},
   222  		},
   223  	}
   224  	for name, tc := range tests {
   225  		t.Run(name, func(t *testing.T) {
   226  			io, _, stdout, stderr := iostreams.Test()
   227  			io.SetStdoutTTY(true)
   228  			io.SetStdinTTY(true)
   229  			io.SetStderrTTY(true)
   230  
   231  			httpReg := &httpmock.Registry{}
   232  			defer httpReg.Verify(t)
   233  
   234  			httpReg.Register(httpmock.GraphQL(`query IssueByNumber\b`), httpmock.FileResponse(tc.fixture))
   235  
   236  			opts := ViewOptions{
   237  				IO: io,
   238  				Now: func() time.Time {
   239  					t, _ := time.Parse(time.RFC822, "03 Nov 20 15:04 UTC")
   240  					return t
   241  				},
   242  				HttpClient: func() (*http.Client, error) {
   243  					return &http.Client{Transport: httpReg}, nil
   244  				},
   245  				BaseRepo: func() (ghrepo.Interface, error) {
   246  					return ghrepo.New("OWNER", "REPO"), nil
   247  				},
   248  				SelectorArg: "123",
   249  			}
   250  
   251  			err := viewRun(&opts)
   252  			assert.NoError(t, err)
   253  
   254  			assert.Equal(t, "", stderr.String())
   255  
   256  			//nolint:staticcheck // prefer exact matchers over ExpectLines
   257  			test.ExpectLines(t, stdout.String(), tc.expectedOutputs...)
   258  		})
   259  	}
   260  }
   261  
   262  func TestIssueView_web_notFound(t *testing.T) {
   263  	http := &httpmock.Registry{}
   264  	defer http.Verify(t)
   265  
   266  	http.Register(
   267  		httpmock.GraphQL(`query IssueByNumber\b`),
   268  		httpmock.StringResponse(`
   269  			{ "errors": [
   270  				{ "message": "Could not resolve to an Issue with the number of 9999." }
   271  			] }
   272  			`),
   273  	)
   274  
   275  	_, cmdTeardown := run.Stub()
   276  	defer cmdTeardown(t)
   277  
   278  	_, err := runCommand(http, true, "-w 9999")
   279  	if err == nil || err.Error() != "GraphQL error: Could not resolve to an Issue with the number of 9999." {
   280  		t.Errorf("error running command `issue view`: %v", err)
   281  	}
   282  }
   283  
   284  func TestIssueView_disabledIssues(t *testing.T) {
   285  	http := &httpmock.Registry{}
   286  	defer http.Verify(t)
   287  
   288  	http.Register(
   289  		httpmock.GraphQL(`query IssueByNumber\b`),
   290  		httpmock.StringResponse(`
   291  			{ "data": { "repository": {
   292  				"id": "REPOID",
   293  				"hasIssuesEnabled": false
   294  			} } }
   295  		`),
   296  	)
   297  
   298  	_, err := runCommand(http, true, `6666`)
   299  	if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
   300  		t.Errorf("error running command `issue view`: %v", err)
   301  	}
   302  }
   303  
   304  func TestIssueView_tty_Comments(t *testing.T) {
   305  	tests := map[string]struct {
   306  		cli             string
   307  		fixtures        map[string]string
   308  		expectedOutputs []string
   309  		wantsErr        bool
   310  	}{
   311  		"without comments flag": {
   312  			cli: "123",
   313  			fixtures: map[string]string{
   314  				"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
   315  			},
   316  			expectedOutputs: []string{
   317  				`some title #123`,
   318  				`some body`,
   319  				`———————— Not showing 5 comments ————————`,
   320  				`marseilles \(Collaborator\) • Jan  1, 2020 • Newest comment`,
   321  				`Comment 5`,
   322  				`Use --comments to view the full conversation`,
   323  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   324  			},
   325  		},
   326  		"with comments flag": {
   327  			cli: "123 --comments",
   328  			fixtures: map[string]string{
   329  				"IssueByNumber":    "./fixtures/issueView_previewSingleComment.json",
   330  				"CommentsForIssue": "./fixtures/issueView_previewFullComments.json",
   331  			},
   332  			expectedOutputs: []string{
   333  				`some title #123`,
   334  				`some body`,
   335  				`monalisa • Jan  1, 2020 • Edited`,
   336  				`1 \x{1f615} • 2 \x{1f440} • 3 \x{2764}\x{fe0f} • 4 \x{1f389} • 5 \x{1f604} • 6 \x{1f680} • 7 \x{1f44e} • 8 \x{1f44d}`,
   337  				`Comment 1`,
   338  				`johnnytest \(Contributor\) • Jan  1, 2020`,
   339  				`Comment 2`,
   340  				`elvisp \(Member\) • Jan  1, 2020`,
   341  				`Comment 3`,
   342  				`loislane \(Owner\) • Jan  1, 2020`,
   343  				`Comment 4`,
   344  				`sam-spam • This comment has been marked as spam`,
   345  				`marseilles \(Collaborator\) • Jan  1, 2020 • Newest comment`,
   346  				`Comment 5`,
   347  				`View this issue on GitHub: https://github.com/OWNER/REPO/issues/123`,
   348  			},
   349  		},
   350  		"with invalid comments flag": {
   351  			cli:      "123 --comments 3",
   352  			wantsErr: true,
   353  		},
   354  	}
   355  	for name, tc := range tests {
   356  		t.Run(name, func(t *testing.T) {
   357  			http := &httpmock.Registry{}
   358  			defer http.Verify(t)
   359  			for name, file := range tc.fixtures {
   360  				name := fmt.Sprintf(`query %s\b`, name)
   361  				http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
   362  			}
   363  			output, err := runCommand(http, true, tc.cli)
   364  			if tc.wantsErr {
   365  				assert.Error(t, err)
   366  				return
   367  			}
   368  			assert.NoError(t, err)
   369  			assert.Equal(t, "", output.Stderr())
   370  			//nolint:staticcheck // prefer exact matchers over ExpectLines
   371  			test.ExpectLines(t, output.String(), tc.expectedOutputs...)
   372  		})
   373  	}
   374  }
   375  
   376  func TestIssueView_nontty_Comments(t *testing.T) {
   377  	tests := map[string]struct {
   378  		cli             string
   379  		fixtures        map[string]string
   380  		expectedOutputs []string
   381  		wantsErr        bool
   382  	}{
   383  		"without comments flag": {
   384  			cli: "123",
   385  			fixtures: map[string]string{
   386  				"IssueByNumber": "./fixtures/issueView_previewSingleComment.json",
   387  			},
   388  			expectedOutputs: []string{
   389  				`title:\tsome title`,
   390  				`state:\tOPEN`,
   391  				`author:\tmarseilles`,
   392  				`comments:\t6`,
   393  				`number:\t123`,
   394  				`some body`,
   395  			},
   396  		},
   397  		"with comments flag": {
   398  			cli: "123 --comments",
   399  			fixtures: map[string]string{
   400  				"IssueByNumber":    "./fixtures/issueView_previewSingleComment.json",
   401  				"CommentsForIssue": "./fixtures/issueView_previewFullComments.json",
   402  			},
   403  			expectedOutputs: []string{
   404  				`author:\tmonalisa`,
   405  				`association:\t`,
   406  				`edited:\ttrue`,
   407  				`Comment 1`,
   408  				`author:\tjohnnytest`,
   409  				`association:\tcontributor`,
   410  				`edited:\tfalse`,
   411  				`Comment 2`,
   412  				`author:\telvisp`,
   413  				`association:\tmember`,
   414  				`edited:\tfalse`,
   415  				`Comment 3`,
   416  				`author:\tloislane`,
   417  				`association:\towner`,
   418  				`edited:\tfalse`,
   419  				`Comment 4`,
   420  				`author:\tmarseilles`,
   421  				`association:\tcollaborator`,
   422  				`edited:\tfalse`,
   423  				`Comment 5`,
   424  			},
   425  		},
   426  		"with invalid comments flag": {
   427  			cli:      "123 --comments 3",
   428  			wantsErr: true,
   429  		},
   430  	}
   431  	for name, tc := range tests {
   432  		t.Run(name, func(t *testing.T) {
   433  			http := &httpmock.Registry{}
   434  			defer http.Verify(t)
   435  			for name, file := range tc.fixtures {
   436  				name := fmt.Sprintf(`query %s\b`, name)
   437  				http.Register(httpmock.GraphQL(name), httpmock.FileResponse(file))
   438  			}
   439  			output, err := runCommand(http, false, tc.cli)
   440  			if tc.wantsErr {
   441  				assert.Error(t, err)
   442  				return
   443  			}
   444  			assert.NoError(t, err)
   445  			assert.Equal(t, "", output.Stderr())
   446  			//nolint:staticcheck // prefer exact matchers over ExpectLines
   447  			test.ExpectLines(t, output.String(), tc.expectedOutputs...)
   448  		})
   449  	}
   450  }