builds_spec.rb 18.5 KB
Newer Older
1 2
require 'spec_helper'

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
3
describe Ci::API::API do
4 5
  include ApiHelpers

6
  let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
7
  let(:project) { FactoryGirl.create(:empty_project) }
8

Kamil Trzcinski's avatar
Kamil Trzcinski committed
9
  before do
10
    stub_ci_pipeline_to_return_yaml_file
Kamil Trzcinski's avatar
Kamil Trzcinski committed
11 12
  end

13
  describe "Builds API for runners" do
14
    let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
15
    let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
16 17

    before do
18
      FactoryGirl.create :ci_runner_project, project: project, runner: runner
19 20 21 22
    end

    describe "POST /builds/register" do
      it "should start a build" do
23 24 25
        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
        pipeline.create_builds(nil)
        build = pipeline.builds.first
26

Valery Sizov's avatar
Valery Sizov committed
27
        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
28

29
        expect(response).to have_http_status(201)
30 31
        expect(json_response['sha']).to eq(build.sha)
        expect(runner.reload.platform).to eq("darwin")
32 33 34
      end

      it "should return 404 error if no pending build found" do
Valery Sizov's avatar
Valery Sizov committed
35
        post ci_api("/builds/register"), token: runner.token
36

37
        expect(response).to have_http_status(404)
38 39 40
      end

      it "should return 404 error if no builds for specific runner" do
41 42
        pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
43

Valery Sizov's avatar
Valery Sizov committed
44
        post ci_api("/builds/register"), token: runner.token
45

46
        expect(response).to have_http_status(404)
47 48 49
      end

      it "should return 404 error if no builds for shared runner" do
50 51
        pipeline = FactoryGirl.create(:ci_pipeline, project: project)
        FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
52

Valery Sizov's avatar
Valery Sizov committed
53
        post ci_api("/builds/register"), token: shared_runner.token
54

55
        expect(response).to have_http_status(404)
56 57 58
      end

      it "returns options" do
59 60
        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
        pipeline.create_builds(nil)
61

Valery Sizov's avatar
Valery Sizov committed
62
        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
63

64
        expect(response).to have_http_status(201)
Valery Sizov's avatar
Valery Sizov committed
65
        expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
66 67 68
      end

      it "returns variables" do
69 70
        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
        pipeline.create_builds(nil)
71
        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
72

Valery Sizov's avatar
Valery Sizov committed
73
        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
74

75
        expect(response).to have_http_status(201)
76
        expect(json_response["variables"]).to eq([
77 78
          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
Valery Sizov's avatar
Valery Sizov committed
79
          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
80
          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
81
        ])
82 83 84
      end

      it "returns variables for triggers" do
85
        trigger = FactoryGirl.create(:ci_trigger, project: project)
86
        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
87

88
        trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
89
        pipeline.create_builds(nil, trigger_request)
90
        project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
91

Valery Sizov's avatar
Valery Sizov committed
92
        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
93

94
        expect(response).to have_http_status(201)
95
        expect(json_response["variables"]).to eq([
96 97 98
          { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
          { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
          { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
Valery Sizov's avatar
Valery Sizov committed
99 100 101
          { "key" => "DB_NAME", "value" => "postgres", "public" => true },
          { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
          { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
102
        ])
103
      end
104 105

      it "returns dependent builds" do
106 107 108
        pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
        pipeline.create_builds(nil, nil)
        pipeline.builds.where(stage: 'test').each(&:success)
109 110 111

        post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }

112
        expect(response).to have_http_status(201)
113 114
        expect(json_response["depends_on_builds"].count).to eq(2)
        expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
115
      end
116 117 118 119 120 121 122 123 124

      %w(name version revision platform architecture).each do |param|
        context "updates runner #{param}" do
          let(:value) { "#{param}_value" }

          subject { runner.read_attribute(param.to_sym) }

          it do
            post ci_api("/builds/register"), token: runner.token, info: { param => value }
125
            expect(response).to have_http_status(404)
126 127 128 129 130
            runner.reload
            is_expected.to eq(value)
          end
        end
      end
131 132 133

      context 'when build has no tags' do
        before do
134 135
          pipeline = create(:ci_pipeline, project: project)
          create(:ci_build, pipeline: pipeline, tags: [])
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
        end

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

          it 'picks build' do
            register_builds

            expect(response).to have_http_status 201
          end
        end

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

          it 'does not pick build' do
            register_builds

            expect(response).to have_http_status 404
          end
        end

        def register_builds
          post ci_api("/builds/register"), token: runner.token,
                                           info: { platform: :darwin }
        end
      end
163 164 165
    end

    describe "PUT /builds/:id" do
166 167
      let(:pipeline) {create(:ci_pipeline, project: project)}
      let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
168

169
      before do
170
        build.run!
Valery Sizov's avatar
Valery Sizov committed
171
        put ci_api("/builds/#{build.id}"), token: runner.token
172 173 174
      end

      it "should update a running build" do
175
        expect(response).to have_http_status(200)
176 177
      end

178 179 180 181 182 183 184 185 186 187
      it 'should not override trace information when no trace is given' do
        expect(build.reload.trace).to eq 'BUILD TRACE'
      end

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

        it 'should respond with forbidden' do
          expect(response.status).to eq 403
        end
188 189
      end
    end
190

191 192
    describe 'PATCH /builds/:id/trace.txt' do
      let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
Tomasz Maczukin's avatar
Tomasz Maczukin committed
193 194
      let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
      let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
195 196 197

      before do
        build.run!
Tomasz Maczukin's avatar
Tomasz Maczukin committed
198
        patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
199 200
      end

Tomasz Maczukin's avatar
Tomasz Maczukin committed
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
      context 'when request is valid' do
        it { expect(response.status).to eq 202 }
        it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header).to have_key 'Build-Status' }
      end

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

        it { expect(response.status).to eq 416 }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header['Range']).to eq '0-11' }
      end

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

        it { expect(response.status).to eq 416 }
        it { expect(response.header).to have_key 'Range' }
        it { expect(response.header['Range']).to eq '0-11' }
      end

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

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

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

