create_service_spec.rb 18.9 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require 'spec_helper'

5
describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
6 7
  include ProjectForksHelper

8
  let(:project) { create(:project, :repository) }
9
  let(:user) { create(:user) }
10
  let(:assignee) { create(:user) }
11

12
  describe '#execute' do
13 14 15
    context 'valid params' do
      let(:opts) do
        {
16 17
          title: 'Awesome merge_request',
          description: 'please fix',
18
          source_branch: 'feature',
19 20
          target_branch: 'master',
          force_remove_source_branch: '1'
21
        }
22
      end
23

24
      let(:service) { described_class.new(project, user, opts) }
25
      let(:merge_request) { service.execute }
26 27

      before do
28
        project.add_maintainer(user)
29
        project.add_developer(assignee)
30
        allow(service).to receive(:execute_hooks)
31 32
      end

33
      it 'creates an MR' do
34
        expect(merge_request).to be_valid
A
Adam Pahlevi 已提交
35
        expect(merge_request.work_in_progress?).to be(false)
36
        expect(merge_request.title).to eq('Awesome merge_request')
37
        expect(merge_request.assignees).to be_empty
38
        expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
39
      end
40

41
      it 'executes hooks with default action' do
42 43 44
        expect(service).to have_received(:execute_hooks).with(merge_request)
      end

45
      it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do
46 47
        expect { service.execute }
          .to change { project.open_merge_requests_count }.from(0).to(1)
48
      end
49

50
      it 'does not creates todos' do
51 52
        attributes = {
          project: project,
53 54
          target_id: merge_request.id,
          target_type: merge_request.class.name
55 56
        }

57
        expect(Todo.where(attributes).count).to be_zero
58 59
      end

60
      it 'creates exactly 1 create MR event', :sidekiq_might_not_need_inline do
61 62
        attributes = {
          action: Event::CREATED,
63 64
          target_id: merge_request.id,
          target_type: merge_request.class.name
65 66 67 68 69
        }

        expect(Event.where(attributes).count).to eq(1)
      end

A
Adam Pahlevi 已提交
70 71 72 73 74 75 76 77
      describe 'when marked with /wip' do
        context 'in title and in description' do
          let(:opts) do
            {
              title: 'WIP: Awesome merge_request',
              description: "well this is not done yet\n/wip",
              source_branch: 'feature',
              target_branch: 'master',
78
              assignees: [assignee]
A
Adam Pahlevi 已提交
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
            }
          end

          it 'sets MR to WIP' do
            expect(merge_request.work_in_progress?).to be(true)
          end
        end

        context 'in description only' do
          let(:opts) do
            {
              title: 'Awesome merge_request',
              description: "well this is not done yet\n/wip",
              source_branch: 'feature',
              target_branch: 'master',
94
              assignees: [assignee]
A
Adam Pahlevi 已提交
95 96 97 98 99 100 101 102 103
            }
          end

          it 'sets MR to WIP' do
            expect(merge_request.work_in_progress?).to be(true)
          end
        end
      end

104 105 106 107 108 109 110
      context 'when merge request is assigned to someone' do
        let(:opts) do
          {
            title: 'Awesome merge_request',
            description: 'please fix',
            source_branch: 'feature',
            target_branch: 'master',
111
            assignees: [assignee]
112 113 114
          }
        end

115
        it { expect(merge_request.assignees).to eq([assignee]) }
116

117
        it 'creates a todo for new assignee' do
118 119 120 121
          attributes = {
            project: project,
            author: user,
            user: assignee,
122 123
            target_id: merge_request.id,
            target_type: merge_request.class.name,
124
            action: Todo::ASSIGNED,
125 126 127
            state: :pending
          }

128
          expect(Todo.where(attributes).count).to eq 1
129 130
        end
      end
131 132

      context 'when head pipelines already exist for merge request source branch' do
133 134 135
        let(:shas) { project.repository.commits(opts[:source_branch], limit: 2).map(&:id) }
        let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) }
        let!(:pipeline_2) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[0]) }
136 137 138
        let!(:pipeline_3) { create(:ci_pipeline, project: project, ref: "other_branch", project_id: project.id) }

        before do
139
          # rubocop: disable DestroyAll
140 141 142
          project.merge_requests
            .where(source_branch: opts[:source_branch], target_branch: opts[:target_branch])
            .destroy_all
