github.1485827954.workers.dev/nektos/act@v0.2.63/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(input git.NewGitCloneExecutorInput) common.Executor {
   131  				return func(ctx 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(ctx context.Context) error { return tt.runError })
   173  
   174  				cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx 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(ctx 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(ctx 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(ctx context.Context) error {
   187  					return nil
   188  				})
   189  
   190  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
   191  			}
   192  
   193  			err := sar.pre()(ctx)
   194  			if err == nil {
   195  				err = sar.main()(ctx)
   196  			}
   197  
   198  			assert.Equal(t, tt.runError, err)
   199  			assert.Equal(t, tt.mocks.cloned, clonedAction)
   200  			assert.Equal(t, tt.result, sar.RunContext.StepResults["step"])
   201  
   202  			sarm.AssertExpectations(t)
   203  			cm.AssertExpectations(t)
   204  		})
   205  	}
   206  }
   207  
   208  func TestStepActionRemotePre(t *testing.T) {
   209  	table := []struct {
   210  		name      string
   211  		stepModel *model.Step
   212  	}{
   213  		{
   214  			name: "run-pre",
   215  			stepModel: &model.Step{
   216  				Uses: "org/repo/path@ref",
   217  			},
   218  		},
   219  	}
   220  
   221  	for _, tt := range table {
   222  		t.Run(tt.name, func(t *testing.T) {
   223  			ctx := context.Background()
   224  
   225  			clonedAction := false
   226  			sarm := &stepActionRemoteMocks{}
   227  
   228  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   229  			stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
   230  				return func(ctx context.Context) error {
   231  					clonedAction = true
   232  					return nil
   233  				}
   234  			}
   235  			defer (func() {
   236  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   237  			})()
   238  
   239  			sar := &stepActionRemote{
   240  				Step: tt.stepModel,
   241  				RunContext: &RunContext{
   242  					Config: &Config{
   243  						GitHubInstance: "https://github.com",
   244  					},
   245  					Run: &model.Run{
   246  						JobID: "1",
   247  						Workflow: &model.Workflow{
   248  							Jobs: map[string]*model.Job{
   249  								"1": {},
   250  							},
   251  						},
   252  					},
   253  				},
   254  				readAction: sarm.readAction,
   255  			}
   256  
   257  			suffixMatcher := func(suffix string) interface{} {
   258  				return mock.MatchedBy(func(actionDir string) bool {
   259  					return strings.HasSuffix(actionDir, suffix)
   260  				})
   261  			}
   262  
   263  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   264  
   265  			err := sar.pre()(ctx)
   266  
   267  			assert.Nil(t, err)
   268  			assert.Equal(t, true, clonedAction)
   269  
   270  			sarm.AssertExpectations(t)
   271  		})
   272  	}
   273  }
   274  
   275  func TestStepActionRemotePreThroughAction(t *testing.T) {
   276  	table := []struct {
   277  		name      string
   278  		stepModel *model.Step
   279  	}{
   280  		{
   281  			name: "run-pre",
   282  			stepModel: &model.Step{
   283  				Uses: "org/repo/path@ref",
   284  			},
   285  		},
   286  	}
   287  
   288  	for _, tt := range table {
   289  		t.Run(tt.name, func(t *testing.T) {
   290  			ctx := context.Background()
   291  
   292  			clonedAction := false
   293  			sarm := &stepActionRemoteMocks{}
   294  
   295  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   296  			stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
   297  				return func(ctx context.Context) error {
   298  					if input.URL == "https://github.com/org/repo" {
   299  						clonedAction = true
   300  					}
   301  					return nil
   302  				}
   303  			}
   304  			defer (func() {
   305  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   306  			})()
   307  
   308  			sar := &stepActionRemote{
   309  				Step: tt.stepModel,
   310  				RunContext: &RunContext{
   311  					Config: &Config{
   312  						GitHubInstance:                "https://enterprise.github.com",
   313  						ReplaceGheActionWithGithubCom: []string{"org/repo"},
   314  					},
   315  					Run: &model.Run{
   316  						JobID: "1",
   317  						Workflow: &model.Workflow{
   318  							Jobs: map[string]*model.Job{
   319  								"1": {},
   320  							},
   321  						},
   322  					},
   323  				},
   324  				readAction: sarm.readAction,
   325  			}
   326  
   327  			suffixMatcher := func(suffix string) interface{} {
   328  				return mock.MatchedBy(func(actionDir string) bool {
   329  					return strings.HasSuffix(actionDir, suffix)
   330  				})
   331  			}
   332  
   333  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   334  
   335  			err := sar.pre()(ctx)
   336  
   337  			assert.Nil(t, err)
   338  			assert.Equal(t, true, clonedAction)
   339  
   340  			sarm.AssertExpectations(t)
   341  		})
   342  	}
   343  }
   344  
   345  func TestStepActionRemotePreThroughActionToken(t *testing.T) {
   346  	table := []struct {
   347  		name      string
   348  		stepModel *model.Step
   349  	}{
   350  		{
   351  			name: "run-pre",
   352  			stepModel: &model.Step{
   353  				Uses: "org/repo/path@ref",
   354  			},
   355  		},
   356  	}
   357  
   358  	for _, tt := range table {
   359  		t.Run(tt.name, func(t *testing.T) {
   360  			ctx := context.Background()
   361  
   362  			clonedAction := false
   363  			sarm := &stepActionRemoteMocks{}
   364  
   365  			origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor
   366  			stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor {
   367  				return func(ctx context.Context) error {
   368  					if input.URL == "https://github.com/org/repo" && input.Token == "PRIVATE_ACTIONS_TOKEN_ON_GITHUB" {
   369  						clonedAction = true
   370  					}
   371  					return nil
   372  				}
   373  			}
   374  			defer (func() {
   375  				stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor
   376  			})()
   377  
   378  			sar := &stepActionRemote{
   379  				Step: tt.stepModel,
   380  				RunContext: &RunContext{
   381  					Config: &Config{
   382  						GitHubInstance:                     "https://enterprise.github.com",
   383  						ReplaceGheActionWithGithubCom:      []string{"org/repo"},
   384  						ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB",
   385  					},
   386  					Run: &model.Run{
   387  						JobID: "1",
   388  						Workflow: &model.Workflow{
   389  							Jobs: map[string]*model.Job{
   390  								"1": {},
   391  							},
   392  						},
   393  					},
   394  				},
   395  				readAction: sarm.readAction,
   396  			}
   397  
   398  			suffixMatcher := func(suffix string) interface{} {
   399  				return mock.MatchedBy(func(actionDir string) bool {
   400  					return strings.HasSuffix(actionDir, suffix)
   401  				})
   402  			}
   403  
   404  			sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil)
   405  
   406  			err := sar.pre()(ctx)
   407  
   408  			assert.Nil(t, err)
   409  			assert.Equal(t, true, clonedAction)
   410  
   411  			sarm.AssertExpectations(t)
   412  		})
   413  	}
   414  }
   415  
   416  func TestStepActionRemotePost(t *testing.T) {
   417  	table := []struct {
   418  		name               string
   419  		stepModel          *model.Step
   420  		actionModel        *model.Action
   421  		initialStepResults map[string]*model.StepResult
   422  		IntraActionState   map[string]map[string]string
   423  		expectedEnv        map[string]string
   424  		err                error
   425  		mocks              struct {
   426  			env  bool
   427  			exec bool
   428  		}
   429  	}{
   430  		{
   431  			name: "main-success",
   432  			stepModel: &model.Step{
   433  				ID:   "step",
   434  				Uses: "remote/action@v1",
   435  			},
   436  			actionModel: &model.Action{
   437  				Runs: model.ActionRuns{
   438  					Using:  "node16",
   439  					Post:   "post.js",
   440  					PostIf: "always()",
   441  				},
   442  			},
   443  			initialStepResults: map[string]*model.StepResult{
   444  				"step": {
   445  					Conclusion: model.StepStatusSuccess,
   446  					Outcome:    model.StepStatusSuccess,
   447  					Outputs:    map[string]string{},
   448  				},
   449  			},
   450  			IntraActionState: map[string]map[string]string{
   451  				"step": {
   452  					"key": "value",
   453  				},
   454  			},
   455  			expectedEnv: map[string]string{
   456  				"STATE_key": "value",
   457  			},
   458  			mocks: struct {
   459  				env  bool
   460  				exec bool
   461  			}{
   462  				env:  true,
   463  				exec: true,
   464  			},
   465  		},
   466  		{
   467  			name: "main-failed",
   468  			stepModel: &model.Step{
   469  				ID:   "step",
   470  				Uses: "remote/action@v1",
   471  			},
   472  			actionModel: &model.Action{
   473  				Runs: model.ActionRuns{
   474  					Using:  "node16",
   475  					Post:   "post.js",
   476  					PostIf: "always()",
   477  				},
   478  			},
   479  			initialStepResults: map[string]*model.StepResult{
   480  				"step": {
   481  					Conclusion: model.StepStatusFailure,
   482  					Outcome:    model.StepStatusFailure,
   483  					Outputs:    map[string]string{},
   484  				},
   485  			},
   486  			mocks: struct {
   487  				env  bool
   488  				exec bool
   489  			}{
   490  				env:  true,
   491  				exec: true,
   492  			},
   493  		},
   494  		{
   495  			name: "skip-if-failed",
   496  			stepModel: &model.Step{
   497  				ID:   "step",
   498  				Uses: "remote/action@v1",
   499  			},
   500  			actionModel: &model.Action{
   501  				Runs: model.ActionRuns{
   502  					Using:  "node16",
   503  					Post:   "post.js",
   504  					PostIf: "success()",
   505  				},
   506  			},
   507  			initialStepResults: map[string]*model.StepResult{
   508  				"step": {
   509  					Conclusion: model.StepStatusFailure,
   510  					Outcome:    model.StepStatusFailure,
   511  					Outputs:    map[string]string{},
   512  				},
   513  			},
   514  			mocks: struct {
   515  				env  bool
   516  				exec bool
   517  			}{
   518  				env:  true,
   519  				exec: false,
   520  			},
   521  		},
   522  		{
   523  			name: "skip-if-main-skipped",
   524  			stepModel: &model.Step{
   525  				ID:   "step",
   526  				If:   yaml.Node{Value: "failure()"},
   527  				Uses: "remote/action@v1",
   528  			},
   529  			actionModel: &model.Action{
   530  				Runs: model.ActionRuns{
   531  					Using:  "node16",
   532  					Post:   "post.js",
   533  					PostIf: "always()",
   534  				},
   535  			},
   536  			initialStepResults: map[string]*model.StepResult{
   537  				"step": {
   538  					Conclusion: model.StepStatusSkipped,
   539  					Outcome:    model.StepStatusSkipped,
   540  					Outputs:    map[string]string{},
   541  				},
   542  			},
   543  			mocks: struct {
   544  				env  bool
   545  				exec bool
   546  			}{
   547  				env:  false,
   548  				exec: false,
   549  			},
   550  		},
   551  	}
   552  
   553  	for _, tt := range table {
   554  		t.Run(tt.name, func(t *testing.T) {
   555  			ctx := context.Background()
   556  
   557  			cm := &containerMock{}
   558  
   559  			sar := &stepActionRemote{
   560  				env: map[string]string{},
   561  				RunContext: &RunContext{
   562  					Config: &Config{
   563  						GitHubInstance: "https://github.com",
   564  					},
   565  					JobContainer: cm,
   566  					Run: &model.Run{
   567  						JobID: "1",
   568  						Workflow: &model.Workflow{
   569  							Jobs: map[string]*model.Job{
   570  								"1": {},
   571  							},
   572  						},
   573  					},
   574  					StepResults:      tt.initialStepResults,
   575  					IntraActionState: tt.IntraActionState,
   576  				},
   577  				Step:   tt.stepModel,
   578  				action: tt.actionModel,
   579  			}
   580  			sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx)
   581  
   582  			if tt.mocks.exec {
   583  				cm.On("Exec", []string{"node", "/var/run/act/actions/remote-action@v1/post.js"}, sar.env, "", "").Return(func(ctx context.Context) error { return tt.err })
   584  
   585  				cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error {
   586  					return nil
   587  				})
   588  
   589  				cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
   590  					return nil
   591  				})
   592  
   593  				cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
   594  					return nil
   595  				})
   596  
   597  				cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error {
   598  					return nil
   599  				})
   600  
   601  				cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
   602  			}
   603  
   604  			err := sar.post()(ctx)
   605  
   606  			assert.Equal(t, tt.err, err)
   607  			if tt.expectedEnv != nil {
   608  				for key, value := range tt.expectedEnv {
   609  					assert.Equal(t, value, sar.env[key])
   610  				}
   611  			}
   612  			// Enshure that StepResults is nil in this test
   613  			assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil))
   614  			cm.AssertExpectations(t)
   615  		})
   616  	}
   617  }
   618  
   619  func Test_safeFilename(t *testing.T) {
   620  	tests := []struct {
   621  		s    string
   622  		want string
   623  	}{
   624  		{
   625  			s:    "https://test.com/test/",
   626  			want: "https---test.com-test-",
   627  		},
   628  		{
   629  			s:    `<>:"/\|?*`,
   630  			want: "---------",
   631  		},
   632  	}
   633  	for _, tt := range tests {
   634  		t.Run(tt.s, func(t *testing.T) {
   635  			assert.Equalf(t, tt.want, safeFilename(tt.s), "safeFilename(%v)", tt.s)
   636  		})
   637  	}
   638  }