This instance will be upgraded to Heptapod 0.31.0rc on 2022-05-19 at 11:00 UTC+2 (a few minutes of down time)

jobs_controller_spec.rb 20.6 KB
Newer Older
1
# coding: utf-8
Shinya Maeda's avatar
Shinya Maeda committed
2
3
require 'spec_helper'

Shinya Maeda's avatar
Shinya Maeda committed
4
describe Projects::JobsController, :clean_gitlab_redis_shared_state do
5
  include ApiHelpers
6
  include HttpIOHelpers
Shinya Maeda's avatar
Shinya Maeda committed
7

8
  let(:project) { create(:project, :public) }
9
10
  let(:pipeline) { create(:ci_pipeline, project: project) }
  let(:user) { create(:user) }
Shinya Maeda's avatar
Shinya Maeda committed
11

12
  before do
Shinya Maeda's avatar
Shinya Maeda committed
13
    stub_feature_flags(ci_enable_live_trace: true)
14
15
16
    stub_not_protect_default_branch
  end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
17
  describe 'GET index' do
18
19
20
21
22
23
24
25
    context 'when scope is pending' do
      before do
        create(:ci_build, :pending, pipeline: pipeline)

        get_index(scope: 'pending')
      end

      it 'has only pending builds' do
26
        expect(response).to have_gitlab_http_status(:ok)
27
28
29
30
31
32
33
34
35
36
37
        expect(assigns(:builds).first.status).to eq('pending')
      end
    end

    context 'when scope is running' do
      before do
        create(:ci_build, :running, pipeline: pipeline)

        get_index(scope: 'running')
      end

38
      it 'has only running jobs' do
39
        expect(response).to have_gitlab_http_status(:ok)
40
41
42
43
44
45
46
47
48
49
50
        expect(assigns(:builds).first.status).to eq('running')
      end
    end

    context 'when scope is finished' do
      before do
        create(:ci_build, :success, pipeline: pipeline)

        get_index(scope: 'finished')
      end

51
      it 'has only finished jobs' do
52
        expect(response).to have_gitlab_http_status(:ok)
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
        expect(assigns(:builds).first.status).to eq('success')
      end
    end

    context 'when page is specified' do
      let(:last_page) { project.builds.page.total_pages }

      context 'when page number is eligible' do
        before do
          create_list(:ci_build, 2, pipeline: pipeline)

          get_index(page: last_page.to_param)
        end

        it 'redirects to the page' do
68
          expect(response).to have_gitlab_http_status(:ok)
69
70
71
72
73
          expect(assigns(:builds).current_page).to eq(last_page)
        end
      end
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
74
75
76
    context 'number of queries' do
      before do
        Ci::Build::AVAILABLE_STATUSES.each do |status|
77
          create_job(status, status)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
78
79
80
        end
      end

81
      it 'verifies number of queries', :request_store do
82
        recorded = ActiveRecord::QueryRecorder.new { get_index }
83
        expect(recorded.count).to be_within(5).of(7)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
84
85
      end

86
      def create_job(name, status)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
87
88
89
90
91
        pipeline = create(:ci_pipeline, project: project)
        create(:ci_build, :tags, :triggered, :artifacts,
          pipeline: pipeline, name: name, status: status)
      end
    end
92
93
94
95
96
97
98
99
100
101
102
103

    def get_index(**extra_params)
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project
      }

      get :index, params.merge(extra_params)
    end
  end

  describe 'GET show' do
104
    let!(:job) { create(:ci_build, :failed, pipeline: pipeline) }
105
106
    let!(:second_job) { create(:ci_build, :failed, pipeline: pipeline) }
    let!(:third_job) { create(:ci_build, :failed) }
107

108
    context 'when requesting HTML' do
109
      context 'when job exists' do
110
        before do
111
          get_show(id: job.id)
112
113
        end

114
        it 'has a job' do
115
          expect(response).to have_gitlab_http_status(:ok)
116
          expect(assigns(:build).id).to eq(job.id)
117
        end
118
119
120
121
122
123
124

        it 'has the correct build collection' do
          builds = assigns(:builds).map(&:id)

          expect(builds).to include(job.id, second_job.id)
          expect(builds).not_to include(third_job.id)
        end
