issuable_spec.rb 30.8 KB
Newer Older
1 2
# frozen_string_literal: true

3 4
require 'spec_helper'

5
RSpec.describe Issuable do
6 7
  include ProjectForksHelper

Sean McGivern's avatar
Sean McGivern committed
8
  let(:issuable_class) { Issue }
9
  let(:issue) { create(:issue, title: 'An issue', description: 'A description') }
10
  let(:user) { create(:user) }
11 12

  describe "Associations" do
Sean McGivern's avatar
Sean McGivern committed
13 14
    subject { build(:issue) }

15 16 17
    it { is_expected.to belong_to(:project) }
    it { is_expected.to belong_to(:author) }
    it { is_expected.to have_many(:notes).dependent(:destroy) }
18
    it { is_expected.to have_many(:todos).dependent(:destroy) }
19
    it { is_expected.to have_many(:labels) }
Robert May's avatar
Robert May committed
20
    it { is_expected.to have_many(:note_authors).through(:notes) }
21 22 23 24 25 26 27 28 29 30

    context 'Notes' do
      let!(:note) { create(:note, noteable: issue, project: issue.project) }
      let(:scoped_issue) { Issue.includes(notes: :author).find(issue.id) }

      it 'indicates if the notes have their authors loaded' do
        expect(issue.notes).not_to be_authors_loaded
        expect(scoped_issue.notes).to be_authors_loaded
      end
    end
31 32
  end

33
  describe 'Included modules' do
Sean McGivern's avatar
Sean McGivern committed
34 35
    let(:described_class) { issuable_class }

36 37 38
    it { is_expected.to include_module(Awardable) }
  end

39
  describe "Validation" do
40 41 42 43 44 45
    context 'general validations' do
      subject { build(:issue) }

      before do
        allow(InternalId).to receive(:generate_next).and_return(nil)
      end
Sean McGivern's avatar
Sean McGivern committed
46

47 48 49 50
      it { is_expected.to validate_presence_of(:project) }
      it { is_expected.to validate_presence_of(:iid) }
      it { is_expected.to validate_presence_of(:author) }
      it { is_expected.to validate_presence_of(:title) }
Stan Hu's avatar
Stan Hu committed
51 52
      it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
      it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
53 54 55

      it_behaves_like 'validates description length with custom validation'
      it_behaves_like 'truncates the description to its allowed maximum length on import'
56
    end
57 58 59
  end

  describe "Scope" do
Sean McGivern's avatar
Sean McGivern committed
60 61 62
    it { expect(issuable_class).to respond_to(:opened) }
    it { expect(issuable_class).to respond_to(:closed) }
    it { expect(issuable_class).to respond_to(:assigned) }
63 64
  end

65 66 67 68 69 70 71 72 73 74 75 76 77
  describe 'author_name' do
    it 'is delegated to author' do
      expect(issue.author_name).to eq issue.author.name
    end

    it 'returns nil when author is nil' do
      issue.author_id = nil
      issue.save(validate: false)

      expect(issue.author_name).to eq nil
    end
  end

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
  describe '.initialize' do
    it 'maps the state to the right state_id' do
      described_class::STATE_ID_MAP.each do |key, value|
        issuable = MergeRequest.new(state: key)

        expect(issuable.state).to eq(key)
        expect(issuable.state_id).to eq(value)
      end
    end

    it 'maps a string version of the state to the right state_id' do
      described_class::STATE_ID_MAP.each do |key, value|
        issuable = MergeRequest.new('state' => key)

        expect(issuable.state).to eq(key)
        expect(issuable.state_id).to eq(value)
      end
    end

    it 'gives preference to state_id if present' do
      issuable = MergeRequest.new('state' => 'opened',
                                  'state_id' => described_class::STATE_ID_MAP['merged'])

      expect(issuable.state).to eq('merged')
      expect(issuable.state_id).to eq(described_class::STATE_ID_MAP['merged'])
    end
  end

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
  describe '.any_label' do
    let_it_be(:issue_with_label) { create(:labeled_issue, labels: [create(:label)]) }
    let_it_be(:issue_with_multiple_labels) { create(:labeled_issue, labels: [create(:label), create(:label)]) }
    let_it_be(:issue_without_label) { create(:issue) }

    it 'returns an issuable with at least one label' do
      expect(issuable_class.any_label).to match_array([issue_with_label, issue_with_multiple_labels])
    end

    context 'for custom sorting' do
      it 'returns an issuable with at least one label' do
        expect(issuable_class.any_label('created_at')).to eq([issue_with_label, issue_with_multiple_labels])
      end
    end
  end

