BigW Consortium Gitlab

runner_spec.rb 37.6 KB
Newer Older
1 2
require 'spec_helper'

3
describe API::Runner do
4 5 6 7 8 9 10 11 12 13 14 15 16 17
  include StubGitlabCalls

  let(:registration_token) { 'abcdefg123456' }

  before do
    stub_gitlab_calls
    stub_application_setting(runners_registration_token: registration_token)
  end

  describe '/api/v4/runners' do
    describe 'POST /api/v4/runners' do
      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/runners')
18

19 20 21 22 23 24 25
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/runners'), token: 'invalid'
26

27 28 29 30 31 32 33 34 35 36 37 38 39 40
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
        it 'creates runner with default values' do
          post api('/runners'), token: registration_token

          runner = Ci::Runner.first

          expect(response).to have_http_status 201
          expect(json_response['id']).to eq(runner.id)
          expect(json_response['token']).to eq(runner.token)
          expect(runner.run_untagged).to be true
41
          expect(runner.token).not_to eq(registration_token)
42 43 44 45 46 47 48 49 50 51
        end

        context 'when project token is used' do
          let(:project) { create(:empty_project) }

          it 'creates runner' do
            post api('/runners'), token: project.runners_token

            expect(response).to have_http_status 201
            expect(project.runners.size).to eq(1)
52 53
            expect(Ci::Runner.first.token).not_to eq(registration_token)
            expect(Ci::Runner.first.token).not_to eq(project.runners_token)
54 55 56 57 58 59 60
          end
        end
      end

      context 'when runner description is provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin committed
61
                                description: 'server.hostname'
62 63 64 65 66 67 68 69 70

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.description).to eq('server.hostname')
        end
      end

      context 'when runner tags are provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin committed
71
                                tag_list: 'tag1, tag2'
72 73 74 75 76 77 78 79 80 81

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
        end
      end

      context 'when option for running untagged jobs is provided' do
        context 'when tags are provided' do
          it 'creates runner' do
            post api('/runners'), token: registration_token,
Tomasz Maczukin committed
82 83
                                  run_untagged: false,
                                  tag_list: ['tag']
84 85 86 87 88 89 90 91 92 93

            expect(response).to have_http_status 201
            expect(Ci::Runner.first.run_untagged).to be false
            expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
          end
        end

        context 'when tags are not provided' do
          it 'returns 404 error' do
            post api('/runners'), token: registration_token,
Tomasz Maczukin committed
94
                                  run_untagged: false
95 96 97 98 99 100 101 102 103

            expect(response).to have_http_status 404
          end
        end
      end

      context 'when option for locking Runner is provided' do
        it 'creates runner' do
          post api('/runners'), token: registration_token,
Tomasz Maczukin committed
104
                                locked: true
105 106 107 108 109 110 111 112 113 114

          expect(response).to have_http_status 201
          expect(Ci::Runner.first.locked).to be true
        end
      end

      %w(name version revision platform architecture).each do |param|
        context "when info parameter '#{param}' info is present" do
          let(:value) { "#{param}_value" }

115
          it "updates provided Runner's parameter" do
116
            post api('/runners'), token: registration_token,
Tomasz Maczukin committed
117
                                  info: { param => value }
118 119 120 121 122 123 124 125 126 127 128 129

            expect(response).to have_http_status 201
            expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
          end
        end
      end
    end

    describe 'DELETE /api/v4/runners' do
      context 'when no token is provided' do
        it 'returns 400 error' do
          delete api('/runners')
130

131 132 133 134 135 136 137
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          delete api('/runners'), token: 'invalid'
138

139 140 141 142 143 144 145 146 147
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
        let(:runner) { create(:ci_runner) }

        it 'deletes Runner' do
          delete api('/runners'), token: runner.token
148 149

          expect(response).to have_http_status 204
150 151 152 153
          expect(Ci::Runner.count).to eq(0)
        end
      end
    end
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

    describe 'POST /api/v4/runners/verify' do
      let(:runner) { create(:ci_runner) }

      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/runners/verify')

          expect(response).to have_http_status :bad_request
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/runners/verify'), token: 'invalid-token'

          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