125
126
      end

127
      context 'when job does not exist' do
128
129
130
131
132
        before do
          get_show(id: 1234)
        end

        it 'renders not_found' do
133
          expect(response).to have_gitlab_http_status(:not_found)
134
        end
135
136
137
      end
    end

138
    context 'when requesting JSON with failed job' do
139
140
      let(:merge_request) { create(:merge_request, source_project: project) }

141
      before do
142
143
144
145
146
        project.add_developer(user)
        sign_in(user)

        allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)

147
        get_show(id: job.id, format: :json)
148
149
      end

150
      it 'exposes needed information' do
151
        expect(response).to have_gitlab_http_status(:ok)
152
        expect(response).to match_response_schema('job/job_details')
153
        expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
        expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z})
        expect(json_response['new_issue_path']).to include('/issues/new')
      end
    end

    context 'when request JSON for successful job' do
      let(:merge_request) { create(:merge_request, source_project: project) }
      let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }

      before do
        project.add_developer(user)
        sign_in(user)

        allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)

        get_show(id: job.id, format: :json)
      end

      it 'exposes needed information' do
        expect(response).to have_gitlab_http_status(:ok)
174
        expect(response).to match_response_schema('job/job_details')
175
176
177
178
179
        expect(json_response['artifact']['download_path']).to match(%r{artifacts/download})
        expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse})
        expect(json_response['artifact']).not_to have_key(:expired)
        expect(json_response['artifact']).not_to have_key(:expired_at)
        expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
180
        expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
181
182
      end

183
      context 'when request JSON for successful job with expired artifacts' do
184
185
186
187
188
189
190
191
192
193
194
195
196
197
        let(:merge_request) { create(:merge_request, source_project: project) }
        let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) }

        before do
          project.add_developer(user)
          sign_in(user)

          allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)

          get_show(id: job.id, format: :json)
        end

        it 'exposes needed information' do
          expect(response).to have_gitlab_http_status(:ok)
198
          expect(response).to match_response_schema('job/job_details')
199
200
201
202
203
204
205
          expect(json_response['artifact']).not_to have_key(:download_path)
          expect(json_response['artifact']).not_to have_key(:browse_path)
          expect(json_response['artifact']['expired']).to eq(true)
          expect(json_response['artifact']['expire_at']).not_to be_empty
          expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
          expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
        end
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
      end
    end

    def get_show(**extra_params)
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project
      }

      get :show, params.merge(extra_params)
    end
  end

  describe 'GET trace.json' do
    before do
      get_trace
    end

224
225
226
227
228
229
230
231
232
233
234
    context 'when job has a trace artifact' do
      let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }

      it 'returns a trace' do
        expect(response).to have_gitlab_http_status(:ok)
        expect(json_response['id']).to eq job.id
        expect(json_response['status']).to eq job.status
        expect(json_response['html']).to eq(job.trace.html)
      end
    end

235
    context 'when job has a trace' do
236
      let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
237
238

      it 'returns a trace' do
239
        expect(response).to have_gitlab_http_status(:ok)
240
241
        expect(json_response['id']).to eq job.id
        expect(json_response['status']).to eq job.status
242
243
244
245
        expect(json_response['html']).to eq('BUILD TRACE')
      end
    end

246
247
    context 'when job has no traces' do
      let(:job) { create(:ci_build, pipeline: pipeline) }
248
249

      it 'returns no traces' do
250
        expect(response).to have_gitlab_http_status(:ok)
251
252
        expect(json_response['id']).to eq job.id
        expect(json_response['status']).to eq job.status
253
        expect(json_response['html']).to be_nil
254
255
256
      end
    end

257
    context 'when job has a trace with ANSI sequence and Unicode' do
258
      let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
259
260

      it 'returns a trace with Unicode' do
261
        expect(response).to have_gitlab_http_status(:ok)
262
263
        expect(json_response['id']).to eq job.id
        expect(json_response['status']).to eq job.status
264
265
266
267
        expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ")
      end
    end

268
    context 'when trace artifact is in ObjectStorage' do
Kamil Trzciński's avatar
Kamil Trzciński committed
269
270
      let(:url) { 'http://object-storage/trace' }
      let(:file_path) { expand_fixture_path('trace/sample_trace') }