143
          # rubocop: enable DestroyAll
144 145 146 147 148
        end

        it 'sets head pipeline' do
          merge_request = service.execute

149
          expect(merge_request.reload.head_pipeline).to eq(pipeline_2)
150 151 152
          expect(merge_request).to be_persisted
        end

153 154 155
        context 'when the new pipeline is associated with an old sha' do
          let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[0]) }
          let!(:pipeline_2) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) }
156

157
          it 'sets an old pipeline with associated with the latest sha as the head pipeline' do
158 159
            merge_request = service.execute

160 161 162 163 164 165 166 167 168 169 170 171 172
            expect(merge_request.reload.head_pipeline).to eq(pipeline_1)
            expect(merge_request).to be_persisted
          end
        end

        context 'when there are no pipelines with the diff head sha' do
          let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) }
          let!(:pipeline_2) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: shas[1]) }

          it 'does not set the head pipeline' do
            merge_request = service.execute

            expect(merge_request.reload.head_pipeline).to be_nil
173 174 175 176
            expect(merge_request).to be_persisted
          end
        end
      end
S
Shinya Maeda 已提交
177

178
      describe 'Pipelines for merge requests' do
S
Shinya Maeda 已提交
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
        before do
          stub_ci_pipeline_yaml_file(YAML.dump(config))
        end

        context "when .gitlab-ci.yml has merge_requests keywords" do
          let(:config) do
            {
              test: {
                stage: 'test',
                script: 'echo',
                only: ['merge_requests']
              }
            }
          end

194
          it 'creates a detached merge request pipeline and sets it as a head pipeline' do
S
Shinya Maeda 已提交
195 196 197
            expect(merge_request).to be_persisted

            merge_request.reload
198
            expect(merge_request.pipelines_for_merge_request.count).to eq(1)
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
            expect(merge_request.actual_head_pipeline).to be_detached_merge_request_pipeline
          end

          context 'when merge request is submitted from forked project' do
            let(:target_project) { fork_project(project, nil, repository: true) }

            let(:opts) do
              {
                title: 'Awesome merge_request',
                source_branch: 'feature',
                target_branch: 'master',
                target_project_id: target_project.id
              }
            end

            before do
              target_project.add_developer(assignee)
              target_project.add_maintainer(user)
            end

219
            it 'create legacy detached merge request pipeline for fork merge request', :sidekiq_might_not_need_inline do
220 221 222 223 224 225 226 227 228 229 230 231 232 233
              expect(merge_request.actual_head_pipeline)
                .to be_legacy_detached_merge_request_pipeline
            end
          end

          context 'when ci_use_merge_request_ref feature flag is false' do
            before do
              stub_feature_flags(ci_use_merge_request_ref: false)
            end

            it 'create legacy detached merge request pipeline for non-fork merge request' do
              expect(merge_request.actual_head_pipeline)
                .to be_legacy_detached_merge_request_pipeline
            end
S
Shinya Maeda 已提交
234 235
          end

H
Hiroyuki Sato 已提交
236 237 238 239 240 241 242 243 244 245
          context 'when there are no commits between source branch and target branch' do
            let(:opts) do
              {
                title: 'Awesome merge_request',
                description: 'please fix',
                source_branch: 'not-merged-branch',
                target_branch: 'master'
              }
            end

246
            it 'does not create a detached merge request pipeline' do
H
Hiroyuki Sato 已提交
247 248 249
              expect(merge_request).to be_persisted

              merge_request.reload
250
              expect(merge_request.pipelines_for_merge_request.count).to eq(0)
H
Hiroyuki Sato 已提交
251 252 253
            end
          end

S
Shinya Maeda 已提交
254 255 256 257 258 259 260 261 262 263
          context "when branch pipeline was created before a merge request pipline has been created" do
            before do
              create(:ci_pipeline, project: merge_request.source_project,
                                   sha: merge_request.diff_head_sha,
                                   ref: merge_request.source_branch,
                                   tag: false)

              merge_request
            end

264
            it 'sets the latest detached merge request pipeline as the head pipeline' do
265
              expect(merge_request.actual_head_pipeline).to be_merge_request_event
S
Shinya Maeda 已提交
266 267 268 269 270 271 272 273 274 275 276 277 278 279
            end
          end
        end

        context "when .gitlab-ci.yml does not have merge_requests keywords" do
          let(:config) do
            {
              test: {
                stage: 'test',
                script: 'echo'
              }
            }
          end

