milestone_spec.rb 18.3 KB
Newer Older
1 2
# frozen_string_literal: true

D
Dmitriy Zaporozhets 已提交
3 4
require 'spec_helper'

5
describe Milestone do
6 7 8 9 10
  describe 'modules' do
    context 'with a project' do
      it_behaves_like 'AtomicInternalId' do
        let(:internal_id_attribute) { :iid }
        let(:instance) { build(:milestone, project: build(:project), group: nil) }
11
        let(:scope) { :project }
12 13 14 15 16 17 18 19 20
        let(:scope_attrs) { { project: instance.project } }
        let(:usage) { :milestones }
      end
    end

    context 'with a group' do
      it_behaves_like 'AtomicInternalId' do
        let(:internal_id_attribute) { :iid }
        let(:instance) { build(:milestone, project: nil, group: build(:group)) }
21
        let(:scope) { :group }
22 23 24 25 26 27
        let(:scope_attrs) { { namespace: instance.group } }
        let(:usage) { :milestones }
      end
    end
  end

28
  describe "Validation" do
29 30 31 32
    before do
      allow(subject).to receive(:set_iid).and_return(false)
    end

V
Valery Sizov 已提交
33
    describe 'start_date' do
34
      it 'adds an error when start_date is greater then due_date' do
V
Valery Sizov 已提交
35 36 37
        milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)

        expect(milestone).not_to be_valid
38
        expect(milestone.errors[:due_date]).to include("must be greater than start date")
V
Valery Sizov 已提交
39
      end
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

      it 'adds an error when start_date is greater than 9999-12-31' do
        milestone = build(:milestone, start_date: Date.new(10000, 1, 1))

        expect(milestone).not_to be_valid
        expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
      end
    end

    describe 'due_date' do
      it 'adds an error when due_date is greater than 9999-12-31' do
        milestone = build(:milestone, due_date: Date.new(10000, 1, 1))

        expect(milestone).not_to be_valid
        expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
      end
V
Valery Sizov 已提交
56
    end
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75

    describe 'milestone_release' do
      let(:milestone) { build(:milestone, project: project) }

      context 'when it is tied to a release for another project' do
        it 'creates a validation error' do
          other_project = create(:project)
          milestone.release = build(:release, project: other_project)
          expect(milestone).not_to be_valid
        end
      end

      context 'when it is tied to a release for the same project' do
        it 'is valid' do
          milestone.release = build(:release, project: project)
          expect(milestone).to be_valid
        end
      end
    end
V
Valery Sizov 已提交
76 77 78 79 80
  end

  describe "Associations" do
    it { is_expected.to belong_to(:project) }
    it { is_expected.to have_many(:issues) }
81
    it { is_expected.to have_one(:release) }
82 83
  end

84
  let(:project) { create(:project, :public) }
85 86
  let(:milestone) { create(:milestone, project: project) }
  let(:issue) { create(:issue, project: project) }
87
  let(:user) { create(:user) }
88

89
  describe "#title" do
90
    let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
91 92

    it "sanitizes title" do
93
      expect(milestone.title).to eq("foo & bar -> 2.2")
94 95 96
    end
  end

F
Felipe Artur 已提交
97 98 99
  describe "unique milestone title" do
    context "per project" do
      it "does not accept the same title in a project twice" do
100
        new_milestone = described_class.new(project: milestone.project, title: milestone.title)
F
Felipe Artur 已提交
101 102 103 104
        expect(new_milestone).not_to be_valid
      end

      it "accepts the same title in another project" do
105
        project = create(:project)
106
        new_milestone = described_class.new(project: project, title: milestone.title)
F
Felipe Artur 已提交
107 108 109

        expect(new_milestone).to be_valid
      end
110 111
    end

F
Felipe Artur 已提交
112 113 114 115 116 117 118 119 120
    context "per group" do
      let(:group) { create(:group) }
      let(:milestone) { create(:milestone, group: group) }

      before do
        project.update(group: group)
      end

      it "does not accept the same title in a group twice" do
121
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
122 123 124

        expect(new_milestone).not_to be_valid
      end
125

F
Felipe Artur 已提交
126 127 128
      it "does not accept the same title of a child project milestone" do
        create(:milestone, project: group.projects.first)

129
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
130 131 132

        expect(new_milestone).not_to be_valid
      end
