Merge pull request #10784 from opf/fix/40921-avoid_validating_automatically_updated_wps

Fix/40921 avoid validating automatically updated wps
pull/10804/head
ulferts 2 years ago committed by GitHub
commit aee3d16dcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      app/contracts/work_packages/update_dependent_contract.rb
  2. 10
      app/services/attachments/replace_attachments.rb
  3. 6
      app/services/base_services/base_contracted.rb
  4. 2
      app/services/base_services/copy.rb
  5. 4
      app/services/base_services/create.rb
  6. 3
      app/services/journals/create_service.rb
  7. 2
      app/services/relations/base_service.rb
  8. 1
      app/services/work_packages/schedule_dependency.rb
  9. 13
      app/services/work_packages/set_attributes_service.rb
  10. 4
      app/services/work_packages/set_schedule_service.rb
  11. 173
      app/services/work_packages/update_service.rb
  12. 2
      lib_static/plugins/acts_as_customizable/lib/acts_as_customizable.rb
  13. 6
      modules/backlogs/lib/open_project/backlogs/list.rb
  14. 44
      modules/backlogs/lib/open_project/backlogs/patches/update_service_patch.rb
  15. 198
      spec/requests/api/v3/work_packages/dependent_errors_spec.rb
  16. 27
      spec/services/work_packages/set_attributes_service_spec.rb
  17. 1982
      spec/services/work_packages/update_service_integration_spec.rb
  18. 235
      spec/services/work_packages/update_service_spec.rb

@ -0,0 +1,36 @@
# OpenProject is an open source project management software.
# Copyright (C) 2010-2022 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.
# Disables all validations but still allows to be queried for assignable_* (e.g. statuses)
# that are necessary whenever a dependent (e.g. descendant of a work package) is moved to a project
# where the type is switched.
module WorkPackages
class UpdateDependentContract < UpdateContract
def validate
true
end
end
end

@ -34,13 +34,11 @@ module Attachments
private
def set_attributes(attributes)
call = super
if call.success? && call.result.attachments_replacements
call.result.attachments = call.result.attachments_replacements
super.tap do |call|
if call.success? && call.result.attachments_replacements
call.result.attachments = call.result.attachments_replacements
end
end
call
end
end
end

@ -50,12 +50,12 @@ module BaseServices
# Determine the type of context
# this service is running in
# e.g., within a resource lock or just executing as the given user
def service_context(&)
in_context(model, true, &)
def service_context(send_notifications: true, &block)
in_context(model, send_notifications, &block)
end
def perform(params = {})
service_context do
service_context(send_notifications: (params || {}).fetch(:send_notifications, true)) do
service_call = validate_params(params)
service_call = before_perform(params, service_call) if service_call.success?
service_call = validate_contract(service_call) if service_call.success?

@ -94,7 +94,7 @@ module BaseServices
##
# Disabling sending regular notifications
def service_context(&)
def service_context(*_args, &)
in_context(model, false, &)
end

@ -30,8 +30,8 @@ module BaseServices
class Create < Write
protected
def service_context(&)
in_user_context(true, &)
def service_context(send_notifications: true, &block)
in_user_context(send_notifications, &block)
end
def instance(_params)

@ -54,9 +54,6 @@ module Journals
return ServiceResult.new success: true unless journal
destroy_predecessor(journal)
journal
reload_journals
touch_journable(journal)

@ -70,7 +70,7 @@ class Relations::BaseService < ::BaseServices::BaseCallable
# The to-work_package will not be altered by the schedule service so
# we do not have to save the result of the service.
save_result = if schedule_result.success?
schedule_result.dependent_results.all? { |dr| !dr.result.changed? || dr.result.save }
schedule_result.dependent_results.all? { |dr| !dr.result.changed? || dr.result.save(validate: false) }
end || false
schedule_result.success = save_result

@ -101,6 +101,7 @@ class WorkPackages::ScheduleDependency
descendants + WorkPackage
.with_ancestor(descendants)
.includes(follows_relations: :to)
.where.not(id: known_work_packages_by_id.keys)
end

