pipelines_controller_spec.rb 22.4 KB
Newer Older
G
gfyoung 已提交
1 2
# frozen_string_literal: true

3 4 5
require 'spec_helper'

describe Projects::PipelinesController do
6 7
  include ApiHelpers

8
  let_it_be(:user) { create(:user) }
B
Bob Van Landuyt 已提交
9
  let(:project) { create(:project, :public, :repository) }
10
  let(:feature) { ProjectFeature::ENABLED }
11 12

  before do
13 14
    stub_not_protect_default_branch
    project.add_developer(user)
15
    project.project_feature.update(builds_access_level: feature)
16

17 18 19
    sign_in(user)
  end

20 21
  describe 'GET index.json' do
    before do
22
      %w(pending running success failed canceled).each_with_index do |status, index|
23 24
        create_pipeline(status, project.commit("HEAD~#{index}"))
      end
25 26
    end

27 28 29
    context 'when using persisted stages', :request_store do
      before do
        stub_feature_flags(ci_pipeline_persisted_stages: true)
30
      end
31

32
      it 'returns serialized pipelines', :request_store do
33 34
        expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original

35 36 37
        queries = ActiveRecord::QueryRecorder.new do
          get_pipelines_index_json
        end
38

39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('pipeline')

        expect(json_response).to include('pipelines')
        expect(json_response['pipelines'].count).to eq 5
        expect(json_response['count']['all']).to eq '5'
        expect(json_response['count']['running']).to eq '1'
        expect(json_response['count']['pending']).to eq '1'
        expect(json_response['count']['finished']).to eq '3'

        json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
          expect(stages.count).to eq 3
        end

        expect(queries.count).to be
      end
    end

57
    context 'when using legacy stages', :request_store do
58 59 60 61
      before do
        stub_feature_flags(ci_pipeline_persisted_stages: false)
      end

62 63
      it 'returns JSON with serialized pipelines' do
        get_pipelines_index_json
64 65 66 67 68 69 70 71 72 73 74 75 76 77

        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to match_response_schema('pipeline')

        expect(json_response).to include('pipelines')
        expect(json_response['pipelines'].count).to eq 5
        expect(json_response['count']['all']).to eq '5'
        expect(json_response['count']['running']).to eq '1'
        expect(json_response['count']['pending']).to eq '1'
        expect(json_response['count']['finished']).to eq '3'

        json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages|
          expect(stages.count).to eq 3
        end
78 79 80 81 82 83
      end

      it 'does not execute N+1 queries' do
        queries = ActiveRecord::QueryRecorder.new do
          get_pipelines_index_json
        end
84

85
        expect(queries.count).to be <= 36
86
      end
87
    end
88

89
    it 'does not include coverage data for the pipelines' do
90
      get_pipelines_index_json
91 92 93 94

      expect(json_response['pipelines'][0]).not_to include('coverage')
    end

95 96
    context 'when performing gitaly calls', :request_store do
      it 'limits the Gitaly requests' do
97 98 99 100 101
        # Isolate from test preparation (Repository#exists? is also cached in RequestStore)
        RequestStore.end!
        RequestStore.clear!
        RequestStore.begin!

102 103
        expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original

104 105
        expect { get_pipelines_index_json }
          .to change { Gitlab::GitalyClient.get_request_count }.by(2)
106 107
      end
    end
108

B
Bob Van Landuyt 已提交
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    context 'when the project is private' do
      let(:project) { create(:project, :private, :repository) }

      it 'returns `not_found` when the user does not have access' do
        sign_in(create(:user))

        get_pipelines_index_json

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

      it 'returns the pipelines when the user has access' do
        get_pipelines_index_json

        expect(json_response['pipelines'].size).to eq(5)
      end
    end

127
    def get_pipelines_index_json
B
blackst0ne 已提交
128 129 130 131
      get :index, params: {
                    namespace_id: project.namespace,
                    project_id: project
                  },
132 133 134
                  format: :json
    end

