mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
628 lines
20 KiB
Ruby
628 lines
20 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
require "spec_helper"
|
|
|
|
RSpec.describe Version do
|
|
subject(:version) { build(:version, name: "Test Version") }
|
|
|
|
it { is_expected.to be_valid }
|
|
|
|
describe "default values" do
|
|
let(:version) { described_class.new }
|
|
|
|
it "sets the status to be open" do
|
|
expect(version.status)
|
|
.to eq "open"
|
|
end
|
|
end
|
|
|
|
describe "validations" do
|
|
context "with finish date that is smaller than the start date" do
|
|
before do
|
|
version.start_date = "2013-05-01"
|
|
version.effective_date = "2012-01-01"
|
|
end
|
|
|
|
it "is invalid" do
|
|
expect(version).not_to be_valid
|
|
expect(version.errors[:effective_date])
|
|
.to eq [I18n.t("activerecord.errors.messages.greater_than_start_date")]
|
|
end
|
|
end
|
|
|
|
context "with an invalid date" do
|
|
before do
|
|
version.start_date = "2013-05-01"
|
|
version.effective_date = "99999-01-01"
|
|
end
|
|
|
|
it "is invalid" do
|
|
expect(version).not_to be_valid
|
|
expect(version.errors[:effective_date])
|
|
.to eq [I18n.t("activerecord.errors.messages.not_a_date")]
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#to_s_for_project" do
|
|
let(:other_project) { build(:project) }
|
|
|
|
it "returns only the version for the same project" do
|
|
expect(version.to_s_for_project(version.project)).to eq(version.name.to_s)
|
|
end
|
|
|
|
it "returns the project name and the version name for a different project" do
|
|
expect(version.to_s_for_project(other_project)).to eq("#{version.project.name} - #{version.name}")
|
|
end
|
|
end
|
|
|
|
describe "#systemwide" do
|
|
it "contains the version if it is shared with all projects" do
|
|
version.sharing = "system"
|
|
version.save!
|
|
|
|
expect(described_class.systemwide).to contain_exactly(version)
|
|
end
|
|
|
|
it "is empty if the version is not shared" do
|
|
version.sharing = "none"
|
|
version.save!
|
|
|
|
expect(described_class.systemwide).to be_empty
|
|
end
|
|
|
|
it "is empty if the version is shared with the project hierarchy" do
|
|
version.sharing = "hierarchy"
|
|
version.save!
|
|
|
|
expect(described_class.systemwide).to be_empty
|
|
end
|
|
end
|
|
|
|
describe "#<=>" do
|
|
let(:version1) { build_stubbed(:version) }
|
|
let(:version2) { build_stubbed(:version) }
|
|
|
|
it "is 0 if name and project are equal" do
|
|
version1.project = version2.project
|
|
version1.name = version2.name
|
|
|
|
expect(version1 <=> version2).to be 0
|
|
end
|
|
|
|
it "is -1 if the project name is alphabetically before the other's project name" do
|
|
version1.name = "BBBB"
|
|
version1.project.name = "AAAA"
|
|
version2.name = "AAAA"
|
|
version2.project.name = "BBBB"
|
|
|
|
expect(version1 <=> version2).to be -1
|
|
end
|
|
|
|
it "is 1 if the project name is alphabetically after the other's project name" do
|
|
version1.name = "AAAA"
|
|
version1.project.name = "BBBB"
|
|
version2.name = "BBBB"
|
|
version2.project.name = "AAAA"
|
|
|
|
expect(version1 <=> version2).to be 1
|
|
end
|
|
|
|
it "is -1 if the project name is equal and the version's name is alphabetically before the other's name" do
|
|
version1.project.name = version2.project.name
|
|
version1.name = "AAAA"
|
|
version2.name = "BBBB"
|
|
|
|
expect(version1 <=> version2).to be -1
|
|
end
|
|
|
|
it "is 1 if the project name is equal and the version's name is alphabetically after the other's name" do
|
|
version1.project.name = version2.project.name
|
|
version1.name = "BBBB"
|
|
version2.name = "AAAA"
|
|
|
|
expect(version1 <=> version2).to be 1
|
|
end
|
|
|
|
it "is 0 if name and project are equal except for case" do
|
|
version1.project.name = version2.project.name.upcase
|
|
version1.name = version2.name.upcase
|
|
|
|
expect(version1 <=> version2).to be 0
|
|
end
|
|
|
|
it "is -1 if the project name is alphabetically before the other's project name ignoring case" do
|
|
version1.name = "BBBB"
|
|
version1.project.name = "aaaa"
|
|
version2.name = "AAAA"
|
|
version2.project.name = "BBBB"
|
|
|
|
expect(version1 <=> version2).to be -1
|
|
end
|
|
|
|
it "is 1 if the project name is alphabetically after the other's project name ignoring case" do
|
|
version1.name = "AAAA"
|
|
version1.project.name = "BBBB"
|
|
version2.name = "BBBB"
|
|
version2.project.name = "aaaa"
|
|
|
|
expect(version1 <=> version2).to be 1
|
|
end
|
|
|
|
it "is -1 if the project name is equal and the version's name is alphabetically before the other's name ignoring case" do
|
|
version1.project.name = version2.project.name
|
|
version1.name = "aaaa"
|
|
version2.name = "BBBB"
|
|
|
|
expect(version1 <=> version2).to be -1
|
|
end
|
|
|
|
it "is 1 if the project name is equal and the version's name is alphabetically after the other's name ignoring case" do
|
|
version1.project.name = version2.project.name
|
|
version1.name = "BBBB"
|
|
version2.name = "aaaa"
|
|
|
|
expect(version1 <=> version2).to be 1
|
|
end
|
|
end
|
|
|
|
describe "#projects" do
|
|
let(:grand_parent_project) do
|
|
build(:project, name: "grand_parent_project")
|
|
end
|
|
let(:parent_project) do
|
|
build(:project, parent: grand_parent_project, name: "parent_project")
|
|
end
|
|
let(:sibling_parent_project) do
|
|
build(:project, parent: grand_parent_project, name: "sibling_parent_project")
|
|
end
|
|
let(:child_project) do
|
|
build(:project, parent: parent_project, name: "child_project")
|
|
end
|
|
let(:sibling_project) do
|
|
build(:project, parent: parent_project, name: "sibling_project")
|
|
end
|
|
let(:unrelated_project) do
|
|
build(:project, name: "unrelated_project")
|
|
end
|
|
|
|
let(:unshared_version) do
|
|
build(:version, project: parent_project, sharing: "none")
|
|
end
|
|
let(:hierarchy_shared_version) do
|
|
build(:version, project: parent_project, sharing: "hierarchy")
|
|
end
|
|
let(:descendants_shared_version) do
|
|
build(:version, project: parent_project, sharing: "descendants")
|
|
end
|
|
let(:system_shared_version) do
|
|
build(:version, project: parent_project, sharing: "system")
|
|
end
|
|
let(:tree_shared_version) do
|
|
build(:version, project: parent_project, sharing: "tree")
|
|
end
|
|
|
|
def save_all_projects
|
|
grand_parent_project.save!
|
|
parent_project.save!
|
|
sibling_parent_project.save!
|
|
child_project.save!
|
|
sibling_project.save!
|
|
unrelated_project.save!
|
|
end
|
|
|
|
before do
|
|
save_all_projects
|
|
end
|
|
|
|
it "returns a scope" do
|
|
unshared_version.save
|
|
|
|
expect(unshared_version.projects).to be_a(ActiveRecord::Relation)
|
|
end
|
|
|
|
it "is empty for a new version" do
|
|
expect(described_class.new.projects).to be_empty
|
|
end
|
|
|
|
it "returns project the version is defined in for unshared" do
|
|
unshared_version.save
|
|
|
|
expect(unshared_version.projects).to contain_exactly(parent_project)
|
|
end
|
|
|
|
it "returns all projects the version is shared with (hierarchy)" do
|
|
hierarchy_shared_version.save!
|
|
|
|
expect(hierarchy_shared_version.projects).to contain_exactly(grand_parent_project, parent_project, child_project,
|
|
sibling_project)
|
|
end
|
|
|
|
it "returns all projects the version is shared with (descendants)" do
|
|
descendants_shared_version.save!
|
|
|
|
expect(descendants_shared_version.projects).to contain_exactly(parent_project, child_project, sibling_project)
|
|
end
|
|
|
|
it "returns all projects the version is shared with (tree)" do
|
|
tree_shared_version.save!
|
|
|
|
expect(tree_shared_version.projects).to contain_exactly(grand_parent_project, parent_project, sibling_parent_project,
|
|
child_project, sibling_project)
|
|
end
|
|
|
|
it "returns all projects the version is shared with (system)" do
|
|
system_shared_version.save!
|
|
|
|
expect(system_shared_version.projects).to contain_exactly(grand_parent_project, parent_project, sibling_parent_project,
|
|
child_project, sibling_project, unrelated_project)
|
|
end
|
|
|
|
it "returns only the projects for the version although there is a system shared version" do
|
|
unshared_version.save
|
|
system_shared_version.save!
|
|
|
|
expect(unshared_version.projects).to contain_exactly(parent_project)
|
|
end
|
|
end
|
|
|
|
describe "#estimated_hours" do
|
|
before do
|
|
version.save
|
|
end
|
|
|
|
context "without assigned work packages" do
|
|
it "returns 0.0" do
|
|
expect(version.estimated_hours)
|
|
.to eq 0.0
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages without estimated hours" do
|
|
let!(:work_package) { create(:work_package, version:) }
|
|
|
|
it "returns 0.0" do
|
|
expect(version.estimated_hours)
|
|
.to eq 0.0
|
|
end
|
|
end
|
|
|
|
context "with two assigned work packages with estimated hours" do
|
|
let!(:work_package1) { create(:work_package, version:, estimated_hours: 2.5) }
|
|
let!(:work_package2) { create(:work_package, version:, estimated_hours: 5) }
|
|
|
|
it "returns the sum of estimated hours" do
|
|
expect(version.estimated_hours)
|
|
.to eq 7.5
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages with estimated hours in the leaves" do
|
|
let!(:parent) { create(:work_package, version:) }
|
|
let!(:work_package1) { create(:work_package, parent:, version:, estimated_hours: 2.5) }
|
|
let!(:work_package2) { create(:work_package, parent:, version:, estimated_hours: 5) }
|
|
|
|
it "returns the sum of estimated hours" do
|
|
expect(version.estimated_hours)
|
|
.to eq 7.5
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#start_date" do
|
|
context "with a value saved and a work package with its own start_date" do
|
|
let(:version) { create(:version, start_date: "2010-01-05") }
|
|
let!(:work_package) { create(:work_package, version:, start_date: "2010-03-01") }
|
|
|
|
it "is the value" do
|
|
expect(version.start_date)
|
|
.to eq Date.parse("2010-01-05")
|
|
end
|
|
end
|
|
|
|
context "without a value saved and a work package with its own start_date" do
|
|
let(:version) { create(:version) }
|
|
let!(:work_package) { create(:work_package, version:, start_date: "2010-03-01") }
|
|
|
|
it "is nil" do
|
|
expect(version.start_date)
|
|
.to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#completed_percent and #closed_percent" do
|
|
create_shared_association_defaults_for_work_package_factory
|
|
|
|
let(:project) { create(:project) }
|
|
let(:version) { create(:version, project:) }
|
|
let(:closed_status) { create(:status, is_closed: true) }
|
|
|
|
context "without a work package" do
|
|
it "is 0 for completed_percent" do
|
|
expect(version.completed_percent)
|
|
.to eq 0
|
|
end
|
|
|
|
it "is 0 for closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 0
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that are not begun" do
|
|
before do
|
|
create(:work_package, version:)
|
|
create(:work_package, version:, done_ratio: 0)
|
|
end
|
|
|
|
it "is 0 for completed_percent" do
|
|
expect(version.completed_percent)
|
|
.to eq 0
|
|
end
|
|
|
|
it "is 0 for closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 0
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that are closed" do
|
|
before do
|
|
create(:work_package, status: closed_status, version:)
|
|
create(:work_package, status: closed_status, version:, done_ratio: 20)
|
|
create(:work_package, status: closed_status, version:, done_ratio: 70, estimated_hours: 25)
|
|
create(:work_package, status: closed_status, version:, estimated_hours: 15)
|
|
end
|
|
|
|
it "is 100 for completed_percent" do
|
|
expect(version.completed_percent)
|
|
.to eq 100
|
|
end
|
|
|
|
it "is 100 for closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 100
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that have only done ratio" do
|
|
before do
|
|
create(:work_package, version:)
|
|
create(:work_package, version:, done_ratio: 20)
|
|
create(:work_package, version:, done_ratio: 70)
|
|
end
|
|
|
|
it "considers the done ratio of open work packages" do
|
|
expect(version.completed_percent)
|
|
.to eq (0.0 + 20.0 + 70.0) / 3
|
|
end
|
|
|
|
it "is 0 for closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 0
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that have only done ratio with one being closed" do
|
|
before do
|
|
create(:work_package, version:)
|
|
create(:work_package, version:, done_ratio: 20)
|
|
create(:work_package, status: closed_status, version:)
|
|
end
|
|
|
|
it "considers the done ratio of open work packages" do
|
|
expect(version.completed_percent)
|
|
.to eq (0.0 + 20.0 + 100.0) / 3
|
|
end
|
|
|
|
it "is 33 for closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 100.0 / 3
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that have weighted done ratio" do
|
|
before do
|
|
create(:work_package, version:, estimated_hours: 10)
|
|
create(:work_package, version:, done_ratio: 30, estimated_hours: 20)
|
|
create(:work_package, version:, done_ratio: 10, estimated_hours: 40)
|
|
create(:work_package, status: closed_status, version:, estimated_hours: 25)
|
|
end
|
|
|
|
it "considers the weighted done ratio of open work packages" do
|
|
expect(version.completed_percent)
|
|
.to eq ((10.0 * 0) + (20.0 * 0.3) + (40 * 0.1) + (25.0 * 1)) / 95.0 * 100
|
|
end
|
|
|
|
it "is considers the weighted closed_percent" do
|
|
expect(version.closed_percent)
|
|
.to eq 25.0 / 95.0 * 100
|
|
end
|
|
end
|
|
|
|
context "with assigned work packages that have partly weighted done ratio" do
|
|
before do
|
|
create(:work_package, version:, done_ratio: 20)
|
|
create(:work_package, version:, done_ratio: 30, estimated_hours: 10)
|
|
create(:work_package, version:, done_ratio: 10, estimated_hours: 40)
|
|
create(:work_package, status: closed_status, version:)
|
|
end
|
|
|
|
it "considers the weighted done ratio of open work packages and uses default weighting if unset" do
|
|
expect(version.completed_percent)
|
|
.to eq ((25.0 * 0.2) + (25.0 * 1) + (10.0 * 0.3) + (40.0 * 0.1)) / 100.0 * 100
|
|
end
|
|
|
|
it "is considers the weighted closed_percent using average for the estimated hours" do
|
|
expect(version.closed_percent)
|
|
.to eq 25.0 / 100.0 * 100
|
|
end
|
|
end
|
|
end
|
|
|
|
it_behaves_like "acts_as_customizable included", admin_only_allowed: false, comments: false do
|
|
let!(:model_instance) { create(:version) }
|
|
let!(:new_model_instance) { version }
|
|
let!(:custom_field) { create(:version_custom_field) }
|
|
end
|
|
|
|
describe ".visible scope" do
|
|
let(:user) { create(:user) }
|
|
let(:project1) { create(:project) }
|
|
let(:project2) { create(:project) }
|
|
let(:role) { create(:project_role, permissions: [:view_work_packages]) }
|
|
let!(:member) { create(:member, user: user, project: project1, roles: [role]) }
|
|
|
|
let!(:version_in_project1) { create(:version, project: project1) }
|
|
let!(:systemwide_version) { create(:version, project: project2, sharing: "system") }
|
|
let!(:version_in_project2) { create(:version, project: project2) }
|
|
let!(:work_package) { create(:work_package, project: project2, version: version_in_project2) }
|
|
|
|
before do
|
|
# Simulate that the user can see the work package in project2 (e.g., via sharing)
|
|
allow(WorkPackage).to receive(:visible).with(user).and_return(WorkPackage.where(id: work_package.id))
|
|
end
|
|
|
|
it "returns versions from visible projects, systemwide, and referenced by visible work packages" do
|
|
visible_versions = described_class.visible(user)
|
|
expect(visible_versions).to include(version_in_project1)
|
|
expect(visible_versions).to include(systemwide_version)
|
|
expect(visible_versions).to include(version_in_project2)
|
|
end
|
|
|
|
it "does not return unrelated versions" do
|
|
unrelated_version = create(:version)
|
|
expect(described_class.visible(user)).not_to include(unrelated_version)
|
|
end
|
|
|
|
context "when user has the manage_work_packages permission in project" do
|
|
let(:role) { create(:project_role, permissions: [:manage_versions]) }
|
|
|
|
it "returns the version from that project" do
|
|
visible_versions = described_class.visible(user)
|
|
expect(visible_versions).to include(version_in_project1)
|
|
end
|
|
end
|
|
|
|
context "when user has an unrelated permission in project" do
|
|
let(:role) { create(:project_role, permissions: [:manage_users]) }
|
|
|
|
it "does not return the version from that project" do
|
|
visible_versions = described_class.visible(user)
|
|
expect(visible_versions).not_to include(version_in_project1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#visible?" do
|
|
subject { version.visible?(user) }
|
|
|
|
let(:user) { create(:user) }
|
|
let(:project) { create(:project) }
|
|
let(:role) { create(:project_role, permissions: [:view_work_packages]) }
|
|
let!(:member) { create(:member, user: user, project: project, roles: [role]) }
|
|
let(:version) { create(:version, project: project) }
|
|
|
|
context "when the user has project access" do
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context "when the user has access to a shared work package but not the project" do
|
|
let(:other_user) { create(:user) }
|
|
let(:other_project) { create(:project) }
|
|
let!(:other_version) { create(:version, project: other_project) }
|
|
let!(:shared_wp) { create(:work_package, project: other_project, version: other_version) }
|
|
|
|
before do
|
|
# Simulate that the user can see the work package in other_project (e.g., via sharing)
|
|
allow(WorkPackage).to receive(:visible).with(other_user).and_return(WorkPackage.where(id: shared_wp.id))
|
|
end
|
|
|
|
it "returns true if the user can see a work package of the version" do
|
|
expect(other_version.visible?(other_user)).to be true
|
|
end
|
|
end
|
|
|
|
context "when the user has access to manage_versions in the project" do
|
|
let(:role) { create(:project_role, permissions: [:manage_versions]) }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context "when the user only has access to unrelated permission in the project" do
|
|
let(:role) { create(:project_role, permissions: [:manage_users]) }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context "when the user has no access to the project or any work package" do
|
|
let(:version) { create(:version) }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context "when the version is systemwide" do
|
|
let(:version) { create(:version, sharing: "system") }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
end
|
|
|
|
describe "order by name" do
|
|
shared_let(:project) { create(:project) }
|
|
shared_let(:ordered_names) do
|
|
[
|
|
"1. xxxx",
|
|
"1.1. aaa",
|
|
"1.1. zzz",
|
|
"1.2. mmm",
|
|
"1.10. aaa",
|
|
"9",
|
|
"10.2",
|
|
"10.10.2",
|
|
"10.10.10",
|
|
"aaaaa",
|
|
"aaaaa 1."
|
|
]
|
|
end
|
|
shared_let(:versions) { ordered_names.shuffle.map { |name| create(:version, name:, project:) } }
|
|
|
|
it "returns the versions in ascending semver order" do
|
|
expect(described_class.order(:name).pluck(:name)).to eql ordered_names
|
|
end
|
|
|
|
it "returns the versions in descending semver order" do
|
|
expect(described_class.order(name: :desc).pluck(:name)).to eql ordered_names.reverse
|
|
end
|
|
end
|
|
end
|