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 }