133 134 135
    end
  end

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
  describe '.order_by_name_asc' do
    it 'sorts by name ascending' do
      milestone1 = create(:milestone, title: 'Foo')
      milestone2 = create(:milestone, title: 'Bar')

      expect(described_class.order_by_name_asc).to eq([milestone2, milestone1])
    end
  end

  describe '.reorder_by_due_date_asc' do
    it 'reorders the input relation' do
      milestone1 = create(:milestone, due_date: Date.new(2018, 9, 30))
      milestone2 = create(:milestone, due_date: Date.new(2018, 10, 20))

      expect(described_class.reorder_by_due_date_asc).to eq([milestone1, milestone2])
    end
  end

154
  describe "#percent_complete" do
155
    it "does not count open issues" do
156
      milestone.issues << issue
157
      expect(milestone.percent_complete(user)).to eq(0)
158 159
    end

160
    it "counts closed issues" do
A
Andrew8xx8 已提交
161
      issue.close
162
      milestone.issues << issue
163
      expect(milestone.percent_complete(user)).to eq(100)
164
    end
165

166
    it "recovers from dividing by zero" do
167
      expect(milestone.percent_complete(user)).to eq(0)
168 169 170
    end
  end

171
  describe '#expired?' do
172 173
    context "expired" do
      before do
174
        allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
175 176
      end

177 178 179
      it 'returns true when due_date is in the past' do
        expect(milestone.expired?).to be_truthy
      end
180 181 182 183
    end

    context "not expired" do
      before do
184
        allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
185 186
      end

187 188 189
      it 'returns false when due_date is in the future' do
        expect(milestone.expired?).to be_falsey
      end
190 191 192
    end
  end

V
Valery Sizov 已提交
193
  describe '#upcoming?' do
194
    it 'returns true when start_date is in the future' do
V
Valery Sizov 已提交
195 196 197 198
      milestone = build(:milestone, start_date: Time.now + 1.month)
      expect(milestone.upcoming?).to be_truthy
    end

199
    it 'returns false when start_date is in the past' do
V
Valery Sizov 已提交
200 201 202 203 204
      milestone = build(:milestone, start_date: Date.today.prev_year)
      expect(milestone.upcoming?).to be_falsey
    end
  end

205
  describe '#can_be_closed?' do
206
    it { expect(milestone.can_be_closed?).to be_truthy }
207
  end
A
Andrew8xx8 已提交
208

209
  describe '#can_be_closed?' do
210
    before do
211 212
      milestone = create :milestone, project: project
      create :closed_issue, milestone: milestone, project: project
213

214
      create :issue, project: project
215
    end
A
Andrew8xx8 已提交
216

217
    it 'returns true if milestone active and all nested issues closed' do
218
      expect(milestone.can_be_closed?).to be_truthy
A
Andrew8xx8 已提交
219 220
    end

221
    it 'returns false if milestone active and not all nested issues closed' do
222
      issue.milestone = milestone
A
Andrey Kumanyaev 已提交
223
      issue.save
A
Andrew8xx8 已提交
224

225
      expect(milestone.can_be_closed?).to be_falsey
A
Andrew8xx8 已提交
226 227 228
    end
  end

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
  describe '.search' do
    let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }

    it 'returns milestones with a matching title' do
      expect(described_class.search(milestone.title)).to eq([milestone])
    end

    it 'returns milestones with a partially matching title' do
      expect(described_class.search(milestone.title[0..2])).to eq([milestone])
    end

    it 'returns milestones with a matching title regardless of the casing' do
      expect(described_class.search(milestone.title.upcase)).to eq([milestone])
    end

    it 'returns milestones with a matching description' do
      expect(described_class.search(milestone.description)).to eq([milestone])
    end

    it 'returns milestones with a partially matching description' do
249 250
      expect(described_class.search(milestone.description[0..2]))
        .to eq([milestone])
251 252 253
    end

    it 'returns milestones with a matching description regardless of the casing' do
254 255
      expect(described_class.search(milestone.description.upcase))
        .to eq([milestone])
256 257
    end
  end
258