@ -90,7 +90,7 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes
elsif parent_start_earlier_than_due?
work_package.parent.start_date
elsif Setting.work_package_startdate_is_adddate?
Date.today
Time.zone.today
end
end
@ -111,7 +111,7 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes
# And the new type has a default text
default_description = work_package.type&.description
return unless default_description.present?
return if default_description.blank?
# And the current description matches ANY current default text
return unless work_package.description.blank? || default_description?
@ -155,6 +155,7 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes
model.change_by_system do
set_version_to_nil
reassign_category
set_parent_to_nil
reassign_type unless work_package.type_id_changed?
end
@ -194,6 +195,14 @@ class WorkPackages::SetAttributesService < ::BaseServices::SetAttributes
end
end
def set_parent_to_nil
if !Setting.cross_project_work_package_relations? &&
!work_package.parent_changed?
work_package.parent = nil
end
end
def reassign_category
# work_package is moved to another project
# reassign to the category with same name if any

@ -176,9 +176,9 @@ class WorkPackages::SetScheduleService
def date_rescheduling_delta(predecessor)
if predecessor.due_date.present?
predecessor.due_date - (predecessor.due_date_was || predecessor.due_date)
predecessor.due_date - (predecessor.due_date_before_last_save || predecessor.due_date_was || predecessor.due_date)
elsif predecessor.start_date.present?
predecessor.start_date - (predecessor.start_date_was || predecessor.start_date)
predecessor.start_date - (predecessor.start_date_before_last_save || predecessor.start_date_was || predecessor.start_date)
else
0
end

