milestone_spec.rb 16.9 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 57 58 59 60 61
    end
  end

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

64
  let(:project) { create(:project, :public) }
65 66
  let(:milestone) { create(:milestone, project: project) }
  let(:issue) { create(:issue, project: project) }
67
  let(:user) { create(:user) }
68

69
  describe "#title" do
70
    let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
71 72

    it "sanitizes title" do
73
      expect(milestone.title).to eq("foo & bar -> 2.2")
74 75 76
    end
  end

F
Felipe Artur 已提交
77 78 79
  describe "unique milestone title" do
    context "per project" do
      it "does not accept the same title in a project twice" do
80
        new_milestone = described_class.new(project: milestone.project, title: milestone.title)
F
Felipe Artur 已提交
81 82 83 84
        expect(new_milestone).not_to be_valid
      end

      it "accepts the same title in another project" do
85
        project = create(:project)
86
        new_milestone = described_class.new(project: project, title: milestone.title)
F
Felipe Artur 已提交
87 88 89

        expect(new_milestone).to be_valid
      end
90 91
    end

F
Felipe Artur 已提交
92 93 94 95 96 97 98 99 100
    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
101
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
102 103 104

        expect(new_milestone).not_to be_valid
      end
105

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

109
        new_milestone = described_class.new(group: group, title: milestone.title)
F
Felipe Artur 已提交
110 111 112

        expect(new_milestone).not_to be_valid
      end
113 114 115
    end
  end

116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
  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

134
  describe "#percent_complete" do
135
    it "does not count open issues" do
136
      milestone.issues << issue
137
      expect(milestone.percent_complete(user)).to eq(0)
138 139
    end

140
    it "counts closed issues" do
A
Andrew8xx8 已提交
141
      issue.close
142
      milestone.issues << issue
143
      expect(milestone.percent_complete(user)).to eq(100)
144
    end
145

146
    it "recovers from dividing by zero" do
147
      expect(milestone.percent_complete(user)).to eq(0)
148 149 150
    end
  end

151
  describe '#expired?' do
152 153
    context "expired" do
      before do
154
        allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
155 156
      end

157 158 159
      it 'returns true when due_date is in the past' do
        expect(milestone.expired?).to be_truthy
      end
160 161 162 163
    end

    context "not expired" do
      before do
164
        allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
165 166
      end

167 168 169
      it 'returns false when due_date is in the future' do
        expect(milestone.expired?).to be_falsey
      end
170 171 172
    end
  end

V
Valery Sizov 已提交
173
  describe '#upcoming?' do
174
    it 'returns true when start_date is in the future' do
V
Valery Sizov 已提交
175 176 177 178
      milestone = build(:milestone, start_date: Time.now + 1.month)
      expect(milestone.upcoming?).to be_truthy
    end

179
    it 'returns false when start_date is in the past' do
V
Valery Sizov 已提交
180 181 182 183 184
      milestone = build(:milestone, start_date: Date.today.prev_year)
      expect(milestone.upcoming?).to be_falsey
    end
  end

185
  describe '#can_be_closed?' do
186
    it { expect(milestone.can_be_closed?).to be_truthy }
187
  end
A
Andrew8xx8 已提交
188

189
  describe '#can_be_closed?' do
190
    before do
191 192
      milestone = create :milestone, project: project
      create :closed_issue, milestone: milestone, project: project
193

194
      create :issue, project: project
195
    end
A
Andrew8xx8 已提交
196

197
    it 'returns true if milestone active and all nested issues closed' do
198
      expect(milestone.can_be_closed?).to be_truthy
A
Andrew8xx8 已提交
199 200
    end

201
    it 'returns false if milestone active and not all nested issues closed' do
202
      issue.milestone = milestone
A
Andrey Kumanyaev 已提交
203
      issue.save
A
Andrew8xx8 已提交
204

205
      expect(milestone.can_be_closed?).to be_falsey
A
Andrew8xx8 已提交
206 207 208
    end
  end

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

    it 'returns milestones with a matching description regardless of the casing' do
234 235
      expect(described_class.search(milestone.description.upcase))
        .to eq([milestone])
236 237
    end
  end
238

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

262 263 264 265 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
  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
308 309
      let(:projects) { Project.where(id: project.id).select(:id) }
      let(:groups) { Group.where(id: group.id).select(:id) }
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327

      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

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
  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) }

344 345 346
    let(:project_1) { create(:project) }
    let(:project_2) { create(:project) }
    let(:project_3) { create(:project) }
347 348 349 350 351 352 353 354 355 356 357 358
    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) }

359
    let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) }
360 361 362 363 364 365 366 367

    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
      )
368 369
    end

370
    context 'when the projects and groups have no open upcoming milestones' do
371
      let(:projects) { [project_3] }
372
      let(:groups) { [group_3] }
373 374 375 376 377 378

      it 'returns no results' do
        expect(milestone_ids).to be_empty
      end
    end
  end
379 380

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

396
      it 'supports a cross-project reference' do
397
        expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
398
      end
399 400
    end

401 402 403
    context 'for a group milestone' do
      let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }

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

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

      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
420 421
    end
  end
422

423 424 425 426 427 428 429 430 431
  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

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

  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
464 465 466 467

  describe '.states_count' do
    context 'when the projects have milestones' do
      before do
E
Eagllus 已提交
468 469 470 471 472
        project_1 = create(:project)
        project_2 = create(:project)
        group_1 = create(:group)
        group_2 = create(:group)

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

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

        count = described_class.states_count(Project.all, Group.all)
487 488 489 490 491 492 493 494 495 496 497 498 499 500
        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 已提交
501
end