122
  describe ".search" do
123
    let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
124
    let!(:searchable_issue2) { create(:issue, title: 'Aw') }
125

126
    it 'returns issues with a matching title' do
127 128
      expect(issuable_class.search(searchable_issue.title))
        .to eq([searchable_issue])
129 130
    end

131
    it 'returns issues with a partially matching title' do
Sean McGivern's avatar
Sean McGivern committed
132
      expect(issuable_class.search('able')).to eq([searchable_issue])
133
    end
134

135
    it 'returns issues with a matching title regardless of the casing' do
136 137
      expect(issuable_class.search(searchable_issue.title.upcase))
        .to eq([searchable_issue])
138
    end
139 140 141 142 143

    it 'returns issues with a fuzzy matching title' do
      expect(issuable_class.search('searchable issue')).to eq([searchable_issue])
    end

144 145
    it 'returns issues with a matching title for a query shorter than 3 chars' do
      expect(issuable_class.search(searchable_issue2.title.downcase)).to eq([searchable_issue2])
146
    end
147 148 149 150
  end

  describe ".full_search" do
    let!(:searchable_issue) do
151
      create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
152
    end
153

154
    let!(:searchable_issue2) { create(:issue, title: "Aw", description: "Cu") }
155

156
    it 'returns issues with a matching title' do
157 158
      expect(issuable_class.full_search(searchable_issue.title))
        .to eq([searchable_issue])
159 160
    end

161
    it 'returns issues with a partially matching title' do
Sean McGivern's avatar
Sean McGivern committed
162
      expect(issuable_class.full_search('able')).to eq([searchable_issue])
163 164
    end

165
    it 'returns issues with a matching title regardless of the casing' do
166 167
      expect(issuable_class.full_search(searchable_issue.title.upcase))
        .to eq([searchable_issue])
168 169
    end

170 171 172 173 174
    it 'returns issues with a fuzzy matching title' do
      expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue])
    end

    it 'returns issues with a matching description' do
175 176
      expect(issuable_class.full_search(searchable_issue.description))
        .to eq([searchable_issue])
177 178
    end

179
    it 'returns issues with a partially matching description' do
180
      expect(issuable_class.full_search('cut')).to eq([searchable_issue])
181 182
    end

183
    it 'returns issues with a matching description regardless of the casing' do
184 185
      expect(issuable_class.full_search(searchable_issue.description.upcase))
        .to eq([searchable_issue])
186
    end
187 188 189 190 191

    it 'returns issues with a fuzzy matching description' do
      expect(issuable_class.full_search('many kittens')).to eq([searchable_issue])
    end

192 193
    it 'returns issues with a matching description for a query shorter than 3 chars' do
      expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2])
194
    end
195

196 197 198 199 200 201 202 203 204 205
    it 'returns issues with a fuzzy matching description for a query shorter than 3 chars if told to do so' do
      search = searchable_issue2.description.downcase.scan(/\w+/).sample[-1]

      expect(issuable_class.full_search(search, use_minimum_char_limit: false)).to include(searchable_issue2)
    end

    it 'returns issues with a fuzzy matching title for a query shorter than 3 chars if told to do so' do
      expect(issuable_class.full_search('i', use_minimum_char_limit: false)).to include(searchable_issue)
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
206
    context 'when matching columns is "title"' do
207 208 209 210 211 212 213 214 215 216 217
      it 'returns issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title'))
          .to eq([searchable_issue])
      end

      it 'returns no issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title'))
          .to be_empty
      end
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
218
    context 'when matching columns is "description"' do
219 220 221 222 223 224 225 226 227 228 229
      it 'returns no issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'description'))
          .to be_empty
      end

      it 'returns issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'description'))
          .to eq([searchable_issue])
      end
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
230
    context 'when matching columns is "title,description"' do
231 232 233 234 235 236 237 238 239 240 241
      it 'returns issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title,description'))
          .to eq([searchable_issue])
      end

      it 'returns issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title,description'))
          .to eq([searchable_issue])
      end
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
242
    context 'when matching columns is nil"' do