@ -26,97 +26,63 @@
# See COPYRIGHT and LICENSE files for more details.
#++
# TODO: use default update base class
class WorkPackages::UpdateService < ::BaseServices::BaseCallable
class WorkPackages::UpdateService < ::BaseServices::Update
include ::WorkPackages::Shared::UpdateAncestors
include ::Shared::ServiceContext
include Attachments::ReplaceAttachments
attr_accessor :user,
:model,
:contract_class
private
def initialize(user:, model:, contract_class: WorkPackages::UpdateContract)
self.user = user
self.model = model
self.contract_class = contract_class
end
def after_perform(service_call)
update_related_work_packages(service_call)
cleanup(service_call.result)
def perform(send_notifications: true, **attributes)
in_context(model, send_notifications) do
update(attributes)
end
service_call
end
private
def update(attributes)
result = set_attributes(attributes)
if result.success?
work_package.attachments = work_package.attachments_replacements if work_package.attachments_replacements
result.merge!(update_dependent)
def update_related_work_packages(service_call)
update_ancestors([service_call.result]).each do |ancestor_service_call|
service_call.merge!(ancestor_service_call)
end
if save_if_valid(result)
update_ancestors([work_package]).each do |ancestor_result|
result.merge!(ancestor_result)
end
update_related(service_call.result).each do |related_service_call|
service_call.merge!(related_service_call)
end
result
end
def save_if_valid(result)
if result.success?
result.success = consolidated_results(result)
.all?(&:save)
end
result.success?
def update_related(work_package)
consolidated_calls(update_descendants(work_package) + reschedule_related(work_package))
.reject { |dependent_call| dependent_call.result.id == work_package.id }
.each { |dependent_call| dependent_call.result.save(validate: false) }
end
def update_dependent
result = ServiceResult.new(success: true, result: work_package)
result.merge!(update_descendants)
cleanup if result.success?
result.merge!(reschedule_related)
def update_descendants(work_package)
if work_package.saved_change_to_project_id?
attributes = { project: work_package.project }
result
work_package.descendants.map do |descendant|
set_descendant_attributes(attributes, descendant)
end
else
[]
end
end
def set_attributes(attributes, wp = work_package)
def set_descendant_attributes(attributes, descendant)
WorkPackages::SetAttributesService
.new(user:,
model: wp,
contract_class:)
model: descendant,
contract_class: WorkPackages::UpdateDependentContract)
.call(attributes)
end
def update_descendants
result = ServiceResult.new(success: true, result: work_package)
if work_package.project_id_changed?
attributes = { project: work_package.project }
work_package.descendants.each do |descendant|
result.add_dependent!(set_attributes(attributes, descendant))
end
end
result
end
def cleanup
if work_package.project_id_changed?
def cleanup(work_package)
if work_package.saved_change_to_project_id?
moved_work_packages = [work_package] + work_package.descendants
delete_relations(moved_work_packages)
move_time_entries(moved_work_packages, work_package.project_id)
end
if work_package.type_id_changed?
reset_custom_values
if work_package.saved_change_to_type_id?
reset_custom_values(work_package)
end
end
@ -134,60 +100,29 @@ class WorkPackages::UpdateService < ::BaseServices::BaseCallable
.update_all(project_id:)
end
def reset_custom_values
def reset_custom_values(work_package)
work_package.reset_custom_values!
end
def reschedule_related
result = ServiceResult.new(success: true, result: work_package)
with_temporarily_persisted_parent_changes do
if work_package.parent_id_changed? && work_package.parent_id_was
result.merge!(reschedule_former_siblings)
end
result.merge!(reschedule(work_package))
end
result
end
def with_temporarily_persisted_parent_changes
# Explicitly using requires_new: true since we are already within a transaction.
# Because of that, raising ActiveRecord::Rollback would have no effect:
# https://www.bigbinary.com/learn-rubyonrails-book/activerecord-transactions-in-depth#nested-transactions
WorkPackage.transaction(requires_new: true) do
if work_package.parent_id_changed?
# HACK: we need to persist the parent relation before rescheduling the parent
# and the former parent since we rely on the database for scheduling.
# The following will update the parent_id of the work package without that being noticed by the
# work package instance (work_package) that is already instantiated. That way, the change can be rolled
# back without any side effects to the instance (e.g. dirty tracking).
WorkPackage.where(id: work_package.id).update_all(parent_id: work_package.parent_id)
work_package.rebuild! # using the ClosureTree#rebuild! method to update the transitive hierarchy information
end
yield
def reschedule_related(work_package)
rescheduled = if work_package.saved_change_to_parent_id? && work_package.parent_id_before_last_save
reschedule_former_siblings(work_package).dependent_results
else
[]
end
# Always rolling back the changes we made in here
raise ActiveRecord::Rollback
end
rescheduled + reschedule(work_package, [work_package]).dependent_results
end
# Rescheduling the former siblings will lead to the whole former tree being rescheduled.
def reschedule_former_siblings
reschedule(WorkPackage.where(parent_id: work_package.parent_id_was))
def reschedule_former_siblings(work_package)
reschedule(work_package, WorkPackage.where(parent_id: work_package.parent_id_before_last_save))
end
def reschedule(work_packages)
def reschedule(work_package, work_packages)
WorkPackages::SetScheduleService
.new(user:,
work_package: work_packages)
.call(changed_attributes)
end
def changed_attributes
work_package.changed.map(&:to_sym)
.call(work_package.saved_changes.keys.map(&:to_sym))
end
# When multiple services change a work package, we still only want one update to the database due to:
@ -195,19 +130,15 @@ class WorkPackages::UpdateService < ::BaseServices::BaseCallable
# * having only one journal entry
# * stale object errors
# we thus consolidate the results so that one instance contains the changes made by all the services.
def consolidated_results(result)
result.all_results.group_by(&:id).inject([]) do |a, (_, instances)|
master = instances.pop
instances.each do |instance|
master.attributes = instance.changes.transform_values(&:last)
def consolidated_calls(service_calls)
service_calls
.group_by { |sc| sc.result.id }
.map do |(_, same_work_package_calls)|
same_work_package_calls.pop.tap do |master|
same_work_package_calls.each do |sc|
master.result.attributes = sc.result.changes.transform_values(&:last)
end
end
a + [master]
end
end
def work_package
model
end
end

@ -179,6 +179,8 @@ module Redmine
end
def ensure_custom_values_complete
return unless custom_values.loaded? && (custom_values.any?(&:changed?) || custom_value_destroyed)
self.custom_values = custom_field_values
end