271
272
273
274
      let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }

      before do
        allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
Kamil Trzciński's avatar
Kamil Trzciński committed
275
276
        allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
        allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
277
278
279
280
      end

      context 'when there are no network issues' do
        before do
Kamil Trzciński's avatar
Kamil Trzciński committed
281
          stub_remote_url_206(url, file_path)
282
283
284
285
286
287
288
289
290
291
292
293
294
295

          get_trace
        end

        it 'returns a trace' do
          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['id']).to eq job.id
          expect(json_response['status']).to eq job.status
          expect(json_response['html']).to eq(job.trace.html)
        end
      end

      context 'when there is a network issue' do
        before do
296
          stub_remote_url_500(url)
297
298
299
        end

        it 'returns a trace' do
300
          expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
301
302
303
304
        end
      end
    end

305
306
307
    def get_trace
      get :trace, namespace_id: project.namespace,
                  project_id: project,
308
                  id: job.id,
309
310
                  format: :json
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
311
312
  end

Shinya Maeda's avatar
Shinya Maeda committed
313
  describe 'GET status.json' do
314
315
    let(:job) { create(:ci_build, pipeline: pipeline) }
    let(:status) { job.detailed_status(double('user')) }
Shinya Maeda's avatar
Shinya Maeda committed
316

317
318
319
    before do
      get :status, namespace_id: project.namespace,
                   project_id: project,
320
                   id: job.id,
321
322
                   format: :json
    end
Shinya Maeda's avatar
Shinya Maeda committed
323

324
    it 'return a detailed job status in json' do
325
      expect(response).to have_gitlab_http_status(:ok)
326
327
328
      expect(json_response['text']).to eq status.text
      expect(json_response['label']).to eq status.label
      expect(json_response['icon']).to eq status.icon
329
      expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png"
Shinya Maeda's avatar
Shinya Maeda committed
330
331
    end
  end
332

333
334
  describe 'POST retry' do
    before do
335
      project.add_developer(user)
336
337
338
339
340
      sign_in(user)

      post_retry
    end

341
342
    context 'when job is retryable' do
      let(:job) { create(:ci_build, :retryable, pipeline: pipeline) }
343

344
      it 'redirects to the retried job page' do
345
        expect(response).to have_gitlab_http_status(:found)
346
        expect(response).to redirect_to(namespace_project_job_path(id: Ci::Build.last.id))
347
348
349
      end
    end

350
351
    context 'when job is not retryable' do
      let(:job) { create(:ci_build, pipeline: pipeline) }
352
353

      it 'renders unprocessable_entity' do
354
        expect(response).to have_gitlab_http_status(:unprocessable_entity)
355
356
357
358
359
360
      end
    end

    def post_retry
      post :retry, namespace_id: project.namespace,
                   project_id: project,
361
                   id: job.id
362
363
364
365
366
    end
  end

  describe 'POST play' do
    before do
367
      project.add_developer(user)
368
369
370
371

      create(:protected_branch, :developers_can_merge,
             name: 'master', project: project)

372
373
374
375
376
      sign_in(user)

      post_play
    end

377
378
    context 'when job is playable' do
      let(:job) { create(:ci_build, :playable, pipeline: pipeline) }
379

380
      it 'redirects to the played job page' do
381
        expect(response).to have_gitlab_http_status(:found)
382
        expect(response).to redirect_to(namespace_project_job_path(id: job.id))
383
384
385
      end

      it 'transits to pending' do
386
        expect(job.reload).to be_pending
387
388
389
      end
    end

390
391
    context 'when job is not playable' do
      let(:job) { create(:ci_build, pipeline: pipeline) }
392
393

      it 'renders unprocessable_entity' do
394
        expect(response).to have_gitlab_http_status(:unprocessable_entity)
395
396
397
398
399
400
      end
    end

    def post_play
      post :play, namespace_id: project.namespace,
                  project_id: project,
401
                  id: job.id
402
403
404
405
406
    end
  end

  describe 'POST cancel' do
    before do
407
      project.add_developer(user)
408
409
410
411
412
      sign_in(user)

      post_cancel
    end

