github.com/secure-build/gitlab-runner@v12.5.0+incompatible/network/gitlab_test.go (about) 1 package network 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9 "net/http/httptest" 10 "os" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "testing" 15 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/mock" 18 "github.com/stretchr/testify/require" 19 20 . "gitlab.com/gitlab-org/gitlab-runner/common" 21 ) 22 23 var brokenCredentials = RunnerCredentials{ 24 URL: "broken", 25 } 26 27 var brokenConfig = RunnerConfig{ 28 RunnerCredentials: brokenCredentials, 29 } 30 31 func TestClients(t *testing.T) { 32 c := NewGitLabClient() 33 c1, _ := c.getClient(&RunnerCredentials{ 34 URL: "http://test/", 35 }) 36 c2, _ := c.getClient(&RunnerCredentials{ 37 URL: "http://test2/", 38 }) 39 c4, _ := c.getClient(&RunnerCredentials{ 40 URL: "http://test/", 41 TLSCAFile: "ca_file", 42 }) 43 c5, _ := c.getClient(&RunnerCredentials{ 44 URL: "http://test/", 45 TLSCAFile: "ca_file", 46 }) 47 c6, _ := c.getClient(&RunnerCredentials{ 48 URL: "http://test/", 49 TLSCAFile: "ca_file", 50 TLSCertFile: "cert_file", 51 TLSKeyFile: "key_file", 52 }) 53 c7, _ := c.getClient(&RunnerCredentials{ 54 URL: "http://test/", 55 TLSCAFile: "ca_file", 56 TLSCertFile: "cert_file", 57 TLSKeyFile: "key_file2", 58 }) 59 c8, c8err := c.getClient(&brokenCredentials) 60 assert.NotEqual(t, c1, c2) 61 assert.NotEqual(t, c1, c4) 62 assert.Equal(t, c4, c5) 63 assert.NotEqual(t, c5, c6) 64 assert.Equal(t, c6, c7) 65 assert.Nil(t, c8) 66 assert.Error(t, c8err) 67 } 68 69 func testRegisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 70 if r.URL.Path != "/api/v4/runners" { 71 w.WriteHeader(http.StatusNotFound) 72 return 73 } 74 75 if r.Method != "POST" { 76 w.WriteHeader(http.StatusNotAcceptable) 77 return 78 } 79 80 body, err := ioutil.ReadAll(r.Body) 81 assert.NoError(t, err) 82 83 var req map[string]interface{} 84 err = json.Unmarshal(body, &req) 85 assert.NoError(t, err) 86 87 res := make(map[string]interface{}) 88 89 switch req["token"].(string) { 90 case "valid": 91 if req["description"].(string) != "test" { 92 w.WriteHeader(http.StatusBadRequest) 93 return 94 } 95 96 res["token"] = req["token"].(string) 97 case "invalid": 98 w.WriteHeader(http.StatusForbidden) 99 return 100 default: 101 w.WriteHeader(http.StatusBadRequest) 102 return 103 } 104 105 if r.Header.Get("Accept") != "application/json" { 106 w.WriteHeader(http.StatusBadRequest) 107 return 108 } 109 110 output, err := json.Marshal(res) 111 if err != nil { 112 w.WriteHeader(http.StatusInternalServerError) 113 return 114 } 115 116 w.Header().Set("Content-Type", "application/json") 117 w.WriteHeader(http.StatusCreated) 118 w.Write(output) 119 } 120 121 func TestRegisterRunner(t *testing.T) { 122 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 123 testRegisterRunnerHandler(w, r, t) 124 })) 125 defer s.Close() 126 127 validToken := RunnerCredentials{ 128 URL: s.URL, 129 Token: "valid", 130 } 131 132 invalidToken := RunnerCredentials{ 133 URL: s.URL, 134 Token: "invalid", 135 } 136 137 otherToken := RunnerCredentials{ 138 URL: s.URL, 139 Token: "other", 140 } 141 142 c := NewGitLabClient() 143 144 res := c.RegisterRunner(validToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, Active: true}) 145 if assert.NotNil(t, res) { 146 assert.Equal(t, validToken.Token, res.Token) 147 } 148 149 res = c.RegisterRunner(validToken, RegisterRunnerParameters{Description: "invalid description", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true}) 150 assert.Nil(t, res) 151 152 res = c.RegisterRunner(invalidToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true}) 153 assert.Nil(t, res) 154 155 res = c.RegisterRunner(otherToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true}) 156 assert.Nil(t, res) 157 158 res = c.RegisterRunner(brokenCredentials, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true}) 159 assert.Nil(t, res) 160 } 161 162 func testUnregisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 163 if r.URL.Path != "/api/v4/runners" { 164 w.WriteHeader(http.StatusNotFound) 165 return 166 } 167 168 if r.Method != "DELETE" { 169 w.WriteHeader(http.StatusNotAcceptable) 170 return 171 } 172 173 body, err := ioutil.ReadAll(r.Body) 174 assert.NoError(t, err) 175 176 var req map[string]interface{} 177 err = json.Unmarshal(body, &req) 178 assert.NoError(t, err) 179 180 switch req["token"].(string) { 181 case "valid": 182 w.WriteHeader(http.StatusNoContent) 183 case "invalid": 184 w.WriteHeader(http.StatusForbidden) 185 default: 186 w.WriteHeader(http.StatusBadRequest) 187 } 188 } 189 190 func TestUnregisterRunner(t *testing.T) { 191 handler := func(w http.ResponseWriter, r *http.Request) { 192 testUnregisterRunnerHandler(w, r, t) 193 } 194 195 s := httptest.NewServer(http.HandlerFunc(handler)) 196 defer s.Close() 197 198 validToken := RunnerCredentials{ 199 URL: s.URL, 200 Token: "valid", 201 } 202 203 invalidToken := RunnerCredentials{ 204 URL: s.URL, 205 Token: "invalid", 206 } 207 208 otherToken := RunnerCredentials{ 209 URL: s.URL, 210 Token: "other", 211 } 212 213 c := NewGitLabClient() 214 215 state := c.UnregisterRunner(validToken) 216 assert.True(t, state) 217 218 state = c.UnregisterRunner(invalidToken) 219 assert.False(t, state) 220 221 state = c.UnregisterRunner(otherToken) 222 assert.False(t, state) 223 224 state = c.UnregisterRunner(brokenCredentials) 225 assert.False(t, state) 226 } 227 228 func testVerifyRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 229 if r.URL.Path != "/api/v4/runners/verify" { 230 w.WriteHeader(http.StatusNotFound) 231 return 232 } 233 234 if r.Method != "POST" { 235 w.WriteHeader(http.StatusNotAcceptable) 236 return 237 } 238 239 body, err := ioutil.ReadAll(r.Body) 240 assert.NoError(t, err) 241 242 var req map[string]interface{} 243 err = json.Unmarshal(body, &req) 244 assert.NoError(t, err) 245 246 switch req["token"].(string) { 247 case "valid": 248 w.WriteHeader(http.StatusOK) // since the job id is broken, we should not find this job 249 case "invalid": 250 w.WriteHeader(http.StatusForbidden) 251 default: 252 w.WriteHeader(http.StatusBadRequest) 253 } 254 } 255 256 func TestVerifyRunner(t *testing.T) { 257 handler := func(w http.ResponseWriter, r *http.Request) { 258 testVerifyRunnerHandler(w, r, t) 259 } 260 261 s := httptest.NewServer(http.HandlerFunc(handler)) 262 defer s.Close() 263 264 validToken := RunnerCredentials{ 265 URL: s.URL, 266 Token: "valid", 267 } 268 269 invalidToken := RunnerCredentials{ 270 URL: s.URL, 271 Token: "invalid", 272 } 273 274 otherToken := RunnerCredentials{ 275 URL: s.URL, 276 Token: "other", 277 } 278 279 c := NewGitLabClient() 280 281 state := c.VerifyRunner(validToken) 282 assert.True(t, state) 283 284 state = c.VerifyRunner(invalidToken) 285 assert.False(t, state) 286 287 state = c.VerifyRunner(otherToken) 288 assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's") 289 290 state = c.VerifyRunner(brokenCredentials) 291 assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's") 292 } 293 294 func getRequestJobResponse() (res map[string]interface{}) { 295 jobToken := "job-token" 296 297 res = make(map[string]interface{}) 298 res["id"] = 10 299 res["token"] = jobToken 300 res["allow_git_fetch"] = false 301 302 jobInfo := make(map[string]interface{}) 303 jobInfo["name"] = "test-job" 304 jobInfo["stage"] = "test" 305 jobInfo["project_id"] = 123 306 jobInfo["project_name"] = "test-project" 307 res["job_info"] = jobInfo 308 309 gitInfo := make(map[string]interface{}) 310 gitInfo["repo_url"] = "https://gitlab-ci-token:testTokenHere1234@gitlab.example.com/test/test-project.git" 311 gitInfo["ref"] = "master" 312 gitInfo["sha"] = "abcdef123456" 313 gitInfo["before_sha"] = "654321fedcba" 314 gitInfo["ref_type"] = "branch" 315 res["git_info"] = gitInfo 316 317 runnerInfo := make(map[string]interface{}) 318 runnerInfo["timeout"] = 3600 319 res["runner_info"] = runnerInfo 320 321 variables := make([]map[string]interface{}, 1) 322 variables[0] = make(map[string]interface{}) 323 variables[0]["key"] = "CI_REF_NAME" 324 variables[0]["value"] = "master" 325 variables[0]["public"] = true 326 variables[0]["file"] = true 327 res["variables"] = variables 328 329 steps := make([]map[string]interface{}, 2) 330 steps[0] = make(map[string]interface{}) 331 steps[0]["name"] = "script" 332 steps[0]["script"] = []string{"date", "ls -ls"} 333 steps[0]["timeout"] = 3600 334 steps[0]["when"] = "on_success" 335 steps[0]["allow_failure"] = false 336 steps[1] = make(map[string]interface{}) 337 steps[1]["name"] = "after_script" 338 steps[1]["script"] = []string{"ls -ls"} 339 steps[1]["timeout"] = 3600 340 steps[1]["when"] = "always" 341 steps[1]["allow_failure"] = true 342 res["steps"] = steps 343 344 image := make(map[string]interface{}) 345 image["name"] = "ruby:2.0" 346 image["entrypoint"] = []string{"/bin/sh"} 347 res["image"] = image 348 349 services := make([]map[string]interface{}, 2) 350 services[0] = make(map[string]interface{}) 351 services[0]["name"] = "postgresql:9.5" 352 services[0]["entrypoint"] = []string{"/bin/sh"} 353 services[0]["command"] = []string{"sleep", "30"} 354 services[0]["alias"] = "db-pg" 355 services[1] = make(map[string]interface{}) 356 services[1]["name"] = "mysql:5.6" 357 services[1]["alias"] = "db-mysql" 358 res["services"] = services 359 360 artifacts := make([]map[string]interface{}, 1) 361 artifacts[0] = make(map[string]interface{}) 362 artifacts[0]["name"] = "artifact.zip" 363 artifacts[0]["untracked"] = false 364 artifacts[0]["paths"] = []string{"out/*"} 365 artifacts[0]["when"] = "always" 366 artifacts[0]["expire_in"] = "7d" 367 res["artifacts"] = artifacts 368 369 cache := make([]map[string]interface{}, 1) 370 cache[0] = make(map[string]interface{}) 371 cache[0]["key"] = "$CI_COMMIT_SHA" 372 cache[0]["untracked"] = false 373 cache[0]["paths"] = []string{"vendor/*"} 374 cache[0]["policy"] = "push" 375 res["cache"] = cache 376 377 credentials := make([]map[string]interface{}, 1) 378 credentials[0] = make(map[string]interface{}) 379 credentials[0]["type"] = "Registry" 380 credentials[0]["url"] = "http://registry.gitlab.example.com/" 381 credentials[0]["username"] = "gitlab-ci-token" 382 credentials[0]["password"] = jobToken 383 res["credentials"] = credentials 384 385 dependencies := make([]map[string]interface{}, 1) 386 dependencies[0] = make(map[string]interface{}) 387 dependencies[0]["id"] = 9 388 dependencies[0]["name"] = "other-job" 389 dependencies[0]["token"] = "other-job-token" 390 artifactsFile0 := make(map[string]interface{}) 391 artifactsFile0["filename"] = "binaries.zip" 392 artifactsFile0["size"] = 13631488 393 dependencies[0]["artifacts_file"] = artifactsFile0 394 res["dependencies"] = dependencies 395 396 return 397 } 398 399 func testRequestJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 400 if r.URL.Path != "/api/v4/jobs/request" { 401 w.WriteHeader(http.StatusNotFound) 402 return 403 } 404 405 if r.Method != "POST" { 406 w.WriteHeader(http.StatusNotAcceptable) 407 return 408 } 409 410 body, err := ioutil.ReadAll(r.Body) 411 assert.NoError(t, err) 412 413 var req map[string]interface{} 414 err = json.Unmarshal(body, &req) 415 assert.NoError(t, err) 416 417 switch req["token"].(string) { 418 case "valid": 419 case "no-jobs": 420 w.Header().Add("X-GitLab-Last-Update", "a nice timestamp") 421 w.WriteHeader(http.StatusNoContent) 422 return 423 case "invalid": 424 w.WriteHeader(http.StatusForbidden) 425 return 426 default: 427 w.WriteHeader(http.StatusBadRequest) 428 return 429 } 430 431 if r.Header.Get("Accept") != "application/json" { 432 w.WriteHeader(http.StatusBadRequest) 433 return 434 } 435 436 output, err := json.Marshal(getRequestJobResponse()) 437 if err != nil { 438 w.WriteHeader(http.StatusInternalServerError) 439 return 440 } 441 442 w.Header().Set("Content-Type", "application/json") 443 w.WriteHeader(http.StatusCreated) 444 w.Write(output) 445 t.Logf("JobRequest response: %s\n", output) 446 } 447 448 func TestRequestJob(t *testing.T) { 449 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 450 testRequestJobHandler(w, r, t) 451 })) 452 defer s.Close() 453 454 validToken := RunnerConfig{ 455 RunnerCredentials: RunnerCredentials{ 456 URL: s.URL, 457 Token: "valid", 458 }, 459 } 460 461 noJobsToken := RunnerConfig{ 462 RunnerCredentials: RunnerCredentials{ 463 URL: s.URL, 464 Token: "no-jobs", 465 }, 466 } 467 468 invalidToken := RunnerConfig{ 469 RunnerCredentials: RunnerCredentials{ 470 URL: s.URL, 471 Token: "invalid", 472 }, 473 } 474 475 c := NewGitLabClient() 476 477 res, ok := c.RequestJob(validToken, nil) 478 if assert.NotNil(t, res) { 479 assert.NotEmpty(t, res.ID) 480 } 481 assert.True(t, ok) 482 483 assert.Equal(t, "ruby:2.0", res.Image.Name) 484 assert.Equal(t, []string{"/bin/sh"}, res.Image.Entrypoint) 485 require.Len(t, res.Services, 2) 486 assert.Equal(t, "postgresql:9.5", res.Services[0].Name) 487 assert.Equal(t, []string{"/bin/sh"}, res.Services[0].Entrypoint) 488 assert.Equal(t, []string{"sleep", "30"}, res.Services[0].Command) 489 assert.Equal(t, "db-pg", res.Services[0].Alias) 490 assert.Equal(t, "mysql:5.6", res.Services[1].Name) 491 assert.Equal(t, "db-mysql", res.Services[1].Alias) 492 493 assert.Empty(t, c.getLastUpdate(&noJobsToken.RunnerCredentials), "Last-Update should not be set") 494 res, ok = c.RequestJob(noJobsToken, nil) 495 assert.Nil(t, res) 496 assert.True(t, ok, "If no jobs, runner is healthy") 497 assert.Equal(t, "a nice timestamp", c.getLastUpdate(&noJobsToken.RunnerCredentials), "Last-Update should be set") 498 499 res, ok = c.RequestJob(invalidToken, nil) 500 assert.Nil(t, res) 501 assert.False(t, ok, "If token is invalid, the runner is unhealthy") 502 503 res, ok = c.RequestJob(brokenConfig, nil) 504 assert.Nil(t, res) 505 assert.False(t, ok) 506 } 507 508 func setStateForUpdateJobHandlerResponse(w http.ResponseWriter, req map[string]interface{}) { 509 switch req["state"].(string) { 510 case "running": 511 w.WriteHeader(http.StatusOK) 512 case "failed": 513 failureReason, ok := req["failure_reason"].(string) 514 if ok && (JobFailureReason(failureReason) == ScriptFailure || 515 JobFailureReason(failureReason) == RunnerSystemFailure) { 516 w.WriteHeader(http.StatusOK) 517 } else { 518 w.WriteHeader(http.StatusBadRequest) 519 } 520 case "forbidden": 521 w.WriteHeader(http.StatusForbidden) 522 default: 523 w.WriteHeader(http.StatusBadRequest) 524 } 525 } 526 527 func testUpdateJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 528 if r.URL.Path != "/api/v4/jobs/10" { 529 w.WriteHeader(http.StatusNotFound) 530 return 531 } 532 533 if r.Method != "PUT" { 534 w.WriteHeader(http.StatusNotAcceptable) 535 return 536 } 537 538 body, err := ioutil.ReadAll(r.Body) 539 assert.NoError(t, err) 540 541 var req map[string]interface{} 542 err = json.Unmarshal(body, &req) 543 assert.NoError(t, err) 544 545 assert.Equal(t, "token", req["token"]) 546 547 setStateForUpdateJobHandlerResponse(w, req) 548 } 549 550 func TestUpdateJob(t *testing.T) { 551 handler := func(w http.ResponseWriter, r *http.Request) { 552 testUpdateJobHandler(w, r, t) 553 } 554 555 s := httptest.NewServer(http.HandlerFunc(handler)) 556 defer s.Close() 557 558 config := RunnerConfig{ 559 RunnerCredentials: RunnerCredentials{ 560 URL: s.URL, 561 }, 562 } 563 564 jobCredentials := &JobCredentials{ 565 Token: "token", 566 } 567 568 c := NewGitLabClient() 569 570 var state UpdateState 571 572 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "running", FailureReason: ""}) 573 assert.Equal(t, UpdateSucceeded, state, "Update should continue when running") 574 575 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "forbidden", FailureReason: ""}) 576 assert.Equal(t, UpdateAbort, state, "Update should be aborted if the state is forbidden") 577 578 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "other", FailureReason: ""}) 579 assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request") 580 581 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 4, State: "state", FailureReason: ""}) 582 assert.Equal(t, UpdateAbort, state, "Update should abort for unknown job") 583 584 state = c.UpdateJob(brokenConfig, jobCredentials, UpdateJobInfo{ID: 4, State: "state", FailureReason: ""}) 585 assert.Equal(t, UpdateAbort, state) 586 587 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: "script_failure"}) 588 assert.Equal(t, UpdateSucceeded, state, "Update should continue when running") 589 590 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: "unknown_failure_reason"}) 591 assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request") 592 593 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: ""}) 594 assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request") 595 } 596 597 func testUpdateJobKeepAliveHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 598 if r.Method != "PUT" { 599 w.WriteHeader(http.StatusNotAcceptable) 600 return 601 } 602 603 switch r.URL.Path { 604 case "/api/v4/jobs/10": 605 case "/api/v4/jobs/11": 606 w.Header().Set("Job-Status", "canceled") 607 case "/api/v4/jobs/12": 608 w.Header().Set("Job-Status", "failed") 609 default: 610 w.WriteHeader(http.StatusNotFound) 611 return 612 } 613 614 body, err := ioutil.ReadAll(r.Body) 615 assert.NoError(t, err) 616 617 var req map[string]interface{} 618 err = json.Unmarshal(body, &req) 619 assert.NoError(t, err) 620 621 assert.Equal(t, "token", req["token"]) 622 623 w.WriteHeader(http.StatusOK) 624 } 625 626 func TestUpdateJobAsKeepAlive(t *testing.T) { 627 handler := func(w http.ResponseWriter, r *http.Request) { 628 testUpdateJobKeepAliveHandler(w, r, t) 629 } 630 631 s := httptest.NewServer(http.HandlerFunc(handler)) 632 defer s.Close() 633 634 config := RunnerConfig{ 635 RunnerCredentials: RunnerCredentials{ 636 URL: s.URL, 637 }, 638 } 639 640 jobCredentials := &JobCredentials{ 641 Token: "token", 642 } 643 644 c := NewGitLabClient() 645 646 var state UpdateState 647 648 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "running"}) 649 assert.Equal(t, UpdateSucceeded, state, "Update should continue when running") 650 651 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 11, State: "running"}) 652 assert.Equal(t, UpdateAbort, state, "Update should be aborted when Job-Status=canceled") 653 654 state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 12, State: "running"}) 655 assert.Equal(t, UpdateAbort, state, "Update should continue when Job-Status=failed") 656 } 657 658 var patchToken = "token" 659 var patchTraceContent = []byte("trace trace trace") 660 661 func getPatchServer(t *testing.T, handler func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int)) (*httptest.Server, *GitLabClient, RunnerConfig) { 662 patchHandler := func(w http.ResponseWriter, r *http.Request) { 663 if r.URL.Path != "/api/v4/jobs/1/trace" { 664 w.WriteHeader(http.StatusNotFound) 665 return 666 } 667 668 if r.Method != "PATCH" { 669 w.WriteHeader(http.StatusNotAcceptable) 670 return 671 } 672 673 assert.Equal(t, patchToken, r.Header.Get("JOB-TOKEN")) 674 675 body, err := ioutil.ReadAll(r.Body) 676 assert.NoError(t, err) 677 678 contentRange := r.Header.Get("Content-Range") 679 ranges := strings.Split(contentRange, "-") 680 681 offset, err := strconv.Atoi(ranges[0]) 682 assert.NoError(t, err) 683 684 limit, err := strconv.Atoi(ranges[1]) 685 assert.NoError(t, err) 686 687 handler(w, r, body, offset, limit) 688 } 689 690 server := httptest.NewServer(http.HandlerFunc(patchHandler)) 691 692 config := RunnerConfig{ 693 RunnerCredentials: RunnerCredentials{ 694 URL: server.URL, 695 }, 696 } 697 698 return server, NewGitLabClient(), config 699 } 700 701 func TestUnknownPatchTrace(t *testing.T) { 702 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 703 w.WriteHeader(http.StatusNotFound) 704 } 705 706 server, client, config := getPatchServer(t, handler) 707 defer server.Close() 708 709 _, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 710 patchTraceContent, 0) 711 assert.Equal(t, UpdateNotFound, state) 712 } 713 714 func TestForbiddenPatchTrace(t *testing.T) { 715 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 716 w.WriteHeader(http.StatusForbidden) 717 } 718 719 server, client, config := getPatchServer(t, handler) 720 defer server.Close() 721 722 _, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 723 patchTraceContent, 0) 724 assert.Equal(t, UpdateAbort, state) 725 } 726 727 func TestPatchTrace(t *testing.T) { 728 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 729 assert.Equal(t, patchTraceContent[offset:limit+1], body) 730 w.WriteHeader(http.StatusAccepted) 731 } 732 733 server, client, config := getPatchServer(t, handler) 734 defer server.Close() 735 736 endOffset, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 737 patchTraceContent, 0) 738 assert.Equal(t, UpdateSucceeded, state) 739 assert.Equal(t, len(patchTraceContent), endOffset) 740 741 endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 742 patchTraceContent[3:], 3) 743 assert.Equal(t, UpdateSucceeded, state) 744 assert.Equal(t, len(patchTraceContent), endOffset) 745 746 endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 747 patchTraceContent[3:10], 3) 748 assert.Equal(t, UpdateSucceeded, state) 749 assert.Equal(t, 10, endOffset) 750 } 751 752 func TestRangeMismatchPatchTrace(t *testing.T) { 753 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 754 if offset > 10 { 755 w.Header().Set("Range", "0-10") 756 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 757 } 758 759 w.WriteHeader(http.StatusAccepted) 760 } 761 762 server, client, config := getPatchServer(t, handler) 763 defer server.Close() 764 765 endOffset, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 766 patchTraceContent[11:], 11) 767 assert.Equal(t, UpdateRangeMismatch, state) 768 assert.Equal(t, 10, endOffset) 769 770 endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 771 patchTraceContent[15:], 15) 772 assert.Equal(t, UpdateRangeMismatch, state) 773 assert.Equal(t, 10, endOffset) 774 775 endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 776 patchTraceContent[5:], 5) 777 assert.Equal(t, UpdateSucceeded, state) 778 assert.Equal(t, len(patchTraceContent), endOffset) 779 } 780 781 func TestJobFailedStatePatchTrace(t *testing.T) { 782 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 783 w.Header().Set("Job-Status", "failed") 784 w.WriteHeader(http.StatusAccepted) 785 } 786 787 server, client, config := getPatchServer(t, handler) 788 defer server.Close() 789 790 _, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 791 patchTraceContent, 0) 792 assert.Equal(t, UpdateAbort, state) 793 } 794 795 func TestPatchTraceCantConnect(t *testing.T) { 796 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {} 797 798 server, client, config := getPatchServer(t, handler) 799 server.Close() 800 801 _, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 802 patchTraceContent, 0) 803 assert.Equal(t, UpdateFailed, state) 804 } 805 806 func TestPatchTraceUpdatedTrace(t *testing.T) { 807 sentTrace := 0 808 traceContent := []byte{} 809 810 updates := []struct { 811 traceUpdate []byte 812 expectedContentRange string 813 expectedContentLength int64 814 expectedResult UpdateState 815 shouldNotCallPatchTrace bool 816 }{ 817 { 818 traceUpdate: []byte("test"), 819 expectedContentRange: "0-3", 820 expectedContentLength: 4, 821 expectedResult: UpdateSucceeded, 822 }, 823 { 824 traceUpdate: []byte{}, 825 expectedContentLength: 4, 826 expectedResult: UpdateSucceeded, 827 shouldNotCallPatchTrace: true, 828 }, 829 { 830 traceUpdate: []byte(" "), 831 expectedContentRange: "4-4", expectedContentLength: 1, 832 expectedResult: UpdateSucceeded, 833 }, 834 { 835 traceUpdate: []byte("test"), 836 expectedContentRange: "5-8", expectedContentLength: 4, 837 expectedResult: UpdateSucceeded, 838 }, 839 } 840 841 for id, update := range updates { 842 t.Run(fmt.Sprintf("patch-%d", id+1), func(t *testing.T) { 843 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 844 if update.shouldNotCallPatchTrace { 845 t.Error("PatchTrace endpoint should not be called") 846 return 847 } 848 849 if limit+1 <= len(traceContent) { 850 assert.Equal(t, traceContent[offset:limit+1], body) 851 } 852 853 assert.Equal(t, update.traceUpdate, body) 854 assert.Equal(t, update.expectedContentRange, r.Header.Get("Content-Range")) 855 assert.Equal(t, update.expectedContentLength, r.ContentLength) 856 w.WriteHeader(http.StatusAccepted) 857 } 858 859 server, client, config := getPatchServer(t, handler) 860 defer server.Close() 861 862 traceContent = append(traceContent, update.traceUpdate...) 863 endOffset, result := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 864 traceContent[sentTrace:], sentTrace) 865 assert.Equal(t, update.expectedResult, result) 866 867 sentTrace = endOffset 868 }) 869 } 870 } 871 872 func TestPatchTraceContentRangeAndLength(t *testing.T) { 873 tests := map[string]struct { 874 name string 875 trace []byte 876 expectedContentRange string 877 expectedContentLength int64 878 expectedResult UpdateState 879 shouldNotCallPatchTrace bool 880 }{ 881 "0 bytes": { 882 trace: []byte{}, 883 expectedResult: UpdateSucceeded, 884 shouldNotCallPatchTrace: true, 885 }, 886 "1 byte": { 887 name: "1 byte", 888 trace: []byte("1"), 889 expectedContentRange: "0-0", 890 expectedContentLength: 1, 891 expectedResult: UpdateSucceeded, 892 shouldNotCallPatchTrace: false, 893 }, 894 "2 bytes": { 895 trace: []byte("12"), 896 expectedContentRange: "0-1", 897 expectedContentLength: 2, 898 expectedResult: UpdateSucceeded, 899 shouldNotCallPatchTrace: false, 900 }, 901 } 902 903 for name, test := range tests { 904 t.Run(name, func(t *testing.T) { 905 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 906 if test.shouldNotCallPatchTrace { 907 t.Error("PatchTrace endpoint should not be called") 908 return 909 } 910 911 assert.Equal(t, test.expectedContentRange, r.Header.Get("Content-Range")) 912 assert.Equal(t, test.expectedContentLength, r.ContentLength) 913 w.WriteHeader(http.StatusAccepted) 914 } 915 916 server, client, config := getPatchServer(t, handler) 917 defer server.Close() 918 919 _, result := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 920 test.trace, 0) 921 assert.Equal(t, test.expectedResult, result) 922 }) 923 } 924 } 925 926 func TestPatchTraceContentRangeHeaderValues(t *testing.T) { 927 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 928 contentRange := r.Header.Get("Content-Range") 929 bytes := strings.Split(contentRange, "-") 930 931 startByte, err := strconv.Atoi(bytes[0]) 932 require.NoError(t, err, "Should not set error when parsing Content-Range startByte component") 933 934 endByte, err := strconv.Atoi(bytes[1]) 935 require.NoError(t, err, "Should not set error when parsing Content-Range endByte component") 936 937 assert.Equal(t, 0, startByte, "Content-Range should contain start byte as first field") 938 assert.Equal(t, len(patchTraceContent)-1, endByte, "Content-Range should contain end byte as second field") 939 940 w.WriteHeader(http.StatusAccepted) 941 } 942 943 server, client, config := getPatchServer(t, handler) 944 defer server.Close() 945 946 client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 947 patchTraceContent, 0) 948 } 949 950 func TestAbortedPatchTrace(t *testing.T) { 951 statuses := []string{"canceled", "failed"} 952 953 for _, status := range statuses { 954 t.Run(status, func(t *testing.T) { 955 handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) { 956 w.Header().Set("Job-Status", status) 957 w.WriteHeader(http.StatusAccepted) 958 } 959 960 server, client, config := getPatchServer(t, handler) 961 defer server.Close() 962 963 _, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, 964 patchTraceContent, 0) 965 assert.Equal(t, UpdateAbort, state) 966 }) 967 } 968 } 969 970 func checkTestArtifactsUploadHandlerContent(w http.ResponseWriter, r *http.Request, body string) { 971 switch body { 972 case "too-large": 973 w.WriteHeader(http.StatusRequestEntityTooLarge) 974 return 975 976 case "content": 977 w.WriteHeader(http.StatusCreated) 978 return 979 980 case "zip": 981 if r.FormValue("artifact_format") == "zip" { 982 w.WriteHeader(http.StatusCreated) 983 return 984 } 985 986 case "gzip": 987 if r.FormValue("artifact_format") == "gzip" { 988 w.WriteHeader(http.StatusCreated) 989 return 990 } 991 992 case "junit": 993 if r.FormValue("artifact_type") == "junit" { 994 w.WriteHeader(http.StatusCreated) 995 return 996 } 997 } 998 999 w.WriteHeader(http.StatusBadRequest) 1000 } 1001 1002 func testArtifactsUploadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 1003 if r.URL.Path != "/api/v4/jobs/10/artifacts" { 1004 w.WriteHeader(http.StatusNotFound) 1005 return 1006 } 1007 1008 if r.Method != "POST" { 1009 w.WriteHeader(http.StatusNotAcceptable) 1010 return 1011 } 1012 1013 if r.Header.Get("JOB-TOKEN") != "token" { 1014 w.WriteHeader(http.StatusForbidden) 1015 return 1016 } 1017 1018 file, _, err := r.FormFile("file") 1019 if err != nil { 1020 w.WriteHeader(http.StatusBadRequest) 1021 return 1022 } 1023 1024 body, err := ioutil.ReadAll(file) 1025 require.NoError(t, err) 1026 1027 checkTestArtifactsUploadHandlerContent(w, r, string(body)) 1028 } 1029 1030 func uploadArtifacts(client *GitLabClient, config JobCredentials, artifactsFile, artifactType string, artifactFormat ArtifactFormat) UploadState { 1031 file, err := os.Open(artifactsFile) 1032 if err != nil { 1033 return UploadFailed 1034 } 1035 defer file.Close() 1036 1037 fi, err := file.Stat() 1038 if err != nil { 1039 return UploadFailed 1040 } 1041 if fi.IsDir() { 1042 return UploadFailed 1043 } 1044 1045 options := ArtifactsOptions{ 1046 BaseName: filepath.Base(artifactsFile), 1047 Format: artifactFormat, 1048 Type: artifactType, 1049 } 1050 return client.UploadRawArtifacts(config, file, options) 1051 } 1052 func TestArtifactsUpload(t *testing.T) { 1053 handler := func(w http.ResponseWriter, r *http.Request) { 1054 testArtifactsUploadHandler(w, r, t) 1055 } 1056 1057 s := httptest.NewServer(http.HandlerFunc(handler)) 1058 defer s.Close() 1059 1060 config := JobCredentials{ 1061 ID: 10, 1062 URL: s.URL, 1063 Token: "token", 1064 } 1065 invalidToken := JobCredentials{ 1066 ID: 10, 1067 URL: s.URL, 1068 Token: "invalid-token", 1069 } 1070 1071 tempFile, err := ioutil.TempFile("", "artifacts") 1072 assert.NoError(t, err) 1073 tempFile.Close() 1074 defer os.Remove(tempFile.Name()) 1075 1076 c := NewGitLabClient() 1077 1078 ioutil.WriteFile(tempFile.Name(), []byte("content"), 0600) 1079 state := uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatDefault) 1080 assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded") 1081 1082 ioutil.WriteFile(tempFile.Name(), []byte("too-large"), 0600) 1083 state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatDefault) 1084 assert.Equal(t, UploadTooLarge, state, "Artifacts should be not uploaded, because of too large archive") 1085 1086 ioutil.WriteFile(tempFile.Name(), []byte("zip"), 0600) 1087 state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatZip) 1088 assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as zip") 1089 1090 ioutil.WriteFile(tempFile.Name(), []byte("gzip"), 0600) 1091 state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatGzip) 1092 assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as gzip") 1093 1094 ioutil.WriteFile(tempFile.Name(), []byte("junit"), 0600) 1095 state = uploadArtifacts(c, config, tempFile.Name(), "junit", ArtifactFormatGzip) 1096 assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as gzip") 1097 1098 state = uploadArtifacts(c, config, "not/existing/file", "", ArtifactFormatDefault) 1099 assert.Equal(t, UploadFailed, state, "Artifacts should fail to be uploaded") 1100 1101 state = uploadArtifacts(c, invalidToken, tempFile.Name(), "", ArtifactFormatDefault) 1102 assert.Equal(t, UploadForbidden, state, "Artifacts should be rejected if invalid token") 1103 } 1104 1105 func testArtifactsDownloadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) { 1106 if r.URL.Path != "/api/v4/jobs/10/artifacts" { 1107 w.WriteHeader(http.StatusNotFound) 1108 return 1109 } 1110 1111 if r.Method != "GET" { 1112 w.WriteHeader(http.StatusNotAcceptable) 1113 return 1114 } 1115 1116 if r.Header.Get("JOB-TOKEN") != "token" { 1117 w.WriteHeader(http.StatusForbidden) 1118 return 1119 } 1120 1121 w.WriteHeader(http.StatusOK) 1122 w.Write(bytes.NewBufferString("Test artifact file content").Bytes()) 1123 } 1124 1125 func TestArtifactsDownload(t *testing.T) { 1126 handler := func(w http.ResponseWriter, r *http.Request) { 1127 testArtifactsDownloadHandler(w, r, t) 1128 } 1129 1130 s := httptest.NewServer(http.HandlerFunc(handler)) 1131 defer s.Close() 1132 1133 credentials := JobCredentials{ 1134 ID: 10, 1135 URL: s.URL, 1136 Token: "token", 1137 } 1138 invalidTokenCredentials := JobCredentials{ 1139 ID: 10, 1140 URL: s.URL, 1141 Token: "invalid-token", 1142 } 1143 fileNotFoundTokenCredentials := JobCredentials{ 1144 ID: 11, 1145 URL: s.URL, 1146 Token: "token", 1147 } 1148 1149 c := NewGitLabClient() 1150 1151 tempDir, err := ioutil.TempDir("", "artifacts") 1152 assert.NoError(t, err) 1153 1154 artifactsFileName := filepath.Join(tempDir, "downloaded-artifact") 1155 defer os.Remove(artifactsFileName) 1156 1157 state := c.DownloadArtifacts(credentials, artifactsFileName) 1158 assert.Equal(t, DownloadSucceeded, state, "Artifacts should be downloaded") 1159 1160 state = c.DownloadArtifacts(invalidTokenCredentials, artifactsFileName) 1161 assert.Equal(t, DownloadForbidden, state, "Artifacts should be not downloaded if invalid token is used") 1162 1163 state = c.DownloadArtifacts(fileNotFoundTokenCredentials, artifactsFileName) 1164 assert.Equal(t, DownloadNotFound, state, "Artifacts should be bit downloaded if it's not found") 1165 } 1166 1167 func TestRunnerVersion(t *testing.T) { 1168 c := NewGitLabClient() 1169 info := c.getRunnerVersion(RunnerConfig{ 1170 RunnerSettings: RunnerSettings{ 1171 Executor: "my-executor", 1172 Shell: "my-shell", 1173 }, 1174 }) 1175 1176 assert.NotEmpty(t, info.Name) 1177 assert.NotEmpty(t, info.Version) 1178 assert.NotEmpty(t, info.Revision) 1179 assert.NotEmpty(t, info.Platform) 1180 assert.NotEmpty(t, info.Architecture) 1181 assert.Equal(t, "my-executor", info.Executor) 1182 assert.Equal(t, "my-shell", info.Shell) 1183 } 1184 1185 func TestRunnerVersionToGetExecutorAndShellFeaturesWithTheDefaultShell(t *testing.T) { 1186 executorProvider := MockExecutorProvider{} 1187 defer executorProvider.AssertExpectations(t) 1188 executorProvider.On("GetDefaultShell").Return("my-default-executor-shell").Twice() 1189 executorProvider.On("CanCreate").Return(true).Once() 1190 executorProvider.On("GetFeatures", mock.Anything).Return(nil).Run(func(args mock.Arguments) { 1191 features := args[0].(*FeaturesInfo) 1192 features.Shared = true 1193 }) 1194 RegisterExecutor("my-test-executor", &executorProvider) 1195 1196 shell := MockShell{} 1197 defer shell.AssertExpectations(t) 1198 shell.On("GetName").Return("my-default-executor-shell") 1199 shell.On("GetFeatures", mock.Anything).Return(nil).Run(func(args mock.Arguments) { 1200 features := args[0].(*FeaturesInfo) 1201 features.Variables = true 1202 }) 1203 RegisterShell(&shell) 1204 1205 c := NewGitLabClient() 1206 info := c.getRunnerVersion(RunnerConfig{ 1207 RunnerSettings: RunnerSettings{ 1208 Executor: "my-test-executor", 1209 Shell: "", 1210 }, 1211 }) 1212 1213 assert.Equal(t, "my-test-executor", info.Executor) 1214 assert.Equal(t, "my-default-executor-shell", info.Shell) 1215 assert.False(t, info.Features.Artifacts, "dry-run that this is not enabled") 1216 assert.True(t, info.Features.Shared, "feature is enabled by executor") 1217 assert.True(t, info.Features.Variables, "feature is enabled by shell") 1218 }