kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
543 lines
15 KiB
543 lines
15 KiB
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2021 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 docs/COPYRIGHT.rdoc for more details.
|
|
#++
|
|
|
|
require 'spec_helper'
|
|
|
|
describe 'API v3 Relation resource', type: :request, content_type: :json do
|
|
include API::V3::Utilities::PathHelper
|
|
|
|
let(:user) { FactoryBot.create :admin }
|
|
let(:current_user) { user }
|
|
|
|
let!(:from) { FactoryBot.create :work_package }
|
|
let!(:to) { FactoryBot.create :work_package }
|
|
|
|
let(:type) { "follows" }
|
|
let(:description) { "This first" }
|
|
let(:delay) { 3 }
|
|
|
|
let(:params) do
|
|
{
|
|
_links: {
|
|
from: {
|
|
href: "/api/v3/work_packages/#{from.id}"
|
|
},
|
|
to: {
|
|
href: "/api/v3/work_packages/#{to.id}"
|
|
}
|
|
},
|
|
type: type,
|
|
description: description,
|
|
delay: delay
|
|
}
|
|
end
|
|
let(:relation) do
|
|
FactoryBot.create :relation,
|
|
from: from,
|
|
to: to,
|
|
relation_type: type,
|
|
description: description,
|
|
delay: delay
|
|
end
|
|
|
|
before do
|
|
login_as current_user
|
|
end
|
|
|
|
describe "creating a relation" do
|
|
shared_examples_for 'creates the relation' do
|
|
it 'creates the relation correctly' do
|
|
rel = ::API::V3::Relations::RelationPayloadRepresenter.new(Relation.new, current_user: user).from_json last_response.body
|
|
|
|
expect(rel.from).to eq from
|
|
expect(rel.to).to eq to
|
|
expect(rel.relation_type).to eq type
|
|
expect(rel.description).to eq description
|
|
expect(rel.delay).to eq delay
|
|
end
|
|
end
|
|
|
|
let(:setup) {}
|
|
before do
|
|
setup
|
|
|
|
header "Content-Type", "application/json"
|
|
post "/api/v3/work_packages/#{from.id}/relations", params.to_json
|
|
end
|
|
|
|
it 'should return 201 (created)' do
|
|
expect(last_response.status).to eq(201)
|
|
end
|
|
|
|
it 'should have created a new relation' do
|
|
# reflexive relations + created one
|
|
expect(Relation.count).to eq 3
|
|
end
|
|
|
|
it_behaves_like 'creates the relation'
|
|
|
|
context 'relation that would create a circular scheduling dependency' do
|
|
let(:from_child) do
|
|
FactoryBot.create(:work_package, parent: from)
|
|
end
|
|
let(:to_child) do
|
|
FactoryBot.create(:work_package, parent: to)
|
|
end
|
|
let(:children_follows_relation) do
|
|
FactoryBot.create :relation,
|
|
from: to_child,
|
|
to: from_child,
|
|
relation_type: Relation::TYPE_FOLLOWS
|
|
end
|
|
let(:relation_type) { Relation::TYPE_FOLLOWS }
|
|
let(:setup) do
|
|
children_follows_relation
|
|
end
|
|
|
|
it 'responds with error' do
|
|
expect(last_response.status).to eql 422
|
|
end
|
|
|
|
it 'states the reason for the error' do
|
|
expect(last_response.body)
|
|
.to be_json_eql(I18n.t(:'activerecord.errors.messages.circular_dependency').to_json)
|
|
.at_path('message')
|
|
end
|
|
end
|
|
|
|
context "'relates to' relation that would create a circular dependency" do
|
|
let(:work_package_a) { FactoryBot.create(:work_package) }
|
|
let(:work_package_b) { FactoryBot.create(:work_package, project: work_package_a.project) }
|
|
let(:work_package_c) { FactoryBot.create(:work_package, project: work_package_b.project) }
|
|
let(:relation_a_b) do
|
|
FactoryBot.create(:relation,
|
|
from: work_package_a,
|
|
to: work_package_b,
|
|
relation_type: Relation::TYPE_RELATES)
|
|
end
|
|
let(:relation_b_c) do
|
|
FactoryBot.create(:relation,
|
|
from: work_package_b,
|
|
to: work_package_c,
|
|
relation_type: Relation::TYPE_RELATES)
|
|
end
|
|
|
|
let!(:from) { work_package_c }
|
|
let!(:to) { work_package_a }
|
|
|
|
let(:type) { Relation::TYPE_RELATES }
|
|
|
|
let(:setup) do
|
|
relation_a_b
|
|
relation_b_c
|
|
end
|
|
|
|
it 'returns 201 (created) and creates the relation with an inverted direction' do
|
|
expect(last_response.status)
|
|
.to eq(201)
|
|
|
|
expect(Relation.direct.count).to eq 3
|
|
|
|
new_relation = Relation.direct.last
|
|
|
|
expect(new_relation.to)
|
|
.to eql work_package_c
|
|
|
|
expect(new_relation.from)
|
|
.to eql work_package_a
|
|
end
|
|
end
|
|
|
|
context 'follows relation within siblings' do
|
|
let(:sibling) do
|
|
FactoryBot.create(:work_package)
|
|
end
|
|
let(:other_sibling) do
|
|
FactoryBot.create(:work_package)
|
|
end
|
|
let(:parent) do
|
|
wp = FactoryBot.create(:work_package)
|
|
|
|
wp.children = [sibling, from, to, other_sibling]
|
|
end
|
|
let(:existing_follows) do
|
|
FactoryBot.create(:relation, relation_type: 'follows', from: to, to: sibling)
|
|
FactoryBot.create(:relation, relation_type: 'follows', from: other_sibling, to: from)
|
|
end
|
|
|
|
let(:setup) do
|
|
parent
|
|
existing_follows
|
|
end
|
|
|
|
it_behaves_like 'creates the relation'
|
|
end
|
|
|
|
context 'follows relation to sibling\'s child' do
|
|
let(:sibling) do
|
|
FactoryBot.create(:work_package)
|
|
end
|
|
let(:sibling_child) do
|
|
FactoryBot.create(:work_package, parent: sibling)
|
|
end
|
|
let(:parent) do
|
|
wp = FactoryBot.create(:work_package)
|
|
|
|
wp.children = [sibling, from, to]
|
|
end
|
|
let(:existing_follows) do
|
|
FactoryBot.create(:relation, relation_type: 'follows', from: to, to: sibling_child)
|
|
end
|
|
|
|
let(:setup) do
|
|
parent
|
|
existing_follows
|
|
end
|
|
|
|
it_behaves_like 'creates the relation'
|
|
end
|
|
end
|
|
|
|
describe "updating a relation" do
|
|
let(:new_description) { "This is another description" }
|
|
let(:new_delay) { 42 }
|
|
|
|
let(:update) do
|
|
{
|
|
description: new_description,
|
|
delay: new_delay
|
|
}
|
|
end
|
|
|
|
before do
|
|
relation
|
|
|
|
header "Content-Type", "application/json"
|
|
patch "/api/v3/relations/#{relation.id}", update.to_json
|
|
end
|
|
|
|
it "should return 200 (ok)" do
|
|
expect(last_response.status).to eq 200
|
|
end
|
|
|
|
it "updates the relation's description" do
|
|
expect(relation.reload.description).to eq new_description
|
|
end
|
|
|
|
it "updates the relation's delay" do
|
|
expect(relation.reload.delay).to eq new_delay
|
|
end
|
|
|
|
it "should return the updated relation" do
|
|
rel = ::API::V3::Relations::RelationPayloadRepresenter.new(Relation.new, current_user: user).from_json last_response.body
|
|
|
|
expect(rel).to eq relation.reload
|
|
end
|
|
|
|
context "with invalid type" do
|
|
let(:update) do
|
|
{
|
|
type: "foobar"
|
|
}
|
|
end
|
|
|
|
it "should return 422" do
|
|
expect(last_response.status).to eq 422
|
|
end
|
|
|
|
it "should indicate an error with the type attribute" do
|
|
attr = JSON.parse(last_response.body).dig "_embedded", "details", "attribute"
|
|
|
|
expect(attr).to eq "type"
|
|
end
|
|
end
|
|
|
|
context "with trying to change an immutable attribute" do
|
|
let(:other_wp) { FactoryBot.create :work_package }
|
|
|
|
let(:update) do
|
|
{
|
|
_links: {
|
|
from: {
|
|
href: "/api/v3/work_packages/#{other_wp.id}"
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
it "should return 422" do
|
|
expect(last_response.status).to eq 422
|
|
end
|
|
|
|
it "should indicate an error with the `from` attribute" do
|
|
attr = JSON.parse(last_response.body).dig "_embedded", "details", "attribute"
|
|
|
|
expect(attr).to eq "from"
|
|
end
|
|
|
|
it "should let the user know the attribute is read-only" do
|
|
msg = JSON.parse(last_response.body)["message"]
|
|
|
|
expect(msg).to include "Work package an existing relation's `from` link is immutable"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "permissions" do
|
|
let(:user) { FactoryBot.create :user }
|
|
|
|
let(:permissions) { %i(view_work_packages manage_work_package_relations) }
|
|
|
|
let(:role) do
|
|
FactoryBot.create :existing_role, permissions: permissions
|
|
end
|
|
|
|
let(:project) { FactoryBot.create :project }
|
|
|
|
let!(:from) { FactoryBot.create :work_package, project: project }
|
|
let!(:to) { FactoryBot.create :work_package, project: project }
|
|
|
|
before do
|
|
project.add_member! user, role
|
|
|
|
header "Content-Type", "application/json"
|
|
post "/api/v3/work_packages/#{from.id}/relations", params.to_json
|
|
end
|
|
|
|
context "with the required permissions" do
|
|
it "works" do
|
|
expect(last_response.status).to eq 201
|
|
end
|
|
end
|
|
|
|
context "without manage_work_package_relations" do
|
|
let(:permissions) { [:view_work_packages] }
|
|
|
|
it "is forbidden" do
|
|
expect(last_response.status).to eq 403
|
|
end
|
|
end
|
|
|
|
##
|
|
# This one is expected to fail (422) because the `to` work package
|
|
# is in another project for which the user does not have permission to
|
|
# view work packages.
|
|
context "without manage_work_package_relations" do
|
|
let!(:to) { FactoryBot.create :work_package }
|
|
|
|
it "should return 422" do
|
|
expect(last_response.status).to eq 422
|
|
end
|
|
|
|
it "should indicate an error with the `to` attribute" do
|
|
attr = JSON.parse(last_response.body).dig "_embedded", "details", "attribute"
|
|
|
|
expect(attr).to eq "to"
|
|
end
|
|
|
|
it "should have a localized error message" do
|
|
message = JSON.parse(last_response.body)["message"]
|
|
|
|
expect(message).not_to include "translation missing"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "deleting a relation" do
|
|
let(:path) do
|
|
api_v3_paths.relation(relation.id)
|
|
end
|
|
|
|
let(:permissions) { %i[view_work_packages manage_work_package_relations] }
|
|
let(:role) { FactoryBot.create(:role, permissions: permissions) }
|
|
|
|
let(:current_user) do
|
|
FactoryBot.create(:user).tap do |user|
|
|
FactoryBot.create(:member,
|
|
project: to.project,
|
|
user: user,
|
|
roles: [role])
|
|
FactoryBot.create(:member,
|
|
project: from.project,
|
|
user: user,
|
|
roles: [role])
|
|
end
|
|
end
|
|
|
|
before do
|
|
delete path
|
|
end
|
|
|
|
it "should return 204 and destroy the relation" do
|
|
expect(last_response.status).to eq 204
|
|
expect(Relation.exists?(relation.id)).to be_falsey
|
|
end
|
|
|
|
context 'lacking the permission' do
|
|
let(:permissions) { %i[view_work_packages] }
|
|
|
|
it 'returns 403' do
|
|
expect(last_response.status).to eq 403
|
|
end
|
|
|
|
it 'leaves the relation' do
|
|
expect(Relation.exists?(relation.id)).to be_truthy
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v3/relations?[filter]' do
|
|
let(:user) { FactoryBot.create(:user) }
|
|
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
|
|
let(:member_project_to) do
|
|
FactoryBot.build(:member,
|
|
project: to.project,
|
|
user: user,
|
|
roles: [role])
|
|
end
|
|
|
|
let(:member_project_from) do
|
|
FactoryBot.build(:member,
|
|
project: from.project,
|
|
user: user,
|
|
roles: [role])
|
|
end
|
|
let(:invisible_relation) do
|
|
invisible_wp = FactoryBot.create(:work_package)
|
|
|
|
FactoryBot.create :relation,
|
|
from: from,
|
|
to: invisible_wp
|
|
end
|
|
let(:other_visible_work_package) do
|
|
FactoryBot.create(:work_package,
|
|
project: to.project,
|
|
type: to.type)
|
|
end
|
|
let(:other_visible_relation) do
|
|
FactoryBot.create :relation,
|
|
from: to,
|
|
to: other_visible_work_package
|
|
end
|
|
|
|
let(:members) { [member_project_to, member_project_from] }
|
|
let(:filter) do
|
|
[{ involved: { operator: '=', values: [from.id.to_s] } }]
|
|
end
|
|
|
|
before do
|
|
members.each(&:save!)
|
|
relation
|
|
invisible_relation
|
|
other_visible_relation
|
|
|
|
get "#{api_v3_paths.relations}?filters=#{CGI::escape(JSON::dump(filter))}"
|
|
end
|
|
|
|
it 'returns 200' do
|
|
expect(last_response.status).to eql 200
|
|
end
|
|
|
|
it 'returns the visible relation (and only the visible one) satisfying the filter' do
|
|
expect(last_response.body)
|
|
.to be_json_eql('1')
|
|
.at_path('total')
|
|
|
|
expect(last_response.body)
|
|
.to be_json_eql('1')
|
|
.at_path('count')
|
|
|
|
expect(last_response.body)
|
|
.to be_json_eql(relation.id.to_json)
|
|
.at_path('_embedded/elements/0/id')
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v3/relations/:id' do
|
|
let(:path) do
|
|
api_v3_paths.relation(relation.id)
|
|
end
|
|
|
|
let(:role) { FactoryBot.create(:role, permissions: [:view_work_packages]) }
|
|
|
|
let(:current_user) do
|
|
FactoryBot.create(:user).tap do |user|
|
|
FactoryBot.create(:member,
|
|
project: to.project,
|
|
user: user,
|
|
roles: [role])
|
|
FactoryBot.create(:member,
|
|
project: from.project,
|
|
user: user,
|
|
roles: [role])
|
|
end
|
|
end
|
|
|
|
before do
|
|
get path
|
|
end
|
|
|
|
context 'for a relation with visible work packages' do
|
|
it 'returns 200' do
|
|
expect(last_response.status).to eql 200
|
|
end
|
|
|
|
it 'returns the relation' do
|
|
# Creation leads to journal creation which leads to touching the work package which is not
|
|
# reflected in the value returned from the wp factory.
|
|
from.reload
|
|
to.reload
|
|
|
|
expected = API::V3::Relations::RelationRepresenter.new(relation,
|
|
current_user: current_user,
|
|
embed_links: true).to_json
|
|
|
|
expect(last_response.body)
|
|
.to be_json_eql expected
|
|
end
|
|
end
|
|
|
|
context 'for a relation with an invisible work package' do
|
|
let(:invisible_relation) do
|
|
invisible_wp = FactoryBot.create(:work_package)
|
|
|
|
FactoryBot.create :relation,
|
|
from: from,
|
|
to: invisible_wp
|
|
end
|
|
|
|
let(:path) do
|
|
api_v3_paths.relation(invisible_relation.id)
|
|
end
|
|
|
|
it 'returns 404 NOT FOUND' do
|
|
expect(last_response.status).to eql 404
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|