413
414
    context 'when job is cancelable' do
      let(:job) { create(:ci_build, :cancelable, pipeline: pipeline) }
415

416
      it 'redirects to the canceled job page' do
417
        expect(response).to have_gitlab_http_status(:found)
418
        expect(response).to redirect_to(namespace_project_job_path(id: job.id))
419
420
421
      end

      it 'transits to canceled' do
422
        expect(job.reload).to be_canceled
423
424
425
      end
    end

426
427
    context 'when job is not cancelable' do
      let(:job) { create(:ci_build, :canceled, pipeline: pipeline) }
428
429

      it 'returns unprocessable_entity' do
430
        expect(response).to have_gitlab_http_status(:unprocessable_entity)
431
432
433
434
435
436
      end
    end

    def post_cancel
      post :cancel, namespace_id: project.namespace,
                    project_id: project,
437
                    id: job.id
438
439
440
441
442
    end
  end

  describe 'POST cancel_all' do
    before do
443
      project.add_developer(user)
444
445
446
      sign_in(user)
    end

447
    context 'when jobs are cancelable' do
448
449
450
451
452
453
454
      before do
        create_list(:ci_build, 2, :cancelable, pipeline: pipeline)

        post_cancel_all
      end

      it 'redirects to a index page' do
455
        expect(response).to have_gitlab_http_status(:found)
456
        expect(response).to redirect_to(namespace_project_jobs_path)
457
458
459
460
461
462
463
      end

      it 'transits to canceled' do
        expect(Ci::Build.all).to all(be_canceled)
      end
    end

464
    context 'when jobs are not cancelable' do
465
466
467
468
469
470
471
      before do
        create_list(:ci_build, 2, :canceled, pipeline: pipeline)

        post_cancel_all
      end

      it 'redirects to a index page' do
472
        expect(response).to have_gitlab_http_status(:found)
473
        expect(response).to redirect_to(namespace_project_jobs_path)
474
475
476
477
478
479
480
481
482
483
      end
    end

    def post_cancel_all
      post :cancel_all, namespace_id: project.namespace,
                        project_id: project
    end
  end

  describe 'POST erase' do
484
    let(:role) { :maintainer }
Shinya Maeda's avatar
Shinya Maeda committed
485

486
    before do
487
      project.add_role(user, role)
488
489
490
491
492
      sign_in(user)

      post_erase
    end

493
    context 'when job is erasable' do
494
      let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline) }
495

496
      it 'redirects to the erased job page' do
497
        expect(response).to have_gitlab_http_status(:found)
498
        expect(response).to redirect_to(namespace_project_job_path(id: job.id))
499
500
501
      end

      it 'erases artifacts' do
502
503
        expect(job.artifacts_file.exists?).to be_falsey
        expect(job.artifacts_metadata.exists?).to be_falsey
504
505
506
      end

      it 'erases trace' do
507
        expect(job.trace.exist?).to be_falsey
508
509
510
      end
    end

511
512
    context 'when job is not erasable' do
      let(:job) { create(:ci_build, :erased, pipeline: pipeline) }
513
514

      it 'returns unprocessable_entity' do
515
        expect(response).to have_gitlab_http_status(:unprocessable_entity)
516
517
518
      end
    end

519
520
    context 'when user is developer' do
      let(:role) { :developer }
521
      let(:job) { create(:ci_build, :erasable, :trace_artifact, pipeline: pipeline, user: triggered_by) }
522
523
524
525
526
527
528
529
530
531
532
533
534

      context 'when triggered by same user' do
        let(:triggered_by) { user }

        it 'has successful status' do
          expect(response).to have_gitlab_http_status(:found)
        end
      end

      context 'when triggered by different user' do
        let(:triggered_by) { create(:user) }

        it 'does not have successful status' do
Shinya Maeda's avatar
Shinya Maeda committed
535
          expect(response).not_to have_gitlab_http_status(:found)
536
537
538
539
        end
      end
    end

540
541
542
    def post_erase
      post :erase, namespace_id: project.namespace,
                   project_id: project,
543
                   id: job.id
544
545
546
547
    end
  end

  describe 'GET raw' do
548
549
550
551
    subject do
      post :raw, namespace_id: project.namespace,
                 project_id: project,
                 id: job.id
