issue_spec.rb 20.5 KB
Newer Older
gitlabhq's avatar
gitlabhq committed
1 2
require 'spec_helper'

Douwe Maan's avatar
Douwe Maan committed
3
describe Issue, models: true do
gitlabhq's avatar
gitlabhq committed
4
  describe "Associations" do
5
    it { is_expected.to belong_to(:milestone) }
gitlabhq's avatar
gitlabhq committed
6 7
  end

8
  describe 'modules' do
9 10 11
    subject { described_class }

    it { is_expected.to include_module(InternalId) }
12
    it { is_expected.to include_module(Issuable) }
13 14 15
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
    it { is_expected.to include_module(Taskable) }
16 17
  end

18
  subject { create(:issue) }
19

20 21 22 23 24
  describe "act_as_paranoid" do
    it { is_expected.to have_db_column(:deleted_at) }
    it { is_expected.to have_db_index(:deleted_at) }
  end

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
  describe '#order_by_position_and_priority' do
    let(:project) { create :empty_project }
    let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
    let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
    let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
    let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
    let!(:issue3) { create(:issue, project: project, relative_position: 100) }
    let!(:issue4) { create(:issue, project: project, relative_position: 200) }

    it 'returns ordered list' do
      expect(project.issues.order_by_position_and_priority).
        to match [issue3, issue4, issue1, issue2]
    end
  end

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
  describe '#closed_at' do
    after do
      Timecop.return
    end

    let!(:now) { Timecop.freeze(Time.now) }

    it 'sets closed_at to Time.now when issue is closed' do
      issue = create(:issue, state: 'opened')

      issue.close

      expect(issue.closed_at).to eq(now)
    end

    it 'sets closed_at to nil when issue is reopened' do
      issue = create(:issue, state: 'closed')

      issue.reopen

      expect(issue.closed_at).to be_nil
    end
  end

64
  describe '#to_reference' do
65 66 67 68
    let(:namespace) { build(:namespace, path: 'sample-namespace') }
    let(:project)   { build(:empty_project, name: 'sample-project', namespace: namespace) }
    let(:issue)     { build(:issue, iid: 1, project: project) }
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
69

70 71 72 73
    context 'when nil argument' do
      it 'returns issue id' do
        expect(issue.to_reference).to eq "#1"
      end
74 75
    end

76
    context 'when full is true' do
77
      it 'returns complete path to the issue' do
78 79 80
        expect(issue.to_reference(full: true)).to          eq 'sample-namespace/sample-project#1'
        expect(issue.to_reference(project, full: true)).to eq 'sample-namespace/sample-project#1'
        expect(issue.to_reference(group, full: true)).to   eq 'sample-namespace/sample-project#1'
81
      end
82 83
    end