243 244 245 246 247 248 249 250 251 252 253
      it 'returns issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: nil))
          .to eq([searchable_issue])
      end

      it 'returns issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: nil))
          .to eq([searchable_issue])
      end
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
254
    context 'when matching columns is "invalid"' do
255 256 257 258 259 260 261 262 263 264 265
      it 'returns issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'invalid'))
          .to eq([searchable_issue])
      end

      it 'returns issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'invalid'))
          .to eq([searchable_issue])
      end
    end

Hiroyuki Sato's avatar
Hiroyuki Sato committed
266
    context 'when matching columns is "title,invalid"' do
267 268 269 270 271 272 273 274 275 276
      it 'returns issues with a matching title' do
        expect(issuable_class.full_search(searchable_issue.title, matched_columns: 'title,invalid'))
          .to eq([searchable_issue])
      end

      it 'returns no issues with a matching description' do
        expect(issuable_class.full_search(searchable_issue.description, matched_columns: 'title,invalid'))
          .to be_empty
      end
    end
277 278
  end

279 280 281 282 283
  describe '.to_ability_name' do
    it { expect(Issue.to_ability_name).to eq("issue") }
    it { expect(MergeRequest.to_ability_name).to eq("merge_request") }
  end

284 285 286
  describe "#today?" do
    it "returns true when created today" do
      # Avoid timezone differences and just return exactly what we want
287 288
      allow(Date).to receive(:today).and_return(issue.created_at.to_date)
      expect(issue.today?).to be_truthy
289 290 291
    end

    it "returns false when not created today" do
292 293
      allow(Date).to receive(:today).and_return(Date.yesterday)
      expect(issue.today?).to be_falsey
294 295 296 297
    end
  end

  describe "#new?" do
298 299
    it "returns false when created 30 hours ago" do
      allow(issue).to receive(:created_at).and_return(Time.current - 30.hours)
300
      expect(issue.new?).to be_falsey
301 302
    end

303 304 305
    it "returns true when created 20 hours ago" do
      allow(issue).to receive(:created_at).and_return(Time.current - 20.hours)
      expect(issue.new?).to be_truthy
306 307
    end
  end
308

309
  describe "#sort_by_attribute" do
310
    let(:project) { create(:project) }
311 312

    context "by milestone due date" do
313 314 315 316
      # Correct order is:
      # Issues/MRs with milestones ordered by date
      # Issues/MRs with milestones without dates
      # Issues/MRs without milestones
317

