interpret_service_spec.rb 64.7 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require 'spec_helper'

5
RSpec.describe QuickActions::InterpretService do
6 7 8 9
  let_it_be(:public_project) { create(:project, :public) }
  let_it_be(:repository_project) { create(:project, :repository) }
  let_it_be(:project) { public_project }
  let_it_be(:developer) { create(:user) }
10
  let_it_be(:developer2) { create(:user) }
11
  let_it_be_with_reload(:issue) { create(:issue, project: project) }
12
  let(:milestone) { create(:milestone, project: project, title: '9.10') }
13
  let(:commit) { create(:commit, project: project) }
14 15 16
  let_it_be(:inprogress) { create(:label, project: project, title: 'In Progress') }
  let_it_be(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') }
  let_it_be(:bug) { create(:label, project: project, title: 'Bug') }
17
  let(:service) { described_class.new(project, developer) }
18

19 20 21 22 23
  before_all do
    public_project.add_developer(developer)
    repository_project.add_developer(developer)
  end

24
  before do
25 26
    stub_licensed_features(multiple_issue_assignees: false,
                           multiple_merge_request_assignees: false)
27 28
  end

29
  describe '#execute' do
30
    let(:merge_request) { create(:merge_request, source_project: project) }
31

Douwe Maan's avatar
Douwe Maan committed
32 33
    shared_examples 'reopen command' do
      it 'returns state_event: "reopen" if content contains /reopen' do
34
        issuable.close!
35
        _, updates, _ = service.execute(content, issuable)
36

37
        expect(updates).to eq(state_event: 'reopen')
38
      end
39 40 41 42 43 44 45

      it 'returns the reopen message' do
        issuable.close!
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Reopened this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
46 47 48
    end

    shared_examples 'close command' do
Douwe Maan's avatar
Douwe Maan committed
49
      it 'returns state_event: "close" if content contains /close' do
50
        _, updates, _ = service.execute(content, issuable)
51

52
        expect(updates).to eq(state_event: 'close')
53
      end
54 55 56 57 58 59

      it 'returns the close message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Closed this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
60 61
    end

62 63
    shared_examples 'title command' do
      it 'populates title: "A brand new title" if content contains /title A brand new title' do
64
        _, updates, _ = service.execute(content, issuable)
65

66
        expect(updates).to eq(title: 'A brand new title')
67
      end
68 69 70 71 72 73

      it 'returns the title message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq(%{Changed the title to "A brand new title".})
      end
74 75
    end

76 77
    shared_examples 'milestone command' do
      it 'fetches milestone and populates milestone_id if content contains /milestone' do
78
        milestone # populate the milestone
79
        _, updates, _ = service.execute(content, issuable)
80

81
        expect(updates).to eq(milestone_id: milestone.id)
82
      end
83 84 85 86 87 88 89 90 91 92 93 94 95

      it 'returns the milestone message' do
        milestone # populate the milestone
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Set the milestone to #{milestone.to_reference}.")
      end

      it 'returns empty milestone message when milestone is wrong' do
        _, _, message = service.execute('/milestone %wrong-milestone', issuable)

        expect(message).to be_empty
      end
96 97
    end

Douwe Maan's avatar
Douwe Maan committed
98 99
    shared_examples 'remove_milestone command' do
      it 'populates milestone_id: nil if content contains /remove_milestone' do
100
        issuable.update!(milestone_id: milestone.id)
101
        _, updates, _ = service.execute(content, issuable)
102

103
        expect(updates).to eq(milestone_id: nil)
104
      end
105 106 107 108 109 110 111

      it 'returns removed milestone message' do
        issuable.update!(milestone_id: milestone.id)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Removed #{milestone.to_reference} milestone.")
      end
112 113
    end

114 115
    shared_examples 'label command' do
      it 'fetches label ids and populates add_label_ids if content contains /label' do
116 117
        bug # populate the label
        inprogress # populate the label
118
        _, updates, _ = service.execute(content, issuable)
119

120
        expect(updates).to eq(add_label_ids: [bug.id, inprogress.id])
121
      end
122 123 124 125 126 127 128 129

      it 'returns the label message' do
        bug # populate the label
        inprogress # populate the label
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Added #{bug.to_reference(format: :name)} #{inprogress.to_reference(format: :name)} labels.")
      end
130 131
    end

barthc's avatar
barthc committed
132 133 134 135
    shared_examples 'multiple label command' do
      it 'fetches label ids and populates add_label_ids if content contains multiple /label' do
        bug # populate the label
        inprogress # populate the label
136
        _, updates, _ = service.execute(content, issuable)
barthc's avatar
barthc committed
137 138 139 140 141 142 143 144

        expect(updates).to eq(add_label_ids: [inprogress.id, bug.id])
      end
    end

    shared_examples 'multiple label with same argument' do
      it 'prevents duplicate label ids and populates add_label_ids if content contains multiple /label' do
        inprogress # populate the label
145
        _, updates, _ = service.execute(content, issuable)
barthc's avatar
barthc committed
146 147 148 149 150

        expect(updates).to eq(add_label_ids: [inprogress.id])
      end
    end

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
    shared_examples 'multiword label name starting without ~' do
      it 'fetches label ids and populates add_label_ids if content contains /label' do
        _, updates = service.execute(content, issuable)

        expect(updates).to eq(add_label_ids: [helmchart.id])
      end
    end

    shared_examples 'label name is included in the middle of another label name' do
      it 'ignores the sublabel when the content contains the includer label name' do
        create(:label, project: project, title: 'Chart')

        _, updates = service.execute(content, issuable)

        expect(updates).to eq(add_label_ids: [helmchart.id])
      end
    end

169 170
    shared_examples 'unlabel command' do
      it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
171
        issuable.update!(label_ids: [inprogress.id]) # populate the label
172
        _, updates, _ = service.execute(content, issuable)
173

174
        expect(updates).to eq(remove_label_ids: [inprogress.id])
175
      end
176 177 178 179 180 181 182

      it 'returns the unlabel message' do
        issuable.update!(label_ids: [inprogress.id]) # populate the label
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Removed #{inprogress.to_reference(format: :name)} label.")
      end
183 184
    end

barthc's avatar
barthc committed
185 186
    shared_examples 'multiple unlabel command' do
      it 'fetches label ids and populates remove_label_ids if content contains  mutiple /unlabel' do
187
        issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label
188
        _, updates, _ = service.execute(content, issuable)
barthc's avatar
barthc committed
189 190 191 192 193

        expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id])
      end
    end

Douwe Maan's avatar
Douwe Maan committed
194 195
    shared_examples 'unlabel command with no argument' do
      it 'populates label_ids: [] if content contains /unlabel with no arguments' do
196
        issuable.update!(label_ids: [inprogress.id]) # populate the label
197
        _, updates, _ = service.execute(content, issuable)
198

199
        expect(updates).to eq(label_ids: [])
200 201 202
      end
    end

Douwe Maan's avatar
Douwe Maan committed
203 204
    shared_examples 'relabel command' do
      it 'populates label_ids: [] if content contains /relabel' do
205
        issuable.update!(label_ids: [bug.id]) # populate the label
Douwe Maan's avatar
Douwe Maan committed
206
        inprogress # populate the label
207
        _, updates, _ = service.execute(content, issuable)
Douwe Maan's avatar
Douwe Maan committed
208 209 210

        expect(updates).to eq(label_ids: [inprogress.id])
      end
211 212 213 214 215 216 217 218

      it 'returns the relabel message' do
        issuable.update!(label_ids: [bug.id]) # populate the label
        inprogress # populate the label
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Replaced all labels with #{inprogress.to_reference(format: :name)} label.")
      end
