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  }