175
        it 'verifies Runner credentials' do
176 177 178 179 180 181
          post api('/runners/verify'), token: runner.token

          expect(response).to have_http_status 200
        end
      end
    end
182
  end
183 184 185 186 187

  describe '/api/v4/jobs' do
    let(:project) { create(:empty_project, shared_runners_enabled: false) }
    let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
    let(:runner) { create(:ci_runner) }
188 189 190 191
    let!(:job) do
      create(:ci_build, :artifacts, :extended_options,
             pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
    end
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231

    before { project.runners << runner }

    describe 'POST /api/v4/jobs/request' do
      let!(:last_update) {}
      let!(:new_update) { }
      let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }

      before { stub_container_registry_config(enabled: false) }

      shared_examples 'no jobs available' do
        before { request_job }

        context 'when runner sends version in User-Agent' do
          context 'for stable version' do
            it 'gives 204 and set X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header).to have_key('X-GitLab-Last-Update')
            end
          end

          context 'when last_update is up-to-date' do
            let(:last_update) { runner.ensure_runner_queue_value }

            it 'gives 204 and set the same X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header['X-GitLab-Last-Update']).to eq(last_update)
            end
          end

          context 'when last_update is outdated' do
            let(:last_update) { runner.ensure_runner_queue_value }
            let(:new_update) { runner.tick_runner_queue }

            it 'gives 204 and set a new X-GitLab-Last-Update' do
              expect(response).to have_http_status(204)
              expect(response.header['X-GitLab-Last-Update']).to eq(new_update)
            end
          end

232
          context 'when beta version is sent' do
233
            let(:user_agent) { 'gitlab-runner 9.0.0~beta.167.g2b2bacc (master; go1.7.4; linux/amd64)' }
234

235 236 237
            it { expect(response).to have_http_status(204) }
          end

238
          context 'when pre-9-0 version is sent' do
239
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0 (1-6-stable; go1.6.3; linux/amd64)' }
240

241 242 243
            it { expect(response).to have_http_status(204) }
          end

244
          context 'when pre-9-0 beta version is sent' do
245
            let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (master; go1.6.3; linux/amd64)' }
246

247 248 249 250 251 252 253 254
            it { expect(response).to have_http_status(204) }
          end
        end
      end

      context 'when no token is provided' do
        it 'returns 400 error' do
          post api('/jobs/request')
255

256 257 258 259 260 261 262
          expect(response).to have_http_status 400
        end
      end

      context 'when invalid token is provided' do
        it 'returns 403 error' do
          post api('/jobs/request'), token: 'invalid'
263

264 265 266 267 268 269 270 271
          expect(response).to have_http_status 403
        end
      end

      context 'when valid token is provided' do
        context 'when Runner is not active' do
          let(:runner) { create(:ci_runner, :inactive) }

272
          it 'returns 204 error' do
273
            request_job
274

275
            expect(response).to have_http_status 204
276 277 278 279 280
          end
        end

        context 'when jobs are finished' do
          before { job.success }
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295
          it_behaves_like 'no jobs available'
        end

        context 'when other projects have pending jobs' do
          before do
            job.success
            create(:ci_build, :pending)
          end

          it_behaves_like 'no jobs available'
        end

        context 'when shared runner requests job for project without shared_runners_enabled' do
          let(:runner) { create(:ci_runner, :shared) }
296

297 298 299 300
          it_behaves_like 'no jobs available'
        end

        context 'when there is a pending job' do
301 302 303 304 305 306
          let(:expected_job_info) do
            { 'name' => job.name,
              'stage' => job.stage,
              'project_id' => job.project.id,
              'project_name' => job.project.name }
          end
307

308 309 310 311 312
          let(:expected_git_info) do
            { 'repo_url' => job.repo_url,
              'ref' => job.ref,
              'sha' => job.sha,
              'before_sha' => job.before_sha,
Tomasz Maczukin committed
313
              'ref_type' => 'branch' }
314
          end
315