Douwe Maan's avatar
Douwe Maan committed
219 220
    end

221
    shared_examples 'todo command' do
222
      it 'populates todo_event: "add" if content contains /todo' do
223
        _, updates, _ = service.execute(content, issuable)
224

225
        expect(updates).to eq(todo_event: 'add')
226
      end
227 228 229 230

      it 'returns the todo message' do
        _, _, message = service.execute(content, issuable)

231
        expect(message).to eq('Added a to do.')
232
      end
233 234 235 236
    end

    shared_examples 'done command' do
      it 'populates todo_event: "done" if content contains /done' do
237
        TodoService.new.mark_todo(issuable, developer)
238
        _, updates, _ = service.execute(content, issuable)
239

240
        expect(updates).to eq(todo_event: 'done')
241
      end
242 243 244 245 246

      it 'returns the done message' do
        TodoService.new.mark_todo(issuable, developer)
        _, _, message = service.execute(content, issuable)

247
        expect(message).to eq('Marked to do as done.')
248
      end
249 250 251 252
    end

    shared_examples 'subscribe command' do
      it 'populates subscription_event: "subscribe" if content contains /subscribe' do
253
        _, updates, _ = service.execute(content, issuable)
254

255
        expect(updates).to eq(subscription_event: 'subscribe')
256
      end
257 258 259 260 261 262

      it 'returns the subscribe message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Subscribed to this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
263 264 265 266
    end

    shared_examples 'unsubscribe command' do
      it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
267
        issuable.subscribe(developer, project)
268
        _, updates, _ = service.execute(content, issuable)
269

270
        expect(updates).to eq(subscription_event: 'unsubscribe')
271
      end
272 273 274 275 276 277 278

      it 'returns the unsubscribe message' do
        issuable.subscribe(developer, project)
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Unsubscribed from this #{issuable.to_ability_name.humanize(capitalize: false)}.")
      end
279 280
    end

Douwe Maan's avatar
Douwe Maan committed
281
    shared_examples 'due command' do
282 283
      let(:expected_date) { Date.new(2016, 8, 28) }

Douwe Maan's avatar
Douwe Maan committed
284
      it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
285 286 287 288
        _, updates, _ = service.execute(content, issuable)

        expect(updates).to eq(due_date: expected_date)
      end
289

290 291 292 293
      it 'returns due_date message: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Set the due date to #{expected_date.to_s(:medium)}.")
294 295 296
      end
    end

Douwe Maan's avatar
Douwe Maan committed
297
    shared_examples 'remove_due_date command' do
298
      before do
299
        issuable.update!(due_date: Date.today)
300 301 302 303
      end

      it 'populates due_date: nil if content contains /remove_due_date' do
        _, updates, _ = service.execute(content, issuable)
304

305
        expect(updates).to eq(due_date: nil)
306
      end
307 308 309 310 311 312

      it 'returns Removed the due date' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed the due date.')
      end
313 314
    end

315 316
    shared_examples 'draft command' do
      it 'returns wip_event: "wip" if content contains /draft' do
317
        _, updates, _ = service.execute(content, issuable)
318 319 320

        expect(updates).to eq(wip_event: 'wip')
      end
321 322 323 324

      it 'returns the wip message' do
        _, _, message = service.execute(content, issuable)

325
        expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as Draft.")
326
      end
327 328
    end

329 330
    shared_examples 'undraft command' do
      it 'returns wip_event: "unwip" if content contains /draft' do
331
        issuable.update!(title: issuable.wip_title)
332
        _, updates, _ = service.execute(content, issuable)
333 334 335

        expect(updates).to eq(wip_event: 'unwip')
      end
336 337 338 339 340

      it 'returns the unwip message' do
        issuable.update!(title: issuable.wip_title)
        _, _, message = service.execute(content, issuable)

341
        expect(message).to eq("Unmarked this #{issuable.to_ability_name.humanize(capitalize: false)} as Draft.")
342
      end
343 344
    end

345 346
    shared_examples 'estimate command' do
      it 'populates time_estimate: 3600 if content contains /estimate 1h' do
347
        _, updates, _ = service.execute(content, issuable)
348 349 350

        expect(updates).to eq(time_estimate: 3600)
      end
351 352 353 354 355 356

      it 'returns the time_estimate formatted message' do
        _, _, message = service.execute('/estimate 79d', issuable)

        expect(message).to eq('Set time estimate to 3mo 3w 4d.')
      end
357 358 359 360
    end

    shared_examples 'spend command' do
      it 'populates spend_time: 3600 if content contains /spend 1h' do
361
        _, updates, _ = service.execute(content, issuable)
362

363 364
        expect(updates).to eq(spend_time: {
                                duration: 3600,
365
                                user_id: developer.id,
366
                                spent_at: DateTime.current.to_date
367
                              })
368
      end
369 370 371 372 373 374

      it 'returns the spend_time message including the formatted duration and verb' do
        _, _, message = service.execute('/spend -120m', issuable)

        expect(message).to eq('Subtracted 2h spent time.')
      end
375 376 377 378
    end

    shared_examples 'spend command with negative time' do
      it 'populates spend_time: -1800 if content contains /spend -30m' do
379
        _, updates, _ = service.execute(content, issuable)
380

381 382
        expect(updates).to eq(spend_time: {
                                duration: -1800,
383
                                user_id: developer.id,
384
                                spent_at: DateTime.current.to_date
385 386 387 388 389 390
                              })
      end
    end

    shared_examples 'spend command with valid date' do
      it 'populates spend time: 1800 with date in date type format' do
391
        _, updates, _ = service.execute(content, issuable)
392 393 394

        expect(updates).to eq(spend_time: {
                                duration: 1800,
395
                                user_id: developer.id,
396 397 398 399 400 401 402
                                spent_at: Date.parse(date)
                              })
      end
    end

    shared_examples 'spend command with invalid date' do
      it 'will not create any note and timelog' do
403
        _, updates, _ = service.execute(content, issuable)
404 405 406 407 408 409 410

        expect(updates).to eq({})
      end
    end

    shared_examples 'spend command with future date' do
      it 'will not create any note and timelog' do
411
        _, updates, _ = service.execute(content, issuable)
412 413

        expect(updates).to eq({})
414 415 416 417 418
      end
    end

    shared_examples 'remove_estimate command' do
      it 'populates time_estimate: 0 if content contains /remove_estimate' do
419
        _, updates, _ = service.execute(content, issuable)
420 421 422

        expect(updates).to eq(time_estimate: 0)
      end
423 424 425 426 427 428

      it 'returns the remove_estimate message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed time estimate.')
      end
429 430 431 432
    end

    shared_examples 'remove_time_spent command' do
      it 'populates spend_time: :reset if content contains /remove_time_spent' do
433
        _, updates, _ = service.execute(content, issuable)
434

435
        expect(updates).to eq(spend_time: { duration: :reset, user_id: developer.id })
436
      end
437 438 439 440 441 442

      it 'returns the remove_time_spent message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Removed spent time.')
      end
443 444
    end

445 446 447 448 449
    shared_examples 'lock command' do
      let(:issue) { create(:issue, project: project, discussion_locked: false) }
      let(:merge_request) { create(:merge_request, source_project: project, discussion_locked: false) }

      it 'returns discussion_locked: true if content contains /lock' do
450
        _, updates, _ = service.execute(content, issuable)
451 452 453

        expect(updates).to eq(discussion_locked: true)
      end
454 455 456 457

      it 'returns the lock discussion message' do
        _, _, message = service.execute(content, issuable)

458
        expect(message).to eq('Locked the discussion.')
459
      end