@ -81,8 +81,10 @@ module OpenProject::Backlogs::List
protected
def assume_bottom_position
update_columns(position: bottom_position_in_list(self).to_i + 1)
# Override acts_as_list implementation to avoid it calling save.
# Calling save would remove the changes/saved_changes information.
def set_list_position(new_position, _raise_exception_if_save_fails = false) # rubocop:disable Style/OptionalBooleanParameter
update_columns(position: new_position)
end
def fix_other_work_package_positions

@ -32,36 +32,44 @@ module OpenProject::Backlogs::Patches::UpdateServicePatch
end
module InstanceMethods
def update_descendants
def update_descendants(work_package)
super_result = super
if work_package.in_backlogs_type? && work_package.version_id_changed?
inherit_version_to_descendants(super_result)
if work_package.in_backlogs_type? && work_package.saved_change_to_version_id?
super_result += inherit_version_to_descendants(work_package)
end
super_result
end
def inherit_version_to_descendants(result)
all_descendants = work_package
.descendants
.includes(project: :enabled_modules)
.order_by_ancestors('asc')
.select('work_packages.*')
stop_descendants_ids = []
descendant_tasks = all_descendants.reject do |t|
if stop_descendants_ids.include?(t.parent_id) || !t.is_task?
stop_descendants_ids << t.id
end
end
def inherit_version_to_descendants(work_package)
all_descendants = sorted_descendants(work_package)
descendant_tasks = descendant_tasks_of(all_descendants)
attributes = { version_id: work_package.version_id }
descendant_tasks.each do |task|
descendant_tasks.map do |task|
# Ensure the parent is already moved to new version so that validation errors are avoided.
task.parent = ([work_package] + all_descendants).detect { |d| d.id == task.parent_id }
result.add_dependent!(set_attributes(attributes, task))
set_descendant_attributes(attributes, task)
end
end
def sorted_descendants(work_package)
work_package
.descendants
.includes(project: :enabled_modules)
.order_by_ancestors('asc')
.select('work_packages.*')
end
def descendant_tasks_of(descendants)
stop_descendants_ids = []
descendants.reject do |t|
if stop_descendants_ids.include?(t.parent_id) || !t.is_task?
stop_descendants_ids << t.id
end
end
end
end