316 317 318 319 320 321 322 323 324 325 326 327
          let(:expected_steps) do
            [{ 'name' => 'script',
               'script' => %w(ls date),
               'timeout' => job.timeout,
               'when' => 'on_success',
               'allow_failure' => false },
             { 'name' => 'after_script',
               'script' => %w(ls date),
               'timeout' => job.timeout,
               'when' => 'always',
               'allow_failure' => true }]
          end
328

329
          let(:expected_variables) do
330 331
            [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
             { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
332 333
             { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
          end
334

335
          let(:expected_artifacts) do
336 337 338 339 340 341
            [{ 'name' => 'artifacts_file',
               'untracked' => false,
               'paths' => %w(out/),
               'when' => 'always',
               'expire_in' => '7d' }]
          end
342

343 344 345 346
          let(:expected_cache) do
            [{ 'key' => 'cache_key',
               'untracked' => false,
               'paths' => ['vendor/*'] }]
347 348
          end

349
          it 'picks a job' do
Tomasz Maczukin committed
350
            request_job info: { platform: :darwin }
351 352 353 354

            expect(response).to have_http_status(201)
            expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            expect(runner.reload.platform).to eq('darwin')
355 356
            expect(json_response['id']).to eq(job.id)
            expect(json_response['token']).to eq(job.token)
357 358 359 360 361 362
            expect(json_response['job_info']).to eq(expected_job_info)
            expect(json_response['git_info']).to eq(expected_git_info)
            expect(json_response['image']).to eq({ 'name' => 'ruby:2.1' })
            expect(json_response['services']).to eq([{ 'name' => 'postgres' }])
            expect(json_response['steps']).to eq(expected_steps)
            expect(json_response['artifacts']).to eq(expected_artifacts)
363
            expect(json_response['cache']).to eq(expected_cache)
364
            expect(json_response['variables']).to include(*expected_variables)
365 366 367 368 369 370 371
          end

          context 'when job is made for tag' do
            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }

            it 'sets branch as ref_type' do
              request_job
372

373 374 375 376 377 378 379 380
              expect(response).to have_http_status(201)
              expect(json_response['git_info']['ref_type']).to eq('tag')
            end
          end

          context 'when job is made for branch' do
            it 'sets tag as ref_type' do
              request_job
381

382 383 384
              expect(response).to have_http_status(201)
              expect(json_response['git_info']['ref_type']).to eq('branch')
            end
385 386 387 388 389 390 391 392 393 394
          end

          it 'updates runner info' do
            expect { request_job }.to change { runner.reload.contacted_at }
          end

          %w(name version revision platform architecture).each do |param|
            context "when info parameter '#{param}' is present" do
              let(:value) { "#{param}_value" }

395
              it "updates provided Runner's parameter" do
Tomasz Maczukin committed
396
                request_job info: { param => value }
397 398

                expect(response).to have_http_status(201)
399
                expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
400 401 402 403 404 405 406 407 408 409 410 411
              end
            end
          end

          context 'when concurrently updating a job' do
            before do
              expect_any_instance_of(Ci::Build).to receive(:run!).
                  and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
            end

            it 'returns a conflict' do
              request_job
412

413 414 415 416 417 418
              expect(response).to have_http_status(409)
              expect(response.headers).not_to have_key('X-GitLab-Last-Update')
            end
          end

          context 'when project and pipeline have multiple jobs' do
419 420 421
            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
            let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
422

423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
            before do
              job.success
              job2.success
            end

            it 'returns dependent jobs' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(test_job.id)
              expect(json_response['dependencies'].count).to eq(2)
              expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
                                                               { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
            end
          end

          context 'when explicit dependencies are defined' do
            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
            let!(:test_job) do
              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
                                stage: 'deploy', stage_idx: 1,
                                options: { dependencies: [job2.name] })
            end

            before do
              job.success
              job2.success
            end
452 453 454 455 456 457

            it 'returns dependent jobs' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(test_job.id)
458
              expect(json_response['dependencies'].count).to eq(1)
459
              expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
460 461 462
            end
          end

463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
          context 'when dependencies is an empty array' do
            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
            let!(:empty_dependencies_job) do
              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
                                stage: 'deploy', stage_idx: 1,
                                options: { dependencies: [] })
            end

            before do
              job.success
              job2.success
            end

            it 'returns an empty array' do
              request_job

              expect(response).to have_http_status(201)
              expect(json_response['id']).to eq(empty_dependencies_job.id)
              expect(json_response['dependencies'].count).to eq(0)
            end
          end

486 487 488 489 490 491 492 493
          context 'when job has no tags' do
            before { job.update(tags: []) }

            context 'when runner is allowed to pick untagged jobs' do
              before { runner.update_column(:run_untagged, true) }

              it 'picks job' do
                request_job
494

495 496 497 498 499 500
                expect(response).to have_http_status 201
              end
            end

            context 'when runner is not allowed to pick untagged jobs' do
              before { runner.update_column(:run_untagged, false) }
501

502 503 504 505 506
              it_behaves_like 'no jobs available'
            end
          end

          context 'when triggered job is available' do
507
            let(:expected_variables) do
508 509 510
              [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
               { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
               { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
511 512 513 514 515
               { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true },
               { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false },
               { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
            end

516 517 518 519 520 521 522 523 524 525
            before do
              trigger = create(:ci_trigger, project: project)
              create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
              project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
            end

            it 'returns variables for triggers' do
              request_job

              expect(response).to have_http_status(201)
526
              expect(json_response['variables']).to include(*expected_variables)
527 528 529 530 531 532
            end
          end

          describe 'registry credentials support' do
            let(:registry_url) { 'registry.example.com:5005' }
            let(:registry_credentials) do
Tomasz Maczukin committed
533 534 535 536
              { 'type' => 'registry',
                'url' => registry_url,
                'username' => 'gitlab-ci-token',
                'password' => job.token }
537 538 539 540 541 542 543
            end

            context 'when registry is enabled' do
              before { stub_container_registry_config(enabled: true, host_port: registry_url) }

              it 'sends registry credentials key' do
                request_job
544

545 546 547 548 549 550 551 552 553 554
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).to include(registry_credentials)
              end
            end

            context 'when registry is disabled' do
              before { stub_container_registry_config(enabled: false, host_port: registry_url) }

              it 'does not send registry credentials' do
                request_job
555

556 557 558 559 560 561 562 563 564
                expect(json_response).to have_key('credentials')
                expect(json_response['credentials']).not_to include(registry_credentials)
              end
            end
          end
        end

        def request_job(token = runner.token, **params)
          new_params = params.merge(token: token, last_update: last_update)
Tomasz Maczukin committed
565
          post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
566 567 568
        end
      end
    end
Tomasz Maczukin committed
569 570 571 572 573 574 575 576 577

    describe 'PUT /api/v4/jobs/:id' do
      let(:job) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }

      before { job.run! }

      context 'when status is given' do
        it 'mark job as succeeded' do
          update_job(state: 'success')
578

Tomasz Maczukin committed
579 580 581 582 583
          expect(job.reload.status).to eq 'success'
        end

        it 'mark job as failed' do
          update_job(state: 'failed')
584

Tomasz Maczukin committed
585 586 587 588 589 590 591 592 593
          expect(job.reload.status).to eq 'failed'
        end
      end

      context 'when tace is given' do
        it 'updates a running build' do
          update_job(trace: 'BUILD TRACE UPDATED')

          expect(response).to have_http_status(200)
594
          expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
Tomasz Maczukin committed
595 596 597 598 599 600
        end
      end

      context 'when no trace is given' do
        it 'does not override trace information' do
          update_job
601

602
          expect(job.reload.trace.raw).to eq 'BUILD TRACE'
Tomasz Maczukin committed
603 604 605 606 607 608 609 610
        end
      end

      context 'when job has been erased' do
        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }

        it 'responds with forbidden' do
          update_job
611

Tomasz Maczukin committed
612 613 614 615 616 617 618 619 620
          expect(response).to have_http_status(403)
        end
      end

      def update_job(token = job.token, **params)
        new_params = params.merge(token: token)
        put api("/jobs/#{job.id}"), new_params
      end
    end
621 622 623 624 625 626 627 628 629 630 631 632

    describe 'PATCH /api/v4/jobs/:id/trace' do
      let(:job) { create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) }
      let(:headers) { { API::Helpers::Runner::JOB_TOKEN_HEADER => job.token, 'Content-Type' => 'text/plain' } }
      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
      let(:update_interval) { 10.seconds.to_i }

      before { initial_patch_the_trace }

      context 'when request is valid' do
        it 'gets correct response' do
          expect(response.status).to eq 202
633
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
634 635 636 637 638 639 640 641 642
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Job-Status'
        end

        context 'when job has been updated recently' do
          it { expect{ patch_the_trace }.not_to change { job.updated_at }}

          it "changes the job's trace" do
            patch_the_trace
643

644
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
645 646 647 648 649 650 651
          end

          context 'when Runner makes a force-patch' do
            it { expect{ force_patch_the_trace }.not_to change { job.updated_at }}

            it "doesn't change the build.trace" do
              force_patch_the_trace
652

653
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
654 655 656 657 658 659 660 661 662 663 664
            end
          end
        end

        context 'when job was not updated recently' do
          let(:update_interval) { 15.minutes.to_i }

          it { expect { patch_the_trace }.to change { job.updated_at } }

          it 'changes the job.trace' do
            patch_the_trace
665

666
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
667 668 669 670 671 672 673
          end

          context 'when Runner makes a force-patch' do
            it { expect { force_patch_the_trace }.to change { job.updated_at } }

            it "doesn't change the job.trace" do
              force_patch_the_trace
674

675
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
            end
          end
        end

        context 'when project for the build has been deleted' do
          let(:job) do
            create(:ci_build, :running, :trace, runner_id: runner.id, pipeline: pipeline) do |job|
              job.project.update(pending_delete: true)
            end
          end

          it 'responds with forbidden' do
            expect(response.status).to eq(403)
          end
        end
      end

      context 'when Runner makes a force-patch' do
        before do
          force_patch_the_trace
        end

        it 'gets correct response' do
          expect(response.status).to eq 202
700
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
          expect(response.header).to have_key 'Range'
          expect(response.header).to have_key 'Job-Status'
        end
      end

      context 'when content-range start is too big' do
        let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }

        it 'gets 416 error response with range headers' do
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
      end

      context 'when content-range start is too small' do
        let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }

        it 'gets 416 error response with range headers' do
          expect(response.status).to eq 416
          expect(response.header).to have_key 'Range'
          expect(response.header['Range']).to eq '0-11'
        end
      end

      context 'when Content-Range header is missing' do
        let(:headers_with_range) { headers }

        it { expect(response.status).to eq 400 }
      end

      context 'when job has been errased' do
        let(:job) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }

        it { expect(response.status).to eq 403 }
      end

      def patch_the_trace(content = ' appended', request_headers = nil)
        unless request_headers
