merge_requests_controller_spec.rb 37.5 KB
Newer Older
1 2
require 'spec_helper'

3
describe Projects::MergeRequestsController do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
4
  let(:project) { create(:project) }
5
  let(:user)    { create(:user) }
6
  let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
7
  let(:merge_request_with_conflicts) do
8
    create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
9 10 11
      mr.mark_as_unmergeable
    end
  end
12 13 14

  before do
    sign_in(user)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
15
    project.team << [user, :master]
16 17
  end

Sean McGivern's avatar
Sean McGivern committed
18
  describe 'GET new' do
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
    context 'merge request that removes a submodule' do
      render_views

      let(:fork_project) { create(:forked_project_with_submodules) }

      before do
        fork_project.team << [user, :master]
      end

      it 'renders it' do
        get :new,
            namespace_id: fork_project.namespace.to_param,
            project_id: fork_project.to_param,
            merge_request: {
              source_branch: 'remove-submodule',
              target_branch: 'master'
            }

        expect(response).to be_success
      end
    end
  end

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
  describe 'POST #create' do
    def create_merge_request(overrides = {})
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project.to_param,
        merge_request: {
          title: 'Test',
          source_branch: 'feature_conflict',
          target_branch: 'master',
          author: user
        }.merge(overrides)
      }

      post :create, params
    end

    context 'the approvals_before_merge param' do
      before { project.update_attributes(approvals_before_merge: 2) }
      let(:created_merge_request) { assigns(:merge_request) }

      context 'when it is less than the one in the target project' do
        before { create_merge_request(approvals_before_merge: 1) }

        it 'sets the param to nil' do
          expect(created_merge_request.approvals_before_merge).to eq(nil)
        end

        it 'creates the merge request' do
          expect(created_merge_request).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
        end
      end

      context 'when it is equal to the one in the target project' do
        before { create_merge_request(approvals_before_merge: 2) }

        it 'sets the param to nil' do
          expect(created_merge_request.approvals_before_merge).to eq(nil)
        end

        it 'creates the merge request' do
          expect(created_merge_request).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
        end
      end

      context 'when it is greater than the one in the target project' do
        before { create_merge_request(approvals_before_merge: 3) }

        it 'saves the param in the merge request' do
          expect(created_merge_request.approvals_before_merge).to eq(3)
        end

        it 'creates the merge request' do
          expect(created_merge_request).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
        end
      end
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

      context 'when the target project is a fork of a deleted project' do
        before do
          original_project = create(:empty_project)
          project.update_attributes(forked_from_project: original_project, approvals_before_merge: 4)
          original_project.update_attributes(pending_delete: true)

          create_merge_request(approvals_before_merge: 3)
        end

        it 'uses the default from the target project' do
          expect(created_merge_request.approvals_before_merge).to eq(nil)
        end

        it 'creates the merge request' do
          expect(created_merge_request).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: created_merge_request.iid, project_id: project.to_param))
        end
      end
119 120 121 122 123 124
    end

    context 'when the merge request is invalid' do
      it 'shows the #new form' do
        expect(create_merge_request(title: nil)).to render_template(:new)
      end
125 126 127
    end
  end

128 129 130 131 132 133 134 135
  shared_examples "loads labels" do |action|
    it "loads labels into the @labels variable" do
      get action,
          namespace_id: project.namespace.to_param,
          project_id: project.to_param,
          id: merge_request.iid,
          format: 'html'
      expect(assigns(:labels)).not_to be_nil
136 137 138
    end
  end

Sean McGivern's avatar
Sean McGivern committed
139
  describe "GET show" do
140
    shared_examples "export merge as" do |format|
141
      it "does generally work" do
142 143 144 145 146
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
            format: format)
147 148 149 150

        expect(response).to be_success
      end

151 152
      it_behaves_like "loads labels", :show

153
      it "generates it" do
154
        expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
155

156 157 158 159 160
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
            format: format)
161 162
      end

163
      it "renders it" do
164 165 166 167 168
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
            format: format)
169

170
        expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
171 172
      end

173
      it "does not escape Html" do
Jeroen van Baarsen's avatar
Jeroen van Baarsen committed
174 175
        allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
          and_return('HTML entities &<>" ')
176