135 136 137 138 139 140 141 142 143 144
    def create_pipeline(status, sha)
      pipeline = create(:ci_empty_pipeline, status: status,
                                            project: project,
                                            sha: sha)

      create_build(pipeline, 'build', 1, 'build')
      create_build(pipeline, 'test', 2, 'test')
      create_build(pipeline, 'deploy', 3, 'deploy')
    end

145
    def create_build(pipeline, stage, stage_idx, name)
146 147
      status = %w[created running pending success failed canceled].sample
      create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status)
148
    end
149 150
  end

151
  describe 'GET show.json' do
152
    let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) }
153 154 155 156

    it 'returns the pipeline' do
      get_pipeline_json

157
      expect(response).to have_gitlab_http_status(:ok)
158 159 160 161 162
      expect(json_response).not_to be_an(Array)
      expect(json_response['id']).to be(pipeline.id)
      expect(json_response['details']).to have_key 'stages'
    end

163
    context 'when the pipeline has multiple stages and groups', :request_store do
164 165 166 167 168 169 170 171
      let(:project) { create(:project, :repository) }

      let(:pipeline) do
        create(:ci_empty_pipeline, project: project,
                                   user: user,
                                   sha: project.commit.id)
      end

172 173 174 175 176 177 178
      before do
        create_build('build', 0, 'build')
        create_build('test', 1, 'rspec 0')
        create_build('deploy', 2, 'production')
        create_build('post deploy', 3, 'pages 0')
      end

179
      it 'does not perform N + 1 queries' do
180 181 182
        # Set up all required variables
        get_pipeline_json

183 184
        control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count

185 186 187 188 189 190 191
        first_build = pipeline.builds.first
        first_build.tag_list << [:hello, :world]
        create(:deployment, deployable: first_build)

        second_build = pipeline.builds.second
        second_build.tag_list << [:docker, :ruby]
        create(:deployment, deployable: second_build)
192

193
        new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
194

195
        expect(new_count).to be_within(1).of(control_count)