Tomasz Maczukin's avatar
Tomasz Maczukin committed
233
        it { expect(response.status).to eq 403 }
234 235 236
      end
    end

237 238 239
    context "Artifacts" do
      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') }
240 241
      let(:pipeline) { create(:ci_pipeline, project: project) }
      let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
242 243 244 245
      let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
      let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
      let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
      let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
246
      let(:headers) { { "GitLab-Workhorse" => "1.0" } }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
247
      let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
248

249 250
      before { build.run! }

251 252 253
      describe "POST /builds/:id/artifacts/authorize" do
        context "should authorize posting artifact to running build" do
          it "using token as parameter" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
254
            post authorize_url, { token: build.token }, headers
255
            expect(response).to have_http_status(200)
256
            expect(json_response["TempPath"]).not_to be_nil
257 258 259 260
          end

          it "using token as header" do
            post authorize_url, {}, headers_with_token
261
            expect(response).to have_http_status(200)
262
            expect(json_response["TempPath"]).not_to be_nil
263 264 265 266 267
          end
        end

        context "should fail to post too large artifact" do
          it "using token as parameter" do
268
            stub_application_setting(max_artifacts_size: 0)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
269
            post authorize_url, { token: build.token, filesize: 100 }, headers
270
            expect(response).to have_http_status(413)
271 272 273
          end

          it "using token as header" do
274
            stub_application_setting(max_artifacts_size: 0)
275
            post authorize_url, { filesize: 100 }, headers_with_token
276
            expect(response).to have_http_status(413)
277 278 279
          end
        end

280 281 282 283
        context 'authorization token is invalid' do
          before { post authorize_url, { token: 'invalid', filesize: 100 } }

          it 'should respond with forbidden' do
284
            expect(response).to have_http_status(403)
285 286 287 288 289
          end
        end
      end

      describe "POST /builds/:id/artifacts" do
290
        context "disable sanitizer" do
291 292 293 294 295
          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

296 297
          context 'build has been erased' do
            let(:build) { create(:ci_build, erased_at: Time.now) }
298 299 300 301

            before do
              upload_artifacts(file_upload, headers_with_token)
            end
302 303 304 305 306 307

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

308 309 310 311
          context 'should post artifact to running build' do
            shared_examples 'post artifact' do
              it 'updates successfully' do
                response_filename =
312
                  json_response['artifacts_file']['filename']
313 314 315 316

                expect(response).to have_http_status(201)
                expect(response_filename).to eq(file_upload.original_filename)
              end
317 318
            end

319 320 321 322 323 324
            context 'uses regular file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, false)
              end

              it_behaves_like 'post artifact'
325 326
            end

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
            context 'uses accelerated file post' do
              before do
                upload_artifacts(file_upload, headers_with_token, true)
              end

              it_behaves_like 'post artifact'
            end

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

              it_behaves_like 'post artifact'
342 343 344
            end
          end

345
          context 'should post artifacts file and metadata file' do
346
            let!(:artifacts) { file_upload }
347
            let!(:metadata) { file_upload2 }