J
Jacopo 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  describe '#search_title' do
    let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }

    it 'returns milestones with a matching title' do
      expect(described_class.search_title(milestone.title)) .to eq([milestone])
    end

    it 'returns milestones with a partially matching title' do
      expect(described_class.search_title(milestone.title[0..2])).to eq([milestone])
    end

    it 'returns milestones with a matching title regardless of the casing' do
      expect(described_class.search_title(milestone.title.upcase))
        .to eq([milestone])
    end

    it 'searches only on the title and ignores milestones with a matching description' do
      create(:milestone, title: 'bar', description: 'foo')

      expect(described_class.search_title(milestone.title)) .to eq([milestone])
    end
  end

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
  describe '#for_projects_and_groups' do
    let(:project) { create(:project) }
    let(:project_other) { create(:project) }
    let(:group) { create(:group) }
    let(:group_other) { create(:group) }

    before do
      create(:milestone, project: project)
      create(:milestone, project: project_other)
      create(:milestone, group: group)
      create(:milestone, group: group_other)
    end

    subject { described_class.for_projects_and_groups(projects, groups) }

    shared_examples 'filters by projects and groups' do
      it 'returns milestones filtered by project' do
        milestones = described_class.for_projects_and_groups(projects, [])

        expect(milestones.count).to eq(1)
        expect(milestones.first.project_id).to eq(project.id)
      end

      it 'returns milestones filtered by group' do
        milestones = described_class.for_projects_and_groups([], groups)

        expect(milestones.count).to eq(1)
        expect(milestones.first.group_id).to eq(group.id)
      end

      it 'returns milestones filtered by both project and group' do
        milestones = described_class.for_projects_and_groups(projects, groups)

        expect(milestones.count).to eq(2)
        expect(milestones).to contain_exactly(project.milestones.first, group.milestones.first)
      end
    end

    context 'ids as params' do
      let(:projects) { [project.id] }
      let(:groups) { [group.id] }

      it_behaves_like 'filters by projects and groups'
    end

    context 'relations as params' do
328 329
      let(:projects) { Project.where(id: project.id).select(:id) }
      let(:groups) { Group.where(id: group.id).select(:id) }
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347

      it_behaves_like 'filters by projects and groups'
    end

    context 'objects as params' do
      let(:projects) { [project] }
      let(:groups) { [group] }

      it_behaves_like 'filters by projects and groups'
    end

    it 'returns no records if projects and groups are nil' do
      milestones = described_class.for_projects_and_groups(nil, nil)

      expect(milestones).to be_empty
    end
  end

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
  describe '.upcoming_ids' do
    let(:group_1) { create(:group) }
    let(:group_2) { create(:group) }
    let(:group_3) { create(:group) }
    let(:groups) { [group_1, group_2, group_3] }

    let!(:past_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now - 1.day) }
    let!(:current_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 1.day) }
    let!(:future_milestone_group_1) { create(:milestone, group: group_1, due_date: Time.now + 2.days) }

    let!(:past_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now - 1.day) }
    let!(:closed_milestone_group_2) { create(:milestone, :closed, group: group_2, due_date: Time.now + 1.day) }
    let!(:current_milestone_group_2) { create(:milestone, group: group_2, due_date: Time.now + 2.days) }

    let!(:past_milestone_group_3) { create(:milestone, group: group_3, due_date: Time.now - 1.day) }

364 365 366
    let(:project_1) { create(:project) }
    let(:project_2) { create(:project) }
    let(:project_3) { create(:project) }
367 368 369 370 371 372 373 374 375 376 377 378
    let(:projects) { [project_1, project_2, project_3] }

    let!(:past_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now - 1.day) }
    let!(:current_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 1.day) }
    let!(:future_milestone_project_1) { create(:milestone, project: project_1, due_date: Time.now + 2.days) }

    let!(:past_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now - 1.day) }
    let!(:closed_milestone_project_2) { create(:milestone, :closed, project: project_2, due_date: Time.now + 1.day) }
    let!(:current_milestone_project_2) { create(:milestone, project: project_2, due_date: Time.now + 2.days) }

    let!(:past_milestone_project_3) { create(:milestone, project: project_3, due_date: Time.now - 1.day) }

379
    let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) }
380 381 382 383 384 385 386 387

    it 'returns the next upcoming open milestone ID for each project and group' do
      expect(milestone_ids).to contain_exactly(
        current_milestone_project_1.id,
        current_milestone_project_2.id,
        current_milestone_group_1.id,
        current_milestone_group_2.id
      )
388 389
    end

390
    context 'when the projects and groups have no open upcoming milestones' do
391
      let(:projects) { [project_3] }
392
      let(:groups) { [group_3] }
393 394 395 396 397 398

      it 'returns no results' do
        expect(milestone_ids).to be_empty
      end
    end
  end
399 400

  describe '#to_reference' do