460 461 462 463 464 465 466
    end

    shared_examples 'unlock command' do
      let(:issue) { create(:issue, project: project, discussion_locked: true) }
      let(:merge_request) { create(:merge_request, source_project: project, discussion_locked: true) }

      it 'returns discussion_locked: true if content contains /unlock' do
467
        _, updates, _ = service.execute(content, issuable)
468 469 470

        expect(updates).to eq(discussion_locked: false)
      end
471 472 473 474

      it 'returns the unlock discussion message' do
        _, _, message = service.execute(content, issuable)

475
        expect(message).to eq('Unlocked the discussion.')
476
      end
477 478
    end

479
    shared_examples 'empty command' do |error_msg|
480
      it 'populates {} if content contains an unsupported command' do
481
        _, updates, _ = service.execute(content, issuable)
482

483
        expect(updates).to be_empty
484
      end
485 486 487 488 489 490 491 492 493 494

      it "returns #{error_msg || 'an empty'} message" do
        _, _, message = service.execute(content, issuable)

        if error_msg
          expect(message).to eq(error_msg)
        else
          expect(message).to be_empty
        end
      end
495 496
    end

497
    shared_examples 'merge immediately command' do
498
      let(:project) { repository_project }
499

500
      it 'runs merge command if content contains /merge' do
501
        _, updates, _ = service.execute(content, issuable)
502 503 504

        expect(updates).to eq(merge: merge_request.diff_head_sha)
      end
505 506 507 508

      it 'returns them merge message' do
        _, _, message = service.execute(content, issuable)

509 510 511 512 513
        expect(message).to eq('Merged this merge request.')
      end
    end

    shared_examples 'merge automatically command' do
514
      let(:project) { repository_project }
515 516 517 518 519 520

      it 'runs merge command if content contains /merge and returns merge message' do
        _, updates, message = service.execute(content, issuable)

        expect(updates).to eq(merge: merge_request.diff_head_sha)
        expect(message).to eq('Scheduled to merge this merge request (Merge when pipeline succeeds).')
521
      end
522 523
    end

mhasbini's avatar
mhasbini committed
524
    shared_examples 'award command' do
525
      it 'toggle award 100 emoji if content contains /award :100:' do
526
        _, updates, _ = service.execute(content, issuable)
mhasbini's avatar
mhasbini committed
527 528 529

        expect(updates).to eq(emoji_award: "100")
      end
530 531 532 533 534 535

      it 'returns the award message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq('Toggled :100: emoji award.')
      end
mhasbini's avatar
mhasbini committed
536 537
    end

538
    shared_examples 'duplicate command' do
539
      it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do
540
        issue_duplicate # populate the issue
541
        _, updates, _ = service.execute(content, issuable)
542

543
        expect(updates).to eq(canonical_issue_id: issue_duplicate.id)
544
      end
545 546 547 548 549 550

      it 'returns the duplicate message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Marked this issue as a duplicate of #{issue_duplicate.to_reference(project)}.")
      end
551 552
    end

553 554
    shared_examples 'copy_metadata command' do
      it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do
555
        source_issuable # populate the issue
556 557
        todo_label # populate this label
        inreview_label # populate this label
558
        _, updates, _ = service.execute(content, issuable)
559 560 561

        expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id])

562 563
        if source_issuable.milestone
          expect(updates[:milestone_id]).to eq(source_issuable.milestone.id)
564 565 566 567
        else
          expect(updates).not_to have_key(:milestone_id)
        end
      end
568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585

      it 'returns the copy metadata message' do
        _, _, message = service.execute("/copy_metadata #{source_issuable.to_reference}", issuable)

        expect(message).to eq("Copied labels and milestone from #{source_issuable.to_reference}.")
      end
    end

    describe 'move issue command' do
      it 'returns the move issue message' do
        _, _, message = service.execute("/move #{project.full_path}", issue)

        expect(message).to eq("Moved this issue to #{project.full_path}.")
      end

      it 'returns move issue failure message when the referenced issue is not found' do
        _, _, message = service.execute('/move invalid', issue)

586
        expect(message).to eq(_("Failed to move this issue because target project doesn't exist."))
587
      end
588 589
    end

590 591
    shared_examples 'confidential command' do
      it 'marks issue as confidential if content contains /confidential' do
592
        _, updates, _ = service.execute(content, issuable)
593 594 595

        expect(updates).to eq(confidential: true)
      end
596 597 598 599

      it 'returns the confidential message' do
        _, _, message = service.execute(content, issuable)

600
        expect(message).to eq('Made this issue confidential.')
601
      end
602 603 604

      context 'when issuable is already confidential' do
        before do
605
          issuable.update!(confidential: true)
606 607 608 609 610 611 612 613 614 615 616 617
        end

        it 'does not return the success message' do
          _, _, message = service.execute(content, issuable)

          expect(message).to be_empty
        end

        it 'is not part of the available commands' do
          expect(service.available_commands(issuable)).not_to include(a_hash_including(name: :confidential))
        end
      end
618 619
    end

620 621
    shared_examples 'shrug command' do
      it 'appends ¯\_(ツ)_/¯ to the comment' do
622
        new_content, _, _ = service.execute(content, issuable)
623 624 625 626 627 628 629

        expect(new_content).to end_with(described_class::SHRUG)
      end
    end

    shared_examples 'tableflip command' do
      it 'appends (╯°□°)╯︵ ┻━┻ to the comment' do
630
        new_content, _, _ = service.execute(content, issuable)
631 632 633 634 635

        expect(new_content).to end_with(described_class::TABLEFLIP)
      end
    end

636 637
    shared_examples 'tag command' do
      it 'tags a commit' do
638
        _, updates, _ = service.execute(content, issuable)
639 640 641

        expect(updates).to eq(tag_name: tag_name, tag_message: tag_message)
      end
642 643 644 645 646 647 648 649 650 651

      it 'returns the tag message' do
        _, _, message = service.execute(content, issuable)

        if tag_message.present?
          expect(message).to eq(%{Tagged this commit to #{tag_name} with "#{tag_message}".})
        else
          expect(message).to eq("Tagged this commit to #{tag_name}.")
        end
      end
652 653
    end

654 655
    shared_examples 'assign command' do
      it 'assigns to a single user' do
656
        _, updates, _ = service.execute(content, issuable)
657 658 659

        expect(updates).to eq(assignee_ids: [developer.id])
      end
660 661 662 663 664 665

      it 'returns the assign message' do
        _, _, message = service.execute(content, issuable)

        expect(message).to eq("Assigned #{developer.to_reference}.")
      end
666 667
    end

Douwe Maan's avatar
Douwe Maan committed
668 669
    it_behaves_like 'reopen command' do
      let(:content) { '/reopen' }
670 671 672
      let(:issuable) { issue }
    end

Douwe Maan's avatar
Douwe Maan committed
673
    it_behaves_like 'reopen command' do
674
      let(:content) { '/reopen' }
Douwe Maan's avatar
Douwe Maan committed
675
      let(:issuable) { merge_request }
676 677 678 679
    end

    it_behaves_like 'close command' do
      let(:content) { '/close' }
680 681 682 683 684 685
      let(:issuable) { issue }
    end

    it_behaves_like 'close command' do
      let(:content) { '/close' }
      let(:issuable) { merge_request }
686 687
    end

688 689 690
    context 'merge command' do
      let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) }

691
      it_behaves_like 'merge immediately command' do
692 693 694 695
        let(:content) { '/merge' }
        let(:issuable) { merge_request }
      end