740 741 742 743 744
          job.trace.read do |stream|
            offset = stream.size
            limit = offset + content.length - 1
            request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
          end
745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760
        end

        Timecop.travel(job.updated_at + update_interval) do
          patch api("/jobs/#{job.id}/trace"), content, request_headers
          job.reload
        end
      end

      def initial_patch_the_trace
        patch_the_trace(' appended', headers_with_range)
      end

      def force_patch_the_trace
        2.times { patch_the_trace('') }
      end
    end
761 762

    describe 'artifacts' do
763
      let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
764 765 766
      let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
      let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
      let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) }
767 768
      let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
      let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783

      before { job.run! }

      describe 'POST /api/v4/jobs/:id/artifacts/authorize' do
        context 'when using token as parameter' do
          it 'authorizes posting artifacts to running job' do
            authorize_artifacts_with_token_in_params

            expect(response).to have_http_status(200)
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
            expect(json_response['TempPath']).not_to be_nil
          end

          it 'fails to post too large artifact' do
            stub_application_setting(max_artifacts_size: 0)
784

785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801
            authorize_artifacts_with_token_in_params(filesize: 100)

            expect(response).to have_http_status(413)
          end
        end

        context 'when using token as header' do
          it 'authorizes posting artifacts to running job' do
            authorize_artifacts_with_token_in_headers

            expect(response).to have_http_status(200)
            expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
            expect(json_response['TempPath']).not_to be_nil
          end

          it 'fails to post too large artifact' do
            stub_application_setting(max_artifacts_size: 0)