84 85 86 87 88 89 90 91 92 93 94 95
    context 'when same project argument' do
      it 'returns issue id' do
        expect(issue.to_reference(project)).to eq("#1")
      end
    end

    context 'when cross namespace project argument' do
      let(:another_namespace_project) { create(:empty_project, name: 'another-project') }

      it 'returns complete path to the issue' do
        expect(issue.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project#1'
      end
96 97
    end

98
    it 'supports a cross-project reference' do
99
      another_project = build(:empty_project, name: 'another-project', namespace: project.namespace)
100
      expect(issue.to_reference(another_project)).to eq "sample-project#1"
101
    end
102

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
    context 'when same namespace / cross-project argument' do
      let(:another_project) { create(:empty_project, namespace: namespace) }

      it 'returns path to the issue with the project name' do
        expect(issue.to_reference(another_project)).to eq 'sample-project#1'
      end
    end

    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace') }
      let(:another_project)   { create(:empty_project, path: 'another-project', namespace: another_namespace) }

      it 'returns full path to the issue' do
        expect(issue.to_reference(another_project)).to eq 'sample-namespace/sample-project#1'
      end
    end

    context 'when argument is a namespace' do
      context 'with same project path' do
        it 'returns path to the issue with the project name' do
          expect(issue.to_reference(namespace)).to eq 'sample-project#1'
        end
      end

      context 'with different project path' do
        it 'returns full path to the issue' do
          expect(issue.to_reference(group)).to eq 'sample-namespace/sample-project#1'
        end
      end
132
    end
133 134
  end

135 136
  describe '#is_being_reassigned?' do
    it 'returns true if the issue assignee has changed' do
137
      subject.assignee = create(:user)
138
      expect(subject.is_being_reassigned?).to be_truthy
139 140
    end
    it 'returns false if the issue assignee has not changed' do
141
      expect(subject.is_being_reassigned?).to be_falsey
142 143
    end
  end
144 145

  describe '#is_being_reassigned?' do
Johannes Schleifenbaum's avatar
Johannes Schleifenbaum committed
146
    it 'returns issues assigned to user' do
147 148
      user = create(:user)
      create_list(:issue, 2, assignee: user)
149

150
      expect(Issue.open_for(user).count).to eq 2
151 152
    end
  end
153

154
  describe '#closed_by_merge_requests' do
155 156 157
    let(:project) { create(:project, :repository) }
    let(:issue) { create(:issue, project: project)}
    let(:closed_issue) { build(:issue, :closed, project: project)}
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180

    let(:mr) do
      opts = {
        title: 'Awesome merge_request',
        description: "Fixes #{issue.to_reference}",
        source_branch: 'feature',
        target_branch: 'master'
      }
      MergeRequests::CreateService.new(project, project.owner, opts).execute
    end

    let(:closed_mr) do
      opts = {
        title: 'Awesome merge_request 2',
        description: "Fixes #{issue.to_reference}",
        source_branch: 'feature',
        target_branch: 'master',
        state: 'closed'
      }
      MergeRequests::CreateService.new(project, project.owner, opts).execute
    end

    it 'returns the merge request to close this issue' do
181
      mr
182

183
      expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
184 185
    end

186 187 188
    it "returns an empty array when the merge request is closed already" do
      closed_mr

189
      expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
190 191
    end

192
    it "returns an empty array when the current issue is closed already" do
193
      expect(closed_issue.closed_by_merge_requests(closed_issue.author)).to eq([])
194 195 196
    end
  end

197 198
  describe '#referenced_merge_requests' do
    it 'returns the referenced merge requests' do
199
      project = create(:empty_project, :public)
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221

      mr1 = create(:merge_request,
                   source_project: project,
                   source_branch:  'master',
                   target_branch:  'feature')

      mr2 = create(:merge_request,
                   source_project: project,
                   source_branch:  'feature',
                   target_branch:  'master')

      issue = create(:issue, description: mr1.to_reference, project: project)

      create(:note_on_issue,
             noteable:   issue,
             note:       mr2.to_reference,
             project_id: project.id)

      expect(issue.referenced_merge_requests).to eq([mr1, mr2])
    end
  end

222 223 224 225 226 227 228 229 230 231
  describe '#can_move?' do
    let(:user) { create(:user) }
    let(:issue) { create(:issue) }
    subject { issue.can_move?(user) }

    context 'user is not a member of project issue belongs to' do
      it { is_expected.to eq false}
    end

    context 'user is reporter in project issue belongs to' do
232
      let(:project) { create(:empty_project) }
233 234 235 236 237 238
      let(:issue) { create(:issue, project: project) }

      before { project.team << [user, :reporter] }

      it { is_expected.to eq true }

239 240 241 242 243
      context 'issue not persisted' do
        let(:issue) { build(:issue, project: project) }
        it { is_expected.to eq false }
      end

244 245
      context 'checking destination project also' do
        subject { issue.can_move?(user, to_project) }
246
        let(:to_project) { create(:empty_project) }
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

        context 'destination project allowed' do
          before { to_project.team << [user, :reporter] }
          it { is_expected.to eq true }
        end

        context 'destination project not allowed' do
          before { to_project.team << [user, :guest] }
          it { is_expected.to eq false }
        end
      end
    end
  end

  describe '#moved?' do
    let(:issue) { create(:issue) }
    subject { issue.moved? }

    context 'issue not moved' do
      it { is_expected.to eq false }
    end

    context 'issue already moved' do
      let(:moved_to_issue) { create(:issue) }
      let(:issue) { create(:issue, moved_to: moved_to_issue) }

      it { is_expected.to eq true }
    end
  end

277
  describe '#related_branches' do
278
    let(:user) { build(:admin) }
279

280
    before do
281
      allow(subject.project.repository).to receive(:branch_names).
282
                                            and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
283 284 285 286 287 288 289 290

      # Without this stub, the `create(:merge_request)` above fails because it can't find
      # the source branch. This seems like a reasonable compromise, in comparison with
      # setting up a full repo here.
      allow_any_instance_of(MergeRequest).to receive(:create_merge_request_diff)
    end

    it "selects the right branches when there are no referenced merge requests" do
291
      expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"])
292
    end
293

294
    it "selects the right branches when there is a referenced merge request" do
Timothy Andrew's avatar
Timothy Andrew committed
295 296
      merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
                                               source_project: subject.project,
297
                                               source_branch: "#{subject.iid}-branch" })