177 178 179 180 181
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
            format: format)
182

183 184 185 186
        expect(response.body).not_to include('&amp;')
        expect(response.body).not_to include('&gt;')
        expect(response.body).not_to include('&lt;')
        expect(response.body).not_to include('&quot;')
187 188 189 190
      end
    end

    describe "as diff" do
191
      it "triggers workhorse to serve the request" do
192 193 194 195
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
196
            format: :diff)
197

Douwe Maan's avatar
Douwe Maan committed
198
        expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
199 200 201 202
      end
    end

    describe "as patch" do
203
      it 'triggers workhorse to serve the request' do
204 205 206 207
        get(:show,
            namespace_id: project.namespace.to_param,
            project_id: project.to_param,
            id: merge_request.iid,
208
            format: :patch)
209

Douwe Maan's avatar
Douwe Maan committed
210
        expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
211 212 213
      end
    end
  end
214

Sean McGivern's avatar
Sean McGivern committed
215
  describe 'GET index' do
216 217 218 219 220 221 222 223 224
    def get_merge_requests
      get :index,
          namespace_id: project.namespace.to_param,
          project_id: project.to_param,
          state: 'opened'
    end

    context 'when filtering by opened state' do
      context 'with opened merge requests' do
225
        it 'lists those merge requests' do
226 227 228 229 230 231 232 233 234 235 236 237
          get_merge_requests

          expect(assigns(:merge_requests)).to include(merge_request)
        end
      end

      context 'with reopened merge requests' do
        before do
          merge_request.close!
          merge_request.reopen!
        end

238
        it 'lists those merge requests' do
239 240 241 242 243 244 245 246
          get_merge_requests

          expect(assigns(:merge_requests)).to include(merge_request)
        end
      end
    end
  end

Sean McGivern's avatar
Sean McGivern committed
247
  describe 'PUT update' do
248 249 250 251 252 253 254 255
    def update_merge_request(params = {})
      post :update,
           namespace_id: project.namespace.to_param,
           project_id: project.to_param,
           id: merge_request.iid,
           merge_request: params
    end

256
    context 'there is no source project' do
257 258 259
      let(:project)       { create(:project) }
      let(:fork_project)  { create(:forked_project_with_submodules) }
      let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
260 261 262 263 264

      before do
        fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
        fork_project.save
        merge_request.reload
265
        fork_project.destroy
266 267 268
      end

      it 'closes MR without errors' do
269
        update_merge_request(state_event: 'close')
270 271 272 273

        expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
        expect(merge_request.reload.closed?).to be_truthy
      end
274

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
275
      it 'allows editing of a closed merge request' do
276 277 278 279 280 281 282 283 284 285 286 287 288 289
        merge_request.close!

        put :update,
            namespace_id: project.namespace.path,
            project_id: project.path,
            id: merge_request.iid,
            merge_request: {
              title: 'New title'
            }

        expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
        expect(merge_request.reload.title).to eq 'New title'
      end

Katarzyna Kobierska's avatar
Katarzyna Kobierska committed
290
      it 'does not allow to update target branch closed merge request' do
291 292 293 294 295 296 297 298 299 300 301 302
        merge_request.close!

        put :update,
            namespace_id: project.namespace.path,
            project_id: project.path,
            id: merge_request.iid,
            merge_request: {
              target_branch: 'new_branch'
            }

        expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
      end
303
    end
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

    context 'the approvals_before_merge param' do
      before { project.update_attributes(approvals_before_merge: 2) }

      context 'when it is less than the one in the target project' do
        before { update_merge_request(approvals_before_merge: 1) }

        it 'sets the param to nil' do
          expect(merge_request.reload.approvals_before_merge).to eq(nil)
        end

        it 'updates the merge request' do
          expect(merge_request.reload).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
        end
      end

      context 'when it is equal to the one in the target project' do
        before { update_merge_request(approvals_before_merge: 2) }

        it 'sets the param to nil' do
          expect(merge_request.reload.approvals_before_merge).to eq(nil)
        end

        it 'updates the merge request' do
          expect(merge_request.reload).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
        end
      end

      context 'when it is greater than the one in the target project' do
        before { update_merge_request(approvals_before_merge: 3) }

        it 'saves the param in the merge request' do
          expect(merge_request.reload.approvals_before_merge).to eq(3)
        end

        it 'updates the merge request' do
          expect(merge_request.reload).to be_valid
          expect(response).to redirect_to(namespace_project_merge_request_path(id: merge_request.iid, project_id: project.to_param))
        end
      end
    end