196 197 198
      end
    end

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
    context 'when builds are disabled' do
      let(:feature) { ProjectFeature::DISABLED }

      it 'users can not see internal pipelines' do
        get_pipeline_json

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

      context 'when pipeline is external' do
        let(:pipeline) { create(:ci_pipeline, source: :external, project: project) }

        it 'users can see the external pipeline' do
          get_pipeline_json

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['id']).to be(pipeline.id)
        end
      end
    end

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    context 'with triggered pipelines' do
      let_it_be(:project) { create(:project, :repository) }
      let_it_be(:source_project) { create(:project, :repository) }
      let_it_be(:target_project) { create(:project, :repository) }
      let_it_be(:root_pipeline) { create_pipeline(project) }
      let_it_be(:source_pipeline) { create_pipeline(source_project) }
      let_it_be(:source_of_source_pipeline) { create_pipeline(source_project) }
      let_it_be(:target_pipeline) { create_pipeline(target_project) }
      let_it_be(:target_of_target_pipeline) { create_pipeline(target_project) }

      before do
        create_link(source_of_source_pipeline, source_pipeline)
        create_link(source_pipeline, root_pipeline)
        create_link(root_pipeline, target_pipeline)
        create_link(target_pipeline, target_of_target_pipeline)
      end

      shared_examples 'not expanded' do
        let(:expected_stages) { be_nil }

        it 'does return base details' do
          get_pipeline_json(root_pipeline)

          expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
          expect(json_response['triggered']).to contain_exactly(
            include('id' => target_pipeline.id))
        end

        it 'does not expand triggered_by pipeline' do
          get_pipeline_json(root_pipeline)

          triggered_by = json_response['triggered_by']
          expect(triggered_by['triggered_by']).to be_nil
          expect(triggered_by['triggered']).to be_nil
          expect(triggered_by['details']['stages']).to expected_stages
        end

        it 'does not expand triggered pipelines' do
          get_pipeline_json(root_pipeline)

          first_triggered = json_response['triggered'].first
          expect(first_triggered['triggered_by']).to be_nil
          expect(first_triggered['triggered']).to be_nil
          expect(first_triggered['details']['stages']).to expected_stages
        end
      end

      shared_examples 'expanded' do
        it 'does return base details' do
          get_pipeline_json(root_pipeline)

          expect(json_response['triggered_by']).to include('id' => source_pipeline.id)
          expect(json_response['triggered']).to contain_exactly(
            include('id' => target_pipeline.id))
        end

        it 'does expand triggered_by pipeline' do
          get_pipeline_json(root_pipeline)

          triggered_by = json_response['triggered_by']
          expect(triggered_by['triggered_by']).to include(
            'id' => source_of_source_pipeline.id)
          expect(triggered_by['details']['stages']).not_to be_nil
        end

        it 'does not recursively expand triggered_by' do
          get_pipeline_json(root_pipeline)

          triggered_by = json_response['triggered_by']
          expect(triggered_by['triggered']).to be_nil
        end

        it 'does expand triggered pipelines' do
          get_pipeline_json(root_pipeline)

          first_triggered = json_response['triggered'].first
          expect(first_triggered['triggered']).to contain_exactly(
            include('id' => target_of_target_pipeline.id))
          expect(first_triggered['details']['stages']).not_to be_nil
        end

        it 'does not recursively expand triggered' do
          get_pipeline_json(root_pipeline)

          first_triggered = json_response['triggered'].first
          expect(first_triggered['triggered_by']).to be_nil
        end
      end

      context 'when it does have permission to read other projects' do
        before do
          source_project.add_developer(user)
          target_project.add_developer(user)
        end

        context 'when not-expanding any pipelines' do
          let(:expanded) { nil }

          it_behaves_like 'not expanded'
        end

        context 'when expanding non-existing pipeline' do
          let(:expanded) { [-1] }

          it_behaves_like 'not expanded'
        end

        context 'when expanding pipeline that is not directly expandable' do
          let(:expanded) { [source_of_source_pipeline.id, target_of_target_pipeline.id] }

          it_behaves_like 'not expanded'
        end

        context 'when expanding self' do
          let(:expanded) { [root_pipeline.id] }

          context 'it does not recursively expand pipelines' do
            it_behaves_like 'not expanded'
          end
        end

        context 'when expanding source and target pipeline' do
          let(:expanded) { [source_pipeline.id, target_pipeline.id] }

          it_behaves_like 'expanded'

          context 'when expand depth is limited to 1' do
            before do
              stub_const('TriggeredPipelineEntity::MAX_EXPAND_DEPTH', 1)
            end

            it_behaves_like 'not expanded' do
              # We expect that triggered/triggered_by is not expanded,
              # but we still return details.stages for that pipeline
              let(:expected_stages) { be_a(Array) }
            end
          end
        end

        context 'when expanding all' do
          let(:expanded) do
            [
              source_of_source_pipeline.id,
              source_pipeline.id,
              root_pipeline.id,
              target_pipeline.id,
              target_of_target_pipeline.id
            ]
          end

          it_behaves_like 'expanded'
        end
      end

      context 'when does not have permission to read other projects' do
        let(:expanded) { [source_pipeline.id, target_pipeline.id] }

        it_behaves_like 'not expanded'
      end

      def create_pipeline(project)
        create(:ci_empty_pipeline, project: project).tap do |pipeline|
          create(:ci_build, pipeline: pipeline, stage: 'test', name: 'rspec')
        end
      end

      def create_link(source_pipeline, pipeline)
        source_pipeline.sourced_pipelines.create!(
          source_job: source_pipeline.builds.all.sample,
          source_project: source_pipeline.project,
          project: pipeline.project,
          pipeline: pipeline
        )
      end

      def get_pipeline_json(pipeline)
        params = {
          namespace_id: pipeline.project.namespace,
          project_id: pipeline.project,
          id: pipeline,
          expanded: expanded
        }

        get :show, params: params.compact, format: :json
      end
    end

407
    def get_pipeline_json
B
blackst0ne 已提交
408
      get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json
409
    end
410 411 412 413

    def create_build(stage, stage_idx, name)
      create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
    end
414 415
  end

416
  describe 'GET stages.json' do
417 418
    let(:pipeline) { create(:ci_pipeline, project: project) }

419 420
    context 'when accessing existing stage' do
      before do
421
        create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build')
422
        create(:ci_build, pipeline: pipeline, stage: 'build')