298
      merge_request.create_cross_references!(user)
299
      expect(subject.referenced_merge_requests(user)).not_to be_empty
300
      expect(subject.related_branches(user)).to eq([subject.to_branch_name])
301
    end
302 303 304 305 306

    it 'excludes stable branches from the related branches' do
      allow(subject.project.repository).to receive(:branch_names).
        and_return(["#{subject.iid}-0-stable"])

307
      expect(subject.related_branches(user)).to eq []
308
    end
309 310
  end

311
  it_behaves_like 'an editable mentionable' do
312
    subject { create(:issue, project: create(:project, :repository)) }
313

314
    let(:backref_text) { "issue #{subject.to_reference}" }
315 316
    let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
  end
Vinnie Okada's avatar
Vinnie Okada committed
317 318 319 320

  it_behaves_like 'a Taskable' do
    let(:subject) { create :issue }
  end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
321 322

  describe "#to_branch_name" do
323
    let(:issue) { create(:issue, title: 'testing-issue') }
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
324

325
    it 'starts with the issue iid' do
326
      expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
327
    end
328 329

    it "contains the issue title if not confidential" do
330
      expect(issue.to_branch_name).to match /testing-issue\z/
331 332 333 334
    end

    it "does not contain the issue title if confidential" do
      issue = create(:issue, title: 'testing-issue', confidential: true)
335
      expect(issue.to_branch_name).to match /confidential-issue\z/
336
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
337
  end
Yorick Peterse's avatar
Yorick Peterse committed
338 339 340

  describe '#participants' do
    context 'using a public project' do
341
      let(:project) { create(:empty_project, :public) }
Yorick Peterse's avatar
Yorick Peterse committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
      let(:issue) { create(:issue, project: project) }

      let!(:note1) do
        create(:note_on_issue, noteable: issue, project: project, note: 'a')
      end

      let!(:note2) do
        create(:note_on_issue, noteable: issue, project: project, note: 'b')
      end

      it 'includes the issue author' do
        expect(issue.participants).to include(issue.author)
      end

      it 'includes the authors of the notes' do
        expect(issue.participants).to include(note1.author, note2.author)
      end
    end

    context 'using a private project' do
      it 'does not include mentioned users that do not have access to the project' do
363
        project = create(:empty_project)
Yorick Peterse's avatar
Yorick Peterse committed
364 365 366 367 368 369 370 371 372 373 374 375
        user = create(:user)
        issue = create(:issue, project: project)

        create(:note_on_issue,
               noteable: issue,
               project: project,
               note: user.to_reference)

        expect(issue.participants).not_to include(user)
      end
    end
  end
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392

  describe 'cached counts' do
    it 'updates when assignees change' do
      user1 = create(:user)
      user2 = create(:user)
      issue = create(:issue, assignee: user1)

      expect(user1.assigned_open_issues_count).to eq(1)
      expect(user2.assigned_open_issues_count).to eq(0)

      issue.assignee = user2
      issue.save

      expect(user1.assigned_open_issues_count).to eq(0)
      expect(user2.assigned_open_issues_count).to eq(1)
    end
  end