802

803 804 805 806 807 808 809 810 811
            authorize_artifacts_with_token_in_headers(filesize: 100)

            expect(response).to have_http_status(413)
          end
        end

        context 'when using runners token' do
          it 'fails to authorize artifacts posting' do
            authorize_artifacts(token: job.project.runners_token)
812

813 814 815 816 817 818
            expect(response).to have_http_status(403)
          end
        end

        it 'reject requests that did not go through gitlab-workhorse' do
          headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
819

820
          authorize_artifacts
821

822 823 824 825 826 827
          expect(response).to have_http_status(500)
        end

        context 'authorization token is invalid' do
          it 'responds with forbidden' do
            authorize_artifacts(token: 'invalid', filesize: 100 )
828

829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
            expect(response).to have_http_status(403)
          end
        end

        def authorize_artifacts(params = {}, request_headers = headers)
          post api("/jobs/#{job.id}/artifacts/authorize"), params, request_headers
        end

        def authorize_artifacts_with_token_in_params(params = {}, request_headers = headers)
          params = params.merge(token: job.token)
          authorize_artifacts(params, request_headers)
        end

        def authorize_artifacts_with_token_in_headers(params = {}, request_headers = headers_with_token)
          authorize_artifacts(params, request_headers)
        end
      end
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862

      describe 'POST /api/v4/jobs/:id/artifacts' do
        context 'when artifacts are being stored inside of tmp path' do
          before do
            # by configuring this path we allow to pass temp file from any path
            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
          end

          context 'when job has been erased' do
            let(:job) { create(:ci_build, erased_at: Time.now) }

            before do
              upload_artifacts(file_upload, headers_with_token)
            end

            it 'responds with forbidden' do
              upload_artifacts(file_upload, headers_with_token)
