milestone_spec.rb 16.8 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1 2
require 'spec_helper'

3
describe Milestone do
4 5 6 7 8
  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) }
9
        let(:scope) { :project }
10 11 12 13 14 15 16 17 18
        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)) }
19
        let(:scope) { :group }
20 21 22 23 24 25
        let(:scope_attrs) { { namespace: instance.group } }
        let(:usage) { :milestones }
      end
    end
  end

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

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

        expect(milestone).not_to be_valid
36
        expect(milestone.errors[:due_date]).to include("must be greater than start date")
V
Valery Sizov 已提交
37 38 39 40 41 42 43
      end
    end
  end

  describe "Associations" do
    it { is_expected.to belong_to(:project) }
    it { is_expected.to have_many(:issues) }
44 45
  end

46
  let(:project) { create(:project, :public) }
47 48
  let(:milestone) { create(:milestone, project: project) }
  let(:issue) { create(:issue, project: project) }
49
  let(:user) { create(:user) }
50

51
  describe "#title" do
52
    let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
53 54

    it "sanitizes title" do
55
      expect(milestone.title).to eq("foo & bar -> 2.2")
56 57 58
    end
  end

F
Felipe Artur 已提交
59 60 61
  describe "unique milestone title" do
    context "per project" do
      it "does not accept the same title in a project twice" do
62
        new_milestone = described_class.new(project: milestone.project, title: milestone.title)
F
Felipe Artur 已提交
63 64 65 66
        expect(new_milestone).not_to be_valid
      end

      it "accepts the same title in another project" do
67
        project = create(:project)
68
        new_milestone = described_class.new(project: project, title: milestone.title)
F
Felipe Artur 已提交
69 70 71

        expect(new_milestone).to be_valid
      end
72 73
    end

F
Felipe Artur 已提交
74 75 76 77 78 79 80 81 82
    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
83
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
84 85 86

        expect(new_milestone).not_to be_valid
      end
87

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

91
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
92 93 94

        expect(new_milestone).not_to be_valid
      end
95 96 97
    end
  end

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
  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

116
  describe "#percent_complete" do
117
    it "does not count open issues" do
118
      milestone.issues << issue
119
      expect(milestone.percent_complete(user)).to eq(0)
120 121
    end

122
    it "counts closed issues" do
A
Andrew8xx8 已提交
123
      issue.close
124
      milestone.issues << issue
125
      expect(milestone.percent_complete(user)).to eq(100)
126
    end
127

128
    it "recovers from dividing by zero" do
129
      expect(milestone.percent_complete(user)).to eq(0)
130 131 132
    end
  end

133
  describe '#expired?' do
134 135
    context "expired" do
      before do
136
        allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
137 138
      end

139 140 141
      it 'returns true when due_date is in the past' do
        expect(milestone.expired?).to be_truthy
      end
142 143 144 145
    end

    context "not expired" do
      before do
146
        allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
147 148
      end

149 150 151
      it 'returns false when due_date is in the future' do
        expect(milestone.expired?).to be_falsey
      end
152 153 154
    end
  end

V
Valery Sizov 已提交
155
  describe '#upcoming?' do
156
    it 'returns true when start_date is in the future' do
V
Valery Sizov 已提交
157 158 159 160
      milestone = build(:milestone, start_date: Time.now + 1.month)
      expect(milestone.upcoming?).to be_truthy
    end

161
    it 'returns false when start_date is in the past' do
V
Valery Sizov 已提交
162 163 164 165 166
      milestone = build(:milestone, start_date: Date.today.prev_year)
      expect(milestone.upcoming?).to be_falsey
    end
  end

167
  describe '#percent_complete' do
168
    before do
169
      allow(milestone).to receive_messages(
170 171 172 173 174
        closed_items_count: 3,
        total_items_count: 4
      )
    end

175
    it { expect(milestone.percent_complete(user)).to eq(75) }
176 177
  end

178
  describe '#can_be_closed?' do
179
    it { expect(milestone.can_be_closed?).to be_truthy }