423 424 425 426 427 428
      end

      context 'without retried' do
        before do
          get_stage('build')
        end
429

430 431 432 433 434 435
        it 'returns pipeline jobs without the retried builds' do
          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to match_response_schema('pipeline_stage')
          expect(json_response['latest_statuses'].length).to eq 1
          expect(json_response).not_to have_key('retried')
        end
436 437
      end

438 439 440 441 442 443 444 445 446 447 448
      context 'with retried' do
        before do
          get_stage('build', retried: true)
        end

        it 'returns pipelines jobs with the retried builds' do
          expect(response).to have_gitlab_http_status(:ok)
          expect(response).to match_response_schema('pipeline_stage')
          expect(json_response['latest_statuses'].length).to eq 1
          expect(json_response['retried'].length).to eq 1
        end
449 450 451 452 453 454 455 456
      end
    end

    context 'when accessing unknown stage' do
      before do
        get_stage('test')
      end

457
      it 'responds with not found' do
458
        expect(response).to have_gitlab_http_status(:not_found)
459
      end
460 461
    end

462
    def get_stage(name, params = {})
B
blackst0ne 已提交
463 464 465 466 467 468 469 470
      get :stage, params: {
**params.merge(
  namespace_id: project.namespace,
  project_id: project,
  id: pipeline.id,
  stage: name,
  format: :json)
}
471
    end
472
  end
S
Shinya Maeda 已提交
473

474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
  describe 'GET stages_ajax.json' do
    let(:pipeline) { create(:ci_pipeline, project: project) }

    context 'when accessing existing stage' do
      before do
        create(:ci_build, pipeline: pipeline, stage: 'build')

        get_stage_ajax('build')
      end

      it 'returns html source for stage dropdown' do
        expect(response).to have_gitlab_http_status(:ok)
        expect(response).to render_template('projects/pipelines/_stage')
        expect(json_response).to include('html')
      end
    end

    context 'when accessing unknown stage' do
      before do
        get_stage_ajax('test')
      end

      it 'responds with not found' do
        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    def get_stage_ajax(name)
B
blackst0ne 已提交
502 503 504 505 506 507
      get :stage_ajax, params: {
                         namespace_id: project.namespace,
                         project_id: project,
                         id: pipeline.id,
                         stage: name
                       },
508
                       format: :json
509 510 511
    end
  end

S
Shinya Maeda 已提交
512
  describe 'GET status.json' do
513 514
    let(:pipeline) { create(:ci_pipeline, project: project) }
    let(:status) { pipeline.detailed_status(double('user')) }
S
Shinya Maeda 已提交
515

516
    before do
B
blackst0ne 已提交
517 518 519 520 521
      get :status, params: {
                     namespace_id: project.namespace,
                     project_id: project,
                     id: pipeline.id
                   },
522 523
                   format: :json
    end
S
Shinya Maeda 已提交
524

525
    it 'return a detailed pipeline status in json' do
526
      expect(response).to have_gitlab_http_status(:ok)
527 528 529
      expect(json_response['text']).to eq status.text
      expect(json_response['label']).to eq status.label
      expect(json_response['icon']).to eq status.icon
530
      expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
S
Shinya Maeda 已提交
531 532
    end
  end
533 534 535 536 537 538

  describe 'POST retry.json' do
    let!(:pipeline) { create(:ci_pipeline, :failed, project: project) }
    let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }

    before do
B
blackst0ne 已提交
539 540 541 542 543
      post :retry, params: {
                     namespace_id: project.namespace,
                     project_id: project,
                     id: pipeline.id
                   },
544 545 546
                   format: :json
    end

547 548 549
    it 'retries a pipeline without returning any content' do
      expect(response).to have_gitlab_http_status(:no_content)
      expect(build.reload).to be_retried
K
Kamil Trzcinski 已提交
550 551 552
    end

    context 'when builds are disabled' do
553 554
      let(:feature) { ProjectFeature::DISABLED }

K
Kamil Trzcinski 已提交
555
      it 'fails to retry pipeline' do
556
        expect(response).to have_gitlab_http_status(:not_found)
