diff options
Diffstat (limited to 'pkg/runner/step_action_remote_test.go')
-rw-r--r-- | pkg/runner/step_action_remote_test.go | 732 |
1 files changed, 732 insertions, 0 deletions
diff --git a/pkg/runner/step_action_remote_test.go b/pkg/runner/step_action_remote_test.go new file mode 100644 index 0000000..9dcc338 --- /dev/null +++ b/pkg/runner/step_action_remote_test.go @@ -0,0 +1,732 @@ +package runner + +import ( + "bytes" + "context" + "errors" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "gopkg.in/yaml.v3" + + "github.com/nektos/act/pkg/common" + "github.com/nektos/act/pkg/common/git" + "github.com/nektos/act/pkg/model" +) + +type stepActionRemoteMocks struct { + mock.Mock +} + +func (sarm *stepActionRemoteMocks) readAction(_ context.Context, step *model.Step, actionDir string, actionPath string, readFile actionYamlReader, writeFile fileWriter) (*model.Action, error) { + args := sarm.Called(step, actionDir, actionPath, readFile, writeFile) + return args.Get(0).(*model.Action), args.Error(1) +} + +func (sarm *stepActionRemoteMocks) runAction(step actionStep, actionDir string, remoteAction *remoteAction) common.Executor { + args := sarm.Called(step, actionDir, remoteAction) + return args.Get(0).(func(context.Context) error) +} + +func TestStepActionRemote(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + result *model.StepResult + mocks struct { + env bool + cloned bool + read bool + run bool + } + runError error + }{ + { + name: "run-successful", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + result: &model.StepResult{ + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: true, + }, + }, + { + name: "run-skipped", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + If: yaml.Node{Value: "false"}, + }, + result: &model.StepResult{ + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: false, + }, + }, + { + name: "run-error", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + result: &model.StepResult{ + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + mocks: struct { + env bool + cloned bool + read bool + run bool + }{ + env: true, + cloned: true, + read: true, + run: true, + }, + runError: errors.New("error"), + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + cm := &containerMock{} + sarm := &stepActionRemoteMocks{} + + clonedAction := false + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + clonedAction = true + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "github.com", + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + StepResults: map[string]*model.StepResult{}, + JobContainer: cm, + }, + Step: tt.stepModel, + readAction: sarm.readAction, + runAction: sarm.runAction, + } + sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + if tt.mocks.read { + sarm.On("readAction", sar.Step, suffixMatcher("act/remote-action@v1"), "", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + } + if tt.mocks.run { + sarm.On("runAction", sar, suffixMatcher("act/remote-action@v1"), newRemoteAction(sar.Step.Uses)).Return(func(ctx context.Context) error { return tt.runError }) + + cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) + } + + err := sar.pre()(ctx) + if err == nil { + err = sar.main()(ctx) + } + + assert.Equal(t, tt.runError, err) + assert.Equal(t, tt.mocks.cloned, clonedAction) + assert.Equal(t, tt.result, sar.RunContext.StepResults["step"]) + + sarm.AssertExpectations(t) + cm.AssertExpectations(t) + }) + } +} + +func TestStepActionRemotePre(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + }{ + { + name: "run-pre", + stepModel: &model.Step{ + Uses: "org/repo/path@ref", + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + clonedAction := false + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + clonedAction = true + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + Step: tt.stepModel, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://github.com", + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + err := sar.pre()(ctx) + + assert.Nil(t, err) + assert.Equal(t, true, clonedAction) + + sarm.AssertExpectations(t) + }) + } +} + +func TestStepActionRemotePreThroughAction(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + }{ + { + name: "run-pre", + stepModel: &model.Step{ + Uses: "org/repo/path@ref", + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + clonedAction := false + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + if input.URL == "https://github.com/org/repo" { + clonedAction = true + } + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + Step: tt.stepModel, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://enterprise.github.com", + ReplaceGheActionWithGithubCom: []string{"org/repo"}, + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + err := sar.pre()(ctx) + + assert.Nil(t, err) + assert.Equal(t, true, clonedAction) + + sarm.AssertExpectations(t) + }) + } +} + +func TestStepActionRemotePreThroughActionToken(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + }{ + { + name: "run-pre", + stepModel: &model.Step{ + Uses: "org/repo/path@ref", + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + clonedAction := false + sarm := &stepActionRemoteMocks{} + + origStepAtionRemoteNewCloneExecutor := stepActionRemoteNewCloneExecutor + stepActionRemoteNewCloneExecutor = func(input git.NewGitCloneExecutorInput) common.Executor { + return func(ctx context.Context) error { + if input.URL == "https://github.com/org/repo" && input.Token == "PRIVATE_ACTIONS_TOKEN_ON_GITHUB" { + clonedAction = true + } + return nil + } + } + defer (func() { + stepActionRemoteNewCloneExecutor = origStepAtionRemoteNewCloneExecutor + })() + + sar := &stepActionRemote{ + Step: tt.stepModel, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://enterprise.github.com", + ReplaceGheActionWithGithubCom: []string{"org/repo"}, + ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB", + }, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + }, + readAction: sarm.readAction, + } + + suffixMatcher := func(suffix string) interface{} { + return mock.MatchedBy(func(actionDir string) bool { + return strings.HasSuffix(actionDir, suffix) + }) + } + + sarm.On("readAction", sar.Step, suffixMatcher("org-repo-path@ref"), "path", mock.Anything, mock.Anything).Return(&model.Action{}, nil) + + err := sar.pre()(ctx) + + assert.Nil(t, err) + assert.Equal(t, true, clonedAction) + + sarm.AssertExpectations(t) + }) + } +} + +func TestStepActionRemotePost(t *testing.T) { + table := []struct { + name string + stepModel *model.Step + actionModel *model.Action + initialStepResults map[string]*model.StepResult + IntraActionState map[string]map[string]string + expectedEnv map[string]string + err error + mocks struct { + env bool + exec bool + } + }{ + { + name: "main-success", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSuccess, + Outcome: model.StepStatusSuccess, + Outputs: map[string]string{}, + }, + }, + IntraActionState: map[string]map[string]string{ + "step": { + "key": "value", + }, + }, + expectedEnv: map[string]string{ + "STATE_key": "value", + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "main-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: true, + }, + }, + { + name: "skip-if-failed", + stepModel: &model.Step{ + ID: "step", + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "success()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusFailure, + Outcome: model.StepStatusFailure, + Outputs: map[string]string{}, + }, + }, + mocks: struct { + env bool + exec bool + }{ + env: true, + exec: false, + }, + }, + { + name: "skip-if-main-skipped", + stepModel: &model.Step{ + ID: "step", + If: yaml.Node{Value: "failure()"}, + Uses: "remote/action@v1", + }, + actionModel: &model.Action{ + Runs: model.ActionRuns{ + Using: "node16", + Post: "post.js", + PostIf: "always()", + }, + }, + initialStepResults: map[string]*model.StepResult{ + "step": { + Conclusion: model.StepStatusSkipped, + Outcome: model.StepStatusSkipped, + Outputs: map[string]string{}, + }, + }, + mocks: struct { + env bool + exec bool + }{ + env: false, + exec: false, + }, + }, + } + + for _, tt := range table { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + cm := &containerMock{} + + sar := &stepActionRemote{ + env: map[string]string{}, + RunContext: &RunContext{ + Config: &Config{ + GitHubInstance: "https://github.com", + }, + JobContainer: cm, + Run: &model.Run{ + JobID: "1", + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{ + "1": {}, + }, + }, + }, + StepResults: tt.initialStepResults, + IntraActionState: tt.IntraActionState, + }, + Step: tt.stepModel, + action: tt.actionModel, + } + sar.RunContext.ExprEval = sar.RunContext.NewExpressionEvaluator(ctx) + + if tt.mocks.exec { + 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 }) + + cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(ctx context.Context) error { + return nil + }) + + cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil) + } + + err := sar.post()(ctx) + + assert.Equal(t, tt.err, err) + if tt.expectedEnv != nil { + for key, value := range tt.expectedEnv { + assert.Equal(t, value, sar.env[key]) + } + } + // Enshure that StepResults is nil in this test + assert.Equal(t, sar.RunContext.StepResults["post-step"], (*model.StepResult)(nil)) + cm.AssertExpectations(t) + }) + } +} + +func Test_newRemoteAction(t *testing.T) { + tests := []struct { + action string + want *remoteAction + wantCloneURL string + }{ + { + action: "actions/heroku@main", + want: &remoteAction{ + URL: "", + Org: "actions", + Repo: "heroku", + Path: "", + Ref: "main", + }, + wantCloneURL: "https://github.com/actions/heroku", + }, + { + action: "actions/aws/ec2@main", + want: &remoteAction{ + URL: "", + Org: "actions", + Repo: "aws", + Path: "ec2", + Ref: "main", + }, + wantCloneURL: "https://github.com/actions/aws", + }, + { + action: "./.github/actions/my-action", // it's valid for GitHub, but act don't support it + want: nil, + }, + { + action: "docker://alpine:3.8", // it's valid for GitHub, but act don't support it + want: nil, + }, + { + action: "https://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it + want: &remoteAction{ + URL: "https://gitea.com", + Org: "actions", + Repo: "heroku", + Path: "", + Ref: "main", + }, + wantCloneURL: "https://gitea.com/actions/heroku", + }, + { + action: "https://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it + want: &remoteAction{ + URL: "https://gitea.com", + Org: "actions", + Repo: "aws", + Path: "ec2", + Ref: "main", + }, + wantCloneURL: "https://gitea.com/actions/aws", + }, + { + action: "http://gitea.com/actions/heroku@main", // it's invalid for GitHub, but gitea supports it + want: &remoteAction{ + URL: "http://gitea.com", + Org: "actions", + Repo: "heroku", + Path: "", + Ref: "main", + }, + wantCloneURL: "http://gitea.com/actions/heroku", + }, + { + action: "http://gitea.com/actions/aws/ec2@main", // it's invalid for GitHub, but gitea supports it + want: &remoteAction{ + URL: "http://gitea.com", + Org: "actions", + Repo: "aws", + Path: "ec2", + Ref: "main", + }, + wantCloneURL: "http://gitea.com/actions/aws", + }, + } + for _, tt := range tests { + t.Run(tt.action, func(t *testing.T) { + got := newRemoteAction(tt.action) + assert.Equalf(t, tt.want, got, "newRemoteAction(%v)", tt.action) + cloneURL := "" + if got != nil { + cloneURL = got.CloneURL("github.com") + } + assert.Equalf(t, tt.wantCloneURL, cloneURL, "newRemoteAction(%v).CloneURL()", tt.action) + }) + } +} + +func Test_safeFilename(t *testing.T) { + tests := []struct { + s string + want string + }{ + { + s: "https://test.com/test/", + want: "https---test.com-test-", + }, + { + s: `<>:"/\|?*`, + want: "---------", + }, + } + for _, tt := range tests { + t.Run(tt.s, func(t *testing.T) { + assert.Equalf(t, tt.want, safeFilename(tt.s), "safeFilename(%v)", tt.s) + }) + } +} |