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 }