696 697 698 699 700 701 702 703 704 705 706 707
      context 'when the head pipeline of merge request is running' do
        before do
          create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request)
          merge_request.update_head_pipeline
        end

        it_behaves_like 'merge automatically command' do
          let(:content) { '/merge' }
          let(:issuable) { merge_request }
        end
      end

708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
      context 'can not be merged when logged user does not have permissions' do
        let(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'empty command' do
          let(:content) { "/merge" }
          let(:issuable) { merge_request }
        end
      end

      context 'can not be merged when sha does not match' do
        let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) }

        it_behaves_like 'empty command' do
          let(:content) { "/merge" }
          let(:issuable) { merge_request }
        end
      end

      context 'when sha is missing' do
727
        let(:project) { repository_project }
728 729 730
        let(:service) { described_class.new(project, developer, {}) }

        it 'precheck passes and returns merge command' do
731
          _, updates, _ = service.execute('/merge', merge_request)
732 733 734 735 736

          expect(updates).to eq(merge: nil)
        end
      end

Jarka Kadlecova's avatar
Jarka Kadlecova committed
737
      context 'issue can not be merged' do
738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
        it_behaves_like 'empty command' do
          let(:content) { "/merge" }
          let(:issuable) { issue }
        end
      end

      context 'non persisted merge request  cant be merged' do
        it_behaves_like 'empty command' do
          let(:content) { "/merge" }
          let(:issuable) { build(:merge_request) }
        end
      end

      context 'not persisted merge request can not be merged' do
        it_behaves_like 'empty command' do
          let(:content) { "/merge" }
          let(:issuable) { build(:merge_request, source_project: project) }
        end
      end
    end

759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
    it_behaves_like 'title command' do
      let(:content) { '/title A brand new title' }
      let(:issuable) { issue }
    end

    it_behaves_like 'title command' do
      let(:content) { '/title A brand new title' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'empty command' do
      let(:content) { '/title' }
      let(:issuable) { issue }
    end

774 775 776 777
    context 'assign command with one user' do
      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { issue }
778 779
      end

780 781 782
      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { merge_request }
783
      end
784 785
    end

786
    # CE does not have multiple assignees
787
    context 'assign command with multiple assignees' do
788
      before do
789
        project.add_developer(developer2)
790
      end
791

792 793 794
      it_behaves_like 'assign command' do
        let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
        let(:issuable) { issue }
795 796
      end

Rémy Coutable's avatar
Rémy Coutable committed
797
      it_behaves_like 'assign command', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/27989' do
798 799
        let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
        let(:issuable) { merge_request }
800
      end
801 802
    end

803
    context 'assign command with me alias' do
804 805 806 807
      it_behaves_like 'assign command' do
        let(:content) { '/assign me' }
        let(:issuable) { issue }
      end
808

809 810 811
      it_behaves_like 'assign command' do
        let(:content) { '/assign me' }
        let(:issuable) { merge_request }
812
      end
813
    end
814

815 816 817 818 819
    context 'assign command with me alias and whitespace' do
      it_behaves_like 'assign command' do
        let(:content) { '/assign  me ' }
        let(:issuable) { issue }
      end
820

821 822 823
      it_behaves_like 'assign command' do
        let(:content) { '/assign  me ' }
        let(:issuable) { merge_request }
824 825 826
      end
    end

827
    it_behaves_like 'empty command', "Failed to assign a user because no user was found." do
828 829
      let(:content) { '/assign @abcd1234' }
      let(:issuable) { issue }
830 831
    end

832
    it_behaves_like 'empty command', "Failed to assign a user because no user was found." do
833 834
      let(:content) { '/assign' }
      let(:issuable) { issue }
835 836
    end

837
    context 'unassign command' do
838 839
      let(:content) { '/unassign' }

840 841
      context 'Issue' do
        it 'populates assignee_ids: [] if content contains /unassign' do
842
          issue.update!(assignee_ids: [developer.id])
843
          _, updates, _ = service.execute(content, issue)
844 845 846

          expect(updates).to eq(assignee_ids: [])
        end
847 848

        it 'returns the unassign message for all the assignee if content contains /unassign' do
849
          issue.update!(assignee_ids: [developer.id, developer2.id])
850 851 852 853
          _, _, message = service.execute(content, issue)

          expect(message).to eq("Removed assignees #{developer.to_reference} and #{developer2.to_reference}.")
        end
854 855 856
      end

      context 'Merge Request' do
857
        it 'populates assignee_ids: [] if content contains /unassign' do
858
          merge_request.update!(assignee_ids: [developer.id])
859
          _, updates, _ = service.execute(content, merge_request)
860

861
          expect(updates).to eq(assignee_ids: [])
862
        end
863 864

        it 'returns the unassign message for all the assignee if content contains /unassign' do
865
          merge_request.update!(assignee_ids: [developer.id, developer2.id])
866 867 868 869
          _, _, message = service.execute(content, merge_request)

          expect(message).to eq("Removed assignees #{developer.to_reference} and #{developer2.to_reference}.")
        end
870
      end
871 872 873 874 875
    end

    it_behaves_like 'milestone command' do
      let(:content) { "/milestone %#{milestone.title}" }
      let(:issuable) { issue }
876 877 878 879
    end

    it_behaves_like 'milestone command' do
      let(:content) { "/milestone %#{milestone.title}" }
880 881 882
      let(:issuable) { merge_request }
    end

883
    context 'only group milestones available' do
884 885 886 887 888 889 890 891
      let_it_be(:ancestor_group) { create(:group) }
      let_it_be(:group) { create(:group, parent: ancestor_group) }
      let_it_be(:project) { create(:project, :public, namespace: group) }
      let_it_be(:milestone) { create(:milestone, group: ancestor_group, title: '10.0') }

      before_all do
        project.add_developer(developer)
      end
892 893 894 895 896 897 898 899 900 901 902 903

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'milestone command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { merge_request }
      end
    end

Douwe Maan's avatar
Douwe Maan committed
904 905
    it_behaves_like 'remove_milestone command' do
      let(:content) { '/remove_milestone' }
906 907 908
      let(:issuable) { issue }
    end

Douwe Maan's avatar
Douwe Maan committed
909
    it_behaves_like 'remove_milestone command' do
910
      let(:content) { '/remove_milestone' }
Douwe Maan's avatar
Douwe Maan committed
911
      let(:issuable) { merge_request }
912
    end
913

914 915 916
    it_behaves_like 'label command' do
      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
      let(:issuable) { issue }
917 918 919 920
    end

    it_behaves_like 'label command' do
      let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
921
      let(:issuable) { merge_request }
922 923
    end

barthc's avatar
barthc committed
924 925 926 927 928 929 930 931
    it_behaves_like 'multiple label command' do
      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) }
      let(:issuable) { issue }
    end

    it_behaves_like 'multiple label with same argument' do
      let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{inprogress.title}) }
      let(:issuable) { issue }
932
    end
barthc's avatar
barthc committed
933