180
  end
A
Andrew8xx8 已提交
181

182
  describe '#total_items_count' do
183
    before do
184
      create :closed_issue, milestone: milestone, project: project
185
      create :merge_request, milestone: milestone
186
    end
A
Andrew8xx8 已提交
187

188
    it 'returns total count of issues and merge requests assigned to milestone' do
189
      expect(milestone.total_items_count(user)).to eq 2
A
Andrew8xx8 已提交
190 191 192
    end
  end

193
  describe '#can_be_closed?' do
194
    before do
A
Andrew8xx8 已提交
195
      milestone = create :milestone
196 197
      create :closed_issue, milestone: milestone

198
      create :issue
199
    end
A
Andrew8xx8 已提交
200

201
    it 'returns true if milestone active and all nested issues closed' do
202
      expect(milestone.can_be_closed?).to be_truthy
A
Andrew8xx8 已提交
203 204
    end

205
    it 'returns false if milestone active and not all nested issues closed' do
206
      issue.milestone = milestone
A
Andrey Kumanyaev 已提交
207
      issue.save
A
Andrew8xx8 已提交
208

209
      expect(milestone.can_be_closed?).to be_falsey
A
Andrew8xx8 已提交
210 211 212
    end
  end

213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
  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
233 234
      expect(described_class.search(milestone.description[0..2]))
        .to eq([milestone])
235 236 237
    end

    it 'returns milestones with a matching description regardless of the casing' do
238 239
      expect(described_class.search(milestone.description.upcase))
        .to eq([milestone])
240 241
    end
  end
242

J
Jacopo 已提交
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
  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

266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 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
  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
312 313
      let(:projects) { Project.where(id: project.id).select(:id) }
      let(:groups) { Group.where(id: group.id).select(:id) }
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331

      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

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
  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) }

348 349 350
    let(:project_1) { create(:project) }
    let(:project_2) { create(:project) }
    let(:project_3) { create(:project) }
351 352 353 354 355 356 357 358 359 360 361 362
    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) }

363
    let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) }
364 365 366 367 368 369 370 371

    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
      )
372 373
    end

374
    context 'when the projects and groups have no open upcoming milestones' do
375
      let(:projects) { [project_3] }
376
      let(:groups) { [group_3] }
377 378 379 380 381 382

      it 'returns no results' do
        expect(milestone_ids).to be_empty
      end
    end
  end
383 384

  describe '#to_reference' do
385 386 387 388 389 390 391 392
    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
393
        expect(milestone.to_reference).to eq '%"milestone"'
394 395 396 397 398
      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
399

400
      it 'supports a cross-project reference' do
401
        expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
402
      end
403 404
    end

405 406 407
    context 'for a group milestone' do
      let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }

408 409
      it 'returns a group milestone reference with a default format' do
        expect(milestone.to_reference).to eq '%"milestone"'
410 411 412 413 414 415
      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

416
      it 'does supports cross-project references within a group' do
417 418
        expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
      end
419 420 421 422 423

      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
424 425
    end
  end
426

427 428 429 430 431 432 433 434 435
  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

436
  describe '#participants' do
437
    let(:project) { build(:project, name: 'sample-project') }
438 439 440 441 442 443 444 445 446 447
    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
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467

  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
468 469 470 471

  describe '.states_count' do
    context 'when the projects have milestones' do
      before do
E
Eagllus 已提交
472 473 474 475 476
        project_1 = create(:project)
        project_2 = create(:project)
        group_1 = create(:group)
        group_2 = create(:group)

477 478
        create(:active_milestone, title: 'Active Group Milestone', project: project_1)
        create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
E
Eagllus 已提交
479
        create(:active_milestone, title: 'Active Group Milestone', project: project_2)
480
        create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
E
Eagllus 已提交
481 482 483 484
        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)
485 486 487
      end

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

        count = described_class.states_count(Project.all, Group.all)
491 492 493 494 495 496 497 498 499 500 501 502 503 504
        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
D
Dmitriy Zaporozhets 已提交
505
end