318 319 320 321 322 323 324 325
      let!(:issue) { create(:issue, project: project) }
      let!(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
      let!(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
      let!(:issue1) { create(:issue, project: project, milestone: early_milestone) }
      let!(:issue2) { create(:issue, project: project, milestone: late_milestone) }
      let!(:issue3) { create(:issue, project: project) }

      it "sorts desc" do
326
        issues = project.issues.sort_by_attribute('milestone_due_desc')
327 328
        expect(issues).to match_array([issue2, issue1, issue, issue3])
      end
329

330
      it "sorts asc" do
331
        issues = project.issues.sort_by_attribute('milestone_due_asc')
332
        expect(issues).to match_array([issue1, issue2, issue, issue3])
333 334
      end
    end
335 336 337

    context 'when all of the results are level on the sort key' do
      let!(:issues) do
Ryan Cobb's avatar
Ryan Cobb committed
338
        create_list(:issue, 10, project: project)
339 340 341 342
      end

      it 'has no duplicates across pages' do
        sorted_issue_ids = 1.upto(10).map do |i|
343
          project.issues.sort_by_attribute('milestone_due_desc').page(i).per(1).first.id
344 345 346 347 348
        end

        expect(sorted_issue_ids).to eq(sorted_issue_ids.uniq)
      end
    end
349 350
  end

351
  describe '#subscribed?' do
352 353
    let(:project) { issue.project }

354
    context 'user is not a participant in the issue' do
355 356 357
      before do
        allow(issue).to receive(:participants).with(user).and_return([])
      end
358 359

      it 'returns false when no subcription exists' do
360
        expect(issue.subscribed?(user, project)).to be_falsey
361 362 363
      end

      it 'returns true when a subcription exists and subscribed is true' do
364
        issue.subscriptions.create(user: user, project: project, subscribed: true)
365

366
        expect(issue.subscribed?(user, project)).to be_truthy
367 368 369
      end

      it 'returns false when a subcription exists and subscribed is false' do
370
        issue.subscriptions.create(user: user, project: project, subscribed: false)
371

372
        expect(issue.subscribed?(user, project)).to be_falsey
373 374 375 376
      end
    end

    context 'user is a participant in the issue' do
377 378 379
      before do
        allow(issue).to receive(:participants).with(user).and_return([user])
      end
380 381

      it 'returns false when no subcription exists' do
382
        expect(issue.subscribed?(user, project)).to be_truthy
383 384 385
      end

      it 'returns true when a subcription exists and subscribed is true' do
386
        issue.subscriptions.create(user: user, project: project, subscribed: true)
387

388
        expect(issue.subscribed?(user, project)).to be_truthy
389 390 391
      end

      it 'returns false when a subcription exists and subscribed is false' do
392
        issue.subscriptions.create(user: user, project: project, subscribed: false)
393

394
        expect(issue.subscribed?(user, project)).to be_falsey
395 396 397 398
      end
    end
  end

399 400 401 402 403 404 405 406
  describe '#time_estimate=' do
    it 'coerces the value below Gitlab::Database::MAX_INT_VALUE' do
      expect { issue.time_estimate = 100 }.to change { issue.time_estimate }.to(100)
      expect { issue.time_estimate = Gitlab::Database::MAX_INT_VALUE + 100 }.to change { issue.time_estimate }.to(Gitlab::Database::MAX_INT_VALUE)
    end

    it 'skips coercion for not Integer values' do
      expect { issue.time_estimate = nil }.to change { issue.time_estimate }.to(nil)
407 408
      expect { issue.time_estimate = 'invalid time' }.not_to raise_error
      expect { issue.time_estimate = 22.33 }.not_to raise_error
409 410 411
    end
  end

412
  describe '#to_hook_data' do
413 414
    let(:builder) { double }

415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
    context 'when old_associations is empty' do
      let(:label) { create(:label) }

      before do
        issue.update!(labels: [label])
        issue.assignees << user
        issue.spend_time(duration: 2, user_id: user.id, spent_at: Time.current)
        expect(Gitlab::HookData::IssuableBuilder)
          .to receive(:new).with(issue).and_return(builder)
      end

      it 'delegates to Gitlab::HookData::IssuableBuilder#build and does not set labels, assignees, nor total_time_spent' do
        expect(builder).to receive(:build).with(
          user: user,
          changes: {})

        # In some cases, old_associations is empty, e.g. on a close event
        issue.to_hook_data(user)
      end
    end

436 437
    context 'labels are updated' do
      let(:labels) { create_list(:label, 2) }
438

439 440
      before do
        issue.update(labels: [labels[1]])
441 442
        expect(Gitlab::HookData::IssuableBuilder)
          .to receive(:new).with(issue).and_return(builder)
443 444
      end

445
      it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
446 447 448 449 450
        expect(builder).to receive(:build).with(
          user: user,
          changes: hash_including(
            'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]]
          ))
451

452
        issue.to_hook_data(user, old_associations: { labels: [labels[0]] })
453 454 455 456 457
      end
    end

    context 'total_time_spent is updated' do
      before do
458
        issue.spend_time(duration: 2, user_id: user.id, spent_at: Time.current)
459
        issue.save
460 461
        expect(Gitlab::HookData::IssuableBuilder)
          .to receive(:new).with(issue).and_return(builder)
462 463 464
      end

      it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
465 466 467
        expect(builder).to receive(:build).with(
          user: user,
          changes: hash_including(
468
            'total_time_spent' => [1, 2]
469 470
          ))

471
        issue.to_hook_data(user, old_associations: { total_time_spent: 1 })
472
      end
473 474
    end

475 476
    context 'issue is assigned' do
      let(:user2) { create(:user) }
477

478
      before do
479
        issue.assignees << user << user2
480 481
        expect(Gitlab::HookData::IssuableBuilder)
          .to receive(:new).with(issue).and_return(builder)
482
      end
483

484 485 486 487 488 489 490
      it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
        expect(builder).to receive(:build).with(
          user: user,
          changes: hash_including(
            'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
          ))

491
        issue.to_hook_data(user, old_associations: { assignees: [user] })
492 493 494
      end
    end

495
    context 'merge_request is assigned' do
496
      let(:merge_request) { create(:merge_request) }
497
      let(:user2) { create(:user) }
498 499

      before do
500 501
        merge_request.update(assignees: [user])
        merge_request.update(assignees: [user, user2])
502 503
        expect(Gitlab::HookData::IssuableBuilder)
          .to receive(:new).with(merge_request).and_return(builder)
504
      end
505

506 507 508 509
      it 'delegates to Gitlab::HookData::IssuableBuilder#build' do
        expect(builder).to receive(:build).with(
          user: user,
          changes: hash_including(
510
            'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
511 512
          ))

513
        merge_request.to_hook_data(user, old_associations: { assignees: [user] })
514
      end
515 516
    end
  end
517

518
  describe '#labels_array' do
519
    let(:project) { create(:project) }
520 521 522
    let(:bug) { create(:label, project: project, title: 'bug') }
    let(:issue) { create(:issue, project: project) }

523
    before do
524 525 526 527 528 529 530 531
      issue.labels << bug
    end

    it 'loads the association and returns it as an array' do
      expect(issue.reload.labels_array).to eq([bug])
    end
  end

532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
  describe '.labels_hash' do
    let(:feature_label) { create(:label, title: 'Feature') }
    let(:second_label) { create(:label, title: 'Second Label') }
    let!(:issues) { create_list(:labeled_issue, 3, labels: [feature_label, second_label]) }
    let(:issue_id) { issues.first.id }

    it 'maps issue ids to labels titles' do
      expect(Issue.labels_hash[issue_id]).to include('Feature')
    end

    it 'works on relations filtered by multiple labels' do
      relation = Issue.with_label(['Feature', 'Second Label'])

      expect(relation.labels_hash[issue_id]).to include('Feature', 'Second Label')
    end

    # This tests the workaround for the lack of a NOT NULL constraint in
    # label_links.label_id:
    # https://gitlab.com/gitlab-org/gitlab/issues/197307
    context 'with a NULL label ID in the link' do
      let(:issue) { create(:labeled_issue, labels: [feature_label, second_label]) }

      before do
        label_link = issue.label_links.find_by(label_id: second_label.id)
        label_link.label_id = nil
        label_link.save(validate: false)
      end

      it 'filters out bad labels' do
        expect(Issue.where(id: issue.id).labels_hash[issue.id]).to match_array(['Feature'])
      end
    end
  end

566
  describe '#user_notes_count' do
567
    let(:project) { create(:project) }
568 569 570 571 572 573 574 575 576 577 578 579 580 581
    let(:issue1) { create(:issue, project: project) }
    let(:issue2) { create(:issue, project: project) }

    before do
      create_list(:note, 3, noteable: issue1, project: project)
      create_list(:note, 6, noteable: issue2, project: project)
    end

    it 'counts the user notes' do
      expect(issue1.user_notes_count).to be(3)
      expect(issue2.user_notes_count).to be(6)
    end
  end

582
  describe "votes" do
583 584
    let(:project) { issue.project }

585
    before do
586 587
      create(:award_emoji, :upvote, awardable: issue)
      create(:award_emoji, :downvote, awardable: issue)
588 589 590 591 592
    end

    it "returns correct values" do
      expect(issue.upvotes).to eq(1)
      expect(issue.downvotes).to eq(1)
593 594 595 596
    end
  end

  describe '.order_due_date_and_labels_priority' do
597
    let(:project) { create(:project) }
598 599 600 601 602 603 604 605 606 607

    def create_issue(milestone, labels)
      create(:labeled_issue, milestone: milestone, labels: labels, project: project)
    end

    it 'sorts issues in order of milestone due date, then label priority' do
      first_priority = create(:label, project: project, priority: 1)
      second_priority = create(:label, project: project, priority: 2)
      no_priority = create(:label, project: project)

608 609
      first_milestone = create(:milestone, project: project, due_date: Time.current)
      second_milestone = create(:milestone, project: project, due_date: Time.current + 1.month)
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
      third_milestone = create(:milestone, project: project)

      # The issues here are ordered by label priority, to ensure that we don't
      # accidentally just sort by creation date.
      second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority])
      third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority])
      first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority])
      second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority])
      no_milestone_second_priority = create_issue(nil, [second_priority, no_priority])
      first_milestone_no_priority = create_issue(first_milestone, [no_priority])
      second_milestone_no_labels = create_issue(second_milestone, [])
      third_milestone_no_priority = create_issue(third_milestone, [no_priority])

      result = Issue.order_due_date_and_labels_priority

      expect(result).to eq([first_milestone_second_priority,
                            first_milestone_no_priority,
                            second_milestone_first_priority,
                            second_milestone_second_priority,
                            second_milestone_no_labels,
                            third_milestone_first_priority,
                            no_milestone_second_priority,
                            third_milestone_no_priority])