280
          it 'does not create a detached merge request pipeline' do
S
Shinya Maeda 已提交
281 282 283
            expect(merge_request).to be_persisted

            merge_request.reload
284
            expect(merge_request.pipelines_for_merge_request.count).to eq(0)
S
Shinya Maeda 已提交
285 286 287
          end
        end
      end
288 289 290 291 292 293

      it 'increments the usage data counter of create event' do
        counter = Gitlab::UsageDataCounters::MergeRequestCounter

        expect { service.execute }.to change { counter.read(:create) }.by(1)
      end
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

      context 'after_save callback to store_mentions' do
        let(:labels) { create_pair(:label, project: project) }
        let(:milestone) { create(:milestone, project: project) }
        let(:req_opts) { { source_branch: 'feature', target_branch: 'master' } }

        context 'when mentionable attributes change' do
          let(:opts) { { title: 'Title', description: "Description with #{user.to_reference}" }.merge(req_opts) }

          it 'saves mentions' do
            expect_next_instance_of(MergeRequest) do |instance|
              expect(instance).to receive(:store_mentions!).and_call_original
            end
            expect(merge_request.user_mentions.count).to eq 1
          end
        end

        context 'when mentionable attributes do not change' do
          let(:opts) { { label_ids: labels.map(&:id), milestone_id: milestone.id }.merge(req_opts) }

          it 'does not call store_mentions' do
            expect_next_instance_of(MergeRequest) do |instance|
              expect(instance).not_to receive(:store_mentions!).and_call_original
            end
            expect(merge_request.valid?).to be false
            expect(merge_request.user_mentions.count).to eq 0
          end
        end

        context 'when save fails' do
          let(:opts) { { label_ids: labels.map(&:id), milestone_id: milestone.id } }

          it 'does not call store_mentions' do
            expect_next_instance_of(MergeRequest) do |instance|
              expect(instance).not_to receive(:store_mentions!).and_call_original
            end
            expect(merge_request.valid?).to be false
          end
        end
      end
334
    end
335

336
    it_behaves_like 'new issuable record that supports quick actions' do
337 338 339 340 341 342 343
      let(:default_params) do
        {
          source_branch: 'feature',
          target_branch: 'master'
        }
      end
    end
344

345
    context 'Quick actions' do
346 347 348 349 350 351
      context 'with assignee and milestone in params and command' do
        let(:merge_request) { described_class.new(project, user, opts).execute }
        let(:milestone) { create(:milestone, project: project) }

        let(:opts) do
          {
352
            assignee_ids: create(:user).id,
353 354 355 356 357 358 359 360 361
            milestone_id: 1,
            title: 'Title',
            description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
            source_branch: 'feature',
            target_branch: 'master'
          }
        end

        before do
362 363
          project.add_maintainer(user)
          project.add_maintainer(assignee)
364 365 366 367
        end

        it 'assigns and sets milestone to issuable from command' do
          expect(merge_request).to be_persisted
368
          expect(merge_request.assignees).to eq([assignee])
369 370 371 372 373 374 375 376 377
          expect(merge_request.milestone).to eq(milestone)
        end
      end
    end

    context 'merge request create service' do
      context 'asssignee_id' do
        let(:assignee) { create(:user) }

378
        before do
379
          project.add_maintainer(user)
380
        end
381 382

        it 'removes assignee_id when user id is invalid' do
383
          opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
384 385 386

          merge_request = described_class.new(project, user, opts).execute

387
          expect(merge_request.assignee_ids).to be_empty
388 389 390
        end

        it 'removes assignee_id when user id is 0' do
391
          opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
392 393 394

          merge_request = described_class.new(project, user, opts).execute

395
          expect(merge_request.assignee_ids).to be_empty
396 397 398
        end

        it 'saves assignee when user id is valid' do
399
          project.add_maintainer(assignee)
400
          opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
401 402 403

          merge_request = described_class.new(project, user, opts).execute

404
          expect(merge_request.assignees).to eq([assignee])
405 406
        end

407 408 409 410 411
        context 'when assignee is set' do
          let(:opts) do
            {
              title: 'Title',
              description: 'Description',
412
              assignee_ids: [assignee.id],
413 414 415 416 417 418
              source_branch: 'feature',
              target_branch: 'master'
            }
          end

          it 'invalidates open merge request counter for assignees when merge request is assigned' do
