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 }