633 634
    end
  end
635

636 637 638 639 640 641 642 643 644 645 646 647 648 649
  describe '.order_labels_priority' do
    let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
    let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }

    subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority }

    before do
      issue.labels << label_1
      issue.labels << label_2
    end

    it { is_expected.to eq(2) }
  end

650
  describe ".with_label" do
651
    let(:project) { create(:project, :public) }
652 653 654 655 656 657 658
    let(:bug) { create(:label, project: project, title: 'bug') }
    let(:feature) { create(:label, project: project, title: 'feature') }
    let(:enhancement) { create(:label, project: project, title: 'enhancement') }
    let(:issue1) { create(:issue, title: "Bugfix1", project: project) }
    let(:issue2) { create(:issue, title: "Bugfix2", project: project) }
    let(:issue3) { create(:issue, title: "Feature1", project: project) }

659
    before do
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
      issue1.labels << bug
      issue1.labels << feature
      issue2.labels << bug
      issue2.labels << enhancement
      issue3.labels << feature
    end

    it 'finds the correct issue containing just enhancement label' do
      expect(Issue.with_label(enhancement.title)).to match_array([issue2])
    end

    it 'finds the correct issues containing the same label' do
      expect(Issue.with_label(bug.title)).to match_array([issue1, issue2])
    end

    it 'finds the correct issues containing only both labels' do
      expect(Issue.with_label([bug.title, enhancement.title])).to match_array([issue2])
    end
  end