393 394

  describe '#visible_to_user?' do
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
    context 'without a user' do
      let(:issue) { build(:issue) }

      it 'returns true when the issue is publicly visible' do
        expect(issue).to receive(:publicly_visible?).and_return(true)

        expect(issue.visible_to_user?).to eq(true)
      end

      it 'returns false when the issue is not publicly visible' do
        expect(issue).to receive(:publicly_visible?).and_return(false)

        expect(issue.visible_to_user?).to eq(false)
      end
    end

411
    context 'with a user' do
412
      let(:user) { create(:user) }
413 414 415 416 417 418 419 420 421 422 423 424 425 426
      let(:issue) { build(:issue) }

      it 'returns true when the issue is readable' do
        expect(issue).to receive(:readable_by?).with(user).and_return(true)

        expect(issue.visible_to_user?(user)).to eq(true)
      end

      it 'returns false when the issue is not readable' do
        expect(issue).to receive(:readable_by?).with(user).and_return(false)

        expect(issue.visible_to_user?(user)).to eq(false)
      end

427 428
      it 'returns false when feature is disabled' do
        expect(issue).not_to receive(:readable_by?)
429

430
        issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
431

432
        expect(issue.visible_to_user?(user)).to eq(false)
433 434
      end

435 436
      it 'returns false when restricted for members' do
        expect(issue).not_to receive(:readable_by?)
437

438 439 440
        issue.project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PRIVATE)

        expect(issue.visible_to_user?(user)).to eq(false)
441 442 443 444 445 446 447 448 449 450 451 452
      end
    end

    describe 'with a regular user that is not a team member' do
      let(:user) { create(:user) }

      context 'using a public project' do
        let(:project) { create(:empty_project, :public) }

        it 'returns true for a regular issue' do
          issue = build(:issue, project: project)

453
          expect(issue.visible_to_user?(user)).to eq(true)
454 455 456 457 458
        end

        it 'returns false for a confidential issue' do
          issue = build(:issue, project: project, confidential: true)

459
          expect(issue.visible_to_user?(user)).to eq(false)
460 461 462 463 464 465 466 467 468 469
        end
      end

      context 'using an internal project' do
        let(:project) { create(:empty_project, :internal) }

        context 'using an internal user' do
          it 'returns true for a regular issue' do
            issue = build(:issue, project: project)

470
            expect(issue.visible_to_user?(user)).to eq(true)
471 472 473 474 475
          end

          it 'returns false for a confidential issue' do
            issue = build(:issue, :confidential, project: project)

476
            expect(issue.visible_to_user?(user)).to eq(false)
477 478 479 480 481 482 483 484 485 486 487
          end
        end

        context 'using an external user' do
          before do
            allow(user).to receive(:external?).and_return(true)
          end

          it 'returns false for a regular issue' do
            issue = build(:issue, project: project)

488
            expect(issue.visible_to_user?(user)).to eq(false)
489 490 491 492 493
          end

          it 'returns false for a confidential issue' do
            issue = build(:issue, :confidential, project: project)

494
            expect(issue.visible_to_user?(user)).to eq(false)
495 496 497 498 499 500 501 502 503 504
          end
        end
      end

      context 'using a private project' do
        let(:project) { create(:empty_project, :private) }

        it 'returns false for a regular issue' do
          issue = build(:issue, project: project)

505
          expect(issue.visible_to_user?(user)).to eq(false)
506 507 508 509 510
        end

        it 'returns false for a confidential issue' do
          issue = build(:issue, :confidential, project: project)

511
          expect(issue.visible_to_user?(user)).to eq(false)
512 513 514
        end

        context 'when the user is the project owner' do
515 516
          before { project.team << [user, :master] }

517 518 519
          it 'returns true for a regular issue' do
            issue = build(:issue, project: project)

520
            expect(issue.visible_to_user?(user)).to eq(true)
521 522 523 524 525
          end

          it 'returns true for a confidential issue' do
            issue = build(:issue, :confidential, project: project)