419
            project.add_maintainer(assignee)
420 421 422 423 424 425 426

            described_class.new(project, user, opts).execute

            expect(assignee.assigned_open_merge_requests_count).to eq 1
          end
        end

427 428 429 430 431 432 433 434 435 436 437
        context "when issuable feature is private" do
          before do
            project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
                                           merge_requests_access_level: ProjectFeature::PRIVATE)
          end

          levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC]

          levels.each do |level|
            it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
              project.update(visibility_level: level)
438
              opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
439 440 441 442 443 444 445 446 447

              merge_request = described_class.new(project, user, opts).execute

              expect(merge_request.assignee_id).to be_nil
            end
          end
        end
      end
    end
448

449 450 451 452 453 454 455 456 457 458 459 460 461 462
    context 'while saving references to issues that the created merge request closes' do
      let(:first_issue) { create(:issue, project: project) }
      let(:second_issue) { create(:issue, project: project) }

      let(:opts) do
        {
          title: 'Awesome merge_request',
          source_branch: 'feature',
          target_branch: 'master',
          force_remove_source_branch: '1'
        }
      end

      before do
463
        project.add_maintainer(user)
464
        project.add_developer(assignee)
465 466 467 468 469 470 471 472
      end

      it 'creates a `MergeRequestsClosingIssues` record for each issue' do
        issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}")
        service = described_class.new(project, user, issue_closing_opts)
        allow(service).to receive(:execute_hooks)
        merge_request = service.execute

473 474
        issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
        expect(issue_ids).to match_array([first_issue.id, second_issue.id])
475 476
      end
    end
477 478

    context 'when source and target projects are different' do
479
      let(:target_project) { fork_project(project, nil, repository: true) }
480 481 482 483 484 485 486 487 488 489 490 491 492

      let(:opts) do
        {
          title: 'Awesome merge_request',
          source_branch: 'feature',
          target_branch: 'master',
          target_project_id: target_project.id
        }
      end

      context 'when user can not access source project' do
        before do
          target_project.add_developer(assignee)
493
          target_project.add_maintainer(user)
494 495 496 497 498 499 500 501 502 503 504
        end

        it 'raises an error' do
          expect { described_class.new(project, user, opts).execute }
            .to raise_error Gitlab::Access::AccessDeniedError
        end
      end

      context 'when user can not access target project' do
        before do
          target_project.add_developer(assignee)
505
          target_project.add_maintainer(user)
506 507 508 509 510 511 512
        end

        it 'raises an error' do
          expect { described_class.new(project, user, opts).execute }
            .to raise_error Gitlab::Access::AccessDeniedError
        end
      end
513 514 515 516 517 518 519

      context 'when the user has access to both projects' do
        before do
          target_project.add_developer(user)
          project.add_developer(user)
        end

520
        it 'creates the merge request', :sidekiq_might_not_need_inline do
521 522 523 524 525
          merge_request = described_class.new(project, user, opts).execute

          expect(merge_request).to be_persisted
        end

526 527 528 529 530 531 532 533
        it 'calls MergeRequests::LinkLfsObjectsService#execute', :sidekiq_might_not_need_inline do
          expect_next_instance_of(MergeRequests::LinkLfsObjectsService) do |service|
            expect(service).to receive(:execute).with(instance_of(MergeRequest))
          end

          described_class.new(project, user, opts).execute
        end

534 535 536 537 538 539 540
        it 'does not create the merge request when the target project is archived' do
          target_project.update!(archived: true)

          expect { described_class.new(project, user, opts).execute }
            .to raise_error Gitlab::Access::AccessDeniedError
        end
      end
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
    end

    context 'when user sets source project id' do
      let(:another_project) { create(:project) }

      let(:opts) do
        {
          title: 'Awesome merge_request',
          source_branch: 'feature',
          target_branch: 'master',
          source_project_id: another_project.id
        }
      end

      before do
        project.add_developer(assignee)
557
        project.add_maintainer(user)
558 559 560 561 562 563 564 565
      end

      it 'ignores source_project_id' do
        merge_request = described_class.new(project, user, opts).execute

        expect(merge_request.source_project_id).to eq(project.id)
      end
    end
566 567
  end
end