github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/run/rerun/rerun_test.go (about) 1 package rerun 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "net/http" 8 "testing" 9 10 "github.com/ungtb10d/cli/v2/internal/ghrepo" 11 "github.com/ungtb10d/cli/v2/pkg/cmd/run/shared" 12 workflowShared "github.com/ungtb10d/cli/v2/pkg/cmd/workflow/shared" 13 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 14 "github.com/ungtb10d/cli/v2/pkg/httpmock" 15 "github.com/ungtb10d/cli/v2/pkg/iostreams" 16 "github.com/ungtb10d/cli/v2/pkg/prompt" 17 "github.com/google/shlex" 18 "github.com/stretchr/testify/assert" 19 ) 20 21 func TestNewCmdRerun(t *testing.T) { 22 tests := []struct { 23 name string 24 cli string 25 tty bool 26 wants RerunOptions 27 wantsErr bool 28 }{ 29 { 30 name: "blank nontty", 31 wantsErr: true, 32 }, 33 { 34 name: "blank tty", 35 tty: true, 36 wants: RerunOptions{ 37 Prompt: true, 38 }, 39 }, 40 { 41 name: "with arg nontty", 42 cli: "1234", 43 wants: RerunOptions{ 44 RunID: "1234", 45 }, 46 }, 47 { 48 name: "with arg tty", 49 tty: true, 50 cli: "1234", 51 wants: RerunOptions{ 52 RunID: "1234", 53 }, 54 }, 55 { 56 name: "failed arg nontty", 57 cli: "4321 --failed", 58 wants: RerunOptions{ 59 RunID: "4321", 60 OnlyFailed: true, 61 }, 62 }, 63 { 64 name: "failed arg", 65 tty: true, 66 cli: "--failed", 67 wants: RerunOptions{ 68 Prompt: true, 69 OnlyFailed: true, 70 }, 71 }, 72 { 73 name: "with arg job", 74 tty: true, 75 cli: "--job 1234", 76 wants: RerunOptions{ 77 JobID: "1234", 78 }, 79 }, 80 { 81 name: "with args jobID and runID fails", 82 tty: true, 83 cli: "1234 --job 5678", 84 wantsErr: true, 85 }, 86 { 87 name: "with arg job with no ID fails", 88 tty: true, 89 cli: "--job", 90 wantsErr: true, 91 }, 92 { 93 name: "with arg job with no ID no tty fails", 94 cli: "--job", 95 wantsErr: true, 96 }, 97 { 98 name: "debug nontty", 99 cli: "4321 --debug", 100 wants: RerunOptions{ 101 RunID: "4321", 102 Debug: true, 103 }, 104 }, 105 { 106 name: "debug tty", 107 tty: true, 108 cli: "--debug", 109 wants: RerunOptions{ 110 Prompt: true, 111 Debug: true, 112 }, 113 }, 114 { 115 name: "debug off", 116 cli: "4321 --debug=false", 117 wants: RerunOptions{ 118 RunID: "4321", 119 Debug: false, 120 }, 121 }, 122 } 123 124 for _, tt := range tests { 125 t.Run(tt.name, func(t *testing.T) { 126 ios, _, _, _ := iostreams.Test() 127 ios.SetStdinTTY(tt.tty) 128 ios.SetStdoutTTY(tt.tty) 129 130 f := &cmdutil.Factory{ 131 IOStreams: ios, 132 } 133 134 argv, err := shlex.Split(tt.cli) 135 assert.NoError(t, err) 136 137 var gotOpts *RerunOptions 138 cmd := NewCmdRerun(f, func(opts *RerunOptions) error { 139 gotOpts = opts 140 return nil 141 }) 142 cmd.SetArgs(argv) 143 cmd.SetIn(&bytes.Buffer{}) 144 cmd.SetOut(io.Discard) 145 cmd.SetErr(io.Discard) 146 147 _, err = cmd.ExecuteC() 148 if tt.wantsErr { 149 assert.Error(t, err) 150 return 151 } 152 153 assert.NoError(t, err) 154 155 assert.Equal(t, tt.wants.RunID, gotOpts.RunID) 156 assert.Equal(t, tt.wants.Prompt, gotOpts.Prompt) 157 }) 158 } 159 160 } 161 162 func TestRerun(t *testing.T) { 163 tests := []struct { 164 name string 165 httpStubs func(*httpmock.Registry) 166 askStubs func(*prompt.AskStubber) 167 opts *RerunOptions 168 tty bool 169 wantErr bool 170 errOut string 171 wantOut string 172 wantDebug bool 173 }{ 174 { 175 name: "arg", 176 tty: true, 177 opts: &RerunOptions{ 178 RunID: "1234", 179 }, 180 httpStubs: func(reg *httpmock.Registry) { 181 reg.Register( 182 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 183 httpmock.JSONResponse(shared.FailedRun)) 184 reg.Register( 185 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 186 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 187 Workflows: []workflowShared.Workflow{ 188 shared.TestWorkflow, 189 }, 190 })) 191 reg.Register( 192 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), 193 httpmock.StringResponse("{}")) 194 }, 195 wantOut: "✓ Requested rerun of run 1234\n", 196 }, 197 { 198 name: "arg including onlyFailed", 199 tty: true, 200 opts: &RerunOptions{ 201 RunID: "1234", 202 OnlyFailed: true, 203 }, 204 httpStubs: func(reg *httpmock.Registry) { 205 reg.Register( 206 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 207 httpmock.JSONResponse(shared.FailedRun)) 208 reg.Register( 209 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 210 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 211 Workflows: []workflowShared.Workflow{ 212 shared.TestWorkflow, 213 }, 214 })) 215 reg.Register( 216 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), 217 httpmock.StringResponse("{}")) 218 }, 219 wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", 220 }, 221 { 222 name: "arg including a specific job", 223 tty: true, 224 opts: &RerunOptions{ 225 JobID: "20", // 20 is shared.FailedJob.ID 226 }, 227 httpStubs: func(reg *httpmock.Registry) { 228 reg.Register( 229 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), 230 httpmock.JSONResponse(shared.FailedJob)) 231 reg.Register( 232 httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), 233 httpmock.StringResponse("{}")) 234 }, 235 wantOut: "✓ Requested rerun of job 20 on run 1234\n", 236 }, 237 { 238 name: "arg including debug", 239 tty: true, 240 opts: &RerunOptions{ 241 RunID: "1234", 242 Debug: true, 243 }, 244 httpStubs: func(reg *httpmock.Registry) { 245 reg.Register( 246 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 247 httpmock.JSONResponse(shared.FailedRun)) 248 reg.Register( 249 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 250 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 251 Workflows: []workflowShared.Workflow{ 252 shared.TestWorkflow, 253 }, 254 })) 255 reg.Register( 256 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), 257 httpmock.StringResponse("{}")) 258 }, 259 wantOut: "✓ Requested rerun of run 1234 with debug logging enabled\n", 260 wantDebug: true, 261 }, 262 { 263 name: "arg including onlyFailed and debug", 264 tty: true, 265 opts: &RerunOptions{ 266 RunID: "1234", 267 OnlyFailed: true, 268 Debug: true, 269 }, 270 httpStubs: func(reg *httpmock.Registry) { 271 reg.Register( 272 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 273 httpmock.JSONResponse(shared.FailedRun)) 274 reg.Register( 275 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 276 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 277 Workflows: []workflowShared.Workflow{ 278 shared.TestWorkflow, 279 }, 280 })) 281 reg.Register( 282 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), 283 httpmock.StringResponse("{}")) 284 }, 285 wantOut: "✓ Requested rerun (failed jobs) of run 1234 with debug logging enabled\n", 286 wantDebug: true, 287 }, 288 { 289 name: "arg including a specific job and debug", 290 tty: true, 291 opts: &RerunOptions{ 292 JobID: "20", // 20 is shared.FailedJob.ID 293 Debug: true, 294 }, 295 httpStubs: func(reg *httpmock.Registry) { 296 reg.Register( 297 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), 298 httpmock.JSONResponse(shared.FailedJob)) 299 reg.Register( 300 httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), 301 httpmock.StringResponse("{}")) 302 }, 303 wantOut: "✓ Requested rerun of job 20 on run 1234 with debug logging enabled\n", 304 wantDebug: true, 305 }, 306 { 307 name: "prompt", 308 tty: true, 309 opts: &RerunOptions{ 310 Prompt: true, 311 }, 312 httpStubs: func(reg *httpmock.Registry) { 313 reg.Register( 314 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 315 httpmock.JSONResponse(shared.RunsPayload{ 316 WorkflowRuns: shared.TestRuns, 317 })) 318 reg.Register( 319 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 320 httpmock.JSONResponse(shared.FailedRun)) 321 reg.Register( 322 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 323 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 324 Workflows: []workflowShared.Workflow{ 325 shared.TestWorkflow, 326 }, 327 })) 328 reg.Register( 329 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 330 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 331 Workflows: []workflowShared.Workflow{ 332 shared.TestWorkflow, 333 }, 334 })) 335 reg.Register( 336 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), 337 httpmock.StringResponse("{}")) 338 }, 339 askStubs: func(as *prompt.AskStubber) { 340 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 341 as.StubOne(2) 342 }, 343 wantOut: "✓ Requested rerun of run 1234\n", 344 }, 345 { 346 name: "prompt but no failed runs", 347 tty: true, 348 opts: &RerunOptions{ 349 Prompt: true, 350 }, 351 httpStubs: func(reg *httpmock.Registry) { 352 reg.Register( 353 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 354 httpmock.JSONResponse(shared.RunsPayload{ 355 WorkflowRuns: []shared.Run{ 356 shared.SuccessfulRun, 357 shared.TestRun(2, shared.InProgress, ""), 358 }})) 359 reg.Register( 360 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 361 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 362 Workflows: []workflowShared.Workflow{ 363 shared.TestWorkflow, 364 }, 365 })) 366 }, 367 wantErr: true, 368 errOut: "no recent runs have failed; please specify a specific `<run-id>`", 369 }, 370 { 371 name: "unrerunnable", 372 tty: true, 373 opts: &RerunOptions{ 374 RunID: "3", 375 }, 376 httpStubs: func(reg *httpmock.Registry) { 377 reg.Register( 378 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 379 httpmock.JSONResponse(shared.SuccessfulRun)) 380 reg.Register( 381 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 382 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 383 Workflows: []workflowShared.Workflow{ 384 shared.TestWorkflow, 385 }, 386 })) 387 reg.Register( 388 httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/3/rerun"), 389 httpmock.StatusStringResponse(403, "no")) 390 }, 391 wantErr: true, 392 errOut: "run 3 cannot be rerun; its workflow file may be broken", 393 }, 394 } 395 396 for _, tt := range tests { 397 reg := &httpmock.Registry{} 398 tt.httpStubs(reg) 399 tt.opts.HttpClient = func() (*http.Client, error) { 400 return &http.Client{Transport: reg}, nil 401 } 402 403 ios, _, stdout, _ := iostreams.Test() 404 ios.SetStdinTTY(tt.tty) 405 ios.SetStdoutTTY(tt.tty) 406 tt.opts.IO = ios 407 tt.opts.BaseRepo = func() (ghrepo.Interface, error) { 408 return ghrepo.FromFullName("OWNER/REPO") 409 } 410 411 //nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber 412 as, teardown := prompt.InitAskStubber() 413 defer teardown() 414 if tt.askStubs != nil { 415 tt.askStubs(as) 416 } 417 418 t.Run(tt.name, func(t *testing.T) { 419 err := runRerun(tt.opts) 420 if tt.wantErr { 421 assert.Error(t, err) 422 assert.Equal(t, tt.errOut, err.Error()) 423 return 424 } 425 assert.NoError(t, err) 426 assert.Equal(t, tt.wantOut, stdout.String()) 427 reg.Verify(t) 428 429 for _, d := range reg.Requests { 430 if d.Method != "POST" { 431 continue 432 } 433 434 if !tt.wantDebug { 435 assert.Nil(t, d.Body) 436 continue 437 } 438 439 data, err := io.ReadAll(d.Body) 440 assert.NoError(t, err) 441 var payload RerunPayload 442 err = json.Unmarshal(data, &payload) 443 assert.NoError(t, err) 444 assert.Equal(t, tt.wantDebug, payload.Debug) 445 } 446 }) 447 } 448 }