builds_spec.rb 16.6 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 10 11 12
  before do
    stub_ci_commit_to_return_yaml_file
  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
Kamil Trzcinski's avatar
Kamil Trzcinski committed
23 24
        commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
        commit.create_builds(nil)
25 26
        build = commit.builds.first

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
        commit = FactoryGirl.create(:ci_commit, project: shared_project)
42
        FactoryGirl.create(:ci_build, commit: commit, 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
        commit = FactoryGirl.create(:ci_commit, project: project)
51
        FactoryGirl.create(:ci_build, commit: commit, 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
Kamil Trzcinski's avatar
Kamil Trzcinski committed
59 60
        commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
        commit.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
Kamil Trzcinski's avatar
Kamil Trzcinski committed
69 70
        commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
        commit.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)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
86
        commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
87

88
        trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
89
        commit.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
Kamil Trzcinski's avatar
Kamil Trzcinski committed
106 107
        commit = FactoryGirl.create(:ci_commit, project: project, ref: 'master')
        commit.create_builds(nil, nil)
108 109 110 111 112
        commit.builds.where(stage: 'test').each(&:success)

        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 134 135 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

      context 'when build has no tags' do
        before do
          commit = create(:ci_commit, project: project)
          create(:ci_build, commit: commit, tags: [])
        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
      let(:commit) {create(:ci_commit, project: project)}
Kamil Trzcinski's avatar
Kamil Trzcinski committed
167
      let(:build) { create(:ci_build, :trace, commit: commit, 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(:commit) { create(:ci_commit, project: project) }
      let(:build) { create(:ci_build, commit: commit, 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)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
256
            expect(json_response["TempPath"]).to_not 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)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
262
            expect(json_response["TempPath"]).to_not 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 368
          context "artifacts file is too large" do
            it "should fail to post too large artifact" do
369
              stub_application_setting(max_artifacts_size: 0)
370 371 372 373 374
              upload_artifacts(file_upload, headers_with_token)
              expect(response.status).to eq(413)
            end
          end

375 376
          context "artifacts post request does not contain file" do
            it "should fail to post artifacts without file" do
377 378 379 380 381
              post post_url, {}, headers_with_token
              expect(response.status).to eq(400)
            end
          end

382 383
          context 'GitLab Workhorse is not configured' do
            it "should fail to post artifacts without GitLab-Workhorse" do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
384
              post post_url, { token: build.token }, {}
385 386 387 388 389
              expect(response.status).to eq(403)
            end
          end
        end

390
        context "artifacts are being stored outside of tmp path" do
391 392 393 394 395 396 397 398 399 400 401
          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

402
          it "should fail to post artifacts for outside of tmp path" do
403 404 405 406 407
            upload_artifacts(file_upload, headers_with_token)
            expect(response.status).to eq(400)
          end
        end

408 409 410 411 412 413 414 415 416
        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
417 418 419
        end
      end

420 421 422
      describe 'DELETE /builds/:id/artifacts' do
        let(:build) { create(:ci_build, :artifacts) }
        before { delete delete_url, token: build.token }
423

424
        it 'should remove build artifacts' do
425
          expect(response.status).to eq(200)
426 427 428 429 430 431 432 433 434 435 436
          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
437 438
            { 'Content-Transfer-Encoding' => 'binary',
              'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
439 440 441
          end

          it 'should download artifact' do
442
            expect(response.status).to eq(200)
443 444
            expect(response.headers).to include download_headers
          end
445 446
        end

447 448 449 450
        context 'build does not has artifacts' do
          it 'should respond with not found' do
            expect(response.status).to eq(404)
          end
451 452 453
        end
      end
    end
454 455
  end
end