347 348
  end

Sean McGivern's avatar
Sean McGivern committed
349
  describe 'POST merge' do
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
    let(:base_params) do
      {
        namespace_id: project.namespace.path,
        project_id: project.path,
        id: merge_request.iid,
        format: 'raw'
      }
    end

    context 'when the user does not have access' do
      before do
        project.team.truncate
        project.team << [user, :reporter]
        post :merge, base_params
      end

      it 'returns not found' do
        expect(response).to be_not_found
      end
    end

    context 'when the merge request is not mergeable' do
      before do
        merge_request.update_attributes(title: "WIP: #{merge_request.title}")

        post :merge, base_params
      end

      it 'returns :failed' do
        expect(assigns(:status)).to eq(:failed)
      end
    end

    context 'when the sha parameter does not match the source SHA' do
      before { post :merge, base_params.merge(sha: 'foo') }

      it 'returns :sha_mismatch' do
        expect(assigns(:status)).to eq(:sha_mismatch)
      end
    end

    context 'when the sha parameter matches the source SHA' do
      def merge_with_sha
393
        post :merge, base_params.merge(sha: merge_request.diff_head_sha)
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
      end

      it 'returns :success' do
        merge_with_sha

        expect(assigns(:status)).to eq(:success)
      end

      it 'starts the merge immediately' do
        expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)

        merge_with_sha
      end

      context 'when merge_when_build_succeeds is passed' do
        def merge_when_build_succeeds
410
          post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_build_succeeds: '1')
411 412 413
        end

        before do
414
          create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
415 416 417 418 419 420 421 422 423 424 425
        end

        it 'returns :merge_when_build_succeeds' do
          merge_when_build_succeeds

          expect(assigns(:status)).to eq(:merge_when_build_succeeds)
        end

        it 'sets the MR to merge when the build succeeds' do
          service = double(:merge_when_build_succeeds_service)

426 427 428
          expect(MergeRequests::MergeWhenPipelineSucceedsService)
            .to receive(:new).with(project, anything, anything)
            .and_return(service)
429 430 431 432
          expect(service).to receive(:execute).with(merge_request)

          merge_when_build_succeeds
        end
433 434 435 436 437 438 439 440 441 442 443 444

        context 'when project.only_allow_merge_if_build_succeeds? is true' do
          before do
            project.update_column(:only_allow_merge_if_build_succeeds, true)
          end

          it 'returns :merge_when_build_succeeds' do
            merge_when_build_succeeds

            expect(assigns(:status)).to eq(:merge_when_build_succeeds)
          end
        end
445
      end
446

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
      describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
        let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }

        context 'when enabled' do
          before do
            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
          end

          context 'with unresolved discussion' do
            before do
              expect(merge_request).not_to be_discussions_resolved
            end

            it 'returns :failed' do
              merge_with_sha

              expect(assigns(:status)).to eq(:failed)
            end
          end
466

467 468 469 470 471
          context 'with all discussions resolved' do
            before do
              merge_request.discussions.each { |d| d.resolve!(user) }
              expect(merge_request).to be_discussions_resolved
            end
472

473 474
            it 'returns :success' do
              merge_with_sha
475

476 477
              expect(assigns(:status)).to eq(:success)
            end
478 479 480
          end
        end

481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
        context 'when disabled' do
          before do
            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
          end

          context 'with unresolved discussion' do
            before do
              expect(merge_request).not_to be_discussions_resolved
            end

            it 'returns :success' do
              merge_with_sha

              expect(assigns(:status)).to eq(:success)
            end
          end

          context 'with all discussions resolved' do
            before do
              merge_request.discussions.each { |d| d.resolve!(user) }
              expect(merge_request).to be_discussions_resolved
            end
503

504 505
            it 'returns :success' do
              merge_with_sha
506

507 508
              expect(assigns(:status)).to eq(:success)
            end
509 510 511
          end
        end
      end
