package list import ( "bytes" "io" "net/http" "net/url" "testing" "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/ghrepo" "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/google/shlex" "github.com/stretchr/testify/assert" ) func TestNewCmdList(t *testing.T) { tests := []struct { name string cli string tty bool wants ListOptions wantsErr bool }{ { name: "blank", wants: ListOptions{ Limit: defaultLimit, }, }, { name: "limit", cli: "--limit 100", wants: ListOptions{ Limit: 100, }, }, { name: "bad limit", cli: "--limit hi", wantsErr: true, }, { name: "workflow", cli: "--workflow foo.yml", wants: ListOptions{ Limit: defaultLimit, WorkflowSelector: "foo.yml", }, }, { name: "branch", cli: "--branch new-cool-stuff", wants: ListOptions{ Limit: defaultLimit, Branch: "new-cool-stuff", }, }, { name: "user", cli: "--user bak1an", wants: ListOptions{ Limit: defaultLimit, Actor: "bak1an", }, }, { name: "status", cli: "--status completed", wants: ListOptions{ Limit: defaultLimit, Status: "completed", }, }, { name: "event", cli: "--event push", wants: ListOptions{ Limit: defaultLimit, Event: "push", }, }, { name: "created", cli: "--created >=2023-04-24", wants: ListOptions{ Limit: defaultLimit, Created: ">=2023-04-24", }, }, } 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 *ListOptions cmd := NewCmdList(f, func(opts *ListOptions) 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.Equal(t, tt.wants.Limit, gotOpts.Limit) assert.Equal(t, tt.wants.WorkflowSelector, gotOpts.WorkflowSelector) assert.Equal(t, tt.wants.Branch, gotOpts.Branch) assert.Equal(t, tt.wants.Actor, gotOpts.Actor) assert.Equal(t, tt.wants.Status, gotOpts.Status) assert.Equal(t, tt.wants.Event, gotOpts.Event) assert.Equal(t, tt.wants.Created, gotOpts.Created) }) } } func TestListRun(t *testing.T) { tests := []struct { name string opts *ListOptions wantErr bool wantOut string wantErrOut string wantErrMsg string stubs func(*httpmock.Registry) isTTY bool }{ { name: "default arguments", opts: &ListOptions{ Limit: defaultLimit, now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), }, isTTY: true, stubs: 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/workflows"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) }, wantOut: heredoc.Doc(` STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE X cool commit CI trunk push 1 4m34s about 4 minutes ago * cool commit CI trunk push 2 4m34s about 4 minutes ago ✓ cool commit CI trunk push 3 4m34s about 4 minutes ago X cool commit CI trunk push 4 4m34s about 4 minutes ago X cool commit CI trunk push 1234 4m34s about 4 minutes ago - cool commit CI trunk push 6 4m34s about 4 minutes ago - cool commit CI trunk push 7 4m34s about 4 minutes ago * cool commit CI trunk push 8 4m34s about 4 minutes ago * cool commit CI trunk push 9 4m34s about 4 minutes ago X cool commit CI trunk push 10 4m34s about 4 minutes ago `), }, { name: "default arguments nontty", opts: &ListOptions{ Limit: defaultLimit, now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), }, isTTY: false, stubs: 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/workflows"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) }, wantOut: heredoc.Doc(` completed timed_out cool commit CI trunk push 1 4m34s 2021-02-23T04:51:00Z in_progress cool commit CI trunk push 2 4m34s 2021-02-23T04:51:00Z completed success cool commit CI trunk push 3 4m34s 2021-02-23T04:51:00Z completed cancelled cool commit CI trunk push 4 4m34s 2021-02-23T04:51:00Z completed failure cool commit CI trunk push 1234 4m34s 2021-02-23T04:51:00Z completed neutral cool commit CI trunk push 6 4m34s 2021-02-23T04:51:00Z completed skipped cool commit CI trunk push 7 4m34s 2021-02-23T04:51:00Z requested cool commit CI trunk push 8 4m34s 2021-02-23T04:51:00Z queued cool commit CI trunk push 9 4m34s 2021-02-23T04:51:00Z completed stale cool commit CI trunk push 10 4m34s 2021-02-23T04:51:00Z `), }, { name: "pagination", opts: &ListOptions{ Limit: 101, now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), }, isTTY: true, stubs: func(reg *httpmock.Registry) { var runID int64 runs := []shared.Run{} for runID < 103 { runs = append(runs, shared.TestRun(runID, shared.InProgress, "")) runID++ } reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.WithHeader(httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: runs[0:100], }), "Link", `; rel="next"`)) reg.Register( httpmock.REST("GET", "repositories/123/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: runs[100:], })) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), httpmock.JSONResponse(workflowShared.WorkflowsPayload{ Workflows: []workflowShared.Workflow{ shared.TestWorkflow, }, })) }, wantOut: heredoc.Doc(` STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE * cool commit CI trunk push 0 4m34s about 4 minutes ago * cool commit CI trunk push 1 4m34s about 4 minutes ago * cool commit CI trunk push 2 4m34s about 4 minutes ago * cool commit CI trunk push 3 4m34s about 4 minutes ago * cool commit CI trunk push 4 4m34s about 4 minutes ago * cool commit CI trunk push 5 4m34s about 4 minutes ago * cool commit CI trunk push 6 4m34s about 4 minutes ago * cool commit CI trunk push 7 4m34s about 4 minutes ago * cool commit CI trunk push 8 4m34s about 4 minutes ago * cool commit CI trunk push 9 4m34s about 4 minutes ago * cool commit CI trunk push 10 4m34s about 4 minutes ago * cool commit CI trunk push 11 4m34s about 4 minutes ago * cool commit CI trunk push 12 4m34s about 4 minutes ago * cool commit CI trunk push 13 4m34s about 4 minutes ago * cool commit CI trunk push 14 4m34s about 4 minutes ago * cool commit CI trunk push 15 4m34s about 4 minutes ago * cool commit CI trunk push 16 4m34s about 4 minutes ago * cool commit CI trunk push 17 4m34s about 4 minutes ago * cool commit CI trunk push 18 4m34s about 4 minutes ago * cool commit CI trunk push 19 4m34s about 4 minutes ago * cool commit CI trunk push 20 4m34s about 4 minutes ago * cool commit CI trunk push 21 4m34s about 4 minutes ago * cool commit CI trunk push 22 4m34s about 4 minutes ago * cool commit CI trunk push 23 4m34s about 4 minutes ago * cool commit CI trunk push 24 4m34s about 4 minutes ago * cool commit CI trunk push 25 4m34s about 4 minutes ago * cool commit CI trunk push 26 4m34s about 4 minutes ago * cool commit CI trunk push 27 4m34s about 4 minutes ago * cool commit CI trunk push 28 4m34s about 4 minutes ago * cool commit CI trunk push 29 4m34s about 4 minutes ago * cool commit CI trunk push 30 4m34s about 4 minutes ago * cool commit CI trunk push 31 4m34s about 4 minutes ago * cool commit CI trunk push 32 4m34s about 4 minutes ago * cool commit CI trunk push 33 4m34s about 4 minutes ago * cool commit CI trunk push 34 4m34s about 4 minutes ago * cool commit CI trunk push 35 4m34s about 4 minutes ago * cool commit CI trunk push 36 4m34s about 4 minutes ago * cool commit CI trunk push 37 4m34s about 4 minutes ago * cool commit CI trunk push 38 4m34s about 4 minutes ago * cool commit CI trunk push 39 4m34s about 4 minutes ago * cool commit CI trunk push 40 4m34s about 4 minutes ago * cool commit CI trunk push 41 4m34s about 4 minutes ago * cool commit CI trunk push 42 4m34s about 4 minutes ago * cool commit CI trunk push 43 4m34s about 4 minutes ago * cool commit CI trunk push 44 4m34s about 4 minutes ago * cool commit CI trunk push 45 4m34s about 4 minutes ago * cool commit CI trunk push 46 4m34s about 4 minutes ago * cool commit CI trunk push 47 4m34s about 4 minutes ago * cool commit CI trunk push 48 4m34s about 4 minutes ago * cool commit CI trunk push 49 4m34s about 4 minutes ago * cool commit CI trunk push 50 4m34s about 4 minutes ago * cool commit CI trunk push 51 4m34s about 4 minutes ago * cool commit CI trunk push 52 4m34s about 4 minutes ago * cool commit CI trunk push 53 4m34s about 4 minutes ago * cool commit CI trunk push 54 4m34s about 4 minutes ago * cool commit CI trunk push 55 4m34s about 4 minutes ago * cool commit CI trunk push 56 4m34s about 4 minutes ago * cool commit CI trunk push 57 4m34s about 4 minutes ago * cool commit CI trunk push 58 4m34s about 4 minutes ago * cool commit CI trunk push 59 4m34s about 4 minutes ago * cool commit CI trunk push 60 4m34s about 4 minutes ago * cool commit CI trunk push 61 4m34s about 4 minutes ago * cool commit CI trunk push 62 4m34s about 4 minutes ago * cool commit CI trunk push 63 4m34s about 4 minutes ago * cool commit CI trunk push 64 4m34s about 4 minutes ago * cool commit CI trunk push 65 4m34s about 4 minutes ago * cool commit CI trunk push 66 4m34s about 4 minutes ago * cool commit CI trunk push 67 4m34s about 4 minutes ago * cool commit CI trunk push 68 4m34s about 4 minutes ago * cool commit CI trunk push 69 4m34s about 4 minutes ago * cool commit CI trunk push 70 4m34s about 4 minutes ago * cool commit CI trunk push 71 4m34s about 4 minutes ago * cool commit CI trunk push 72 4m34s about 4 minutes ago * cool commit CI trunk push 73 4m34s about 4 minutes ago * cool commit CI trunk push 74 4m34s about 4 minutes ago * cool commit CI trunk push 75 4m34s about 4 minutes ago * cool commit CI trunk push 76 4m34s about 4 minutes ago * cool commit CI trunk push 77 4m34s about 4 minutes ago * cool commit CI trunk push 78 4m34s about 4 minutes ago * cool commit CI trunk push 79 4m34s about 4 minutes ago * cool commit CI trunk push 80 4m34s about 4 minutes ago * cool commit CI trunk push 81 4m34s about 4 minutes ago * cool commit CI trunk push 82 4m34s about 4 minutes ago * cool commit CI trunk push 83 4m34s about 4 minutes ago * cool commit CI trunk push 84 4m34s about 4 minutes ago * cool commit CI trunk push 85 4m34s about 4 minutes ago * cool commit CI trunk push 86 4m34s about 4 minutes ago * cool commit CI trunk push 87 4m34s about 4 minutes ago * cool commit CI trunk push 88 4m34s about 4 minutes ago * cool commit CI trunk push 89 4m34s about 4 minutes ago * cool commit CI trunk push 90 4m34s about 4 minutes ago * cool commit CI trunk push 91 4m34s about 4 minutes ago * cool commit CI trunk push 92 4m34s about 4 minutes ago * cool commit CI trunk push 93 4m34s about 4 minutes ago * cool commit CI trunk push 94 4m34s about 4 minutes ago * cool commit CI trunk push 95 4m34s about 4 minutes ago * cool commit CI trunk push 96 4m34s about 4 minutes ago * cool commit CI trunk push 97 4m34s about 4 minutes ago * cool commit CI trunk push 98 4m34s about 4 minutes ago * cool commit CI trunk push 99 4m34s about 4 minutes ago * cool commit CI trunk push 100 4m34s about 4 minutes ago `), }, { name: "no results", opts: &ListOptions{ Limit: defaultLimit, }, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "workflow selector", opts: &ListOptions{ Limit: defaultLimit, WorkflowSelector: "flow.yml", now: shared.TestRunStartTime.Add(time.Minute*4 + time.Second*34), }, isTTY: true, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/flow.yml"), httpmock.JSONResponse(workflowShared.AWorkflow)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123/runs"), httpmock.JSONResponse(shared.RunsPayload{ WorkflowRuns: shared.WorkflowRuns, })) }, wantOut: heredoc.Doc(` STATUS TITLE WORKFLOW BRANCH EVENT ID ELAPSED AGE * cool commit a workflow trunk push 2 4m34s about 4 minute... ✓ cool commit a workflow trunk push 3 4m34s about 4 minute... X cool commit a workflow trunk push 1234 4m34s about 4 minute... `), }, { name: "branch filter applied", opts: &ListOptions{ Limit: defaultLimit, Branch: "the-branch", }, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "branch": []string{"the-branch"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "actor filter applied", opts: &ListOptions{ Limit: defaultLimit, Actor: "bak1an", }, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "actor": []string{"bak1an"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "status filter applied", opts: &ListOptions{ Limit: defaultLimit, Status: "queued", }, isTTY: true, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "status": []string{"queued"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "event filter applied", opts: &ListOptions{ Limit: defaultLimit, Event: "push", }, isTTY: true, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "event": []string{"push"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "created filter applied", opts: &ListOptions{ Limit: defaultLimit, Created: ">=2023-04-24", }, isTTY: true, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "created": []string{">=2023-04-24"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, { name: "commit filter applied", opts: &ListOptions{ Limit: defaultLimit, Commit: "1234567890123456789012345678901234567890", }, isTTY: true, stubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "repos/OWNER/REPO/actions/runs", url.Values{ "head_sha": []string{"1234567890123456789012345678901234567890"}, }), httpmock.JSONResponse(shared.RunsPayload{}), ) }, wantErr: true, wantErrMsg: "no runs found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} defer reg.Verify(t) tt.stubs(reg) tt.opts.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil } ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(tt.isTTY) tt.opts.IO = ios tt.opts.BaseRepo = func() (ghrepo.Interface, error) { return ghrepo.FromFullName("OWNER/REPO") } err := listRun(tt.opts) if tt.wantErr { assert.Error(t, err) assert.Equal(t, tt.wantErrMsg, err.Error()) } else { assert.NoError(t, err) } assert.Equal(t, tt.wantOut, stdout.String()) assert.Equal(t, tt.wantErrOut, stderr.String()) }) } }