552
553
    end

554
    context "when job has a trace artifact" do
555
556
557
      let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }

      it 'returns a trace' do
558
559
        response = subject

560
        expect(response).to have_gitlab_http_status(:ok)
561
562
        expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
        expect(response.body).to eq(job.job_artifacts_trace.open.read)
563
564
565
      end
    end

566
    context "when job has a trace file" do
567
      let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
568

569
      it "send a trace file" do
570
571
        response = subject

572
        expect(response).to have_gitlab_http_status(:ok)
573
574
        expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
        expect(response.body).to eq("BUILD TRACE")
575
576
577
      end
    end

578
    context "when job has a trace in database" do
Shinya Maeda's avatar
Shinya Maeda committed
579
580
581
      let(:job) { create(:ci_build, pipeline: pipeline) }

      before do
582
        job.update_column(:trace, "Sample trace")
Shinya Maeda's avatar
Shinya Maeda committed
583
584
      end

585
      it "send a trace file" do
Shinya Maeda's avatar
Shinya Maeda committed
586
587
588
        response = subject

        expect(response).to have_gitlab_http_status(:ok)
589
590
        expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8")
        expect(response.body).to eq("Sample trace")
Shinya Maeda's avatar
Shinya Maeda committed
591
592
593
      end
    end

594
595
    context 'when job does not have a trace file' do
      let(:job) { create(:ci_build, pipeline: pipeline) }
596
597

      it 'returns not_found' do
598
599
        response = subject

Shinya Maeda's avatar
Shinya Maeda committed
600
601
        expect(response).to have_gitlab_http_status(:ok)
        expect(response.body).to eq ''
602
603
604
      end
    end

605
606
607
608
609
610
611
612
613
614
    context 'when the trace artifact is in ObjectStorage' do
      let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }

      before do
        allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
      end

      it 'redirect to the trace file url' do
        expect(subject).to redirect_to(job.job_artifacts_trace.file.url)
      end
615
616
    end
  end
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717

  describe 'GET #terminal' do
    before do
      project.add_developer(user)
      sign_in(user)
    end

    context 'when job exists' do
      context 'and it has a terminal' do
        let!(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }

        it 'has a job' do
          get_terminal(id: job.id)

          expect(response).to have_gitlab_http_status(:ok)
          expect(assigns(:build).id).to eq(job.id)
        end
      end

      context 'and does not have a terminal' do
        let!(:job) { create(:ci_build, :running, pipeline: pipeline) }

        it 'returns not_found' do
          get_terminal(id: job.id)

          expect(response).to have_gitlab_http_status(:not_found)
        end
      end
    end

    context 'when job does not exist' do
      it 'renders not_found' do
        get_terminal(id: 1234)

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    def get_terminal(**extra_params)
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project
      }

      get :terminal, params.merge(extra_params)
    end
  end

  describe 'GET #terminal_websocket_authorize' do
    let!(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) }

    before do
      project.add_developer(user)
      sign_in(user)
    end

    context 'with valid workhorse signature' do
      before do
        allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_return(nil)
      end

      context 'and valid id' do
        it 'returns the terminal for the job' do
          expect(Gitlab::Workhorse)
            .to receive(:terminal_websocket)
            .and_return(workhorse: :response)

          get_terminal_websocket(id: job.id)

          expect(response).to have_gitlab_http_status(200)
          expect(response.headers["Content-Type"]).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
          expect(response.body).to eq('{"workhorse":"response"}')
        end
      end

      context 'and invalid id' do
        it 'returns 404' do
          get_terminal_websocket(id: 1234)

          expect(response).to have_gitlab_http_status(404)
        end
      end
    end

    context 'with invalid workhorse signature' do
      it 'aborts with an exception' do
        allow(Gitlab::Workhorse).to receive(:verify_api_request!).and_raise(JWT::DecodeError)

        expect { get_terminal_websocket(id: job.id) }.to raise_error(JWT::DecodeError)
      end
    end

    def get_terminal_websocket(**extra_params)
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project
      }

      get :terminal_websocket_authorize, params.merge(extra_params)
    end
  end
Shinya Maeda's avatar
Shinya Maeda committed
718
end