8a49eeed6d
When looking for upcoming milestones, we compare the milestone's due date to `NOW()`. However, the due date is a date and `NOW()` is a timestamp, and in Postgres, timestamps can't extend as far in the future as dates can. This test exposes the issue.
520 lines
17 KiB
Ruby
520 lines
17 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe Milestone do
|
|
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) }
|
|
let(:scope) { :project }
|
|
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)) }
|
|
let(:scope) { :group }
|
|
let(:scope_attrs) { { namespace: instance.group } }
|
|
let(:usage) { :milestones }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Validation" do
|
|
before do
|
|
allow(subject).to receive(:set_iid).and_return(false)
|
|
end
|
|
|
|
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
|
|
expect(milestone.errors[:due_date]).to include("must be greater than start date")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "Associations" do
|
|
it { is_expected.to belong_to(:project) }
|
|
it { is_expected.to have_many(:issues) }
|
|
end
|
|
|
|
let(:project) { create(:project, :public) }
|
|
let(:milestone) { create(:milestone, project: project) }
|
|
let(:issue) { create(:issue, project: project) }
|
|
let(:user) { create(:user) }
|
|
|
|
describe "#title" do
|
|
let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
|
|
|
|
it "sanitizes title" do
|
|
expect(milestone.title).to eq("foo & bar -> 2.2")
|
|
end
|
|
end
|
|
|
|
describe "unique milestone title" do
|
|
context "per project" do
|
|
it "does not accept the same title in a project twice" do
|
|
new_milestone = described_class.new(project: milestone.project, title: milestone.title)
|
|
expect(new_milestone).not_to be_valid
|
|
end
|
|
|
|
it "accepts the same title in another project" do
|
|
project = create(:project)
|
|
new_milestone = described_class.new(project: project, title: milestone.title)
|
|
|
|
expect(new_milestone).to be_valid
|
|
end
|
|
end
|
|
|
|
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
|
|
new_milestone = described_class.new(group: group, title: milestone.title)
|
|
|
|
expect(new_milestone).not_to be_valid
|
|
end
|
|
|
|
it "does not accept the same title of a child project milestone" do
|
|
create(:milestone, project: group.projects.first)
|
|
|
|
new_milestone = described_class.new(group: group, title: milestone.title)
|
|
|
|
expect(new_milestone).not_to be_valid
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
describe "#percent_complete" do
|
|
it "does not count open issues" do
|
|
milestone.issues << issue
|
|
expect(milestone.percent_complete(user)).to eq(0)
|
|
end
|
|
|
|
it "counts closed issues" do
|
|
issue.close
|
|
milestone.issues << issue
|
|
expect(milestone.percent_complete(user)).to eq(100)
|
|
end
|
|
|
|
it "recovers from dividing by zero" do
|
|
expect(milestone.percent_complete(user)).to eq(0)
|
|
end
|
|
end
|
|
|
|
describe '#expired?' do
|
|
context "expired" do
|
|
before do
|
|
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
|
|
end
|
|
|
|
it 'returns true when due_date is in the past' do
|
|
expect(milestone.expired?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context "not expired" do
|
|
before do
|
|
allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
|
|
end
|
|
|
|
it 'returns false when due_date is in the future' do
|
|
expect(milestone.expired?).to be_falsey
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#upcoming?' do
|
|
it 'returns true when start_date is in the future' do
|
|
milestone = build(:milestone, start_date: Time.now + 1.month)
|
|
expect(milestone.upcoming?).to be_truthy
|
|
end
|
|
|
|
it 'returns false when start_date is in the past' do
|
|
milestone = build(:milestone, start_date: Date.today.prev_year)
|
|
expect(milestone.upcoming?).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#percent_complete' do
|
|
before do
|
|
allow(milestone).to receive_messages(
|
|
closed_items_count: 3,
|
|
total_items_count: 4
|
|
)
|
|
end
|
|
|
|
it { expect(milestone.percent_complete(user)).to eq(75) }
|
|
end
|
|
|
|
describe '#can_be_closed?' do
|
|
it { expect(milestone.can_be_closed?).to be_truthy }
|
|
end
|
|
|
|
describe '#total_items_count' do
|
|
before do
|
|
create :closed_issue, milestone: milestone, project: project
|
|
create :merge_request, milestone: milestone, source_project: project
|
|
end
|
|
|
|
it 'returns total count of issues and merge requests assigned to milestone' do
|
|
expect(milestone.total_items_count(user)).to eq 2
|
|
end
|
|
end
|
|
|
|
describe '#can_be_closed?' do
|
|
before do
|
|
milestone = create :milestone, project: project
|
|
create :closed_issue, milestone: milestone, project: project
|
|
|
|
create :issue, project: project
|
|
end
|
|
|
|
it 'returns true if milestone active and all nested issues closed' do
|
|
expect(milestone.can_be_closed?).to be_truthy
|
|
end
|
|
|
|
it 'returns false if milestone active and not all nested issues closed' do
|
|
issue.milestone = milestone
|
|
issue.save
|
|
|
|
expect(milestone.can_be_closed?).to be_falsey
|
|
end
|
|
end
|
|
|
|
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
|
|
expect(described_class.search(milestone.description[0..2]))
|
|
.to eq([milestone])
|
|
end
|
|
|
|
it 'returns milestones with a matching description regardless of the casing' do
|
|
expect(described_class.search(milestone.description.upcase))
|
|
.to eq([milestone])
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
let(:projects) { Project.where(id: project.id).select(:id) }
|
|
let(:groups) { Group.where(id: group.id).select(:id) }
|
|
|
|
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
|
|
|
|
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) }
|
|
|
|
let(:project_1) { create(:project) }
|
|
let(:project_2) { create(:project) }
|
|
let(:project_3) { create(:project) }
|
|
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) }
|
|
|
|
let(:milestone_ids) { described_class.upcoming_ids(projects, groups).map(&:id) }
|
|
|
|
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
|
|
)
|
|
end
|
|
|
|
context 'when the projects and groups have no open upcoming milestones' do
|
|
let(:projects) { [project_3] }
|
|
let(:groups) { [group_3] }
|
|
|
|
it 'returns no results' do
|
|
expect(milestone_ids).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'when there is a milestone with a date after 294276 AD', :postgresql do
|
|
before do
|
|
past_milestone_project_1.update!(due_date: Date.new(294277, 1, 1))
|
|
end
|
|
|
|
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
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#to_reference' do
|
|
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
|
|
expect(milestone.to_reference).to eq '%"milestone"'
|
|
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
|
|
|
|
it 'supports a cross-project reference' do
|
|
expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
|
|
end
|
|
end
|
|
|
|
context 'for a group milestone' do
|
|
let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
|
|
|
|
it 'returns a group milestone reference with a default format' do
|
|
expect(milestone.to_reference).to eq '%"milestone"'
|
|
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
|
|
|
|
it 'does supports cross-project references within a group' do
|
|
expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
describe '#participants' do
|
|
let(:project) { build(:project, name: 'sample-project') }
|
|
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
|
|
|
|
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
|
|
|
|
describe '.states_count' do
|
|
context 'when the projects have milestones' do
|
|
before do
|
|
project_1 = create(:project)
|
|
project_2 = create(:project)
|
|
group_1 = create(:group)
|
|
group_2 = create(:group)
|
|
|
|
create(:active_milestone, title: 'Active Group Milestone', project: project_1)
|
|
create(:closed_milestone, title: 'Closed Group Milestone', project: project_1)
|
|
create(:active_milestone, title: 'Active Group Milestone', project: project_2)
|
|
create(:closed_milestone, title: 'Closed Group Milestone', project: project_2)
|
|
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)
|
|
end
|
|
|
|
it 'returns the quantity of milestones in each possible state' do
|
|
expected_count = { opened: 5, closed: 6, all: 11 }
|
|
|
|
count = described_class.states_count(Project.all, Group.all)
|
|
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
|
|
end
|