934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
    it_behaves_like 'multiword label name starting without ~' do
      let(:content) { %(/label "#{helmchart.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'multiword label name starting without ~' do
      let(:content) { %(/label "#{helmchart.title}") }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'label name is included in the middle of another label name' do
      let(:content) { %(/label ~"#{helmchart.title}") }
      let(:issuable) { issue }
    end

    it_behaves_like 'label name is included in the middle of another label name' do
      let(:content) { %(/label ~"#{helmchart.title}") }
      let(:issuable) { merge_request }
    end

954
    it_behaves_like 'unlabel command' do
955
      let(:content) { %(/unlabel ~"#{inprogress.title}") }
956
      let(:issuable) { issue }
957 958
    end

959 960 961 962 963
    it_behaves_like 'unlabel command' do
      let(:content) { %(/unlabel ~"#{inprogress.title}") }
      let(:issuable) { merge_request }
    end

barthc's avatar
barthc committed
964 965 966 967 968
    it_behaves_like 'multiple unlabel command' do
      let(:content) { %(/unlabel ~"#{inprogress.title}" \n/unlabel ~#{bug.title}) }
      let(:issuable) { issue }
    end

Douwe Maan's avatar
Douwe Maan committed
969 970
    it_behaves_like 'unlabel command with no argument' do
      let(:content) { %(/unlabel) }
971
      let(:issuable) { issue }
972 973
    end

Douwe Maan's avatar
Douwe Maan committed
974 975 976
    it_behaves_like 'unlabel command with no argument' do
      let(:content) { %(/unlabel) }
      let(:issuable) { merge_request }
977 978
    end

Douwe Maan's avatar
Douwe Maan committed
979 980
    it_behaves_like 'relabel command' do
      let(:content) { %(/relabel ~"#{inprogress.title}") }
981
      let(:issuable) { issue }
982 983
    end

Douwe Maan's avatar
Douwe Maan committed
984 985
    it_behaves_like 'relabel command' do
      let(:content) { %(/relabel ~"#{inprogress.title}") }
986
      let(:issuable) { merge_request }
987 988
    end

989 990 991
    it_behaves_like 'done command' do
      let(:content) { '/done' }
      let(:issuable) { issue }
992 993
    end

994 995 996 997
    it_behaves_like 'done command' do
      let(:content) { '/done' }
      let(:issuable) { merge_request }
    end
998

999 1000 1001
    it_behaves_like 'subscribe command' do
      let(:content) { '/subscribe' }
      let(:issuable) { issue }
1002 1003
    end

1004 1005 1006 1007
    it_behaves_like 'subscribe command' do
      let(:content) { '/subscribe' }
      let(:issuable) { merge_request }
    end
1008

1009 1010 1011
    it_behaves_like 'unsubscribe command' do
      let(:content) { '/unsubscribe' }
      let(:issuable) { issue }
1012 1013
    end

1014 1015 1016 1017
    it_behaves_like 'unsubscribe command' do
      let(:content) { '/unsubscribe' }
      let(:issuable) { merge_request }
    end
1018

1019
    it_behaves_like 'empty command' do
Douwe Maan's avatar
Douwe Maan committed
1020
      let(:content) { '/due 2016-08-28' }
1021
      let(:issuable) { merge_request }
1022 1023
    end

Douwe Maan's avatar
Douwe Maan committed
1024 1025
    it_behaves_like 'remove_due_date command' do
      let(:content) { '/remove_due_date' }
1026 1027
      let(:issuable) { issue }
    end
1028

1029
    it_behaves_like 'draft command' do
1030 1031 1032 1033
      let(:content) { '/wip' }
      let(:issuable) { merge_request }
    end

1034
    it_behaves_like 'undraft command' do
1035 1036 1037 1038
      let(:content) { '/wip' }
      let(:issuable) { merge_request }
    end

1039
    it_behaves_like 'draft command' do
1040 1041 1042 1043
      let(:content) { '/draft' }
      let(:issuable) { merge_request }
    end

1044
    it_behaves_like 'undraft command' do
1045 1046 1047 1048
      let(:content) { '/draft' }
      let(:issuable) { merge_request }
    end

1049
    it_behaves_like 'empty command' do
Douwe Maan's avatar
Douwe Maan committed
1050
      let(:content) { '/remove_due_date' }
1051
      let(:issuable) { merge_request }
1052
    end
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
    it_behaves_like 'estimate command' do
      let(:content) { '/estimate 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'empty command' do
      let(:content) { '/estimate' }
      let(:issuable) { issue }
    end

    it_behaves_like 'empty command' do
      let(:content) { '/estimate abc' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command' do
      let(:content) { '/spend 1h' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with negative time' do
      let(:content) { '/spend -30m' }
      let(:issuable) { issue }
    end

1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
    it_behaves_like 'spend command with valid date' do
      let(:date) { '2016-02-02' }
      let(:content) { "/spend 30m #{date}" }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with invalid date' do
      let(:content) { '/spend 30m 17-99-99' }
      let(:issuable) { issue }
    end

    it_behaves_like 'spend command with future date' do
      let(:content) { '/spend 30m 6017-10-10' }
      let(:issuable) { issue }
    end

1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
    it_behaves_like 'empty command' do
      let(:content) { '/spend' }
      let(:issuable) { issue }
    end

    it_behaves_like 'empty command' do
      let(:content) { '/spend abc' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_estimate command' do
      let(:content) { '/remove_estimate' }
      let(:issuable) { issue }
    end

    it_behaves_like 'remove_time_spent command' do
      let(:content) { '/remove_time_spent' }
      let(:issuable) { issue }
    end

1115 1116 1117 1118 1119
    it_behaves_like 'confidential command' do
      let(:content) { '/confidential' }
      let(:issuable) { issue }
    end

1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139
    it_behaves_like 'lock command' do
      let(:content) { '/lock' }
      let(:issuable) { issue }
    end

    it_behaves_like 'lock command' do
      let(:content) { '/lock' }
      let(:issuable) { merge_request }
    end

    it_behaves_like 'unlock command' do
      let(:content) { '/unlock' }
      let(:issuable) { issue }
    end

    it_behaves_like 'unlock command' do
      let(:content) { '/unlock' }
      let(:issuable) { merge_request }
    end

1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161
    context '/todo' do
      let(:content) { '/todo' }

      context 'if issuable is an Issue' do
        it_behaves_like 'todo command' do
          let(:issuable) { issue }
        end
      end

      context 'if issuable is a MergeRequest' do
        it_behaves_like 'todo command' do
          let(:issuable) { merge_request }
        end
      end

      context 'if issuable is a Commit' do
        it_behaves_like 'empty command' do
          let(:issuable) { commit }
        end
      end
    end

1162 1163 1164 1165 1166 1167
    context '/due command' do
      it 'returns invalid date format message when the due date is invalid' do
        issue = build(:issue, project: project)

        _, _, message = service.execute('/due invalid date', issue)

1168
        expect(message).to eq(_('Failed to set due date because the date format is invalid.'))
1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
      end

      it_behaves_like 'due command' do
        let(:content) { '/due 2016-08-28' }
        let(:issuable) { issue }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due tomorrow' }
        let(:issuable) { issue }
        let(:expected_date) { Date.tomorrow }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due 5 days from now' }
        let(:issuable) { issue }
        let(:expected_date) { 5.days.from_now.to_date }
      end

      it_behaves_like 'due command' do
        let(:content) { '/due in 2 days' }
        let(:issuable) { issue }
        let(:expected_date) { 2.days.from_now.to_date }
      end
    end

1195
    context '/copy_metadata command' do
1196 1197
      let(:todo_label) { create(:label, project: project, title: 'To Do') }
      let(:inreview_label) { create(:label, project: project, title: 'In Review') }
1198

1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211
      it 'is available when the user is a developer' do
        expect(service.available_commands(issue)).to include(a_hash_including(name: :copy_metadata))
      end

      context 'when the user does not have permission' do
        let(:guest) { create(:user) }
        let(:service) { described_class.new(project, guest) }

        it 'is not available' do
          expect(service.available_commands(issue)).not_to include(a_hash_including(name: :copy_metadata))
        end
      end

1212 1213
      it_behaves_like 'empty command' do
        let(:content) { '/copy_metadata' }
1214 1215 1216
        let(:issuable) { issue }
      end

1217 1218 1219 1220 1221 1222 1223
      it_behaves_like 'copy_metadata command' do
        let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }

        let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
        let(:issuable) { build(:issue, project: project) }
      end

1224
      it_behaves_like 'copy_metadata command' do
1225
        let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
1226

1227
        let(:content) { "/copy_metadata #{source_issuable.to_reference}" }
1228 1229 1230
        let(:issuable) { issue }
      end

1231
      context 'when the parent issuable has a milestone' do
1232
        it_behaves_like 'copy_metadata command' do
1233
          let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) }
1234

1235
          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
1236 1237
          let(:issuable) { issue }
        end
1238
      end
1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249

      context 'when more than one issuable is passed' do
        it_behaves_like 'copy_metadata command' do
          let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) }
          let(:other_label) { create(:label, project: project, title: 'Other') }
          let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) }

          let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" }
          let(:issuable) { issue }
        end
      end
1250 1251 1252 1253

      context 'cross project references' do
        it_behaves_like 'empty command' do
          let(:other_project) { create(:project, :public) }
1254 1255
          let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) }
          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