512 513 514
    end
  end

Sean McGivern's avatar
Sean McGivern committed
515
  describe "DELETE destroy" do
516
    it "denies access to users unless they're admin or project owner" do
517 518
      delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid

519
      expect(response).to have_http_status(404)
520 521
    end

522 523 524 525 526 527
    context "when the user is owner" do
      let(:owner)     { create(:user) }
      let(:namespace) { create(:namespace, owner: owner) }
      let(:project)   { create(:project, namespace: namespace) }

      before { sign_in owner }
528

529
      it "deletes the merge request" do
530 531
        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid

532
        expect(response).to have_http_status(302)
533
        expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
534
      end
535 536 537 538 539 540

      it 'delegates the update of the todos count cache to TodoService' do
        expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once

        delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
      end
541 542 543
    end
  end

544
  describe 'GET diffs' do
545 546 547 548 549 550 551 552
    def go(extra_params = {})
      params = {
        namespace_id: project.namespace.to_param,
        project_id: project.to_param,
        id: merge_request.iid
      }

      get :diffs, params.merge(extra_params)
553 554
    end

555 556
    it_behaves_like "loads labels", :diffs

557 558 559
    context 'with default params' do
      context 'as html' do
        before { go(format: 'html') }
560

561 562 563
        it 'renders the diff template' do
          expect(response).to render_template('diffs')
        end
564 565
      end

566 567
      context 'as json' do
        before { go(format: 'json') }
568

569 570 571 572
        it 'renders the diffs template to a string' do
          expect(response).to render_template('projects/merge_requests/show/_diffs')
          expect(JSON.parse(response.body)).to have_key('html')
        end
573 574
      end

575 576
      context 'with forked projects with submodules' do
        render_views
577

578 579 580
        let(:project) { create(:project) }
        let(:fork_project) { create(:forked_project_with_submodules) }
        let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
581

582 583 584 585 586 587
        before do
          fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
          fork_project.save
          merge_request.reload
          go(format: 'json')
        end
588

589 590 591 592
        it 'renders' do
          expect(response).to be_success
          expect(response.body).to have_content('Subproject commit')
        end
593 594 595
      end
    end

596 597 598
    context 'with ignore_whitespace_change' do
      context 'as html' do
        before { go(format: 'html', w: 1) }
599

600 601 602 603 604 605 606
        it 'renders the diff template' do
          expect(response).to render_template('diffs')
        end
      end

      context 'as json' do
        before { go(format: 'json', w: 1) }
607

608 609 610 611
        it 'renders the diffs template to a string' do
          expect(response).to render_template('projects/merge_requests/show/_diffs')
          expect(JSON.parse(response.body)).to have_key('html')
        end
612 613
      end
    end
614

615 616
    context 'with view' do
      before { go(view: 'parallel') }
617

618 619
      it 'saves the preferred diff view in a cookie' do
        expect(response.cookies['diff_view']).to eq('parallel')
620 621 622 623
      end
    end
  end

Sean McGivern's avatar
Sean McGivern committed
624
  describe 'GET diff_for_path' do
625
    def diff_for_path(extra_params = {})
626 627
      params = {
        namespace_id: project.namespace.to_param,
628
        project_id: project.to_param
629 630
      }

631
      get :diff_for_path, params.merge(extra_params)
632 633
    end

634 635
    context 'when an ID param is passed' do
      let(:existing_path) { 'files/ruby/popen.rb' }
636

637 638 639 640
      context 'when the merge request exists' do
        context 'when the user can view the merge request' do
          context 'when the path exists in the diff' do
            it 'enables diff notes' do
641
              diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
642 643 644 645 646 647 648

              expect(assigns(:diff_notes_disabled)).to be_falsey
              expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest',
                                                      noteable_id: merge_request.id)
            end

            it 'only renders the diffs for the path given' do
649 650 651
              expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
                expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
                meth.call(diffs)
652 653
              end

654
              diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
655 656 657 658
            end
          end

          context 'when the path does not exist in the diff' do
659
            before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') }
660 661 662 663 664 665 666 667 668 669

            it 'returns a 404' do
              expect(response).to have_http_status(404)
            end
          end
        end

        context 'when the user cannot view the merge request' do
          before do
            project.team.truncate