401 402 403 404 405 406 407 408
    let(:group) { build_stubbed(:group) }
    let(:project) { build_stubbed(:project, name: 'sample-project') }
    let(:another_project) { build_stubbed(:project, name: 'another-project', namespace: project.namespace) }

    context 'for a project milestone' do
      let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }

      it 'returns a String reference to the object' do
409
        expect(milestone.to_reference).to eq '%"milestone"'
410 411 412 413 414
      end

      it 'returns a reference by name when the format is set to :name' do
        expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
      end
415

416
      it 'supports a cross-project reference' do
417
        expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
418
      end
419 420
    end

421 422 423
    context 'for a group milestone' do
      let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }

424 425
      it 'returns a group milestone reference with a default format' do
        expect(milestone.to_reference).to eq '%"milestone"'
426 427 428 429 430 431
      end

      it 'returns a reference by name when the format is set to :name' do
        expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
      end

432
      it 'does supports cross-project references within a group' do
433 434
        expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
      end
435 436 437 438 439

      it 'raises an error when using iid format' do
        expect { milestone.to_reference(format: :iid) }
          .to raise_error(ArgumentError, 'Cannot refer to a group milestone by an internal id!')
      end
440 441
    end
  end
442

443 444 445 446 447 448 449 450 451
  describe '#reference_link_text' do
    let(:project) { build_stubbed(:project, name: 'sample-project') }
    let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }

    it 'returns the title with the reference prefix' do
      expect(milestone.reference_link_text).to eq '%milestone'
    end
  end

452
  describe '#participants' do
453
    let(:project) { build(:project, name: 'sample-project') }
454 455 456 457 458 459 460 461 462 463
    let(:milestone) { build(:milestone, iid: 1, project: project) }

    it 'returns participants without duplicates' do
      user = create :user
      create :issue, project: project, milestone: milestone, assignees: [user]
      create :issue, project: project, milestone: milestone, assignees: [user]

      expect(milestone.participants).to eq [user]
    end
  end
464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

  describe '.sort_by_attribute' do
    set(:milestone_1) { create(:milestone, title: 'Foo') }
    set(:milestone_2) { create(:milestone, title: 'Bar') }
    set(:milestone_3) { create(:milestone, title: 'Zoo') }

    context 'ordering by name ascending' do
      it 'sorts by title ascending' do
        expect(described_class.sort_by_attribute('name_asc'))
          .to eq([milestone_2, milestone_1, milestone_3])
      end
    end

    context 'ordering by name descending' do
      it 'sorts by title descending' do
        expect(described_class.sort_by_attribute('name_desc'))
          .to eq([milestone_3, milestone_1, milestone_2])
      end
    end
  end
484 485 486 487

  describe '.states_count' do
    context 'when the projects have milestones' do
      before do
E
Eagllus 已提交
488 489 490 491 492
        project_1 = create(:project)
        project_2 = create(:project)
        group_1 = create(:group)
        group_2 = create(:group)

493 494
        create(:active_milestone, title: 'Active Group Milestone', project: project_1)
        create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
E
Eagllus 已提交
495
        create(:active_milestone, title: 'Active Group Milestone', project: project_2)
496
        create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
E
Eagllus 已提交
497 498 499 500
        create(:closed_milestone, title: 'Active Group Milestone', group: group_1)
        create(:closed_milestone, title: 'Closed Group Milestone', group: group_1)
        create(:closed_milestone, title: 'Active Group Milestone', group: group_2)
        create(:closed_milestone, title: 'Closed Group Milestone', group: group_2)
501 502 503
      end

      it 'returns the quantity of milestones in each possible state' do
E
Eagllus 已提交
504 505 506
        expected_count = { opened: 5, closed: 6, all: 11 }

        count = described_class.states_count(Project.all, Group.all)
507 508 509 510 511 512 513 514 515 516 517 518 519 520
        expect(count).to eq(expected_count)
      end
    end

    context 'when the projects do not have milestones' do
      it 'returns 0 as the quantity of global milestones in each state' do
        expected_count = { opened: 0, closed: 0, all: 0 }

        count = described_class.states_count([project])

        expect(count).to eq(expected_count)
      end
    end
  end
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536

  describe '.reference_pattern' do
    subject { described_class.reference_pattern }

    it { is_expected.to match('gitlab-org/gitlab-ce%123') }
    it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
  end

  describe '.link_reference_pattern' do
    subject { described_class.link_reference_pattern }

    it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/milestones/123") }
    it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/-/milestones/123") }
    it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/issues/123") }
    it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
  end
D
Dmitriy Zaporozhets 已提交
537
end