Files
act_runner/pkg/runner/step_action_remote_test.go
Christopher Homberger 09d18916bf more lint errors
2026-02-23 00:30:44 +01:00

641 lines
16 KiB
Go

package runner
import (
"bytes"
"context"
"errors"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"gitea.com/gitea/act_runner/pkg/common"
"gitea.com/gitea/act_runner/pkg/model"
)
type stepActionRemoteMocks struct {
mock.Mock
}
func (sarm *stepActionRemoteMocks) readAction(_ context.Context, step *model.Step, readFile actionYamlReader, config model.ActionConfig) (*model.Action, error) {
args := sarm.Called(step, readFile, config)
return args.Get(0).(*model.Action), args.Error(1)
}
func (sarm *stepActionRemoteMocks) runAction(step actionStep) common.Executor {
args := sarm.Called(step)
return args.Get(0).(func(context.Context) error)
}
type TestRepositoryCache struct {
mock.Mock
}
func (l *TestRepositoryCache) Fetch(ctx context.Context, cacheDir, url, ref, token string) (string, error) {
args := l.Called(ctx, cacheDir, url, ref, token)
return args.Get(0).(string), nil
}
func (l *TestRepositoryCache) GetTarArchive(ctx context.Context, cacheDir, sha, includePrefix string) (io.ReadCloser, error) {
args := l.Called(ctx, cacheDir, sha, includePrefix)
return args.Get(0).(io.ReadCloser), nil
}
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
gitHubServerURL string
gitHubAPIServerURL string
gitHubGraphQlAPIServerURL string
}{
{
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-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,
},
gitHubServerURL: "http://localhost:3000",
gitHubAPIServerURL: "http://localhost:3000/api/v1",
gitHubGraphQlAPIServerURL: "http://localhost:3000/api/graphql",
},
{
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{}
cacheMock := &TestRepositoryCache{}
sar := &stepActionRemote{
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "github.com",
ActionCache: cacheMock,
GitHubServerURL: tt.gitHubServerURL,
GitHubAPIServerURL: tt.gitHubAPIServerURL,
GitHubGraphQlAPIServerURL: tt.gitHubGraphQlAPIServerURL,
Action: model.ActionConfig{
Definition: "action-root",
},
},
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)
serverURL := "https://github.com"
if tt.gitHubServerURL != "" {
serverURL = tt.gitHubServerURL
}
cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), serverURL+"/remote/action", "v1", "").Return("someval")
if tt.mocks.read {
sarm.Mock.On("readAction", sar.Step, mock.Anything, sar.RunContext.Config.Action).Return(&model.Action{}, nil)
}
if tt.mocks.run {
remoteAction := newRemoteAction(sar.Step.Uses)
remoteAction.URL = serverURL
sarm.On("runAction", sar).Return(func(_ context.Context) error { return tt.runError })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(io.NopCloser(&bytes.Buffer{}), 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)
}
require.ErrorIs(t, err, tt.runError)
assert.Equal(t, sar.RunContext.StepResults["step"], tt.result)
sarm.AssertExpectations(t)
cm.AssertExpectations(t)
cacheMock.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()
cacheMock := &TestRepositoryCache{}
sarm := &stepActionRemoteMocks{}
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "github.com",
ActionCache: cacheMock,
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
},
readAction: sarm.readAction,
}
sarm.Mock.On("readAction", sar.Step, mock.Anything, sar.RunContext.Config.Action).Return(&model.Action{}, nil)
cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), "https://github.com/org/repo", "ref", "").Return("someval")
err := sar.pre()(ctx)
require.NoError(t, err)
sarm.AssertExpectations(t)
cacheMock.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()
cacheMock := &TestRepositoryCache{}
sarm := &stepActionRemoteMocks{}
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://enterprise.github.com",
ReplaceGheActionWithGithubCom: []string{"org/repo"},
ActionCache: cacheMock,
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
},
readAction: sarm.readAction,
}
sarm.Mock.On("readAction", sar.Step, mock.Anything, sar.RunContext.Config.Action).Return(&model.Action{}, nil)
cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), "https://github.com/org/repo", "ref", "").Return("someval")
err := sar.pre()(ctx)
require.NoError(t, err)
sarm.AssertExpectations(t)
cacheMock.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()
sarm := &stepActionRemoteMocks{}
cacheMock := &TestRepositoryCache{}
sar := &stepActionRemote{
Step: tt.stepModel,
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://enterprise.github.com",
ReplaceGheActionWithGithubCom: []string{"org/repo"},
ReplaceGheActionTokenWithGithubCom: "PRIVATE_ACTIONS_TOKEN_ON_GITHUB",
ActionCache: cacheMock,
},
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
},
readAction: sarm.readAction,
}
sarm.Mock.On("readAction", sar.Step, mock.Anything, sar.RunContext.Config.Action).Return(&model.Action{}, nil)
cacheMock.Mock.On("Fetch", ctx, mock.AnythingOfType("string"), "https://github.com/org/repo", "ref", "PRIVATE_ACTIONS_TOKEN_ON_GITHUB").Return("someval")
err := sar.pre()(ctx)
require.NoError(t, err)
sarm.AssertExpectations(t)
cacheMock.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{}
cacheMock := &TestRepositoryCache{}
sar := &stepActionRemote{
env: map[string]string{},
RunContext: &RunContext{
Config: &Config{
GitHubInstance: "https://github.com",
ActionCache: cacheMock,
},
JobContainer: cm,
Run: &model.Run{
JobID: "1",
Workflow: &model.Workflow{
Jobs: map[string]*model.Job{
"1": {},
},
},
},
StepResults: tt.initialStepResults,
IntraActionState: tt.IntraActionState,
nodeToolFullPath: "node",
},
Step: tt.stepModel,
action: tt.actionModel,
remoteAction: newRemoteAction(tt.stepModel.Uses),
}
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(_ context.Context) error { return tt.err })
cm.On("Copy", "/var/run/act", mock.AnythingOfType("[]*container.FileEntry")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/envs.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/statecmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("UpdateFromEnv", "/var/run/act/workflow/outputcmd.txt", mock.AnythingOfType("*map[string]string")).Return(func(_ context.Context) error {
return nil
})
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/SUMMARY.md").Return(io.NopCloser(&bytes.Buffer{}), nil)
cm.On("GetContainerArchive", ctx, "/var/run/act/workflow/pathcmd.txt").Return(io.NopCloser(&bytes.Buffer{}), nil)
actionArchive := io.NopCloser(&bytes.Buffer{})
cacheMock.On("GetTarArchive", mock.Anything, "", "", "").Return(actionArchive, nil)
cm.On("CopyTarStream", mock.Anything, "/var/run/act/actions/remote-action@v1/", actionArchive).Return(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])
}
}
// Ensure that StepResults is nil in this test
assert.Equal(t, (*model.StepResult)(nil), sar.RunContext.StepResults["post-step"])
cm.AssertExpectations(t)
})
}
}
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)
})
}
}