526
            expect(issue.visible_to_user?(user)).to eq(true)
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543
          end
        end
      end
    end

    context 'with a regular user that is a team member' do
      let(:user) { create(:user) }
      let(:project) { create(:empty_project, :public) }

      context 'using a public project' do
        before do
          project.team << [user, Gitlab::Access::DEVELOPER]
        end

        it 'returns true for a regular issue' do
          issue = build(:issue, project: project)

544
          expect(issue.visible_to_user?(user)).to eq(true)
545 546 547 548 549
        end

        it 'returns true for a confidential issue' do
          issue = build(:issue, :confidential, project: project)

550
          expect(issue.visible_to_user?(user)).to eq(true)
551 552 553 554 555 556 557 558 559 560 561 562 563
        end
      end

      context 'using an internal project' do
        let(:project) { create(:empty_project, :internal) }

        before do
          project.team << [user, Gitlab::Access::DEVELOPER]
        end

        it 'returns true for a regular issue' do
          issue = build(:issue, project: project)

564
          expect(issue.visible_to_user?(user)).to eq(true)
565 566 567 568 569
        end

        it 'returns true for a confidential issue' do
          issue = build(:issue, :confidential, project: project)

570
          expect(issue.visible_to_user?(user)).to eq(true)
571 572 573 574 575 576 577 578 579 580 581 582 583
        end
      end

      context 'using a private project' do
        let(:project) { create(:empty_project, :private) }

        before do
          project.team << [user, Gitlab::Access::DEVELOPER]
        end

        it 'returns true for a regular issue' do
          issue = build(:issue, project: project)

584
          expect(issue.visible_to_user?(user)).to eq(true)
585 586 587 588 589
        end

        it 'returns true for a confidential issue' do
          issue = build(:issue, :confidential, project: project)

590
          expect(issue.visible_to_user?(user)).to eq(true)
591 592 593 594 595 596
        end
      end
    end

    context 'with an admin user' do
      let(:project) { create(:empty_project) }
597
      let(:user) { create(:admin) }
598 599 600 601

      it 'returns true for a regular issue' do
        issue = build(:issue, project: project)

602
        expect(issue.visible_to_user?(user)).to eq(true)
603 604 605 606 607
      end

      it 'returns true for a confidential issue' do
        issue = build(:issue, :confidential, project: project)

608
        expect(issue.visible_to_user?(user)).to eq(true)
609 610 611 612 613 614 615 616 617 618 619
      end
    end
  end

  describe '#publicly_visible?' do
    context 'using a public project' do
      let(:project) { create(:empty_project, :public) }

      it 'returns true for a regular issue' do
        issue = build(:issue, project: project)

620
        expect(issue).to be_truthy
621 622 623 624 625
      end

      it 'returns false for a confidential issue' do
        issue = build(:issue, :confidential, project: project)

626
        expect(issue).not_to be_falsy
627 628 629 630 631 632 633 634 635
      end
    end

    context 'using an internal project' do
      let(:project) { create(:empty_project, :internal) }

      it 'returns false for a regular issue' do
        issue = build(:issue, project: project)

636
        expect(issue).not_to be_falsy
637 638 639 640 641
      end

      it 'returns false for a confidential issue' do
        issue = build(:issue, :confidential, project: project)

642
        expect(issue).not_to be_falsy
643 644 645 646 647 648 649 650 651
      end
    end

    context 'using a private project' do
      let(:project) { create(:empty_project, :private) }

      it 'returns false for a regular issue' do
        issue = build(:issue, project: project)

652
        expect(issue).not_to be_falsy
653 654 655 656 657
      end

      it 'returns false for a confidential issue' do
        issue = build(:issue, :confidential, project: project)

658
        expect(issue).not_to be_falsy
659 660 661
      end
    end
  end
662 663 664 665 666 667 668 669 670 671 672

  describe '#hook_attrs' do
    let(:attrs_hash) { subject.hook_attrs }

    it 'includes time tracking attrs' do
      expect(attrs_hash).to include(:total_time_spent)
      expect(attrs_hash).to include(:human_time_estimate)
      expect(attrs_hash).to include(:human_total_time_spent)
      expect(attrs_hash).to include('time_estimate')
    end
  end
gitlabhq's avatar
gitlabhq committed
673
end