1256 1257 1258 1259
          let(:issuable) { issue }
        end

        it_behaves_like 'empty command' do
1260
          let(:content) { "/copy_metadata imaginary##{non_existing_record_iid}" }
1261 1262 1263 1264 1265
          let(:issuable) { issue }
        end

        it_behaves_like 'empty command' do
          let(:other_project) { create(:project, :private) }
1266
          let(:source_issuable) { create(:issue, project: other_project) }
1267

1268
          let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" }
1269 1270 1271 1272 1273
          let(:issuable) { issue }
        end
      end
    end

Ryan Scott's avatar
Ryan Scott committed
1274 1275 1276 1277 1278 1279
    context '/duplicate command' do
      it_behaves_like 'duplicate command' do
        let(:issue_duplicate) { create(:issue, project: project) }
        let(:content) { "/duplicate #{issue_duplicate.to_reference}" }
        let(:issuable) { issue }
      end
1280

Ryan Scott's avatar
Ryan Scott committed
1281 1282 1283 1284
      it_behaves_like 'empty command' do
        let(:content) { '/duplicate' }
        let(:issuable) { issue }
      end
1285

Ryan Scott's avatar
Ryan Scott committed
1286 1287
      context 'cross project references' do
        it_behaves_like 'duplicate command' do
1288
          let(:other_project) { create(:project, :public) }
Ryan Scott's avatar
Ryan Scott committed
1289 1290 1291 1292 1293
          let(:issue_duplicate) { create(:issue, project: other_project) }
          let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
          let(:issuable) { issue }
        end

1294
        it_behaves_like 'empty command', _('Failed to mark this issue as a duplicate because referenced issue was not found.') do
1295
          let(:content) { "/duplicate imaginary##{non_existing_record_iid}" }
Ryan Scott's avatar
Ryan Scott committed
1296 1297 1298
          let(:issuable) { issue }
        end

1299
        it_behaves_like 'empty command', _('Failed to mark this issue as a duplicate because referenced issue was not found.') do
1300
          let(:other_project) { create(:project, :private) }
Ryan Scott's avatar
Ryan Scott committed
1301 1302 1303 1304 1305 1306
          let(:issue_duplicate) { create(:issue, project: other_project) }

          let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
          let(:issuable) { issue }
        end
      end
1307 1308
    end

1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357
    context 'when current_user cannot :admin_issue' do
      let(:visitor) { create(:user) }
      let(:issue) { create(:issue, project: project, author: visitor) }
      let(:service) { described_class.new(project, visitor) }

      it_behaves_like 'empty command' do
        let(:content) { "/assign @#{developer.username}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { '/unassign' }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { "/milestone %#{milestone.title}" }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { '/remove_milestone' }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { %(/unlabel ~"#{inprogress.title}") }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { %(/relabel ~"#{inprogress.title}") }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { '/due tomorrow' }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { '/remove_due_date' }
        let(:issuable) { issue }
      end
1358

1359 1360 1361 1362 1363
      it_behaves_like 'empty command' do
        let(:content) { '/confidential' }
        let(:issuable) { issue }
      end

1364 1365 1366 1367 1368 1369 1370 1371 1372
      it_behaves_like 'empty command' do
        let(:content) { '/lock' }
        let(:issuable) { issue }
      end

      it_behaves_like 'empty command' do
        let(:content) { '/unlock' }
        let(:issuable) { issue }
      end
1373
    end
1374

mhasbini's avatar
mhasbini committed
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403
    context '/award command' do
      it_behaves_like 'award command' do
        let(:content) { '/award :100:' }
        let(:issuable) { issue }
      end

      it_behaves_like 'award command' do
        let(:content) { '/award :100:' }
        let(:issuable) { merge_request }
      end

      context 'ignores command with no argument' do
        it_behaves_like 'empty command' do
          let(:content) { '/award' }
          let(:issuable) { issue }
        end
      end

      context 'ignores non-existing / invalid  emojis' do
        it_behaves_like 'empty command' do
          let(:content) { '/award noop' }
          let(:issuable) { issue }
        end

        it_behaves_like 'empty command' do
          let(:content) { '/award :lorem_ipsum:' }
          let(:issuable) { issue }
        end
      end
1404 1405 1406 1407

      context 'if issuable is a Commit' do
        let(:content) { '/award :100:' }
        let(:issuable) { commit }
1408

1409 1410
        it_behaves_like 'empty command'
      end
mhasbini's avatar
mhasbini committed
1411 1412
    end

1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436
    context '/shrug command' do
      it_behaves_like 'shrug command' do
        let(:content) { '/shrug people are people' }
        let(:issuable) { issue }
      end

      it_behaves_like 'shrug command' do
        let(:content) { '/shrug' }
        let(:issuable) { issue }
      end
    end

    context '/tableflip command' do
      it_behaves_like 'tableflip command' do
        let(:content) { '/tableflip curse your sudden but enviable betrayal' }
        let(:issuable) { issue }
      end

      it_behaves_like 'tableflip command' do
        let(:content) { '/tableflip' }
        let(:issuable) { issue }
      end
    end

1437
    context '/target_branch command' do
1438
      let(:non_empty_project) { create(:project, :repository) }
1439 1440 1441 1442
      let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
      let(:service) { described_class.new(non_empty_project, developer)}

      it 'updates target_branch if /target_branch command is executed' do
1443
        _, updates, _ = service.execute('/target_branch merge-test', merge_request)
1444 1445 1446 1447 1448

        expect(updates).to eq(target_branch: 'merge-test')
      end

      it 'handles blanks around param' do
1449
        _, updates, _ = service.execute('/target_branch  merge-test     ', merge_request)
1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466

        expect(updates).to eq(target_branch: 'merge-test')
      end

      context 'ignores command with no argument' do
        it_behaves_like 'empty command' do
          let(:content) { '/target_branch' }
          let(:issuable) { another_merge_request }
        end
      end

      context 'ignores non-existing target branch' do
        it_behaves_like 'empty command' do
          let(:content) { '/target_branch totally_non_existing_branch' }
          let(:issuable) { another_merge_request }
        end
      end
1467 1468 1469 1470 1471 1472

      it 'returns the target_branch message' do
        _, _, message = service.execute('/target_branch merge-test', merge_request)

        expect(message).to eq('Set target branch to merge-test.')
      end
1473
    end
1474 1475

    context '/board_move command' do
1476 1477
      let_it_be(:todo) { create(:label, project: project, title: 'To Do') }
      let_it_be(:inreview) { create(:label, project: project, title: 'In Review') }
1478 1479
      let(:content) { %{/board_move ~"#{inreview.title}"} }

1480 1481 1482 1483
      let_it_be(:board) { create(:board, project: project) }
      let_it_be(:todo_list) { create(:list, board: board, label: todo) }
      let_it_be(:inreview_list) { create(:list, board: board, label: inreview) }
      let_it_be(:inprogress_list) { create(:list, board: board, label: inprogress) }
1484 1485 1486 1487

      it 'populates remove_label_ids for all current board columns' do
        issue.update!(label_ids: [todo.id, inprogress.id])

1488
        _, updates, _ = service.execute(content, issue)
1489 1490 1491 1492 1493

        expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id])
      end

      it 'populates add_label_ids with the id of the given label' do