348

349 350
            let(:stored_artifacts_file) { build.reload.artifacts_file.file }
            let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
351
            let(:stored_artifacts_size) { build.reload.artifacts_size }
352

353 354 355
            before do
              post(post_url, post_data, headers_with_token)
            end
356

357 358 359 360 361 362 363 364 365
            context 'post 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
366
                expect(response).to have_http_status(201)
367 368
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
369
                expect(stored_artifacts_size).to eq(71759)
370
              end
371 372
            end

373
            context 'no artifacts file in post data' do
374
              let(:post_data) do
375
                { 'metadata' => metadata }
376 377
              end

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

382
              it 'does not store metadata' do
383 384
                expect(stored_metadata_file).to be_nil
              end
385 386 387
            end
          end

388
          context 'with an expire date' do
389 390 391 392 393 394 395 396 397 398 399 400
            let!(:artifacts) { file_upload }

            let(:post_data) do
              { 'file.path' => artifacts.path,
                'file.name' => artifacts.original_filename,
                'expire_in' => expire_in }
            end

            before do
              post(post_url, post_data, headers_with_token)
            end

401
            context 'with an expire_in given' do
402 403
              let(:expire_in) { '7 days' }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
404
              it 'updates when specified' do
405
                build.reload
406
                expect(response).to have_http_status(201)
407 408 409 410 411
                expect(json_response['artifacts_expire_at']).not_to be_empty
                expect(build.artifacts_expire_at).to be_within(5.minutes).of(Time.now + 7.days)
              end
            end

412
            context 'with no expire_in given' do
413 414
              let(:expire_in) { nil }

Kamil Trzcinski's avatar
Kamil Trzcinski committed
415
              it 'ignores if not specified' do
416
                build.reload
417
                expect(response).to have_http_status(201)
418 419 420
                expect(json_response['artifacts_expire_at']).to be_nil
                expect(build.artifacts_expire_at).to be_nil
              end
421 422 423
            end
          end

424 425
          context "artifacts file is too large" do
            it "should fail to post too large artifact" do
426
              stub_application_setting(max_artifacts_size: 0)
427
              upload_artifacts(file_upload, headers_with_token)
428
              expect(response).to have_http_status(413)
429 430 431
            end
          end

432 433
          context "artifacts post request does not contain file" do
            it "should fail to post artifacts without file" do
434
              post post_url, {}, headers_with_token
435
              expect(response).to have_http_status(400)
436 437 438
            end
          end

439 440
          context 'GitLab Workhorse is not configured' do
            it "should fail to post artifacts without GitLab-Workhorse" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
441
              post post_url, { token: build.token }, {}
442
              expect(response).to have_http_status(403)
443 444 445 446
            end
          end
        end

447
        context "artifacts are being stored outside of tmp path" do
448 449 450 451 452 453 454 455 456 457 458
          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 do
            FileUtils.remove_entry @tmpdir
          end

459
          it "should fail to post artifacts for outside of tmp path" do
460
            upload_artifacts(file_upload, headers_with_token)
461
            expect(response).to have_http_status(400)
462 463 464
          end
        end

465 466 467 468 469 470 471 472 473
        def upload_artifacts(file, headers = {}, accelerated = true)
          if accelerated
            post post_url, {
              'file.path' => file.path,
              'file.name' => file.original_filename
            }, headers
          else
            post post_url, { file: file }, headers
          end
474 475 476
        end
      end

477 478 479
      describe 'DELETE /builds/:id/artifacts' do
        let(:build) { create(:ci_build, :artifacts) }
        before { delete delete_url, token: build.token }
480

481
        it 'should remove build artifacts' do
482
          expect(response).to have_http_status(200)
483 484
          expect(build.artifacts_file.exists?).to be_falsy
          expect(build.artifacts_metadata.exists?).to be_falsy
485
          expect(build.artifacts_size).to be_falsy
486 487 488 489 490 491 492 493 494
        end
      end

      describe 'GET /builds/:id/artifacts' do
        before { get get_url, token: build.token }

        context 'build has artifacts' do
          let(:build) { create(:ci_build, :artifacts) }
          let(:download_headers) do
495 496
            { 'Content-Transfer-Encoding' => 'binary',
              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
497 498 499
          end

          it 'should download artifact' do
500
            expect(response).to have_http_status(200)
501 502
            expect(response.headers).to include download_headers
          end
503 504
        end

505 506
        context 'build does not has artifacts' do
          it 'should respond with not found' do
507
            expect(response).to have_http_status(404)
508
          end
509 510 511
        end
      end
    end
512 513
  end
end