Yorick Peterse's avatar
Yorick Peterse committed
679

680 681 682 683 684
  describe '#spend_time' do
    let(:user) { create(:user) }
    let(:issue) { create(:issue) }

    def spend_time(seconds)
685
      issue.spend_time(duration: seconds, user_id: user.id)
686 687 688 689
      issue.save!
    end

    context 'adding time' do
690
      it 'updates the total time spent' do
691 692 693 694
        spend_time(1800)

        expect(issue.total_time_spent).to eq(1800)
      end
695 696 697 698 699 700 701 702

      it 'updates issues updated_at' do
        issue

        Timecop.travel(1.minute.from_now) do
          expect { spend_time(1800) }.to change { issue.updated_at }
        end
      end
703 704
    end

705
    context 'subtracting time' do
706 707 708 709
      before do
        spend_time(1800)
      end

710
      it 'updates the total time spent' do
711 712 713 714 715
        spend_time(-900)

        expect(issue.total_time_spent).to eq(900)
      end

716
      context 'when time to subtract exceeds the total time spent' do
717
        it 'raise a validation error' do
718 719 720 721 722 723 724
          Timecop.travel(1.minute.from_now) do
            expect do
              expect do
                spend_time(-3600)
              end.to raise_error(ActiveRecord::RecordInvalid)
            end.not_to change { issue.updated_at }
          end
725 726 727 728
        end
      end
    end
  end
729 730 731

  describe '#first_contribution?' do
    let(:group) { create(:group) }
micael.bergeron's avatar
micael.bergeron committed
732 733
    let(:project) { create(:project, namespace: group) }
    let(:other_project) { create(:project) }
734
    let(:owner) { create(:owner) }
735
    let(:maintainer) { create(:user) }
736 737 738 739 740 741 742 743
    let(:reporter) { create(:user) }
    let(:guest) { create(:user) }

    let(:contributor) { create(:user) }
    let(:first_time_contributor) { create(:user) }

    before do
      group.add_owner(owner)
744
      project.add_maintainer(maintainer)
745 746 747 748
      project.add_reporter(reporter)
      project.add_guest(guest)
      project.add_guest(contributor)
      project.add_guest(first_time_contributor)
749
    end
micael.bergeron's avatar
micael.bergeron committed
750

751
    let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
752
    let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) }
753 754 755
    let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) }

    context "for merge requests" do
756 757
      it "is false for MAINTAINER" do
        mr = create(:merge_request, author: maintainer, target_project: project, source_project: project)
758 759

        expect(mr).not_to be_first_contribution
760 761 762 763
      end

      it "is false for OWNER" do
        mr = create(:merge_request, author: owner, target_project: project, source_project: project)
micael.bergeron's avatar
micael.bergeron committed
764