1494
        _, updates, _ = service.execute(content, issue)
1495 1496 1497 1498 1499 1500 1501

        expect(updates[:add_label_ids]).to eq([inreview.id])
      end

      it 'does not include the given label id in remove_label_ids' do
        issue.update!(label_ids: [todo.id, inreview.id])

1502
        _, updates, _ = service.execute(content, issue)
1503 1504 1505 1506 1507 1508 1509

        expect(updates[:remove_label_ids]).to match_array([todo.id])
      end

      it 'does not remove label ids that are not lists on the board' do
        issue.update!(label_ids: [todo.id, bug.id])

1510
        _, updates, _ = service.execute(content, issue)
1511 1512 1513 1514

        expect(updates[:remove_label_ids]).to match_array([todo.id])
      end

1515 1516 1517 1518 1519 1520 1521 1522
      it 'returns board_move message' do
        issue.update!(label_ids: [todo.id, inprogress.id])

        _, _, message = service.execute(content, issue)

        expect(message).to eq("Moved issue to ~#{inreview.id} column in the board.")
      end

1523 1524
      context 'if the project has multiple boards' do
        let(:issuable) { issue }
1525 1526 1527 1528 1529

        before do
          create(:board, project: project)
        end

1530 1531 1532 1533 1534 1535
        it_behaves_like 'empty command'
      end

      context 'if the given label does not exist' do
        let(:issuable) { issue }
        let(:content) { '/board_move ~"Fake Label"' }
1536

1537
        it_behaves_like 'empty command', 'Failed to move this issue because label was not found.'
1538 1539 1540 1541 1542
      end

      context 'if multiple labels are given' do
        let(:issuable) { issue }
        let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} }
1543

1544
        it_behaves_like 'empty command', 'Failed to move this issue because only a single label can be provided.'
1545 1546 1547 1548 1549
      end

      context 'if the given label is not a list on the board' do
        let(:issuable) { issue }
        let(:content) { %{/board_move ~"#{bug.title}"} }
1550

1551
        it_behaves_like 'empty command', 'Failed to move this issue because label was not found.'
1552 1553 1554 1555
      end

      context 'if issuable is not an Issue' do
        let(:issuable) { merge_request }
1556

1557 1558 1559
        it_behaves_like 'empty command'
      end
    end
1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585

    context '/tag command' do
      let(:issuable) { commit }

      context 'ignores command with no argument' do
        it_behaves_like 'empty command' do
          let(:content) { '/tag' }
        end
      end

      context 'tags a commit with a tag name' do
        it_behaves_like 'tag command' do
          let(:tag_name) { 'v1.2.3' }
          let(:tag_message) { nil }
          let(:content) { "/tag #{tag_name}" }
        end
      end

      context 'tags a commit with a tag name and message' do
        it_behaves_like 'tag command' do
          let(:tag_name) { 'v1.2.3' }
          let(:tag_message) { 'Stable release' }
          let(:content) { "/tag #{tag_name} #{tag_message}" }
        end
      end
    end
1586 1587

    it 'limits to commands passed ' do
1588
      content = "/shrug test\n/close"
1589 1590 1591 1592

      text, commands = service.execute(content, issue, only: [:shrug])

      expect(commands).to be_empty
1593
      expect(text).to eq("test #{described_class::SHRUG}\n/close")
1594
    end
1595

1596 1597 1598 1599 1600 1601 1602 1603
    it 'preserves leading whitespace ' do
      content = " - list\n\n/close\n\ntest\n\n"

      text, _ = service.execute(content, issue)

      expect(text).to eq(" - list\n\ntest")
    end

1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617
    context '/create_merge_request command' do
      let(:branch_name) { '1-feature' }
      let(:content) { "/create_merge_request #{branch_name}" }
      let(:issuable) { issue }

      context 'if issuable is not an Issue' do
        let(:issuable) { merge_request }

        it_behaves_like 'empty command'
      end

      context "when logged user cannot create_merge_requests in the project" do
        let(:project) { create(:project, :archived) }

1618 1619 1620 1621
        before do
          project.add_developer(developer)
        end

1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632
        it_behaves_like 'empty command'
      end

      context 'when logged user cannot push code to the project' do
        let(:project) { create(:project, :private) }
        let(:service) { described_class.new(project, create(:user)) }

        it_behaves_like 'empty command'
      end

      it 'populates create_merge_request with branch_name and issue iid' do
1633
        _, updates, _ = service.execute(content, issuable)
1634 1635 1636

        expect(updates).to eq(create_merge_request: { branch_name: branch_name, issue_iid: issuable.iid })
      end
1637 1638 1639 1640

      it 'returns the create_merge_request message' do
        _, _, message = service.execute(content, issuable)

1641
        expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue.")
1642
      end
1643
    end
1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666

    context 'submit_review command' do
      using RSpec::Parameterized::TableSyntax

      where(:note) do
        [
          'I like it',
          '/submit_review'
        ]
      end

      with_them do
        let(:content) { '/submit_review' }
        let!(:draft_note) { create(:draft_note, note: note, merge_request: merge_request, author: developer) }

        it 'submits the users current review' do
          _, _, message = service.execute(content, merge_request)

          expect { draft_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
          expect(message).to eq('Submitted the current review.')
        end
      end
    end
1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763

    context 'relate command' do
      let_it_be_with_refind(:group) { create(:group) }

      shared_examples 'relate command' do
        it 'relates issues' do
          service.execute(content, issue)

          expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
        end
      end

      context 'user is member of group' do
        before do
          group.add_developer(developer)
        end

        context 'relate a single issue' do
          let(:other_issue) { create(:issue, project: project) }
          let(:issues_related) { [other_issue] }
          let(:content) { "/relate #{other_issue.to_reference}" }

          it_behaves_like 'relate command'
        end

        context 'relate multiple issues at once' do
          let(:second_issue) { create(:issue, project: project) }
          let(:third_issue) { create(:issue, project: project) }
          let(:issues_related) { [second_issue, third_issue] }
          let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }

          it_behaves_like 'relate command'
        end

        context 'empty relate command' do
          let(:issues_related) { [] }
          let(:content) { '/relate' }

          it_behaves_like 'relate command'
        end

        context 'already having related issues' do
          let(:second_issue) { create(:issue, project: project) }
          let(:third_issue) { create(:issue, project: project) }
          let(:issues_related) { [second_issue, third_issue] }
          let(:content) { "/relate #{third_issue.to_reference(project)}" }

          before do
            create(:issue_link, source: issue, target: second_issue)
          end

          it_behaves_like 'relate command'
        end

        context 'cross project' do
          let(:another_group) { create(:group, :public) }
          let(:other_project) { create(:project, group: another_group) }

          before do
            another_group.add_developer(developer)
          end

          context 'relate a cross project issue' do
            let(:other_issue) { create(:issue, project: other_project) }
            let(:issues_related) { [other_issue] }
            let(:content) { "/relate #{other_issue.to_reference(project)}" }

            it_behaves_like 'relate command'
          end

          context 'relate multiple cross projects issues at once' do
            let(:second_issue) { create(:issue, project: other_project) }
            let(:third_issue) { create(:issue, project: other_project) }
            let(:issues_related) { [second_issue, third_issue] }
            let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }

            it_behaves_like 'relate command'
          end

          context 'relate a non-existing issue' do
            let(:issues_related) { [] }
            let(:content) { "/relate imaginary##{non_existing_record_iid}" }

            it_behaves_like 'relate command'
          end

          context 'relate a private issue' do
            let(:private_project) { create(:project, :private) }
            let(:other_issue) { create(:issue, project: private_project) }
            let(:issues_related) { [] }
            let(:content) { "/relate #{other_issue.to_reference(project)}" }

            it_behaves_like 'relate command'
          end
        end
      end
    end