863

864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
              expect(response).to have_http_status(403)
            end
          end

          context 'when job is running' do
            shared_examples 'successful artifacts upload' do
              it 'updates successfully' do
                expect(response).to have_http_status(201)
              end
            end

            context 'when uses regular file post' do
              before { upload_artifacts(file_upload, headers_with_token, false) }

              it_behaves_like 'successful artifacts upload'
            end

            context 'when uses accelerated file post' do
              before { upload_artifacts(file_upload, headers_with_token, true) }

              it_behaves_like 'successful artifacts upload'
            end

            context 'when updates artifact' do
              before do
                upload_artifacts(file_upload2, headers_with_token)
                upload_artifacts(file_upload, headers_with_token)
              end

              it_behaves_like 'successful artifacts upload'
            end

            context 'when using runners token' do
              it 'responds with forbidden' do
                upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
899

900 901 902 903 904 905 906 907
                expect(response).to have_http_status(403)
              end
            end
          end

          context 'when artifacts file is too large' do
            it 'fails to post too large artifact' do
              stub_application_setting(max_artifacts_size: 0)
908

909
              upload_artifacts(file_upload, headers_with_token)
910

911 912 913 914 915 916 917
              expect(response).to have_http_status(413)
            end
          end

          context 'when artifacts post request does not contain file' do
            it 'fails to post artifacts without file' do
              post api("/jobs/#{job.id}/artifacts"), {}, headers_with_token
918

919 920 921 922 923 924 925
              expect(response).to have_http_status(400)
            end
          end

          context 'GitLab Workhorse is not configured' do
            it 'fails to post artifacts without GitLab-Workhorse' do
              post api("/jobs/#{job.id}/artifacts"), { token: job.token }, {}
926

927 928 929 930 931 932 933 934 935 936 937 938 939 940
              expect(response).to have_http_status(403)
            end
          end

          context 'when setting an expire date' do
            let(:default_artifacts_expire_in) {}
            let(:post_data) do
              { 'file.path' => file_upload.path,
                'file.name' => file_upload.original_filename,
                'expire_in' => expire_in }
            end

            before do
              stub_application_setting(default_artifacts_expire_in: default_artifacts_expire_in)
941

