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 18
  include ApiHelpers
  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')
19

20 21 22 23 24 25 26
          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'
27

28 29 30 31 32 33 34 35 36 37 38 39 40 41
          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
42
          expect(runner.token).not_to eq(registration_token)
43 44 45 46 47 48 49 50 51 52
        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)
53 54
            expect(Ci::Runner.first.token).not_to eq(registration_token)
            expect(Ci::Runner.first.token).not_to eq(project.runners_token)
55 56 57 58 59 60 61
          end
        end
      end

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

          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
72
                                tag_list: 'tag1, tag2'
73 74 75 76 77 78 79 80 81 82

          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
83 84
                                  run_untagged: false,
                                  tag_list: ['tag']
85 86 87 88 89 90 91 92 93 94

            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
95
                                  run_untagged: false
96 97 98 99 100 101 102 103 104

            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
105
                                locked: true
106 107 108 109 110 111 112 113 114 115

          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" }

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

            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')
131

132 133 134 135 136 137 138
          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'
139

140 141 142 143 144 145 146 147 148
          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
149 150

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

    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
176
        it 'verifies Runner credentials' do
177 178 179 180 181 182
          post api('/runners/verify'), token: runner.token

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

  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) }
189 190 191 192
    let!(:job) do
      create(:ci_build, :artifacts, :extended_options,
             pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
    end
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 232

    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

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

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

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

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

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

248 249 250 251 252 253 254 255
            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')
256

257 258 259 260 261 262 263
          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'
264

265 266 267 268 269 270 271 272
          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) }

273
          it 'returns 204 error' do
274
            request_job
275

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

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

283 284 285 286 287 288 289 290 291 292 293 294 295 296
          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) }
297

298 299 300 301
          it_behaves_like 'no jobs available'
        end

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

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

317 318 319 320 321 322 323 324 325 326 327 328
          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
329

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

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

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

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

            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')
356 357
            expect(json_response['id']).to eq(job.id)
            expect(json_response['token']).to eq(job.token)
358 359 360 361 362 363
            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)
364
            expect(json_response['cache']).to eq(expected_cache)
365
            expect(json_response['variables']).to include(*expected_variables)
366 367 368 369 370 371 372
          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
373

374 375 376 377 378 379 380 381
              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
382

383 384 385
              expect(response).to have_http_status(201)
              expect(json_response['git_info']['ref_type']).to eq('branch')
            end
386 387 388 389 390 391 392 393 394 395
          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" }

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

                expect(response).to have_http_status(201)
400
                expect(runner.reload.read_attribute(param.to_sym)).to eq(value)
401 402 403 404 405 406 407 408 409 410 411 412
              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
413

414 415 416 417 418 419
              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
420 421 422
            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) }
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 452
            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
453 454 455 456 457 458

            it 'returns dependent jobs' do
              request_job

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

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486
          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

487 488 489 490 491 492 493 494
          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
495

496 497 498 499 500 501
                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) }
502

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

          context 'when triggered job is available' do
508
            let(:expected_variables) do
509 510 511
              [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
               { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
               { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
512 513 514 515 516
               { '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

517 518 519 520 521 522 523 524 525 526
            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)
527
              expect(json_response['variables']).to include(*expected_variables)
528 529 530 531 532 533
            end
          end

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

546 547 548 549 550 551 552 553 554 555
                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
556

557 558 559 560 561 562 563 564 565
                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
566
          post api('/jobs/request'), new_params, { 'User-Agent' => user_agent }
567 568 569
        end
      end
    end
Tomasz Maczukin committed
570 571 572 573 574 575 576 577 578

    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')
579

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

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

Tomasz Maczukin committed
586 587 588 589 590 591 592 593 594
          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)
595
          expect(job.reload.trace.raw).to eq 'BUILD TRACE UPDATED'
Tomasz Maczukin committed
596 597 598 599 600 601
        end
      end

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

603
          expect(job.reload.trace.raw).to eq 'BUILD TRACE'
Tomasz Maczukin committed
604 605 606 607 608 609 610 611
        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
612

Tomasz Maczukin committed
613 614 615 616 617 618 619 620 621
          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
622 623 624 625 626 627 628 629 630 631 632 633

    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
634
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
635 636 637 638 639 640 641 642 643
          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
644

645
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
646 647 648 649 650 651 652
          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
653

654
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
655 656 657 658 659 660 661 662 663 664 665
            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
666

667
            expect(job.reload.trace.raw).to eq 'BUILD TRACE appended appended'
668 669 670 671 672 673 674
          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
675

676
              expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700
            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
701
          expect(job.reload.trace.raw).to eq 'BUILD TRACE appended'
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 740
          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
741 742 743 744 745
          job.trace.read do |stream|
            offset = stream.size
            limit = offset + content.length - 1
            request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
          end
746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761
        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
762 763

    describe 'artifacts' do
764
      let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
765 766 767
      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) }
768 769
      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') }
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784

      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)
785

786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802
            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)
803

804 805 806 807 808 809 810 811 812
            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)
813

814 815 816 817 818 819
            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)
820

821
          authorize_artifacts
822

823 824 825 826 827 828
          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 )
829

830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
            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
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863

      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)
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 899
              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))
900

901 902 903 904 905 906 907 908
                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)
909

910
              upload_artifacts(file_upload, headers_with_token)
911

912 913 914 915 916 917 918
              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
919

920 921 922 923 924 925 926
              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 }, {}
927

928 929 930 931 932 933 934 935 936 937 938 939 940 941
              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)
942

943 944 945 946 947 948 949 950
              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)
951
                expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(7.days.from_now)
952 953 954 955 956 957 958 959
              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)
960
                expect(job.reload.artifacts_expire_at).to be_nil
961 962 963 964 965 966 967 968
              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)
969
                    expect(job.reload.artifacts_expire_at).to be_within(5.minutes).of(5.days.from_now)
970 971 972 973 974 975 976 977
                  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)
978
                    expect(job.reload.artifacts_expire_at).to be_nil
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 1040
                  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)
1041

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

        def upload_artifacts(file, headers = {}, accelerated = true)
Tomasz Maczukin committed
1047 1048 1049
          params = if accelerated
                     { 'file.path' => file.path, 'file.name' => file.original_filename }
                   else
1050
                     { 'file' => file }
Tomasz Maczukin committed
1051
                   end
1052 1053 1054
          post api("/jobs/#{job.id}/artifacts"), params, headers
        end
      end
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 1094

      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
1095
    end
1096
  end
Tomasz Maczukin committed
1097
end