670
            diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
671 672 673 674 675 676 677 678 679
          end

          it 'returns a 404' do
            expect(response).to have_http_status(404)
          end
        end
      end

      context 'when the merge request does not exist' do
680
        before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) }
681 682 683 684 685 686 687 688 689 690 691

        it 'returns a 404' do
          expect(response).to have_http_status(404)
        end
      end

      context 'when the merge request belongs to a different project' do
        let(:other_project) { create(:empty_project) }

        before do
          other_project.team << [user, :master]
692
          diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
693 694 695 696 697 698 699 700 701 702 703 704 705
        end

        it 'returns a 404' do
          expect(response).to have_http_status(404)
        end
      end
    end

    context 'when source and target params are passed' do
      let(:existing_path) { 'files/ruby/feature.rb' }

      context 'when both branches are in the same project' do
        it 'disables diff notes' do
706
          diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
707 708 709 710 711

          expect(assigns(:diff_notes_disabled)).to be_truthy
        end

        it 'only renders the diffs for the path given' do
712 713 714
          expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
            expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
            meth.call(diffs)
715 716
          end

717
          diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
718 719 720 721 722 723 724 725 726 727
        end
      end

      context 'when the source branch is in a different project to the target' do
        let(:other_project) { create(:project) }

        before { other_project.team << [user, :master] }

        context 'when the path exists in the diff' do
          it 'disables diff notes' do
728
            diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
729 730 731 732 733

            expect(assigns(:diff_notes_disabled)).to be_truthy
          end

          it 'only renders the diffs for the path given' do
734 735 736
            expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
              expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
              meth.call(diffs)
737 738
            end

739
            diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
740 741 742 743
          end
        end

        context 'when the path does not exist in the diff' do
744
          before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) }
745 746 747 748 749 750

          it 'returns a 404' do
            expect(response).to have_http_status(404)
          end
        end
      end
751 752 753
    end
  end

754 755
  describe 'GET commits' do
    def go(format: 'html')
756 757 758 759 760
      get :commits,
          namespace_id: project.namespace.to_param,
          project_id: project.to_param,
          id: merge_request.iid,
          format: format
761 762
    end

763 764
    it_behaves_like "loads labels", :commits

765 766 767 768 769 770 771 772 773 774 775 776 777 778 779
    context 'as html' do
      it 'renders the show template' do
        go

        expect(response).to render_template('show')
      end
    end

    context 'as json' do
      it 'renders the commits template to a string' do
        go format: 'json'

        expect(response).to render_template('projects/merge_requests/show/_commits')
        expect(JSON.parse(response.body)).to have_key('html')
      end
780 781
    end
  end
782

783 784 785 786
  describe 'GET pipelines' do
    it_behaves_like "loads labels", :pipelines
  end

787
  describe 'GET conflicts' do
788 789 790
    let(:json_response) { JSON.parse(response.body) }

    context 'when the conflicts cannot be resolved in the UI' do
791
      before do
792
        allow_any_instance_of(Gitlab::Conflict::Parser).
793
          to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)
794

795 796 797 798 799 800 801
        get :conflicts,
            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
            project_id: merge_request_with_conflicts.project.to_param,
            id: merge_request_with_conflicts.iid,
            format: 'json'
      end

802 803 804 805 806
      it 'returns a 200 status code' do
        expect(response).to have_http_status(:ok)
      end

      it 'returns JSON with a message' do
807
        expect(json_response.keys).to contain_exactly('message', 'type')
808 809 810 811 812 813 814 815 816 817 818
      end
    end

    context 'with valid conflicts' do
      before do
        get :conflicts,
            namespace_id: merge_request_with_conflicts.project.namespace.to_param,
            project_id: merge_request_with_conflicts.project.to_param,
            id: merge_request_with_conflicts.iid,
            format: 'json'
      end
819

Sean McGivern's avatar
Sean McGivern committed
820 821 822 823
      it 'matches the schema' do
        expect(response).to match_response_schema('conflicts')
      end

