builds_spec.rb 17.8 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 30 31
        expect(response.status).to eq(201)
        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.status).to eq(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.status).to eq(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.status).to eq(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.status).to eq(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 76
        expect(response.status).to eq(201)
        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 95
        expect(response.status).to eq(201)
        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 112

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

        expect(response.status).to eq(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 125 126 127 128 129 130

      %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 }
            expect(response.status).to eq(404)
            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.status).to eq(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.status).to eq(200)
256
            expect(json_response["TempPath"]).not_to be_nil
257 258 259 260 261
          end

          it "using token as header" do
            post authorize_url, {}, headers_with_token
            expect(response.status).to eq(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 271 272 273
            expect(response.status).to eq(413)
          end

          it "using token as header" do
274
            stub_application_setting(max_artifacts_size: 0)
275 276 277 278 279
            post authorize_url, { filesize: 100 }, headers_with_token
            expect(response.status).to eq(413)
          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 285 286 287 288 289
            expect(response.status).to eq(403)
          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 298 299 300 301 302 303 304
          context 'build has been erased' do
            let(:build) { create(:ci_build, erased_at: Time.now) }
            before { upload_artifacts(file_upload, headers_with_token) }

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

305
          context "should post artifact to running build" do
306 307 308 309 310 311 312 313
            it "uses regual file post" do
              upload_artifacts(file_upload, headers_with_token, false)
              expect(response.status).to eq(201)
              expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
            end

            it "uses accelerated file post" do
              upload_artifacts(file_upload, headers_with_token, true)
314 315 316 317 318 319 320 321 322 323 324 325
              expect(response.status).to eq(201)
              expect(json_response["artifacts_file"]["filename"]).to eq(file_upload.original_filename)
            end

            it "updates artifact" do
              upload_artifacts(file_upload, headers_with_token)
              upload_artifacts(file_upload2, headers_with_token)
              expect(response.status).to eq(201)
              expect(json_response["artifacts_file"]["filename"]).to eq(file_upload2.original_filename)
            end
          end

326
          context 'should post artifacts file and metadata file' do
327 328 329
            let!(:artifacts) { file_upload }
            let!(:metadata) { file_upload2 }

330 331 332
            let(:stored_artifacts_file) { build.reload.artifacts_file.file }
            let(:stored_metadata_file) { build.reload.artifacts_metadata.file }

333 334 335
            before do
              post(post_url, post_data, headers_with_token)
            end
336

337 338 339 340 341 342 343 344 345
            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
346
                expect(response.status).to eq(201)
347 348 349
                expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
                expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
              end
350 351
            end

352
            context 'no artifacts file in post data' do
353
              let(:post_data) do
354
                { 'metadata' => metadata }
355 356
              end

357 358
              it 'is expected to respond with bad request' do
                expect(response.status).to eq(400)
359 360
              end

361
              it 'does not store metadata' do
362 363
                expect(stored_metadata_file).to be_nil
              end
364 365 366
            end
          end

367
          context 'with an expire date' do
368 369 370 371 372 373 374 375 376 377 378 379
            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

380
            context 'with an expire_in given' do
381 382 383 384 385 386 387 388 389 390
              let(:expire_in) { '7 days' }

              it do
                build.reload
                expect(response.status).to eq(201)
                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

391
            context 'with no expire_in given' do
392 393 394 395 396 397 398 399
              let(:expire_in) { nil }

              it do
                build.reload
                expect(response.status).to eq(201)
                expect(json_response['artifacts_expire_at']).to be_nil
                expect(build.artifacts_expire_at).to be_nil
              end
400 401 402
            end
          end

403 404
          context "artifacts file is too large" do
            it "should fail to post too large artifact" do
405
              stub_application_setting(max_artifacts_size: 0)
406 407 408 409 410
              upload_artifacts(file_upload, headers_with_token)
              expect(response.status).to eq(413)
            end
          end

411 412
          context "artifacts post request does not contain file" do
            it "should fail to post artifacts without file" do
413 414 415 416 417
              post post_url, {}, headers_with_token
              expect(response.status).to eq(400)
            end
          end

418 419
          context 'GitLab Workhorse is not configured' do
            it "should fail to post artifacts without GitLab-Workhorse" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
420
              post post_url, { token: build.token }, {}
421 422 423 424 425
              expect(response.status).to eq(403)
            end
          end
        end

426
        context "artifacts are being stored outside of tmp path" do
427 428 429 430 431 432 433 434 435 436 437
          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

438
          it "should fail to post artifacts for outside of tmp path" do
439 440 441 442 443
            upload_artifacts(file_upload, headers_with_token)
            expect(response.status).to eq(400)
          end
        end

444 445 446 447 448 449 450 451 452
        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
453 454 455
        end
      end

456 457 458
      describe 'DELETE /builds/:id/artifacts' do
        let(:build) { create(:ci_build, :artifacts) }
        before { delete delete_url, token: build.token }
459

460
        it 'should remove build artifacts' do
461
          expect(response.status).to eq(200)
462 463 464 465 466 467 468 469 470 471 472
          expect(build.artifacts_file.exists?).to be_falsy
          expect(build.artifacts_metadata.exists?).to be_falsy
        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
473 474
            { 'Content-Transfer-Encoding' => 'binary',
              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
475 476 477
          end

          it 'should download artifact' do
478
            expect(response.status).to eq(200)
479 480
            expect(response.headers).to include download_headers
          end
481 482
        end

483 484 485 486
        context 'build does not has artifacts' do
          it 'should respond with not found' do
            expect(response.status).to eq(404)
          end
487 488 489
        end
      end
    end
490 491
  end
end