@ -1,198 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 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'
require 'rack/test'
describe 'API v3 Work package resource', type: :request, content_type: :json do
include Rack::Test::Methods
include Capybara::RSpecMatchers
include API::V3::Utilities::PathHelper
let(:work_package) do
create(
:work_package,
project_id: project.id,
parent:,
subject: "Updated WorkPackage"
)
end
let!(:parent) do
create(:work_package, project_id: project.id, type:, subject: "Invalid Dependent WorkPackage").tap do |parent|
parent.custom_values.create custom_field: custom_field, value: custom_field.possible_values.first.id
cv = parent.custom_values.last
cv.update_column :value, "0"
end
end
let(:project) do
create(:project, identifier: 'deperr', public: false).tap do |project|
project.types << type
end
end
let(:type) do
create(:type).tap do |type|
type.custom_fields << custom_field
end
end
let(:status) { create :status }
let(:custom_field) do
create(
:list_wp_custom_field,
name: "Gate",
possible_values: %w(A B C),
is_required: true
)
end
let(:role) { create(:role, permissions:) }
let(:permissions) { %i[view_work_packages edit_work_packages create_work_packages] }
let(:current_user) do
create(:user, member_in_project: project, member_through_role: role)
end
let(:dependent_error_result) do
proc do |instance, _attributes, work_package|
result = ServiceResult.new(success: true, result: (instance.respond_to?(:model) && instance.model) || work_package)
dep = parent
dep.errors.add :base, "invalid", message: "invalid"
result.add_dependent!(ServiceResult.new(success: false, errors: dep.errors, result: dep))
result
end
end
before do
login_as current_user
end
describe '#patch' do
let(:path) { api_v3_paths.work_package work_package.id }
let(:valid_params) do
{
_type: 'WorkPackage',
lockVersion: work_package.lock_version
}
end
subject(:response) { last_response }
shared_context 'patch request' do
before do
patch path, params.to_json, 'CONTENT_TYPE' => 'application/json'
end
end
before do
allow_any_instance_of(WorkPackages::UpdateService).to receive(:update_dependent, &dependent_error_result)
end
context 'attribute' do
let(:params) { valid_params.merge(startDate: "2018-05-23") }
include_context 'patch request'
it { expect(response.status).to eq(422) }
it 'responds with an error' do
expected_error = {
_type: "Error",
errorIdentifier: "urn:openproject-org:api:v3:errors:PropertyConstraintViolation",
message: "Error attempting to alter dependent object: Work package ##{parent.id} - #{parent.subject}: invalid",
_embedded: {
details: {
attribute: "base"
}
}
}
expect(subject.body).to be_json_eql(expected_error.to_json)
end
end
end
describe '#post' do
let(:current_user) { create :admin }
let(:path) { api_v3_paths.work_packages }
let(:valid_params) do
{
_type: 'WorkPackage',
lockVersion: 0,
_links: {
author: { href: "/api/v3/users/#{current_user.id}" },
project: { href: "/api/v3/projects/#{project.id}" },
status: { href: "/api/v3/statuses/#{status.id}" },
priority: { href: "/api/v3/priorities/#{work_package.priority.id}" }
}
}
end
subject(:response) { last_response }
shared_context 'post request' do
before do
post path, params.to_json, 'CONTENT_TYPE' => 'application/json'
end
end
before do
allow_any_instance_of(WorkPackages::CreateService).to receive(:create, &dependent_error_result)
end
context 'request' do
let(:params) { valid_params.merge(subject: "Test Subject") }
include_context 'post request'
it { expect(response.status).to eq(422) }
it 'responds with an error' do
expected_error = {
_type: "Error",
errorIdentifier: "urn:openproject-org:api:v3:errors:PropertyConstraintViolation",
message: "Error attempting to alter dependent object: Work package ##{parent.id} - #{parent.subject}: invalid",
_embedded: {
details: {
attribute: "base"
}
}
}
expect(subject.body).to be_json_eql(expected_error.to_json)
end
end
end
end

@ -1096,6 +1096,33 @@ describe WorkPackages::SetAttributesService, type: :model do
end
end
end
context 'for parent' do
let(:parent_work_package) { build_stubbed(:work_package, project:) }
let(:work_package) do
build_stubbed(:work_package, project:, type: initial_type, parent: parent_work_package)
end
context 'with cross project relations allowed', with_settings: { cross_project_work_package_relations: true } do
it 'keeps the parent' do
expect(subject)
.to be_success
expect(work_package.parent)
.to eql(parent_work_package)
end
end
context 'with cross project relations disabled', with_settings: { cross_project_work_package_relations: false } do
it 'deletes the parent' do
expect(subject)
.to be_success
expect(work_package.parent)
.to be_nil
end
end
end
end
context 'when updating project before calling the service' do