824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
      it 'includes meta info about the MR' do
        expect(json_response['commit_message']).to include('Merge branch')
        expect(json_response['commit_sha']).to match(/\h{40}/)
        expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
        expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
      end

      it 'includes each file that has conflicts' do
        filenames = json_response['files'].map { |file| file['new_path'] }

        expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
      end

      it 'splits files into sections with lines' do
        json_response['files'].each do |file|
          file['sections'].each do |section|
            expect(section).to include('conflict', 'lines')

            section['lines'].each do |line|
              if section['conflict']
                expect(line['type']).to be_in(['old', 'new'])
                expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
              else
                if line['type'].nil?
                  expect(line['old_line']).not_to eq(nil)
                  expect(line['new_line']).not_to eq(nil)
                else
                  expect(line['type']).to eq('match')
                  expect(line['old_line']).to eq(nil)
                  expect(line['new_line']).to eq(nil)
                end
              end
            end
          end
        end
      end

      it 'has unique section IDs across files' do
        section_ids = json_response['files'].flat_map do |file|
          file['sections'].map { |section| section['id'] }.compact
        end

        expect(section_ids.uniq).to eq(section_ids)
      end
    end
  end
870

871 872 873 874 875 876 877 878 879 880 881 882 883 884
  context 'POST remove_wip' do
    it 'removes the wip status' do
      merge_request.title = merge_request.wip_title
      merge_request.save

      post :remove_wip,
           namespace_id: merge_request.project.namespace.to_param,
           project_id: merge_request.project.to_param,
           id: merge_request.iid

      expect(merge_request.reload.title).to eq(merge_request.wipless_title)
    end
  end

885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
  describe 'GET conflict_for_path' do
    let(:json_response) { JSON.parse(response.body) }

    def conflict_for_path(path)
      get :conflict_for_path,
          namespace_id: merge_request_with_conflicts.project.namespace.to_param,
          project_id: merge_request_with_conflicts.project.to_param,
          id: merge_request_with_conflicts.iid,
          old_path: path,
          new_path: path,
          format: 'json'
    end

    context 'when the conflicts cannot be resolved in the UI' do
      before do
        allow_any_instance_of(Gitlab::Conflict::Parser).
          to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile)

        conflict_for_path('files/ruby/regex.rb')
      end

      it 'returns a 404 status code' do
        expect(response).to have_http_status(:not_found)
      end
    end

    context 'when the file does not exist cannot be resolved in the UI' do
      before { conflict_for_path('files/ruby/regexp.rb') }

      it 'returns a 404 status code' do
        expect(response).to have_http_status(:not_found)
      end
    end

    context 'with an existing file' do
      let(:path) { 'files/ruby/regex.rb' }

      before { conflict_for_path(path) }

      it 'returns a 200 status code' do
        expect(response).to have_http_status(:ok)
      end

      it 'returns the file in JSON format' do
        content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content

        expect(json_response).to include('old_path' => path,
                                         'new_path' => path,
                                         'blob_icon' => 'file-text-o',
                                         'blob_path' => a_string_ending_with(path),
935
                                         'blob_ace_mode' => 'ruby',
936 937
                                         'content' => content)
      end
938 939 940
    end
  end

941
  context 'POST resolve_conflicts' do
942 943 944
    let(:json_response) { JSON.parse(response.body) }
    let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }

945
    def resolve_conflicts(files)
946 947 948 949 950
      post :resolve_conflicts,
           namespace_id: merge_request_with_conflicts.project.namespace.to_param,
           project_id: merge_request_with_conflicts.project.to_param,
           id: merge_request_with_conflicts.iid,
           format: 'json',
951
           files: files,
952
           commit_message: 'Commit message'
953 954 955 956
    end

    context 'with valid params' do
      before do
957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975
        resolved_files = [
          {
            'new_path' => 'files/ruby/popen.rb',
            'old_path' => 'files/ruby/popen.rb',
            'sections' => {
              '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
            }
          }, {
            'new_path' => 'files/ruby/regex.rb',
            'old_path' => 'files/ruby/regex.rb',
            'sections' => {
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
            }
          }
        ]

        resolve_conflicts(resolved_files)
976 977 978
      end

      it 'creates a new commit on the branch' do
979
        expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
980
        expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
981 982
      end

983
      it 'returns an OK response' do
984
        expect(response).to have_http_status(:ok)
985 986
      end
    end
