package rerun import ( "bytes" "encoding/json" "io" "net/http" "testing" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/run/shared" workflowShared "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/go-gh/v2/pkg/api" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) func TestNewCmdRerun(t *testing.T) { tests := []struct { name string cli string tty bool wants RerunOptions wantsErr bool }{ { name: "blank nontty", wantsErr: true, }, { name: "blank tty", tty: true, wants: RerunOptions{ Prompt: true, }, }, { name: "with arg nontty", cli: "1234", wants: RerunOptions{ RunID: "1234", }, }, { name: "with arg tty", tty: true, cli: "1234", wants: RerunOptions{ RunID: "1234", }, }, { name: "failed arg nontty", cli: "4321 --failed", wants: RerunOptions{ RunID: "4321", OnlyFailed: true, }, }, { name: "failed arg", tty: true, cli: "--failed", wants: RerunOptions{ Prompt: true, OnlyFailed: true, }, }, { name: "with arg job", tty: true, cli: "--job 1234", wants: RerunOptions{ JobID: "1234", }, }, { name: "with args jobID and runID uses jobID", tty: true, cli: "1234 --job 5678", wants: RerunOptions{ JobID: "5678", RunID: "", }, }, { name: "with arg job with no ID fails", tty: true, cli: "--job", wantsErr: true, }, { name: "with arg job with no ID no tty fails", cli: "--job", wantsErr: true, }, { name: "debug nontty", cli: "4321 --debug", wants: RerunOptions{ RunID: "4321", Debug: true, }, }, { name: "debug tty", tty: true, cli: "--debug", wants: RerunOptions{ Prompt: true, Debug: true, }, }, { name: "debug off", cli: "4321 --debug=false", wants: RerunOptions{ RunID: "4321", Debug: false, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() ios.SetStdinTTY(tt.tty) ios.SetStdoutTTY(tt.tty) f := &cmdutil.Factory{ IOStreams: ios, } argv, err := shlex.Split(tt.cli) assert.NoError(t, err) var gotOpts *RerunOptions cmd := NewCmdRerun(f, func(opts *RerunOptions) error { gotOpts = opts return nil }) cmd.SetArgs(argv) cmd.SetIn(&bytes.Buffer{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) _, err = cmd.ExecuteC() if tt.wantsErr { assert.Error(t, err) return } assert.NoError(t, err) assert.Equal(t, tt.wants.RunID, gotOpts.RunID) assert.Equal(t, tt.wants.Prompt, gotOpts.Prompt) }) } } func TestRerun(t *testing.T) { tests := []struct { name string httpStubs func(*httpmock.Registry) promptStubs func(*prompter.MockPrompter) opts *RerunOptions tty bool wantErr bool errOut string wantOut string wantDebug bool }{ { name: "arg", tty: true, opts: &RerunOptions{ RunID: "1234", }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun of run 1234\n", }, { name: "arg including onlyFailed", tty: true, opts: &RerunOptions{ RunID: "1234", OnlyFailed: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun (failed jobs) of run 1234\n", }, { name: "arg including a specific job", tty: true, opts: &RerunOptions{ JobID: "20", // 20 is shared.FailedJob.ID }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), httpmock.JSONResponse(shared.FailedJob)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun of job 20 on run 1234\n", }, { name: "arg including debug", tty: true, opts: &RerunOptions{ RunID: "1234", Debug: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun of run 1234 with debug logging enabled\n", wantDebug: true, }, { name: "arg including onlyFailed and debug", tty: true, opts: &RerunOptions{ RunID: "1234", OnlyFailed: true, Debug: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun-failed-jobs"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun (failed jobs) of run 1234 with debug logging enabled\n", wantDebug: true, }, { name: "arg including a specific job and debug", tty: true, opts: &RerunOptions{ JobID: "20", // 20 is shared.FailedJob.ID Debug: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), httpmock.JSONResponse(shared.FailedJob)) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/jobs/20/rerun"), httpmock.StringResponse("{}")) }, wantOut: "✓ Requested rerun of job 20 on run 1234 with debug logging enabled\n", wantDebug: true, }, { name: "prompt", tty: true, opts: &RerunOptions{ Prompt: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: shared.TestRuns, })) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), httpmock.JSONResponse(shared.FailedRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/1234/rerun"), httpmock.StringResponse("{}")) }, promptStubs: func(pm *prompter.MockPrompter) { pm.RegisterSelect("Select a workflow run", []string{"X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "- cool commit, CI [trunk] Feb 23, 2021", "X cool commit, CI [trunk] Feb 23, 2021"}, func(_, _ string, opts []string) (int, error) { return 2, nil }) }, wantOut: "✓ Requested rerun of run 1234\n", }, { name: "prompt but no failed runs", tty: true, opts: &RerunOptions{ Prompt: true, }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: []shared.Run{ shared.SuccessfulRun, shared.TestRun(2, shared.InProgress, ""), }})) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) }, wantErr: true, errOut: "no recent runs have failed; please specify a specific ``", }, { name: "API error (403)", tty: true, opts: &RerunOptions{ RunID: "3", }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/3/rerun"), httpmock.JSONErrorResponse(403, api.HTTPError{ StatusCode: 403, Message: "blah blah", }), ) }, wantErr: true, errOut: "run 3 cannot be rerun; blah blah", }, { name: "API error (non-403)", tty: true, opts: &RerunOptions{ RunID: "3", }, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), httpmock.JSONResponse(shared.SuccessfulRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) reg.Register( httpmock.REST("POST", "repos/OWNER/REPO/actions/runs/3/rerun"), httpmock.JSONErrorResponse(500, api.HTTPError{ StatusCode: 500, Message: "blah blah", }), ) }, wantErr: true, errOut: "failed to rerun: HTTP 500: blah blah (https://api.github.com/repos/OWNER/REPO/actions/runs/3/rerun)", }, } for _, tt := range tests { reg := &httpmock.Registry{} tt.httpStubs(reg) tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } ios, _, stdout, _ := iostreams.Test() ios.SetStdinTTY(tt.tty) ios.SetStdoutTTY(tt.tty) tt.opts.IO = ios tt.opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.FromFullName("OWNER/REPO") } pm := prompter.NewMockPrompter(t) tt.opts.Prompter = pm if tt.promptStubs != nil { tt.promptStubs(pm) } t.Run(tt.name, func(t *testing.T) { err := runRerun(tt.opts) if tt.wantErr { assert.Error(t, err) assert.Equal(t, tt.errOut, err.Error()) return } assert.NoError(t, err) assert.Equal(t, tt.wantOut, stdout.String()) reg.Verify(t) for _, d := range reg.Requests { if d.Method != "POST" { continue } if !tt.wantDebug { assert.Nil(t, d.Body) continue } data, err := io.ReadAll(d.Body) assert.NoError(t, err) var payload RerunPayload err = json.Unmarshal(data, &payload) assert.NoError(t, err) assert.Equal(t, tt.wantDebug, payload.Debug) } }) } }