765
        expect(mr).not_to be_first_contribution
766 767 768 769
      end

      it "is false for REPORTER" do
        mr = create(:merge_request, author: reporter, target_project: project, source_project: project)
micael.bergeron's avatar
micael.bergeron committed
770

771
        expect(mr).not_to be_first_contribution
772 773 774
      end

      it "is true when you don't have any merged MR" do
775 776
        expect(open_mr).to be_first_contribution
        expect(merged_mr).not_to be_first_contribution
777 778
      end

779 780 781
      it "handles multiple projects separately" do
        expect(open_mr).to be_first_contribution
        expect(merged_mr_other_project).not_to be_first_contribution
782 783 784 785 786 787 788
      end
    end

    context "for issues" do
      let(:contributor_issue) { create(:issue, author: contributor, project: project) }
      let(:first_time_contributor_issue) { create(:issue, author: first_time_contributor, project: project) }

789
      it "is false even without merged MR" do
790
        expect(merged_mr).to be
791
        expect(first_time_contributor_issue).not_to be_first_contribution
792
        expect(contributor_issue).not_to be_first_contribution
793 794 795
      end
    end
  end
796

797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820
  describe '#matches_cross_reference_regex?' do
    context "issue description with long path string" do
      let(:mentionable) { build(:issue, description: "/a" * 50000) }

      it_behaves_like 'matches_cross_reference_regex? fails fast'
    end

    context "note with long path string" do
      let(:mentionable) { build(:note, note: "/a" * 50000) }

      it_behaves_like 'matches_cross_reference_regex? fails fast'
    end

    context "note with long path string" do
      let(:project) { create(:project, :public, :repository) }
      let(:mentionable) { project.commit }

      before do
        expect(mentionable.raw).to receive(:message).and_return("/a" * 50000)
      end

      it_behaves_like 'matches_cross_reference_regex? fails fast'
    end
  end
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839

  describe '#supports_time_tracking?' do
    using RSpec::Parameterized::TableSyntax

    where(:issuable_type, :supports_time_tracking) do
      :issue         | true
      :incident      | false
      :merge_request | true
    end

    with_them do
      let(:issuable) { build_stubbed(issuable_type) }

      subject { issuable.supports_time_tracking? }

      it { is_expected.to eq(supports_time_tracking) }
    end
  end

840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
  describe '#supports_severity?' do
    using RSpec::Parameterized::TableSyntax

    where(:issuable_type, :supports_severity) do
      :issue         | false
      :incident      | true
      :merge_request | false
    end

    with_them do
      let(:issuable) { build_stubbed(issuable_type) }

      subject { issuable.supports_severity? }

      it { is_expected.to eq(supports_severity) }
    end
  end

858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874
  describe '#incident?' do
    using RSpec::Parameterized::TableSyntax

    where(:issuable_type, :incident) do
      :issue         | false
      :incident      | true
      :merge_request | false
    end

    with_them do
      let(:issuable) { build_stubbed(issuable_type) }

      subject { issuable.incident? }

      it { is_expected.to eq(incident) }
    end
  end
875

876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
  describe '#supports_issue_type?' do
    using RSpec::Parameterized::TableSyntax

    where(:issuable_type, :supports_issue_type) do
      :issue         | true
      :merge_request | false
    end

    with_them do
      let(:issuable) { build_stubbed(issuable_type) }

      subject { issuable.supports_issue_type? }

      it { is_expected.to eq(supports_issue_type) }
    end
  end

893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
  describe '#severity' do
    subject { issuable.severity }

    context 'when issuable is not an incident' do
      using RSpec::Parameterized::TableSyntax

      where(:issuable_type, :severity) do
        :issue         | 'unknown'
        :merge_request | 'unknown'
      end

      with_them do
        let(:issuable) { build_stubbed(issuable_type) }

        it { is_expected.to eq(severity) }
      end
    end

    context 'when issuable type is an incident' do
      let!(:issuable) { build_stubbed(:incident) }

      context 'when incident does not have issuable_severity' do
        it 'returns default serverity' do
          is_expected.to eq(IssuableSeverity::DEFAULT)
        end
      end

      context 'when incident has issuable_severity' do
        let!(:issuable_severity) { build_stubbed(:issuable_severity, issue: issuable, severity: 'critical') }

        it 'returns issuable serverity' do
          is_expected.to eq('critical')
        end
      end
    end
  end
929
end