987 988 989

    context 'when sections are missing' do
      before do
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
        resolved_files = [
          {
            'new_path' => 'files/ruby/popen.rb',
            'old_path' => 'files/ruby/popen.rb',
            'sections' => {
              '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head'
            }
          }, {
            'new_path' => 'files/ruby/regex.rb',
            'old_path' => 'files/ruby/regex.rb',
            'sections' => {
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head'
            }
          }
        ]

        resolve_conflicts(resolved_files)
1007 1008 1009 1010 1011 1012 1013
      end

      it 'returns a 400 error' do
        expect(response).to have_http_status(:bad_request)
      end

      it 'has a message with the name of the first missing section' do
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078
        expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21')
      end

      it 'does not create a new commit' do
        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
      end
    end

    context 'when files are missing' do
      before do
        resolved_files = [
          {
            'new_path' => 'files/ruby/regex.rb',
            'old_path' => 'files/ruby/regex.rb',
            'sections' => {
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
            }
          }
        ]

        resolve_conflicts(resolved_files)
      end

      it 'returns a 400 error' do
        expect(response).to have_http_status(:bad_request)
      end

      it 'has a message with the name of the missing file' do
        expect(json_response['message']).to include('files/ruby/popen.rb')
      end

      it 'does not create a new commit' do
        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
      end
    end

    context 'when a file has identical content to the conflict' do
      before do
        resolved_files = [
          {
            'new_path' => 'files/ruby/popen.rb',
            'old_path' => 'files/ruby/popen.rb',
            'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content
          }, {
            'new_path' => 'files/ruby/regex.rb',
            'old_path' => 'files/ruby/regex.rb',
            'sections' => {
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
              '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
            }
          }
        ]

        resolve_conflicts(resolved_files)
      end

      it 'returns a 400 error' do
        expect(response).to have_http_status(:bad_request)
      end

      it 'has a message with the path of the problem file' do
        expect(json_response['message']).to include('files/ruby/popen.rb')
1079 1080 1081 1082 1083 1084
      end

      it 'does not create a new commit' do
        expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
      end
    end
1085
  end
1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115

  describe 'POST assign_related_issues' do
    let(:issue1) { create(:issue, project: project) }
    let(:issue2) { create(:issue, project: project) }

    def post_assign_issues
      merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
                            author: user,
                            source_branch: 'feature',
                            target_branch: 'master')

      post :assign_related_issues,
           namespace_id: project.namespace.to_param,
           project_id: project.to_param,
           id: merge_request.iid
    end

    it 'shows a flash message on success' do
      post_assign_issues

      expect(flash[:notice]).to eq '2 issues have been assigned to you'
    end

    it 'correctly pluralizes flash message on success' do
      issue2.update!(assignee: user)

      post_assign_issues

      expect(flash[:notice]).to eq '1 issue has been assigned to you'
    end
1116 1117 1118 1119

    it 'calls MergeRequests::AssignIssuesService' do
      expect(MergeRequests::AssignIssuesService).to receive(:new).
        with(project, user, merge_request: merge_request).
1120
        and_return(double(execute: { count: 1 }))
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132

      post_assign_issues
    end

    it 'is skipped when not signed in' do
      project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
      sign_out(:user)

      expect(MergeRequests::AssignIssuesService).not_to receive(:new)

      post_assign_issues
    end
1133
  end
1134 1135

  describe 'GET ci_environments_status' do
1136
    context 'the environment is from a forked project' do
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
      let!(:forked)       { create(:project) }
      let!(:environment)  { create(:environment, project: forked) }
      let!(:deployment)   { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
      let(:json_response) { JSON.parse(response.body) }
      let(:admin)         { create(:admin) }

      let(:merge_request) do
        create(:forked_project_link, forked_to_project: forked,
                                     forked_from_project: project)

        create(:merge_request, source_project: forked, target_project: project)
      end

      before do
        forked.team << [user, :master]

        get :ci_environments_status,
          namespace_id: merge_request.project.namespace.to_param,
          project_id: merge_request.project.to_param,
          id: merge_request.iid, format: 'json'
      end

      it 'links to the environment on that project' do
        expect(json_response.first['url']).to match /#{forked.path_with_namespace}/
      end
    end
  end
1164
end