@ -27,238 +27,15 @@
#++
require 'spec_helper'
require 'services/base_services/behaves_like_update_service'
describe WorkPackages::UpdateService, type: :model do
let(:user) { build_stubbed(:user) }
let(:project) do
p = build_stubbed(:project)
allow(p).to receive(:shared_versions).and_return([])
p
end
let(:work_package) do
wp = build_stubbed(:work_package, project:)
wp.type = build_stubbed(:type)
wp.send(:clear_changes_information)
wp
end
let(:instance) do
described_class.new(user:,
model: work_package)
end
before do
# Stub update_ancestors because it messes with the jouralizing expectations
allow(instance).to receive(:update_ancestors).and_return []
end
describe 'call' do
let(:set_attributes_service) do
service = double("WorkPackages::SetAttributesService",
new: set_attributes_service_instance)
stub_const('WorkPackages::SetAttributesService', service)
service
end
let(:send_notifications) { true }
let(:set_attributes_service_instance) do
instance = double("WorkPackages::SetAttributesServiceInstance")
allow(instance)
.to receive(:call) do |attributes|
work_package.attributes = attributes
set_service_results
end
instance
end
let(:errors) { [] }
let(:set_service_results) { ServiceResult.new success: true, result: work_package }
let(:work_package_save_result) { true }
# This is now only a very basic test testing the structure of the service.
# The domain tests are in the update_service_integration_spec.rb
it_behaves_like 'BaseServices update service' do
before do
set_attributes_service
end
before do
expect(Journal::NotificationConfiguration)
.to receive(:with)
.with(send_notifications)
.and_yield
allow(work_package)
.to receive(:save)
.and_return work_package_save_result
end
shared_examples_for 'service call' do
subject { instance.call(**call_attributes.merge(send_notifications:).symbolize_keys) }
it 'is successful' do
expect(subject.success?).to be_truthy
end
it 'sets the value' do
subject
attributes.each do |attribute, key|
expect(work_package.send(attribute)).to eql key
end
end
it 'has no errors' do
expect(subject.errors.all?(&:empty?)).to be_truthy
end
context 'when setting the attributes is unsuccessful (invalid)' do
let(:errors) { ActiveModel::Errors.new(work_package) }
let(:set_service_results) { ServiceResult.new success: false, errors:, result: work_package }
it 'is unsuccessful' do
expect(subject.success?).to be_falsey
end
it 'does not persist the changes' do
subject
expect(work_package).not_to receive(:save)
end
it 'exposes the errors' do
errors.add(:base, 'This is a custom error!')
subject
expect(subject.errors).to eql errors
expect(subject.errors[:base]).to include 'This is a custom error!'
end
end
context 'when the saving is unsuccessful' do
let(:work_package_save_result) { false }
let(:saving_errors) { ActiveModel::Errors.new(work_package) }
before do
allow(work_package)
.to receive(:errors)
.and_return(saving_errors)
end
it 'is unsuccessful' do
expect(subject.success?).to be_falsey
end
it 'leaves the value unchanged' do
subject
expect(work_package.changed?).to be_truthy
end
it 'exposes the errors, but is a new instance' do
saving_errors.add(:base, 'This is a custom error!')
subject
expect(subject.errors).not_to eql saving_errors
expect(subject.errors[:base]).to include 'This is a custom error!'
end
end
end
context 'update subject before calling the service' do
let(:call_attributes) { {} }
let(:attributes) { { subject: 'blubs blubs' } }
before do
work_package.attributes = attributes
end
it_behaves_like 'service call'
end
context 'updating subject via attributes' do
let(:call_attributes) { attributes }
let(:attributes) { { subject: 'blubs blubs' } }
it_behaves_like 'service call'
end
context 'when switching the project' do
let(:target_project) do
build_stubbed(:project)
end
let(:call_attributes) { attributes }
let(:attributes) { { project: target_project } }
it_behaves_like 'service call'
context 'relations' do
let!(:scope) do
instance_double(ActiveRecord::Relation).tap do |relations|
allow(Relation)
.to receive(:of_work_package)
.with([work_package])
.and_return(relations)
allow(relations)
.to receive(:destroy_all)
end
end
it 'removes the relations if the setting does not permit cross project relations' do
allow(Setting)
.to receive(:cross_project_work_package_relations?)
.and_return false
instance.call(project: target_project)
expect(scope)
.to have_received(:destroy_all)
end
it 'leaves the relations unchanged if the setting allows cross project relations' do
allow(Setting)
.to receive(:cross_project_work_package_relations?)
.and_return true
instance.call(project: target_project)
expect(scope)
.not_to have_received(:destroy_all)
end
end
context 'time_entries' do
it 'moves the time entries' do
scope = double('scope')
expect(TimeEntry)
.to receive(:on_work_packages)
.with([work_package])
.and_return(scope)
expect(scope)
.to receive(:update_all)
.with(project_id: target_project.id)
instance.call(project: target_project)
end
end
end
context 'when switching the type' do
let(:target_type) { build_stubbed(:type) }
context 'custom_values' do
it 'resets the custom values' do
expect(work_package)
.to receive(:reset_custom_values!)
instance.call(type: target_type)
end
end
allow(set_attributes_errors)
.to receive(:merge!)
end
end
end

Loading…
Cancel
Save