K
Kamil Trzcinski 已提交
557
      end
558 559 560 561 562 563
    end
  end

  describe 'POST cancel.json' do
    let!(:pipeline) { create(:ci_pipeline, project: project) }
    let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
564

565
    before do
B
blackst0ne 已提交
566 567 568 569 570
      post :cancel, params: {
                      namespace_id: project.namespace,
                      project_id: project,
                      id: pipeline.id
                    },
571 572 573
                    format: :json
    end

574 575 576
    it 'cancels a pipeline without returning any content' do
      expect(response).to have_gitlab_http_status(:no_content)
      expect(pipeline.reload).to be_canceled
K
Kamil Trzcinski 已提交
577 578 579
    end

    context 'when builds are disabled' do
580 581
      let(:feature) { ProjectFeature::DISABLED }

K
Kamil Trzcinski 已提交
582
      it 'fails to retry pipeline' do
583
        expect(response).to have_gitlab_http_status(:not_found)
K
Kamil Trzcinski 已提交
584
      end
585 586
    end
  end
587

588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 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
  describe 'GET test_report.json' do
    subject(:get_test_report_json) do
      post :test_report, params: {
        namespace_id: project.namespace,
        project_id: project,
        id: pipeline.id
      },
      format: :json
    end

    context 'when feature is enabled' do
      before do
        stub_feature_flags(junit_pipeline_view: true)
      end

      context 'when pipeline does not have a test report' do
        let(:pipeline) { create(:ci_pipeline, project: project) }

        it 'renders an empty test report' do
          get_test_report_json

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['total_count']).to eq(0)
        end
      end

      context 'when pipeline has a test report' do
        let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }

        it 'renders the test report' do
          get_test_report_json

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['total_count']).to eq(4)
        end
      end

      context 'when pipeline has corrupt test reports' do
        let(:pipeline) { create(:ci_pipeline, project: project) }

        before do
          job = create(:ci_build, pipeline: pipeline)
          create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
        end

        it 'renders the test reports' do
          get_test_report_json

          expect(response).to have_gitlab_http_status(:ok)
          expect(json_response['status']).to eq('error_parsing_report')
        end
      end
    end

    context 'when feature is disabled' do
      let(:pipeline) { create(:ci_empty_pipeline, project: project) }

      before do
        stub_feature_flags(junit_pipeline_view: false)
      end

      it 'renders empty response' do
        get_test_report_json

        expect(response).to have_gitlab_http_status(:no_content)
        expect(response.body).to be_empty
      end
    end
  end

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 718 719 720 721
  describe 'GET latest' do
    let(:branch_main) { project.repository.branches[0] }
    let(:branch_secondary) { project.repository.branches[1] }

    let!(:pipeline_master) do
      create(:ci_pipeline,
             ref: branch_main.name,
             sha: branch_main.target,
             project: project)
    end

    let!(:pipeline_secondary) do
      create(:ci_pipeline,
             ref: branch_secondary.name,
             sha: branch_secondary.target,
             project: project)
    end

    before do
      project.change_head(branch_main.name)
      project.reload_default_branch
    end

    context 'no ref provided' do
      it 'shows latest pipeline for the default project branch' do
        get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: nil }

        expect(response).to have_gitlab_http_status(200)
        expect(assigns(:pipeline)).to have_attributes(id: pipeline_master.id)
      end
    end

    context 'ref provided' do
      before do
        create(:ci_pipeline, ref: 'master', project: project)
      end

      it 'shows the latest pipeline for the provided ref' do
        get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name }

        expect(response).to have_gitlab_http_status(200)
        expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id)
      end

      context 'newer pipeline exists for older sha' do
        before do
          create(:ci_pipeline, ref: branch_secondary.name, sha: project.commit(branch_secondary.name).parent, project: project)
        end

        it 'shows the provided ref with the last sha/pipeline combo' do
          get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name }

          expect(response).to have_gitlab_http_status(200)
          expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id)
        end
      end
    end

    it 'renders a 404 if no pipeline is found for the ref' do
      get :show, params: { namespace_id: project.namespace, project_id: project, ref: 'no-branch' }

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