github.com/nektos/act@v0.2.83/pkg/runner/step_action_remote_test.go (about)

     1  package runner
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"io"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"gopkg.in/yaml.v3"
    14  
    15  	"github.com/nektos/act/pkg/common"
    16  	"github.com/nektos/act/pkg/common/git"
    17  	"github.com/nektos/act/pkg/model"
    18  )
    19  
    20  type stepActionRemoteMocks struct {
    21  	mock.Mock
    22  }
    23  
    24  func (sarm *stepActionRemoteMocks) readAction(_ context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) {
    25  	args := sarm.Called(step, actionDir, actionPath, readFile, writeFile)
    26  	return args.Get(0).(*model.Action), args.Error(1)
    27  }
    28  
    29  func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor {
    30  	args := sarm.Called(step, actionDir, remoteAction)
    31  	return args.Get(0).(func(context.Context) error)
    32  }
    33  
    34  func TestStepActionRemote(t *testing.T) {
    35  	table := []struct {
    36  		name      string
    37  		stepModel *model.Step
    38  		result    *model.StepResult
    39  		mocks     struct {
    40  			env    bool
    41  			cloned bool
    42  			read   bool
    43  			run    bool
    44  		}
    45  		runError error
    46  	}{
    47  		{
    48  			name: "run-successful",
    49  			stepModel: &model.Step{
    50  				ID:   "step",
    51  				Uses: "remote/action@v1",
    52  			},
    53  			result: &model.StepResult{
    54  				Conclusion: model.StepStatusSuccess,
    55  				Outcome:    model.StepStatusSuccess,
    56  				Outputs:    map[string]string{},
    57  			},
    58  			mocks: struct {
    59  				env    bool
    60  				cloned bool
    61  				read   bool
    62  				run    bool
    63  			}{
    64  				env:    true,
    65  				cloned: true,
    66  				read:   true,
    67  				run:    true,
    68  			},
    69  		},
    70  		{
    71  			name: "run-skipped",
    72  			stepModel: &model.Step{
    73  				ID:   "step",
    74  				Uses: "remote/action@v1",
    75  				If:   yaml.Node{Value: "false"},
    76  			},
    77  			result: &model.StepResult{
    78  				Conclusion: model.StepStatusSkipped,
    79  				Outcome:    model.StepStatusSkipped,
    80  				Outputs:    map[string]string{},
    81  			},
    82  			mocks: struct {
    83  				env    bool
    84  				cloned bool
    85  				read   bool
    86  				run    bool
    87  			}{
    88  				env:    true,
    89  				cloned: true,
    90  				read:   true,
    91  				run:    false,
    92  			},
    93  		},
    94  		{
    95  			name: "run-error",
    96  			stepModel: &model.Step{
    97  				ID:   "step",
    98  				Uses: "remote/action@v1",
    99  			},
   100  			result: &model.StepResult{
   101  				Conclusion: model.StepStatusFailure,
   102  				Outcome:    model.StepStatusFailure,
   103  				Outputs:    map[string]string{},
   104  			},
   105  			mocks: struct {
   106  				env    bool
   107  				cloned bool
   108  				read   bool
   109  				run    bool
   110  			}{
   111  				env:    true,
   112  				cloned: true,
   113  				read:   true,
   114  				run:    true,
   115  			},
   116  			runError: errors.New("error"),
   117  		},
   118  	}
   119  
   120  	for _, tt := range table {
   121  		t.Run(tt.name, func(t *testing.T) {
   122  			ctx := context.Background()
   123  
   124  			cm := &containerMock{}
   125  			sarm := &stepActionRemoteMocks{}
   126  
   127  			clonedAction := false
   128  
   129  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   130  			stepActionRemoteNewCloneExecutor = func(_ git.NewGitCloneExecutorInput) common.Executor {
   131  				return func(_ context.Context) error {
   132  					clonedAction = true
   133  					return nil
   134  				}
   135  			}
   136  			defer (func() {
   137  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   138  			})()
   139  
   140  			sar := &stepActionRemote{
   141  				RunContext: &RunContext{
   142  					Config: &Config{
   143  						GitHubInstance: "github.com",
   144  					},
   145  					Run: &model.Run{
   146  						JobID: "1",
   147  						Workflow: &model.Workflow{
   148  							Jobs: map[string]*model.Job{
   149  								"1": {},
   150  							},
   151  						},
   152  					},
   153  					StepResults:  map[string]*model.StepResult{},
   154  					JobContainer: cm,
   155  				},
   156  				Step:       tt.stepModel,
   157  				readAction: sarm.readAction,
   158  				runAction:  sarm.runAction,
   159  			}
   160  			sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
   161  
   162  			suffixMatcher := func(suffix string) interface{} {
   163  				return mock.MatchedBy(func(actionDir string) bool {
   164  					return strings.HasSuffix(actionDir, suffix)
   165  				})
   166  			}
   167  
   168  			if tt.mocks.read {
   169  				sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   170  			}
   171  			if tt.mocks.run {
   172  				sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), newRemoteAction(sar.Step.Uses)).Return(func(_ context.Context) error { return tt.runError })
   173  
   174  				cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(_ context.Context) error {
   175  					return nil
   176  				})
   177  
   178  				cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   179  					return nil
   180  				})
   181  
   182  				cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   183  					return nil
   184  				})
   185  
   186  				cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   187  					return nil
   188  				})
   189  
   190  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(io.NopCloser(&bytes.Buffer{}), nil)
   191  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
   192  			}
   193  
   194  			err := sar.pre()(ctx)
   195  			if err == nil {
   196  				err = sar.main()(ctx)
   197  			}
   198  
   199  			assert.ErrorIs(t, err, tt.runError)
   200  			assert.Equal(t, tt.mocks.cloned, clonedAction)
   201  			assert.Equal(t, sar.RunContext.StepResults["step"], tt.result)
   202  
   203  			sarm.AssertExpectations(t)
   204  			cm.AssertExpectations(t)
   205  		})
   206  	}
   207  }
   208  
   209  func TestStepActionRemotePre(t *testing.T) {
   210  	table := []struct {
   211  		name      string
   212  		stepModel *model.Step
   213  	}{
   214  		{
   215  			name: "run-pre",
   216  			stepModel: &model.Step{
   217  				Uses: "org/repo/path@ref",
   218  			},
   219  		},
   220  	}
   221  
   222  	for _, tt := range table {
   223  		t.Run(tt.name, func(t *testing.T) {
   224  			ctx := context.Background()
   225  
   226  			clonedAction := false
   227  			sarm := &stepActionRemoteMocks{}
   228  
   229  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   230  			stepActionRemoteNewCloneExecutor = func(_ git.NewGitCloneExecutorInput) common.Executor {
   231  				return func(_ context.Context) error {
   232  					clonedAction = true
   233  					return nil
   234  				}
   235  			}
   236  			defer (func() {
   237  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   238  			})()
   239  
   240  			sar := &stepActionRemote{
   241  				Step: tt.stepModel,
   242  				RunContext: &RunContext{
   243  					Config: &Config{
   244  						GitHubInstance: "https://github.com",
   245  					},
   246  					Run: &model.Run{
   247  						JobID: "1",
   248  						Workflow: &model.Workflow{
   249  							Jobs: map[string]*model.Job{
   250  								"1": {},
   251  							},
   252  						},
   253  					},
   254  				},
   255  				readAction: sarm.readAction,
   256  			}
   257  
   258  			suffixMatcher := func(suffix string) interface{} {
   259  				return mock.MatchedBy(func(actionDir string) bool {
   260  					return strings.HasSuffix(actionDir, suffix)
   261  				})
   262  			}
   263  
   264  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   265  
   266  			err := sar.pre()(ctx)
   267  
   268  			assert.Nil(t, err)
   269  			assert.Equal(t, true, clonedAction)
   270  
   271  			sarm.AssertExpectations(t)
   272  		})
   273  	}
   274  }
   275  
   276  func TestStepActionRemotePreThroughAction(t *testing.T) {
   277  	table := []struct {
   278  		name      string
   279  		stepModel *model.Step
   280  	}{
   281  		{
   282  			name: "run-pre",
   283  			stepModel: &model.Step{
   284  				Uses: "org/repo/path@ref",
   285  			},
   286  		},
   287  	}
   288  
   289  	for _, tt := range table {
   290  		t.Run(tt.name, func(t *testing.T) {
   291  			ctx := context.Background()
   292  
   293  			clonedAction := false
   294  			sarm := &stepActionRemoteMocks{}
   295  
   296  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   297  			stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
   298  				return func(_ context.Context) error {
   299  					if input.URL == "https://github.com/org/repo" {
   300  						clonedAction = true
   301  					}
   302  					return nil
   303  				}
   304  			}
   305  			defer (func() {
   306  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   307  			})()
   308  
   309  			sar := &stepActionRemote{
   310  				Step: tt.stepModel,
   311  				RunContext: &RunContext{
   312  					Config: &Config{
   313  						GitHubInstance:                "https://enterprise.github.com",
   314  						ReplaceGheActionWithGithubCom: []string{"org/repo"},
   315  					},
   316  					Run: &model.Run{
   317  						JobID: "1",
   318  						Workflow: &model.Workflow{
   319  							Jobs: map[string]*model.Job{
   320  								"1": {},
   321  							},
   322  						},
   323  					},
   324  				},
   325  				readAction: sarm.readAction,
   326  			}
   327  
   328  			suffixMatcher := func(suffix string) interface{} {
   329  				return mock.MatchedBy(func(actionDir string) bool {
   330  					return strings.HasSuffix(actionDir, suffix)
   331  				})
   332  			}
   333  
   334  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   335  
   336  			err := sar.pre()(ctx)
   337  
   338  			assert.Nil(t, err)
   339  			assert.Equal(t, true, clonedAction)
   340  
   341  			sarm.AssertExpectations(t)
   342  		})
   343  	}
   344  }
   345  
   346  func TestStepActionRemotePreThroughActionToken(t *testing.T) {
   347  	table := []struct {
   348  		name      string
   349  		stepModel *model.Step
   350  	}{
   351  		{
   352  			name: "run-pre",
   353  			stepModel: &model.Step{
   354  				Uses: "org/repo/path@ref",
   355  			},
   356  		},
   357  	}
   358  
   359  	for _, tt := range table {
   360  		t.Run(tt.name, func(t *testing.T) {
   361  			ctx := context.Background()
   362  
   363  			clonedAction := false
   364  			sarm := &stepActionRemoteMocks{}
   365  
   366  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   367  			stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
   368  				return func(_ context.Context) error {
   369  					if input.URL == "https://github.com/org/repo" && input.Token == "PRIVATE_ACTIONS_TOKEN_ON_GITHUB" {
   370  						clonedAction = true
   371  					}
   372  					return nil
   373  				}
   374  			}
   375  			defer (func() {
   376  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   377  			})()
   378  
   379  			sar := &stepActionRemote{
   380  				Step: tt.stepModel,
   381  				RunContext: &RunContext{
   382  					Config: &Config{
   383  						GitHubInstance:                     "https://enterprise.github.com",
   384  						ReplaceGheActionWithGithubCom:      []string{"org/repo"},
   385  						ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB",
   386  					},
   387  					Run: &model.Run{
   388  						JobID: "1",
   389  						Workflow: &model.Workflow{
   390  							Jobs: map[string]*model.Job{
   391  								"1": {},
   392  							},
   393  						},
   394  					},
   395  				},
   396  				readAction: sarm.readAction,
   397  			}
   398  
   399  			suffixMatcher := func(suffix string) interface{} {
   400  				return mock.MatchedBy(func(actionDir string) bool {
   401  					return strings.HasSuffix(actionDir, suffix)
   402  				})
   403  			}
   404  
   405  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   406  
   407  			err := sar.pre()(ctx)
   408  
   409  			assert.Nil(t, err)
   410  			assert.Equal(t, true, clonedAction)
   411  
   412  			sarm.AssertExpectations(t)
   413  		})
   414  	}
   415  }
   416  
   417  func TestStepActionRemotePost(t *testing.T) {
   418  	table := []struct {
   419  		name               string
   420  		stepModel          *model.Step
   421  		actionModel        *model.Action
   422  		initialStepResults map[string]*model.StepResult
   423  		IntraActionState   map[string]map[string]string
   424  		expectedEnv        map[string]string
   425  		err                error
   426  		mocks              struct {
   427  			env  bool
   428  			exec bool
   429  		}
   430  	}{
   431  		{
   432  			name: "main-success",
   433  			stepModel: &model.Step{
   434  				ID:   "step",
   435  				Uses: "remote/action@v1",
   436  			},
   437  			actionModel: &model.Action{
   438  				Runs: model.ActionRuns{
   439  					Using:  "node16",
   440  					Post:   "post.js",
   441  					PostIf: "always()",
   442  				},
   443  			},
   444  			initialStepResults: map[string]*model.StepResult{
   445  				"step": {
   446  					Conclusion: model.StepStatusSuccess,
   447  					Outcome:    model.StepStatusSuccess,
   448  					Outputs:    map[string]string{},
   449  				},
   450  			},
   451  			IntraActionState: map[string]map[string]string{
   452  				"step": {
   453  					"key": "value",
   454  				},
   455  			},
   456  			expectedEnv: map[string]string{
   457  				"STATE_key": "value",
   458  			},
   459  			mocks: struct {
   460  				env  bool
   461  				exec bool
   462  			}{
   463  				env:  true,
   464  				exec: true,
   465  			},
   466  		},
   467  		{
   468  			name: "main-failed",
   469  			stepModel: &model.Step{
   470  				ID:   "step",
   471  				Uses: "remote/action@v1",
   472  			},
   473  			actionModel: &model.Action{
   474  				Runs: model.ActionRuns{
   475  					Using:  "node16",
   476  					Post:   "post.js",
   477  					PostIf: "always()",
   478  				},
   479  			},
   480  			initialStepResults: map[string]*model.StepResult{
   481  				"step": {
   482  					Conclusion: model.StepStatusFailure,
   483  					Outcome:    model.StepStatusFailure,
   484  					Outputs:    map[string]string{},
   485  				},
   486  			},
   487  			mocks: struct {
   488  				env  bool
   489  				exec bool
   490  			}{
   491  				env:  true,
   492  				exec: true,
   493  			},
   494  		},
   495  		{
   496  			name: "skip-if-failed",
   497  			stepModel: &model.Step{
   498  				ID:   "step",
   499  				Uses: "remote/action@v1",
   500  			},
   501  			actionModel: &model.Action{
   502  				Runs: model.ActionRuns{
   503  					Using:  "node16",
   504  					Post:   "post.js",
   505  					PostIf: "success()",
   506  				},
   507  			},
   508  			initialStepResults: map[string]*model.StepResult{
   509  				"step": {
   510  					Conclusion: model.StepStatusFailure,
   511  					Outcome:    model.StepStatusFailure,
   512  					Outputs:    map[string]string{},
   513  				},
   514  			},
   515  			mocks: struct {
   516  				env  bool
   517  				exec bool
   518  			}{
   519  				env:  true,
   520  				exec: false,
   521  			},
   522  		},
   523  		{
   524  			name: "skip-if-main-skipped",
   525  			stepModel: &model.Step{
   526  				ID:   "step",
   527  				If:   yaml.Node{Value: "failure()"},
   528  				Uses: "remote/action@v1",
   529  			},
   530  			actionModel: &model.Action{
   531  				Runs: model.ActionRuns{
   532  					Using:  "node16",
   533  					Post:   "post.js",
   534  					PostIf: "always()",
   535  				},
   536  			},
   537  			initialStepResults: map[string]*model.StepResult{
   538  				"step": {
   539  					Conclusion: model.StepStatusSkipped,
   540  					Outcome:    model.StepStatusSkipped,
   541  					Outputs:    map[string]string{},
   542  				},
   543  			},
   544  			mocks: struct {
   545  				env  bool
   546  				exec bool
   547  			}{
   548  				env:  false,
   549  				exec: false,
   550  			},
   551  		},
   552  	}
   553  
   554  	for _, tt := range table {
   555  		t.Run(tt.name, func(t *testing.T) {
   556  			ctx := context.Background()
   557  
   558  			cm := &containerMock{}
   559  
   560  			sar := &stepActionRemote{
   561  				env: map[string]string{},
   562  				RunContext: &RunContext{
   563  					Config: &Config{
   564  						GitHubInstance: "https://github.com",
   565  					},
   566  					JobContainer: cm,
   567  					Run: &model.Run{
   568  						JobID: "1",
   569  						Workflow: &model.Workflow{
   570  							Jobs: map[string]*model.Job{
   571  								"1": {},
   572  							},
   573  						},
   574  					},
   575  					StepResults:      tt.initialStepResults,
   576  					IntraActionState: tt.IntraActionState,
   577  					nodeToolFullPath: "node",
   578  				},
   579  				Step:   tt.stepModel,
   580  				action: tt.actionModel,
   581  			}
   582  			sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
   583  
   584  			if tt.mocks.exec {
   585  				cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(_ context.Context) error { return tt.err })
   586  
   587  				cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(_ context.Context) error {
   588  					return nil
   589  				})
   590  
   591  				cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   592  					return nil
   593  				})
   594  
   595  				cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   596  					return nil
   597  				})
   598  
   599  				cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
   600  					return nil
   601  				})
   602  
   603  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(io.NopCloser(&bytes.Buffer{}), nil)
   604  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
   605  			}
   606  
   607  			err := sar.post()(ctx)
   608  
   609  			assert.Equal(t, tt.err, err)
   610  			if tt.expectedEnv != nil {
   611  				for key, value := range tt.expectedEnv {
   612  					assert.Equal(t, value, sar.env[key])
   613  				}
   614  			}
   615  			// Enshure that StepResults is nil in this test
   616  			assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
   617  			cm.AssertExpectations(t)
   618  		})
   619  	}
   620  }
   621  
   622  func Test_safeFilename(t *testing.T) {
   623  	tests := []struct {
   624  		s    string
   625  		want string
   626  	}{
   627  		{
   628  			s:    "https://test.com/test/",
   629  			want: "https---test.com-test-",
   630  		},
   631  		{
   632  			s:    `<>:"/\|?*`,
   633  			want: "---------",
   634  		},
   635  	}
   636  	for _, tt := range tests {
   637  		t.Run(tt.s, func(t *testing.T) {
   638  			assert.Equalf(t, tt.want, safeFilename(tt.s), "safeFilename(%v)", tt.s)
   639  		})
   640  	}
   641  }