942 943 944 945 946 947 948 949
              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
            end

            context 'when an expire_in is given' do
              let(:expire_in) { '7 days' }

              it 'updates when specified' do
                expect(response).to have_http_status(201)
950
                expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
951 952 953 954 955 956 957 958
              end
            end

            context 'when no expire_in is given' do
              let(:expire_in) { nil }

              it 'ignores if not specified' do
                expect(response).to have_http_status(201)
959
                expect(job.reload.artifacts_expire_at).to be_nil
960 961 962 963 964 965 966 967
              end

              context 'with application default' do
                context 'when default is 5 days' do
                  let(:default_artifacts_expire_in) { '5 days' }

                  it 'sets to application default' do
                    expect(response).to have_http_status(201)
968
                    expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
969 970 971 972 973 974 975 976
                  end
                end

                context 'when default is 0' do
                  let(:default_artifacts_expire_in) { '0' }

                  it 'does not set expire_in' do
                    expect(response).to have_http_status(201)
977
                    expect(job.reload.artifacts_expire_at).to be_nil
978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
                  end
                end
              end
            end
          end

          context 'posts artifacts file and metadata file' do
            let!(:artifacts) { file_upload }
            let!(:metadata) { file_upload2 }

            let(:stored_artifacts_file) { job.reload.artifacts_file.file }
            let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
            let(:stored_artifacts_size) { job.reload.artifacts_size }

            before do
              post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
            end

            context 'when posts data accelerated by workhorse is correct' do
              let(:post_data) do
                { 'file.path' => artifacts.path,
                  'file.name' => artifacts.original_filename,
                  'metadata.path' => metadata.path,
                  'metadata.name' => metadata.original_filename }
              end

              it 'stores artifacts and artifacts metadata' do
                expect(response).to have_http_status(201)
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
                expect(stored_artifacts_size).to eq(71759)
              end
            end

            context 'when there is no artifacts file in post data' do
              let(:post_data) do
                { 'metadata' => metadata }
              end

              it 'is expected to respond with bad request' do
                expect(response).to have_http_status(400)
              end

              it 'does not store metadata' do
                expect(stored_metadata_file).to be_nil
              end
            end
          end
        end

        context 'when artifacts are being stored outside of tmp path' do
          before do
            # by configuring this path we allow to pass file from @tmpdir only
            # but all temporary files are stored in system tmp directory
            @tmpdir = Dir.mktmpdir
            allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
          end

          after { FileUtils.remove_entry @tmpdir }

          it' "fails to post artifacts for outside of tmp path"' do
            upload_artifacts(file_upload, headers_with_token)
1040

1041 1042 1043 1044 1045
            expect(response).to have_http_status(400)
          end
        end

        def upload_artifacts(file, headers = {}, accelerated = true)
Tomasz Maczukin committed
1046 1047 1048
          params = if accelerated
                     { 'file.path' => file.path, 'file.name' => file.original_filename }
                   else
1049
                     { 'file' => file }
Tomasz Maczukin committed
1050
                   end
1051 1052 1053
          post api("/jobs/#{job.id}/artifacts"), params, headers
        end
      end
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093

      describe 'GET /api/v4/jobs/:id/artifacts' do
        let(:token) { job.token }

        before { download_artifact }

        context 'when job has artifacts' do
          let(:job) { create(:ci_build, :artifacts) }
          let(:download_headers) do
            { 'Content-Transfer-Encoding' => 'binary',
              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
          end

          context 'when using job token' do
            it 'download artifacts' do
              expect(response).to have_http_status(200)
              expect(response.headers).to include download_headers
            end
          end

          context 'when using runnners token' do
            let(:token) { job.project.runners_token }

            it 'responds with forbidden' do
              expect(response).to have_http_status(403)
            end
          end
        end

        context 'when job does not has artifacts' do
          it 'responds with not found' do
            expect(response).to have_http_status(404)
          end
        end

        def download_artifact(params = {}, request_headers = headers)
          params = params.merge(token: token)
          get api("/jobs/#{job.id}/artifacts"), params, request_headers
        end
      end
1094
    end
1095
  end
Tomasz Maczukin committed
1096
end