1764
  end
1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812

  describe '#explain' do
    let(:service) { described_class.new(project, developer) }
    let(:merge_request) { create(:merge_request, source_project: project) }

    describe 'close command' do
      let(:content) { '/close' }

      it 'includes issuable name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Closes this issue.'])
      end
    end

    describe 'reopen command' do
      let(:content) { '/reopen' }
      let(:merge_request) { create(:merge_request, :closed, source_project: project) }

      it 'includes issuable name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Reopens this merge request.'])
      end
    end

    describe 'title command' do
      let(:content) { '/title This is new title' }

      it 'includes new title' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Changes the title to "This is new title".'])
      end
    end

    describe 'assign command' do
      let(:content) { "/assign @#{developer.username} do it!" }

      it 'includes only the user reference' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(["Assigns @#{developer.username}."])
      end
    end

    describe 'unassign command' do
      let(:content) { '/unassign' }
1813
      let(:issue) { create(:issue, project: project, assignees: [developer]) }
1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861

      it 'includes current assignee reference' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Removes assignee @#{developer.username}."])
      end
    end

    describe 'milestone command' do
      let(:content) { '/milestone %wrong-milestone' }
      let!(:milestone) { create(:milestone, project: project, title: '9.10') }

      it 'is empty when milestone reference is wrong' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq([])
      end
    end

    describe 'remove milestone command' do
      let(:content) { '/remove_milestone' }
      let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }

      it 'includes current milestone name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Removes %"9.10" milestone.'])
      end
    end

    describe 'label command' do
      let(:content) { '/label ~missing' }
      let!(:label) { create(:label, project: project) }

      it 'is empty when there are no correct labels' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq([])
      end
    end

    describe 'unlabel command' do
      let(:content) { '/unlabel' }

      it 'says all labels if no parameter provided' do
        merge_request.update!(label_ids: [bug.id])
        _, explanations = service.explain(content, merge_request)

1862
        expect(explanations).to eq([_('Removes all labels.')])
1863 1864 1865 1866
      end
    end

    describe 'relabel command' do
1867
      let(:content) { "/relabel #{bug.title}" }
1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908
      let(:feature) { create(:label, project: project, title: 'Feature') }

      it 'includes label name' do
        issue.update!(label_ids: [feature.id])
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."])
      end
    end

    describe 'subscribe command' do
      let(:content) { '/subscribe' }

      it 'includes issuable name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Subscribes to this issue.'])
      end
    end

    describe 'unsubscribe command' do
      let(:content) { '/unsubscribe' }

      it 'includes issuable name' do
        merge_request.subscribe(developer, project)
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Unsubscribes from this merge request.'])
      end
    end

    describe 'due command' do
      let(:content) { '/due April 1st 2016' }

      it 'includes the date' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Sets the due date to Apr 1, 2016.'])
      end
    end

1909 1910
    describe 'draft command' do
      let(:content) { '/draft' }
1911 1912 1913 1914

      it 'includes the new status' do
        _, explanations = service.explain(content, merge_request)

1915
        expect(explanations).to eq(['Marks this merge request as Draft.'])
1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944
      end
    end

    describe 'award command' do
      let(:content) { '/award :confetti_ball: ' }

      it 'includes the emoji' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(['Toggles :confetti_ball: emoji award.'])
      end
    end

    describe 'estimate command' do
      let(:content) { '/estimate 79d' }

      it 'includes the formatted duration' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.'])
      end
    end

    describe 'spend command' do
      let(:content) { '/spend -120m' }

      it 'includes the formatted duration and proper verb' do
        _, explanations = service.explain(content, issue)

1945
        expect(explanations).to eq(['Subtracts 2h spent time.'])
1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959
      end
    end

    describe 'target branch command' do
      let(:content) { '/target_branch my-feature ' }

      it 'includes the branch name' do
        _, explanations = service.explain(content, merge_request)

        expect(explanations).to eq(['Sets target branch to my-feature.'])
      end
    end

    describe 'board move command' do
1960
      let(:content) { "/board_move ~#{bug.title}" }
1961 1962 1963 1964 1965 1966 1967 1968
      let!(:board) { create(:board, project: project) }

      it 'includes the label name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
      end
    end
1969 1970 1971 1972 1973 1974 1975 1976 1977 1978

    describe 'move issue to another project command' do
      let(:content) { '/move test/project' }

      it 'includes the project name' do
        _, explanations = service.explain(content, issue)

        expect(explanations).to eq(["Moves this issue to test/project."])
      end
    end
1979 1980

    describe 'tag a commit' do
1981 1982 1983
      describe 'with a tag name' do
        context 'without a message' do
          let(:content) { '/tag v1.2.3' }
1984

1985 1986
          it 'includes the tag name only' do
            _, explanations = service.explain(content, commit)
1987

1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010
            expect(explanations).to eq(["Tags this commit to v1.2.3."])
          end
        end

        context 'with an empty message' do
          let(:content) { '/tag v1.2.3 ' }

          it 'includes the tag name only' do
            _, explanations = service.explain(content, commit)

            expect(explanations).to eq(["Tags this commit to v1.2.3."])
          end
        end
      end

      describe 'with a tag name and message' do
        let(:content) { '/tag v1.2.3 Stable release' }

        it 'includes the tag name and message' do
          _, explanations = service.explain(content, commit)

          expect(explanations).to eq(["Tags this commit to v1.2.3 with \"Stable release\"."])
        end
2011 2012
      end
    end
2013 2014 2015 2016 2017 2018 2019 2020

    describe 'create a merge request' do
      context 'with no branch name' do
        let(:content) { '/create_merge_request' }

        it 'uses the default branch name' do
          _, explanations = service.explain(content, issue)

2021
          expect(explanations).to eq([_('Creates a branch and a merge request to resolve this issue.')])
2022
        end
2023 2024 2025 2026

        it 'returns the execution message using the default branch name' do
          _, _, message = service.execute(content, issue)

2027
          expect(message).to eq(_('Created a branch and a merge request to resolve this issue.'))
2028
        end
2029 2030 2031 2032 2033 2034 2035 2036
      end

      context 'with a branch name' do
        let(:content) { '/create_merge_request foo' }

        it 'uses the given branch name' do
          _, explanations = service.explain(content, issue)

2037
          expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue."])
2038
        end
2039 2040 2041 2042

        it 'returns the execution message using the given branch name' do
          _, _, message = service.execute(content, issue)

2043
          expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue.")
2044
        end
2045 2046
      end
    end
2047

Ryan Cobb's avatar
Ryan Cobb committed
2048
    describe "#commands_executed_count" do
2049 2050 2051 2052 2053 2054 2055 2056
      it 'counts commands executed' do
        content = "/close and \n/assign me and \n/title new title"

        service.execute(content, issue)

        expect(service.commands_executed_count).to eq(3)
      end
    end
2057
  end
2058
end