From f4dfd6c6c62168ed554d0bfb94a9f48977f18e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 16 Feb 2021 08:46:53 +0100 Subject: [PATCH] [36238] Extract and fix user references in other objects (#9007) * Move replacing invalid references into separate job for principals * Write migration to remove existing invalid custom values and responsible * Fix other specs * Fix other specs * rewrite replacing user in records * consolidate principal deletion * include placeholder users in spec Co-authored-by: ulferts --- app/controllers/groups_controller.rb | 4 +- .../placeholder_users_controller.rb | 10 +- app/models/associations/groupable.rb | 1 + app/models/group.rb | 14 - app/models/user.rb | 22 - .../journals/user_reference_update_service.rb | 33 -- .../placeholder_users/delete_service.rb | 5 + .../principals/replace_references_service.rb | 126 +++++ app/services/users/delete_service.rb | 2 +- app/workers/principals/delete_job.rb | 85 ++++ config/locales/en.yml | 1 + ...45_replace_invalid_principal_references.rb | 22 + modules/budgets/app/models/budget.rb | 10 - .../journal/budget_journal_factory.rb | 9 +- .../journal/time_entry_journal_factory.rb | 32 ++ .../costs/spec/models/user_deletion_spec.rb | 208 -------- .../journal/document_journal_factory.rb | 32 ++ modules/meeting/app/models/meeting.rb | 4 - modules/meeting/app/models/meeting_content.rb | 4 - .../meeting/app/models/meeting_participant.rb | 4 - .../lib/open_project/meeting/engine.rb | 6 - .../meeting/spec/models/user_deletion_spec.rb | 207 -------- modules/reporting/app/models/cost_query.rb | 27 - .../reporting/app/models/cost_query/filter.rb | 1 + .../cost_query/filter/responsible_id.rb | 41 ++ .../spec/models/cost_query/cost_query_spec.rb | 122 ----- spec/controllers/groups_controller_spec.rb | 3 + .../journal/attachment_journal_factory.rb | 32 ++ .../journal/changeset_journal_factory.rb | 34 ++ .../journal/customizable_journal_factory.rb | 32 ++ .../journal/news_journal_facctory.rb | 32 ++ spec/features/groups/groups_spec.rb | 7 + spec/models/group_performance_spec.rb | 2 +- spec/models/group_spec.rb | 56 +-- spec/models/user_deletion_spec.rb | 464 ------------------ .../api/v3/user/user_resource_spec.rb | 2 +- .../user_reference_update_service_spec.rb | 106 ---- .../placeholder_users/delete_service_spec.rb | 6 +- ...eferences_service_call_integration_spec.rb | 408 +++++++++++++++ spec/services/users/delete_service_spec.rb | 6 +- .../principals/delete_job_integration_spec.rb | 404 +++++++++++++++ 41 files changed, 1317 insertions(+), 1309 deletions(-) delete mode 100644 app/services/journals/user_reference_update_service.rb create mode 100644 app/services/principals/replace_references_service.rb create mode 100644 app/workers/principals/delete_job.rb create mode 100644 db/migrate/20210214205545_replace_invalid_principal_references.rb rename app/workers/delete_user_job.rb => modules/costs/spec/factories/journal/budget_journal_factory.rb (90%) create mode 100644 modules/costs/spec/factories/journal/time_entry_journal_factory.rb delete mode 100644 modules/costs/spec/models/user_deletion_spec.rb create mode 100644 modules/documents/spec/factories/journal/document_journal_factory.rb delete mode 100644 modules/meeting/spec/models/user_deletion_spec.rb create mode 100644 modules/reporting/app/models/cost_query/filter/responsible_id.rb delete mode 100644 modules/reporting/spec/models/cost_query/cost_query_spec.rb create mode 100644 spec/factories/journal/attachment_journal_factory.rb create mode 100644 spec/factories/journal/changeset_journal_factory.rb create mode 100644 spec/factories/journal/customizable_journal_factory.rb create mode 100644 spec/factories/journal/news_journal_facctory.rb delete mode 100644 spec/models/user_deletion_spec.rb delete mode 100644 spec/services/journals/user_reference_update_service_spec.rb create mode 100644 spec/services/principals/replace_references_service_call_integration_spec.rb create mode 100644 spec/workers/principals/delete_job_integration_spec.rb diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index d506794083a..7300f16dcef 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -80,9 +80,9 @@ class GroupsController < ApplicationController end def destroy - @group.destroy + ::Principals::DeleteJob.perform_later(@group) - flash[:notice] = I18n.t(:notice_successful_delete) + flash[:info] = I18n.t(:notice_deletion_scheduled) redirect_to action: :index end diff --git a/app/controllers/placeholder_users_controller.rb b/app/controllers/placeholder_users_controller.rb index 7e5ce55999f..c2bf256e4e7 100644 --- a/app/controllers/placeholder_users_controller.rb +++ b/app/controllers/placeholder_users_controller.rb @@ -120,11 +120,11 @@ class PlaceholderUsersController < ApplicationController end def destroy - Users::DeleteService.new(user: User.current, - model: @placeholder_user) - .call + PlaceholderUsers::DeleteService + .new(user: User.current, model: @placeholder_user) + .call - flash[:notice] = I18n.t('account.deleted') + flash[:info] = I18n.t(:notice_deletion_scheduled) respond_to do |format| format.html do @@ -142,7 +142,7 @@ class PlaceholderUsersController < ApplicationController end def check_if_deletion_allowed - render_404 unless PlaceholderUsers::DeleteService.deletion_allowed? @placeholder_user, User.current + render_404 unless PlaceholderUsers::DeleteContract.deletion_allowed?(current_user) end protected diff --git a/app/models/associations/groupable.rb b/app/models/associations/groupable.rb index 539cbfee0f4..690b82a0bcc 100644 --- a/app/models/associations/groupable.rb +++ b/app/models/associations/groupable.rb @@ -31,6 +31,7 @@ module Associations::Groupable def self.included(base) base.has_and_belongs_to_many :groups, + foreign_key: 'user_id', join_table: "#{base.table_name_prefix}group_users#{base.table_name_suffix}", after_remove: ->(user, group) { group.user_removed(user) } end diff --git a/app/models/group.rb b/app/models/group.rb index f94ed3f8edc..5381cfbc86d 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -36,8 +36,6 @@ class Group < Principal acts_as_customizable - before_destroy :remove_references_before_destroy - alias_attribute(:groupname, :lastname) validates_presence_of :groupname validate :uniqueness_of_groupname @@ -94,18 +92,6 @@ class Group < Principal private - # Removes references that are not handled by associations - def remove_references_before_destroy - return if id.nil? - - deleted_user = DeletedUser.first - - WorkPackage.where(assigned_to_id: id).update_all(assigned_to_id: deleted_user.id) - - Journal::WorkPackageJournal.where(assigned_to_id: id) - .update_all(assigned_to_id: deleted_user.id) - end - def uniqueness_of_groupname groups_with_name = Group.where('lastname = ? AND id <> ?', groupname, id || 0).count if groups_with_name > 0 diff --git a/app/models/user.rb b/app/models/user.rb index 9f45075e41c..4633fdfca31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -134,8 +134,6 @@ class User < Principal after_save :update_password before_create :sanitize_mail_notification_setting - before_destroy :delete_associated_private_queries - before_destroy :reassign_associated scope :admin, -> { where(admin: true) } @@ -733,26 +731,6 @@ class User < Principal (passwords[keep_count..-1] || []).each(&:destroy) end - def reassign_associated - substitute = DeletedUser.first - - [WorkPackage, Attachment, WikiContent, News, Comment, Message].each do |klass| - klass.where(['author_id = ?', id]).update_all ['author_id = ?', substitute.id] - end - - [TimeEntry, ::Query].each do |klass| - klass.where(['user_id = ?', id]).update_all ['user_id = ?', substitute.id] - end - - Journals::UserReferenceUpdateService - .new(self) - .call(substitute) - end - - def delete_associated_private_queries - ::Query.where(user_id: id, is_public: false).delete_all - end - ## # Brute force prevention - class methods # diff --git a/app/services/journals/user_reference_update_service.rb b/app/services/journals/user_reference_update_service.rb deleted file mode 100644 index eb9d25dc3b5..00000000000 --- a/app/services/journals/user_reference_update_service.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Journals - class UserReferenceUpdateService - attr_accessor :original_user - - def initialize(original_user) - self.original_user = original_user - end - - def call(substitute_user) - journal_classes.each do |klass| - foreign_keys.each do |foreign_key| - if klass.column_names.include? foreign_key - klass - .where(foreign_key => original_user.id) - .update_all(foreign_key => substitute_user.id) - end - end - end - - ServiceResult.new success: true - end - - private - - def journal_classes - [Journal] + Journal::BaseJournal.subclasses - end - - def foreign_keys - %w[author_id user_id assigned_to_id responsible_id] - end - end -end diff --git a/app/services/placeholder_users/delete_service.rb b/app/services/placeholder_users/delete_service.rb index bb8336ca2bb..a4b83b9080c 100644 --- a/app/services/placeholder_users/delete_service.rb +++ b/app/services/placeholder_users/delete_service.rb @@ -29,4 +29,9 @@ #++ class PlaceholderUsers::DeleteService < ::BaseServices::Delete + def destroy(placeholder) + ::Principals::DeleteJob.perform_later(placeholder) + + true + end end diff --git a/app/services/principals/replace_references_service.rb b/app/services/principals/replace_references_service.rb new file mode 100644 index 00000000000..f824b0901bb --- /dev/null +++ b/app/services/principals/replace_references_service.rb @@ -0,0 +1,126 @@ +#-- encoding: UTF-8 + +#-- 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. +#++ + +# Rewrites references to a principal from one principal to the other. +# No data is to be removed. +module Principals + class ReplaceReferencesService + def call(from:, to:) + rewrite_active_models(from, to) + rewrite_custom_value(from, to) + rewrite_default_journals(from, to) + rewrite_customizable_journals(from, to) + + ServiceResult.new success: true + end + + private + + # rubocop:disable Rails/SkipsModelValidations + def rewrite_active_models(from, to) + rewrite_author(from, to) + rewrite_user(from, to) + rewrite_assigned_to(from, to) + rewrite_responsible(from, to) + end + + def rewrite_custom_value(from, to) + CustomValue + .where(custom_field_id: CustomField.where(field_format: 'user')) + .where(value: from.id.to_s) + .update_all(value: to.id.to_s) + end + + def rewrite_default_journals(from, to) + journal_classes.each do |klass| + foreign_keys.each do |foreign_key| + if klass.column_names.include? foreign_key + klass + .where(foreign_key => from.id) + .update_all(foreign_key => to.id) + end + end + end + end + + def rewrite_customizable_journals(from, to) + Journal::CustomizableJournal + .joins(:custom_field) + .where(custom_fields: { field_format: 'user' }) + .where(value: from.id.to_s) + .update_all(value: to.id.to_s) + end + + def rewrite_author(from, to) + [WorkPackage, + Attachment, + WikiContent, + News, + Comment, + Message, + Budget, + MeetingAgenda, + MeetingMinutes].each do |klass| + klass.where(author_id: from.id).update_all(author_id: to.id) + end + end + + def rewrite_user(from, to) + [TimeEntry, + ::Query, + Changeset, + CostQuery, + MeetingParticipant].each do |klass| + klass.where(user_id: from.id).update_all(user_id: to.id) + end + end + + def rewrite_assigned_to(from, to) + [WorkPackage].each do |klass| + klass.where(assigned_to_id: from.id).update_all(assigned_to_id: to.id) + end + end + + def rewrite_responsible(from, to) + [WorkPackage].each do |klass| + klass.where(responsible_id: from.id).update_all(responsible_id: to.id) + end + end + # rubocop:enable Rails/SkipsModelValidations + + def journal_classes + [Journal] + Journal::BaseJournal.subclasses + end + + def foreign_keys + %w[author_id user_id assigned_to_id responsible_id] + end + end +end diff --git a/app/services/users/delete_service.rb b/app/services/users/delete_service.rb index f82744216e0..300a6f03656 100644 --- a/app/services/users/delete_service.rb +++ b/app/services/users/delete_service.rb @@ -40,7 +40,7 @@ module Users # as destroying users is a lengthy process we handle it in the background # and lock the account now so that no action can be performed with it user_object.lock! - DeleteUserJob.perform_later(user_object) + ::Principals::DeleteJob.perform_later(user_object) logout! if self_delete? diff --git a/app/workers/principals/delete_job.rb b/app/workers/principals/delete_job.rb new file mode 100644 index 00000000000..0882859e4df --- /dev/null +++ b/app/workers/principals/delete_job.rb @@ -0,0 +1,85 @@ +#-- encoding: UTF-8 + +#-- 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. +#++ + +class Principals::DeleteJob < ApplicationJob + queue_with_priority :low + + def perform(principal) + Principal.transaction do + delete_associated(principal) + replace_references(principal) + update_cost_queries(principal) + + principal.destroy + end + end + + private + + def replace_references(principal) + Principals::ReplaceReferencesService + .new + .call(from: principal, to: DeletedUser.first) + .tap do |call| + raise ActiveRecord::Rollback if call.failure? + end + end + + def delete_associated(principal) + delete_private_queries(principal) + end + + def delete_private_queries(principal) + ::Query.where(user_id: principal.id, is_public: false).delete_all + CostQuery.where(user_id: principal.id, is_public: false).delete_all + end + + # rubocop:disable Rails/SkipsModelValidations + def update_cost_queries(principal) + CostQuery.in_batches.each_record do |query| + serialized = query.serialized + + serialized[:filters] = serialized[:filters].map do |name, options| + remove_cost_query_values(name, options, principal) + end.compact + + CostQuery.where(id: query.id).update_all(serialized: serialized) + end + end + # rubocop:enable Rails/SkipsModelValidations + + def remove_cost_query_values(name, options, principal) + options[:values].delete(principal.id.to_s) if %w[UserId AuthorId AssignedToId ResponsibleId].include?(name) + + if options[:values].nil? || options[:values].any? + [name, options] + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 1f26cb1473e..da77de9957c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1906,6 +1906,7 @@ en: notice_email_sent: "An email was sent to %{value}" notice_failed_to_save_work_packages: "Failed to save %{count} work package(s) on %{total} selected: %{ids}." notice_failed_to_save_members: "Failed to save member(s): %{errors}." + notice_deletion_scheduled: "The deletion has been scheduled and is performed asynchronously." notice_file_not_found: "The page you were trying to access doesn't exist or has been removed." notice_forced_logout: "You have been automatically logged out after %{ttl_time} minutes of inactivity." diff --git a/db/migrate/20210214205545_replace_invalid_principal_references.rb b/db/migrate/20210214205545_replace_invalid_principal_references.rb new file mode 100644 index 00000000000..75ed420dca0 --- /dev/null +++ b/db/migrate/20210214205545_replace_invalid_principal_references.rb @@ -0,0 +1,22 @@ +class ReplaceInvalidPrincipalReferences < ActiveRecord::Migration[6.1] + def up + DeletedUser.reset_column_information + deleted_user_id = DeletedUser.first.id + + say "Replacing invalid custom value user references" + CustomValue + .joins(:custom_field) + .where("#{CustomField.table_name}.field_format" => 'user') + .where("value NOT IN (SELECT id::text FROM users)") + .update_all(value: deleted_user_id) + + say "Replacing invalid responsible user references in work packages" + WorkPackage + .where("responsible_id NOT IN (SELECT id FROM users)") + .update_all(responsible_id: deleted_user_id) + end + + def down + # Nothing to do, as only invalid data is fixed + end +end diff --git a/modules/budgets/app/models/budget.rb b/modules/budgets/app/models/budget.rb index 7d8e84b37ee..47b731947ab 100644 --- a/modules/budgets/app/models/budget.rb +++ b/modules/budgets/app/models/budget.rb @@ -59,10 +59,6 @@ class Budget < ApplicationRecord validates_length_of :subject, maximum: 255 validates_length_of :subject, minimum: 1 - User.before_destroy do |user| - Budget.replace_author_with_deleted_user user - end - class << self def visible(user) includes(:project) @@ -80,12 +76,6 @@ class Budget < ApplicationRecord copy end - def replace_author_with_deleted_user(user) - substitute = DeletedUser.first - - where(author_id: user.id).update_all(author_id: substitute.id) - end - protected def copy_attributes(source) diff --git a/app/workers/delete_user_job.rb b/modules/costs/spec/factories/journal/budget_journal_factory.rb similarity index 90% rename from app/workers/delete_user_job.rb rename to modules/costs/spec/factories/journal/budget_journal_factory.rb index a4a7744b90b..89a0b1e0f37 100644 --- a/app/workers/delete_user_job.rb +++ b/modules/costs/spec/factories/journal/budget_journal_factory.rb @@ -1,5 +1,3 @@ -#-- encoding: UTF-8 - #-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2021 the OpenProject GmbH @@ -28,10 +26,7 @@ # See docs/COPYRIGHT.rdoc for more details. #++ -class DeleteUserJob < ApplicationJob - queue_with_priority :low - - def perform(user) - user.destroy +FactoryBot.define do + factory :journal_budget_journal, class: Journal::BudgetJournal do end end diff --git a/modules/costs/spec/factories/journal/time_entry_journal_factory.rb b/modules/costs/spec/factories/journal/time_entry_journal_factory.rb new file mode 100644 index 00000000000..1ac979db492 --- /dev/null +++ b/modules/costs/spec/factories/journal/time_entry_journal_factory.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_time_entry_journal, class: Journal::TimeEntryJournal do + end +end diff --git a/modules/costs/spec/models/user_deletion_spec.rb b/modules/costs/spec/models/user_deletion_spec.rb deleted file mode 100644 index a4732c420ae..00000000000 --- a/modules/costs/spec/models/user_deletion_spec.rb +++ /dev/null @@ -1,208 +0,0 @@ -#-- 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 File.dirname(__FILE__) + '/../spec_helper' - -describe User, '#destroy', type: :model do - let(:user) { FactoryBot.create(:user) } - let(:user2) { FactoryBot.create(:user) } - let(:substitute_user) { DeletedUser.first } - let(:project) { FactoryBot.create(:valid_project) } - - before do - user - user2 - end - - after do - User.current = nil - end - - shared_examples_for 'costs updated journalized associated object' do - before do - User.current = user2 - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - User.current = user # in order to have the content journal created by the user - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(substitute_user) - end - end - it { expect(associated_instance.journals.first.user).to eq(user2) } - it 'should update first journal details' do - associations.each do |association| - expect(associated_instance.journals.first.details["#{association}_id".to_sym].last).to eq(user2.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(substitute_user) } - it 'should update second journal details' do - associations.each do |association| - expect(associated_instance.journals.last.details["#{association}_id".to_sym].last).to eq(substitute_user.id) - end - end - end - - shared_examples_for 'costs created journalized associated object' do - before do - User.current = user # in order to have the content journal created by the user - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - User.current = user2 - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) } - it 'should keep the current user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(user2) - end - end - it { expect(associated_instance.journals.first.user).to eq(substitute_user) } - it 'should update the first journal' do - associations.each do |association| - expect(associated_instance.journals.first.details["#{association}_id".to_sym].last).to eq(substitute_user.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(user2) } - it 'should update the last journal' do - associations.each do |association| - expect(associated_instance.journals.last.details["#{association}_id".to_sym].first).to eq(substitute_user.id) - expect(associated_instance.journals.last.details["#{association}_id".to_sym].last).to eq(user2.id) - end - end - end - - describe 'WHEN the user updated a cost object' do - let(:associations) { [:author] } - let(:associated_instance) { FactoryBot.build(:budget) } - let(:associated_class) { Budget } - - it_should_behave_like 'costs updated journalized associated object' - end - - describe 'WHEN the user created a cost object' do - let(:associations) { [:author] } - let(:associated_instance) { FactoryBot.build(:budget) } - let(:associated_class) { Budget } - - it_should_behave_like 'costs created journalized associated object' - end - - describe 'WHEN the user has a labor_budget_item associated' do - let(:item) { FactoryBot.build(:labor_budget_item, user: user) } - - before do - item.save! - - user.destroy - end - - it { expect(LaborBudgetItem.find_by_id(item.id)).to eq(item) } - it { expect(item.user_id).to eq(user.id) } - end - - describe 'WHEN the user has a cost entry' do - let(:work_package) { FactoryBot.create(:work_package) } - let(:entry) do - FactoryBot.create(:cost_entry, user: user, - project: work_package.project, - units: 100.0, - spent_on: Date.today, - work_package: work_package, - comments: '') - end - - before do - FactoryBot.create(:member, project: work_package.project, - user: user, - roles: [FactoryBot.build(:role)]) - entry - - user.destroy - - entry.reload - end - - it { expect(entry.user_id).to eq(user.id) } - end - - describe 'WHEN the user is assigned an hourly rate' do - let(:hourly_rate) do - FactoryBot.build(:hourly_rate, user: user, - project: project) - end - - before do - hourly_rate.save! - user.destroy - end - - it { expect(HourlyRate.find_by_id(hourly_rate.id)).to eq(hourly_rate) } - it { expect(hourly_rate.reload.user_id).to eq(user.id) } - end - - describe 'WHEN the user is assigned a default hourly rate' do - let(:default_hourly_rate) do - FactoryBot.build(:default_hourly_rate, user: user, - project: project) - end - - before do - default_hourly_rate.save! - user.destroy - end - - it { expect(DefaultHourlyRate.find_by_id(default_hourly_rate.id)).to eq(default_hourly_rate) } - it { expect(default_hourly_rate.reload.user_id).to eq(user.id) } - end -end diff --git a/modules/documents/spec/factories/journal/document_journal_factory.rb b/modules/documents/spec/factories/journal/document_journal_factory.rb new file mode 100644 index 00000000000..cbd53e68ac9 --- /dev/null +++ b/modules/documents/spec/factories/journal/document_journal_factory.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_document_journal, class: Journal::DocumentJournal do + end +end diff --git a/modules/meeting/app/models/meeting.rb b/modules/meeting/app/models/meeting.rb index ad5f2c868e3..e9ace1483a4 100644 --- a/modules/meeting/app/models/meeting.rb +++ b/modules/meeting/app/models/meeting.rb @@ -89,10 +89,6 @@ class Meeting < ApplicationRecord after_initialize :set_initial_values - User.before_destroy do |user| - Meeting.where(['author_id = ?', user.id]).update_all ['author_id = ?', DeletedUser.first.id] - end - ## # Return the computed start_time when changed def start_time diff --git a/modules/meeting/app/models/meeting_content.rb b/modules/meeting/app/models/meeting_content.rb index 03f48a054d7..ff8f7da7fb4 100644 --- a/modules/meeting/app/models/meeting_content.rb +++ b/modules/meeting/app/models/meeting_content.rb @@ -54,10 +54,6 @@ class MeetingContent < ApplicationRecord title: Proc.new { |o| "#{o.class.model_name.human}: #{o.meeting.title}" }, url: Proc.new { |o| { controller: '/meetings', action: 'show', id: o.meeting } } - User.before_destroy do |user| - MeetingContent.where(['author_id = ?', user.id]).update_all ['author_id = ?', DeletedUser.first] - end - def editable? true end diff --git a/modules/meeting/app/models/meeting_participant.rb b/modules/meeting/app/models/meeting_participant.rb index 798866540d6..9ee486009be 100644 --- a/modules/meeting/app/models/meeting_participant.rb +++ b/modules/meeting/app/models/meeting_participant.rb @@ -33,10 +33,6 @@ class MeetingParticipant < ApplicationRecord scope :invited, -> { where(invited: true) } scope :attended, -> { where(attended: true) } - User.before_destroy do |user| - MeetingParticipant.where(['user_id = ?', user.id]).update_all ['user_id = ?', DeletedUser.first] - end - def name user.present? ? user.name : name end diff --git a/modules/meeting/lib/open_project/meeting/engine.rb b/modules/meeting/lib/open_project/meeting/engine.rb index d130f8dd7e3..8296b4fcf59 100644 --- a/modules/meeting/lib/open_project/meeting/engine.rb +++ b/modules/meeting/lib/open_project/meeting/engine.rb @@ -87,12 +87,6 @@ module OpenProject::Meeting end config.to_prepare do - # load classes so that all User.before_destroy filters are loaded - require_dependency 'meeting' - require_dependency 'meeting_agenda' - require_dependency 'meeting_minutes' - require_dependency 'meeting_participant' - PermittedParams.permit(:search, :meetings) end diff --git a/modules/meeting/spec/models/user_deletion_spec.rb b/modules/meeting/spec/models/user_deletion_spec.rb deleted file mode 100644 index 9cd260d496c..00000000000 --- a/modules/meeting/spec/models/user_deletion_spec.rb +++ /dev/null @@ -1,207 +0,0 @@ -#-- 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 File.dirname(__FILE__) + '/../spec_helper' - -describe User, '#destroy', type: :model do - let!(:user) { FactoryBot.create(:user) } - let!(:user2) { FactoryBot.create(:user) } - let(:substitute_user) { DeletedUser.first } - let(:project) do - FactoryBot.create(:valid_project) - end - - let(:meeting) do - FactoryBot.create(:meeting, - project: project, - author: user2) - end - let(:participant) do - FactoryBot.create(:meeting_participant, - user: user, - meeting: meeting, - invited: true, - attended: true) - end - - shared_examples_for 'updated journalized associated object' do - before do - allow(User).to receive(:current).and_return(user2) - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - allow(User).to receive(:current).and_return(user) # in order to have the content journal created by the user - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(substitute_user) - end - end - it { expect(associated_instance.journals.first.user).to eq(user2) } - it 'should update first journal changes' do - associations.each do |association| - expect(associated_instance.journals.first.details[(association.to_s + '_id').to_sym].last).to eq(user2.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(substitute_user) } - it 'should update second journal changes' do - associations.each do |association| - expect(associated_instance.journals.last.details[(association.to_s + '_id').to_sym].last).to eq(substitute_user.id) - end - end - end - - shared_examples_for 'created journalized associated object' do - before do - allow(User).to receive(:current).and_return(user) # in order to have the content journal created by the user - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - allow(User).to receive(:current).and_return(user2) - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by_id(associated_instance.id)).to eq(associated_instance) } - it 'should keep the current user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(user2) - end - end - it { expect(associated_instance.journals.first.user).to eq(substitute_user) } - it 'should update the first journal' do - associations.each do |association| - expect(associated_instance.journals.first.details[(association.to_s + '_id').to_sym].last).to eq(substitute_user.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(user2) } - it 'should update the last journal' do - associations.each do |association| - expect(associated_instance.journals.last.details[(association.to_s + '_id').to_sym].first).to eq(substitute_user.id) - expect(associated_instance.journals.last.details[(association.to_s + '_id').to_sym].last).to eq(user2.id) - end - end - end - - describe 'WHEN the user created a meeting' do - let(:associations) { [:author] } - let(:associated_instance) { FactoryBot.build(:meeting, project: project) } - let(:associated_class) { Meeting } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user updated a meeting' do - let(:associations) { [:author] } - let(:associated_instance) { FactoryBot.build(:meeting, project: project) } - let(:associated_class) { Meeting } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user created a meeting agenda' do - let(:associations) { [:author] } - let(:associated_instance) do - FactoryBot.build(:meeting_agenda, meeting: meeting, - text: 'lorem') - end - let(:associated_class) { MeetingAgenda } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user updated a meeting agenda' do - let(:associations) { [:author] } - let(:associated_instance) do - FactoryBot.build(:meeting_agenda, meeting: meeting, - text: 'lorem') - end - let(:associated_class) { MeetingAgenda } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user created a meeting minutes' do - let(:associations) { [:author] } - let(:associated_instance) do - FactoryBot.build(:meeting_minutes, - meeting: meeting, - text: 'lorem') - end - let(:associated_class) { MeetingMinutes } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user updated a meeting minutes' do - let(:associations) { [:author] } - let(:associated_instance) do - FactoryBot.build(:meeting_minutes, - meeting: meeting, - text: 'lorem') - end - let(:associated_class) { MeetingMinutes } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user participated in a meeting' do - before do - participant - # user2 added to participants by being the author - - user.destroy - meeting.reload - participant.reload - end - - it { expect(meeting.participants.map(&:user)).to match_array([DeletedUser.first, user2]) } - it { expect(participant.invited).to be_truthy } - it { expect(participant.attended).to be_truthy } - end -end diff --git a/modules/reporting/app/models/cost_query.rb b/modules/reporting/app/models/cost_query.rb index 4c4d8ace0d6..6c269dc40e4 100644 --- a/modules/reporting/app/models/cost_query.rb +++ b/modules/reporting/app/models/cost_query.rb @@ -249,31 +249,4 @@ class CostQuery < ApplicationRecord def private? !public? end - - User.before_destroy do |user| - CostQuery.where(user_id: user.id, is_public: false).delete_all - CostQuery.where(['user_id = ?', user.id]).update_all ['user_id = ?', DeletedUser.first.id] - - max_query_id = 0 - while (current_queries = CostQuery.limit(1000) - .where(["id > ?", max_query_id]) - .order("id ASC")).size > 0 - - current_queries.each do |query| - serialized = query.serialized - - serialized[:filters] = serialized[:filters].map do |name, options| - options[:values].delete(user.id.to_s) if ["UserId", "AuthorId", "AssignedToId"].include?(name) - - if options[:values].nil? || options[:values].size > 0 - [name, options] - end - end.compact - - CostQuery.where(["id = ?", query.id]).update_all ["serialized = ?", YAML::dump(serialized)] - - max_query_id = query.id - end - end - end end diff --git a/modules/reporting/app/models/cost_query/filter.rb b/modules/reporting/app/models/cost_query/filter.rb index 66bb41307bd..c8461c73c25 100644 --- a/modules/reporting/app/models/cost_query/filter.rb +++ b/modules/reporting/app/models/cost_query/filter.rb @@ -44,6 +44,7 @@ class CostQuery::Filter < Report::Filter CostQuery::Filter::OverriddenCosts, CostQuery::Filter::PriorityId, CostQuery::Filter::ProjectId, + CostQuery::Filter::ResponsibleId, CostQuery::Filter::SpentOn, CostQuery::Filter::StartDate, CostQuery::Filter::StatusId, diff --git a/modules/reporting/app/models/cost_query/filter/responsible_id.rb b/modules/reporting/app/models/cost_query/filter/responsible_id.rb new file mode 100644 index 00000000000..979663f8038 --- /dev/null +++ b/modules/reporting/app/models/cost_query/filter/responsible_id.rb @@ -0,0 +1,41 @@ +#-- 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. +#++ + +class CostQuery::Filter::ResponsibleId < CostQuery::Filter::UserId + use :null_operators + join_table WorkPackage + applies_for :label_work_package_attributes + + def self.label + WorkPackage.human_attribute_name(:responsible) + end + + def self.available_values(*) + CostQuery::Filter::UserId.available_values + end +end diff --git a/modules/reporting/spec/models/cost_query/cost_query_spec.rb b/modules/reporting/spec/models/cost_query/cost_query_spec.rb deleted file mode 100644 index d90f54fc1f7..00000000000 --- a/modules/reporting/spec/models/cost_query/cost_query_spec.rb +++ /dev/null @@ -1,122 +0,0 @@ -#-- 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 File.dirname(__FILE__) + '/../../spec_helper' - -describe User, "#destroy", type: :model do - let(:substitute_user) { DeletedUser.first } - let(:private_query) { FactoryBot.create(:private_cost_query) } - let(:public_query) { FactoryBot.create(:public_cost_query) } - let(:user) { FactoryBot.create(:user) } - let(:user2) { FactoryBot.create(:user) } - - describe "WHEN the user has saved private cost queries" do - before do - private_query.user.destroy - end - - it { expect(CostQuery.find_by_id(private_query.id)).to eq(nil) } - end - - describe "WHEN the user has saved public cost queries" do - before do - public_query.user.destroy - end - - it { expect(CostQuery.find_by_id(public_query.id)).to eq(public_query) } - it { expect(public_query.reload.user_id).to eq(substitute_user.id) } - end - - shared_examples_for "public query" do - let(:filter_symbol) { filter.to_s.demodulize.underscore.to_sym } - - describe "WHEN the filter has the deleted user as it's value" do - before do - public_query.filter(filter_symbol, values: [user.id.to_s], operator: "=") - public_query.save! - - user.destroy - end - - it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any? { |f| f.is_a?(filter) }).to be_falsey } - end - - describe "WHEN the filter has another user as it's value" do - before do - public_query.filter(filter_symbol, values: [user2.id.to_s], operator: "=") - public_query.save! - - user.destroy - end - - it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any? { |f| f.is_a?(filter) }).to be_truthy } - it { - expect(CostQuery.find_by_id(public_query.id).deserialize.filters.detect do |f| - f.is_a?(filter) - end.values).to eq([user2.id.to_s]) - } - end - - describe "WHEN the filter has the deleted user and another user as it's value" do - before do - public_query.filter(filter_symbol, values: [user.id.to_s, user2.id.to_s], operator: "=") - public_query.save! - - user.destroy - end - - it { expect(CostQuery.find_by_id(public_query.id).deserialize.filters.any? { |f| f.is_a?(filter) }).to be_truthy } - it { - expect(CostQuery.find_by_id(public_query.id).deserialize.filters.detect do |f| - f.is_a?(filter) - end.values).to eq([user2.id.to_s]) - } - end - end - - describe "WHEN someone has saved a public cost query - WHEN the query has a user_id filter" do - let(:filter) { CostQuery::Filter::UserId } - - it_should_behave_like "public query" - end - - describe "WHEN someone has saved a public cost query - WHEN the query has a author_id filter" do - let(:filter) { CostQuery::Filter::AuthorId } - - it_should_behave_like "public query" - end - - describe "WHEN someone has saved a public cost query - WHEN the query has a assigned_to_id filter" do - let(:filter) { CostQuery::Filter::AssignedToId } - - it_should_behave_like "public query" - end -end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 457a00a4bee..a23bfba61bd 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -82,6 +82,9 @@ describe GroupsController, type: :controller do it 'should destroy' do delete :destroy, params: { id: group.id } + + perform_enqueued_jobs + expect { group.reload }.to raise_error ActiveRecord::RecordNotFound expect(response).to redirect_to groups_path diff --git a/spec/factories/journal/attachment_journal_factory.rb b/spec/factories/journal/attachment_journal_factory.rb new file mode 100644 index 00000000000..b3db61a3a9f --- /dev/null +++ b/spec/factories/journal/attachment_journal_factory.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_attachment_journal, class: Journal::AttachmentJournal do + end +end diff --git a/spec/factories/journal/changeset_journal_factory.rb b/spec/factories/journal/changeset_journal_factory.rb new file mode 100644 index 00000000000..5a571568c7a --- /dev/null +++ b/spec/factories/journal/changeset_journal_factory.rb @@ -0,0 +1,34 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_changeset_journal, class: Journal::ChangesetJournal do + revision { 5 } + committed_on { Time.zone.today } + end +end diff --git a/spec/factories/journal/customizable_journal_factory.rb b/spec/factories/journal/customizable_journal_factory.rb new file mode 100644 index 00000000000..ffee4c6e7a4 --- /dev/null +++ b/spec/factories/journal/customizable_journal_factory.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_customizable_journal, class: Journal::CustomizableJournal do + end +end diff --git a/spec/factories/journal/news_journal_facctory.rb b/spec/factories/journal/news_journal_facctory.rb new file mode 100644 index 00000000000..ba9ea24b4fb --- /dev/null +++ b/spec/factories/journal/news_journal_facctory.rb @@ -0,0 +1,32 @@ +#-- 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. +#++ + +FactoryBot.define do + factory :journal_news_journal, class: Journal::NewsJournal do + end +end diff --git a/spec/features/groups/groups_spec.rb b/spec/features/groups/groups_spec.rb index 3f283dec06f..9a3a18df44a 100644 --- a/spec/features/groups/groups_spec.rb +++ b/spec/features/groups/groups_spec.rb @@ -44,6 +44,13 @@ feature 'group memberships through groups page', type: :feature do expect(groups_page).to have_group "Bob's Team" groups_page.delete_group! "Bob's Team" + + expect(page).to have_selector('.flash.info', text: I18n.t(:notice_deletion_scheduled)) + expect(groups_page).to have_group "Bob's Team" + + perform_enqueued_jobs + + groups_page.visit! expect(groups_page).not_to have_group "Bob's Team" end end diff --git a/spec/models/group_performance_spec.rb b/spec/models/group_performance_spec.rb index 695814e5b80..49270714f69 100644 --- a/spec/models/group_performance_spec.rb +++ b/spec/models/group_performance_spec.rb @@ -78,7 +78,7 @@ describe Group, type: :model do puts "Destroying group ..." start = Time.now.to_i - group.destroy + Principals::DeleteJob.perform_now group @seconds = Time.now.to_i - start puts "Destroyed group in #{@seconds} seconds" diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index bff8486259f..d514e3f6e19 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -30,8 +30,6 @@ require 'spec_helper' require_relative '../support/shared/become_member' describe Group, type: :model do - include BecomeMember - let(:group) { FactoryBot.create(:group) } let(:user) { FactoryBot.create(:user) } let(:watcher) { FactoryBot.create :user } @@ -81,15 +79,7 @@ describe Group, type: :model do it 'should roles removed when removing group membership' do expect(user).to be_member_of project - member.destroy - user.reload - project.reload - expect(user).not_to be_member_of project - end - - it 'should roles removed when removing user from group' do - expect(user).to be_member_of project - group.destroy + Principals::DeleteJob.perform_now group user.reload project.reload expect(user).not_to be_member_of project @@ -117,50 +107,6 @@ describe Group, type: :model do end end - describe '#destroy' do - describe 'work packages assigned to the group' do - let(:group) { FactoryBot.create(:group, members: [user, watcher]) } - before do - become_member_with_permissions project, group, [:view_work_packages] - package.assigned_to = group - - package.save! - end - - it 'should reassign the work package to nobody' do - group.destroy - - package.reload - - expect(package.assigned_to).to eq(DeletedUser.first) - end - - it 'should update all journals to have the deleted user as assigned' do - group.destroy - - package.reload - - expect(package.journals.all? { |j| j.data.assigned_to_id == DeletedUser.first.id }).to be_truthy - end - - describe 'watchers' do - before do - package.watcher_users << watcher - end - - context 'with user only in project through group' do - it 'should remove the watcher' do - group.destroy - package.reload - project.reload - - expect(package.watchers).to be_empty - end - end - end - end - end - describe '#create' do describe 'group with empty group name' do let(:group) { FactoryBot.build(:group, lastname: '') } diff --git a/spec/models/user_deletion_spec.rb b/spec/models/user_deletion_spec.rb deleted file mode 100644 index d5d7d1e304b..00000000000 --- a/spec/models/user_deletion_spec.rb +++ /dev/null @@ -1,464 +0,0 @@ -#-- 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 User, 'deletion', type: :model do - let(:project) { FactoryBot.create(:project_with_types) } - let(:user) { FactoryBot.create(:user, member_in_project: project) } - let(:user2) { FactoryBot.create(:user) } - let(:member) { project.members.first } - let(:role) { member.roles.first } - let(:status) { FactoryBot.create(:status) } - let(:issue) do - FactoryBot.create(:work_package, type: project.types.first, - author: user, - project: project, - status: status, - assigned_to: user) - end - let(:issue2) do - FactoryBot.create(:work_package, type: project.types.first, - author: user2, - project: project, - status: status, - assigned_to: user2) - end - - let(:substitute_user) { DeletedUser.first } - - describe 'WHEN there is the user' do - before do - user.destroy - end - - it { expect(User.find_by(id: user.id)).to be_nil } - end - - shared_examples_for 'updated journalized associated object' do - before do - allow(User).to receive(:current).and_return user2 - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - allow(User).to receive(:current).and_return user # in order to have the content journal created by the user - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by(id: associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(substitute_user) - end - end - it { expect(associated_instance.journals.first.user).to eq(user2) } - it 'should update first journal changes' do - associations.each do |association| - expect(associated_instance.journals.first.details[association_key association].last).to eq(user2.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(substitute_user) } - it 'should update second journal changes' do - associations.each do |association| - expect(associated_instance.journals.last.details[association_key association].last).to eq(substitute_user.id) - end - end - end - - def association_key(association) - "#{association}_id".parameterize.underscore.to_sym - end - - shared_examples_for 'created associated object' do - before do - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by(id: associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(substitute_user) - end - end - end - - shared_examples_for 'created journalized associated object' do - before do - allow(User).to receive(:current).and_return user # in order to have the content journal created by the user - associations.each do |association| - associated_instance.send(association.to_s + '=', user) - end - associated_instance.save! - - allow(User).to receive(:current).and_return user2 - associated_instance.reload - associations.each do |association| - associated_instance.send(association.to_s + '=', user2) - end - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by(id: associated_instance.id)).to eq(associated_instance) } - it 'should keep the current user on all associations' do - associations.each do |association| - expect(associated_instance.send(association)).to eq(user2) - end - end - it { expect(associated_instance.journals.first.user).to eq(substitute_user) } - it 'should update the first journal' do - associations.each do |association| - expect(associated_instance.journals.first.details[association_key association].last).to eq(substitute_user.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(user2) } - it 'should update the last journal' do - associations.each do |association| - expect(associated_instance.journals.last.details[association_key association].first).to eq(substitute_user.id) - expect(associated_instance.journals.last.details[association_key association].last).to eq(user2.id) - end - end - end - - describe 'WHEN the user has created one attachment' do - let(:associated_instance) { FactoryBot.build(:attachment) } - let(:associated_class) { Attachment } - let(:associations) { [:author] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has updated one attachment' do - let(:associated_instance) { FactoryBot.build(:attachment) } - let(:associated_class) { Attachment } - let(:associations) { [:author] } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user has an issue created and assigned' do - let(:associated_instance) do - FactoryBot.build(:work_package, type: project.types.first, - project: project, - status: status) - end - let(:associated_class) { WorkPackage } - let(:associations) { %i[author assigned_to responsible] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has an issue updated and assigned' do - let(:associated_instance) do - FactoryBot.build(:work_package, type: project.types.first, - project: project, - status: status) - end - let(:associated_class) { WorkPackage } - let(:associations) { %i[author assigned_to responsible] } - - before do - allow(User).to receive(:current).and_return user2 - associated_instance.author = user2 - associated_instance.assigned_to = user2 - associated_instance.responsible = user2 - associated_instance.save! - - allow(User).to receive(:current).and_return user # in order to have the content journal created by the user - associated_instance.reload - associated_instance.author = user - associated_instance.assigned_to = user - associated_instance.responsible = user - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by(id: associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - expect(associated_instance.author).to eq(substitute_user) - expect(associated_instance.assigned_to).to be_nil - expect(associated_instance.responsible).to be_nil - end - it { expect(associated_instance.journals.first.user).to eq(user2) } - it 'should update first journal changes' do - associations.each do |association| - expect(associated_instance.journals.first.details[association_key association].last).to eq(user2.id) - end - end - it { expect(associated_instance.journals.last.user).to eq(substitute_user) } - it 'should update second journal changes' do - associations.each do |association| - expect(associated_instance.journals.last.details[association_key association].last).to eq(substitute_user.id) - end - end - end - - describe 'WHEN the user has updated a wiki content' do - let(:associated_instance) { FactoryBot.build(:wiki_content) } - let(:associated_class) { WikiContent } - let(:associations) { [:author] } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user has created a wiki content' do - let(:associated_instance) { FactoryBot.build(:wiki_content) } - let(:associated_class) { WikiContent } - let(:associations) { [:author] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has created a news' do - let(:associated_instance) { FactoryBot.build(:news) } - let(:associated_class) { News } - let(:associations) { [:author] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has worked on news' do - let(:associated_instance) { FactoryBot.build(:news) } - let(:associated_class) { News } - let(:associations) { [:author] } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user has created a message' do - let(:associated_instance) { FactoryBot.build(:message) } - let(:associated_class) { Message } - let(:associations) { [:author] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has worked on message' do - let(:associated_instance) { FactoryBot.build(:message) } - let(:associated_class) { Message } - let(:associations) { [:author] } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user has created a time entry' do - let(:associated_instance) do - FactoryBot.build(:time_entry, project: project, - work_package: issue, - hours: 2, - activity: FactoryBot.create(:time_entry_activity)) - end - let(:associated_class) { TimeEntry } - let(:associations) { [:user] } - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has worked on time_entry' do - let(:associated_instance) do - FactoryBot.build(:time_entry, project: project, - work_package: issue, - hours: 2, - activity: FactoryBot.create(:time_entry_activity)) - end - let(:associated_class) { TimeEntry } - let(:associations) { [:user] } - - it_should_behave_like 'updated journalized associated object' - end - - describe 'WHEN the user has commented' do - let(:news) { FactoryBot.create(:news, author: user) } - - let(:associated_instance) do - Comment.new(commented: news, - comments: 'lorem') - end - - let(:associated_class) { Comment } - let(:associations) { [:author] } - - it_should_behave_like 'created associated object' - end - - describe 'WHEN the user is a member of a project' do - before do - user - member - end - - it 'removes that member' do - user.destroy - - expect(Member.find_by(id: member.id)).to be_nil - expect(Role.find_by(id: role.id)).to eq(role) - expect(Project.find_by(id: project.id)).to eq(project) - end - end - - describe 'WHEN the user is watching something' do - let(:watched) { FactoryBot.create(:work_package, project: project) } - let(:watch) do - Watcher.new(user: user, - watchable: watched) - end - - before do - watch.save! - - user.destroy - end - - it { expect(Watcher.find_by(id: watch.id)).to be_nil } - end - - describe 'WHEN the user has a token created' do - let(:token) do - Token::RSS.new(user: user, value: 'loremipsum') - end - - before do - token.save! - - user.destroy - end - - it { expect(Token::RSS.find_by(id: token.id)).to be_nil } - end - - describe 'WHEN the user has created a private query' do - let(:query) { FactoryBot.build(:private_query, user: user) } - - before do - query.save! - - user.destroy - end - - it { expect(Query.find_by(id: query.id)).to be_nil } - end - - describe 'WHEN the user has created a public query' do - let(:associated_instance) { FactoryBot.build(:public_query) } - - let(:associated_class) { Query } - let(:associations) { [:user] } - - it_should_behave_like 'created associated object' - end - - describe 'WHEN the user has created a changeset' do - with_virtual_subversion_repository do - let(:associated_instance) do - FactoryBot.build(:changeset, - repository_id: repository.id, - committer: user.login) - end - - let(:associated_class) { Changeset } - let(:associations) { [:user] } - end - - it_should_behave_like 'created journalized associated object' - end - - describe 'WHEN the user has updated a changeset' do - with_virtual_subversion_repository do - let(:associated_instance) do - FactoryBot.build(:changeset, - repository_id: repository.id, - committer: user2.login) - end - end - - let(:associated_class) { Changeset } - let(:associations) { [:user] } - - before do - allow(User).to receive(:current).and_return user2 - associated_instance.user = user2 - associated_instance.save! - - allow(User).to receive(:current).and_return user # in order to have the content journal created by the user - associated_instance.reload - associated_instance.user = user - associated_instance.save! - - user.destroy - associated_instance.reload - end - - it { expect(associated_class.find_by(id: associated_instance.id)).to eq(associated_instance) } - it 'should replace the user on all associations' do - expect(associated_instance.user).to be_nil - end - it { expect(associated_instance.journals.first.user).to eq(user2) } - it 'should update first journal changes' do - expect(associated_instance.journals.first.details[:user_id].last).to eq(user2.id) - end - it { expect(associated_instance.journals.last.user).to eq(substitute_user) } - it 'should update second journal changes' do - expect(associated_instance.journals.last.details[:user_id].last).to eq(substitute_user.id) - end - end - - describe 'WHEN the user is assigned an issue category' do - let(:category) do - FactoryBot.build(:category, assigned_to: user, - project: project) - end - - before do - category.save! - user.destroy - category.reload - end - - it { expect(Category.find_by(id: category.id)).to eq(category) } - it { expect(category.assigned_to).to be_nil } - end -end diff --git a/spec/requests/api/v3/user/user_resource_spec.rb b/spec/requests/api/v3/user/user_resource_spec.rb index 65a2f40eb40..5b35d006712 100644 --- a/spec/requests/api/v3/user/user_resource_spec.rb +++ b/spec/requests/api/v3/user/user_resource_spec.rb @@ -249,7 +249,7 @@ describe 'API v3 User resource', end it 'should lock the account and mark for deletion' do - expect(DeleteUserJob) + expect(Principals::DeleteJob) .to have_been_enqueued .with(user) diff --git a/spec/services/journals/user_reference_update_service_spec.rb b/spec/services/journals/user_reference_update_service_spec.rb deleted file mode 100644 index 257ca4eae15..00000000000 --- a/spec/services/journals/user_reference_update_service_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -#-- 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 Journals::UserReferenceUpdateService, type: :model do - let!(:work_package) { FactoryBot.create :work_package } - let!(:doomed_user) { work_package.author } - let!(:other_user) { FactoryBot.create(:user) } - let!(:data1) do - FactoryBot.build(:journal_work_package_journal, - subject: work_package.subject, - status_id: work_package.status_id, - type_id: work_package.type_id, - author_id: doomed_user.id, - assigned_to_id: other_user.id, - responsible_id: doomed_user.id, - project_id: work_package.project_id) - end - let!(:data2) do - FactoryBot.build(:journal_work_package_journal, - subject: work_package.subject, - status_id: work_package.status_id, - type_id: work_package.type_id, - author_id: doomed_user.id, - assigned_to_id: doomed_user.id, - responsible_id: other_user.id, - project_id: work_package.project_id) - end - let!(:doomed_user_journal) do - FactoryBot.create :work_package_journal, - notes: '1', - user: doomed_user, - journable_id: work_package.id, - data: data1 - end - let!(:some_other_journal) do - FactoryBot.create :work_package_journal, - notes: '2', - journable_id: work_package.id, - data: data2 - end - - describe '.call' do - subject do - described_class - .new(doomed_user) - .call(DeletedUser.first) - end - - before do - subject - end - - it "is success" do - expect(subject) - .to be_success - end - - it "marks only the user's journal as deleted" do - expect(doomed_user_journal.reload.user.is_a?(DeletedUser)).to be_truthy - expect(some_other_journal.reload.user.is_a?(DeletedUser)).to be_falsey - end - - it "marks the assignee stored in the WorkPackageJournal as deleted" do - expect(data2.reload.assigned_to_id) - .to eql(DeletedUser.first.id) - - expect(data1.reload.assigned_to_id) - .to eql(other_user.id) - end - - it "marks the responsible stored in the WorkPackageJournal as deleted" do - expect(data1.reload.responsible_id) - .to eql(DeletedUser.first.id) - - expect(data2.reload.responsible_id) - .to eql(other_user.id) - end - end -end diff --git a/spec/services/placeholder_users/delete_service_spec.rb b/spec/services/placeholder_users/delete_service_spec.rb index fd0d17b1440..7be49097f06 100644 --- a/spec/services/placeholder_users/delete_service_spec.rb +++ b/spec/services/placeholder_users/delete_service_spec.rb @@ -40,7 +40,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do shared_examples 'deletes the user' do it do expect(input_user).to receive(:lock!) - expect(DeleteUserJob).to receive(:perform_later).with(input_user) + expect(Principals::DeleteJob).to receive(:perform_later).with(input_user) expect(subject).to eq true end end @@ -48,7 +48,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do shared_examples 'does not delete the user' do it do expect(input_user).not_to receive(:lock!) - expect(DeleteUserJob).not_to receive(:perform_later) + expect(Principals::DeleteJob).not_to receive(:perform_later) expect(subject).to eq false end end @@ -76,7 +76,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do it 'performs deletion' do actor.run_given do expect(input_user).to receive(:lock!) - expect(DeleteUserJob).to receive(:perform_later).with(input_user) + expect(Principals::DeleteJob).to receive(:perform_later).with(input_user) expect(subject).to eq true end end diff --git a/spec/services/principals/replace_references_service_call_integration_spec.rb b/spec/services/principals/replace_references_service_call_integration_spec.rb new file mode 100644 index 00000000000..e3edeaa5671 --- /dev/null +++ b/spec/services/principals/replace_references_service_call_integration_spec.rb @@ -0,0 +1,408 @@ +#-- 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 Principals::ReplaceReferencesService, '#call', type: :model do + subject(:service_call) { instance.call(from: principal, to: to_principal) } + + shared_let(:other_user) { FactoryBot.create(:user) } + shared_let(:user) { FactoryBot.create(:user) } + shared_let(:to_principal) { FactoryBot.create :user } + + let(:instance) do + described_class.new + end + + context 'with a user' do + let(:principal) { user } + + it 'is successful' do + expect(service_call) + .to be_success + end + + context 'with a Journal' do + let!(:journal) do + FactoryBot.create(:work_package_journal, + user_id: user_id, + data: instance_double(Journal::WorkPackageJournal, + 'journal=': nil, + save: true)) + end + + context 'with the replaced user' do + let(:user_id) { principal.id } + + before do + service_call + journal.reload + end + + it 'replaces user_id' do + expect(journal.user_id) + .to eql to_principal.id + end + end + + context 'with a different user' do + let(:user_id) { other_user.id } + + before do + service_call + journal.reload + end + + it 'replaces user_id' do + expect(journal.user_id) + .to eql other_user.id + end + end + end + + shared_examples_for 'rewritten record' do |factory, attribute, format = Integer| + let!(:model) do + klass = FactoryBot.factories.find(factory).build_class + all_attributes = other_attributes.merge(attribute => principal_id) + + inserted = ActiveRecord::Base.connection.select_one <<~SQL + INSERT INTO #{klass.table_name} + (#{all_attributes.keys.join(', ')}) + VALUES + (#{all_attributes.values.join(', ')}) + RETURNING id + SQL + + klass.find(inserted['id']) + end + + let(:other_attributes) do + defined?(attributes) ? attributes : {} + end + + def expected(user, format) + if format == String + user.id.to_s + else + user.id + end + end + + context "for #{factory}" do + context 'with the replaced user' do + let(:principal_id) { principal.id } + + before do + service_call + model.reload + end + + it "replaces #{attribute}" do + expect(model.send(attribute)) + .to eql expected(to_principal, format) + end + end + + context 'with a different user' do + let(:principal_id) { other_user.id } + + before do + service_call + model.reload + end + + it "keeps #{attribute}" do + expect(model.send(attribute)) + .to eql expected(other_user, format) + end + end + end + end + + context 'with Attachment' do + it_behaves_like 'rewritten record', + :attachment, + :author_id + + it_behaves_like 'rewritten record', + :journal_attachment_journal, + :author_id do + let(:attributes) do + { journal_id: 1 } + end + end + end + + context 'with Comment' do + it_behaves_like 'rewritten record', + :comment, + :author_id + end + + context 'with CustomValue' do + it_behaves_like 'rewritten record', + :custom_value, + :value, + String do + let(:user_cf) { FactoryBot.create(:user_wp_custom_field) } + let(:attributes) do + { custom_field_id: user_cf.id } + end + end + + it_behaves_like 'rewritten record', + :journal_customizable_journal, + :value, + String do + let(:user_cf) { FactoryBot.create(:user_wp_custom_field) } + let(:attributes) do + { journal_id: 1, + custom_field_id: user_cf.id } + end + end + end + + context 'with Changeset' do + it_behaves_like 'rewritten record', + :changeset, + :user_id do + let(:attributes) do + { repository_id: 1, + revision: 1, + committed_on: "date '2012-02-02'" } + end + end + + it_behaves_like 'rewritten record', + :journal_changeset_journal, + :user_id do + let(:attributes) do + { journal_id: 1, + repository_id: 1, + revision: 1, + committed_on: "date '2012-02-02'" } + end + end + end + + context 'with Message' do + it_behaves_like 'rewritten record', + :message, + :author_id do + let(:attributes) do + { forum_id: 1 } + end + end + + it_behaves_like 'rewritten record', + :journal_message_journal, + :author_id do + let(:attributes) do + { journal_id: 1, + forum_id: 1 } + end + end + end + + context 'with MeetingContent' do + it_behaves_like 'rewritten record', + :meeting_agenda, + :author_id do + let(:attributes) do + { type: "'MeetingAgenda'", + created_at: 'NOW()', + updated_at: 'NOW()' } + end + end + + it_behaves_like 'rewritten record', + :meeting_minutes, + :author_id do + let(:attributes) do + { type: "'MeetingMinutes'", + created_at: 'NOW()', + updated_at: 'NOW()' } + end + end + + it_behaves_like 'rewritten record', + :journal_meeting_content_journal, + :author_id do + let(:attributes) do + { journal_id: 1 } + end + end + end + + context 'with MeetingParticipant' do + it_behaves_like 'rewritten record', + :meeting_participant, + :user_id do + let(:attributes) do + { created_at: 'NOW()', + updated_at: 'NOW()' } + end + end + end + + context 'with News' do + it_behaves_like 'rewritten record', + :news, + :author_id + + it_behaves_like 'rewritten record', + :journal_news_journal, + :author_id do + let(:attributes) do + { journal_id: 1 } + end + end + end + + context 'with WikiContent' do + it_behaves_like 'rewritten record', + :wiki_content, + :author_id do + let(:attributes) do + { page_id: 1, + lock_version: 5 } + end + end + + it_behaves_like 'rewritten record', + :journal_wiki_content_journal, + :author_id do + let(:attributes) do + { journal_id: 1, + page_id: 1 } + end + end + end + + context 'with WorkPackage' do + it_behaves_like 'rewritten record', + :work_package, + :assigned_to_id + + it_behaves_like 'rewritten record', + :work_package, + :responsible_id + + it_behaves_like 'rewritten record', + :journal_work_package_journal, + :assigned_to_id do + let(:attributes) do + { journal_id: 1 } + end + end + + it_behaves_like 'rewritten record', + :journal_work_package_journal, + :responsible_id do + let(:attributes) do + { journal_id: 1 } + end + end + end + + context 'with TimeEntry' do + it_behaves_like 'rewritten record', + :time_entry, + :user_id do + let(:attributes) do + { project_id: 1, + hours: 5, + activity_id: 1, + spent_on: "date '2012-02-02'", + tyear: 2021, + tmonth: 12, + tweek: 5 } + end + end + + it_behaves_like 'rewritten record', + :journal_time_entry_journal, + :user_id do + let(:attributes) do + { journal_id: 1, + project_id: 1, + hours: 5, + activity_id: 1, + spent_on: "date '2012-02-02'", + tyear: 2021, + tmonth: 12, + tweek: 5 } + end + end + end + + context 'with Budget' do + it_behaves_like 'rewritten record', + :budget, + :author_id do + let(:attributes) do + { project_id: 1, + subject: "'abc'", + description: "'cde'", + fixed_date: "date '2012-02-02'" } + end + end + + it_behaves_like 'rewritten record', + :journal_budget_journal, + :author_id do + let(:attributes) do + { journal_id: 1, + project_id: 1, + subject: "'abc'", + fixed_date: "date '2012-02-02'" } + end + end + end + + context 'with Query' do + it_behaves_like 'rewritten record', + :query, + :user_id + end + + context 'with CostQuery' do + let(:query) { FactoryBot.create(:cost_query, user: principal) } + + it_behaves_like 'rewritten record', + :cost_query, + :user_id do + let(:attributes) do + { name: "'abc'", + serialized: "'cde'" } + end + end + + end + end +end diff --git a/spec/services/users/delete_service_spec.rb b/spec/services/users/delete_service_spec.rb index 7eb1d4374ca..50d932209bb 100644 --- a/spec/services/users/delete_service_spec.rb +++ b/spec/services/users/delete_service_spec.rb @@ -39,7 +39,7 @@ describe ::Users::DeleteService, type: :model do shared_examples 'deletes the user' do it do expect(input_user).to receive(:lock!) - expect(DeleteUserJob).to receive(:perform_later).with(input_user) + expect(Principals::DeleteJob).to receive(:perform_later).with(input_user) expect(subject).to be_success end end @@ -47,7 +47,7 @@ describe ::Users::DeleteService, type: :model do shared_examples 'does not delete the user' do it do expect(input_user).not_to receive(:lock!) - expect(DeleteUserJob).not_to receive(:perform_later) + expect(Principals::DeleteJob).not_to receive(:perform_later) expect(subject).not_to be_success end end @@ -75,7 +75,7 @@ describe ::Users::DeleteService, type: :model do it 'performs deletion' do actor.run_given do expect(input_user).to receive(:lock!) - expect(DeleteUserJob).to receive(:perform_later).with(input_user) + expect(Principals::DeleteJob).to receive(:perform_later).with(input_user) expect(subject).to be_success end end diff --git a/spec/workers/principals/delete_job_integration_spec.rb b/spec/workers/principals/delete_job_integration_spec.rb new file mode 100644 index 00000000000..bb6f7919153 --- /dev/null +++ b/spec/workers/principals/delete_job_integration_spec.rb @@ -0,0 +1,404 @@ +#-- 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 Principals::DeleteJob, type: :model do + subject(:job) { described_class.perform_now(principal) } + + shared_let(:project) { FactoryBot.create(:project) } + + shared_let(:deleted_user) do + FactoryBot.create(:deleted_user) + end + let(:principal) do + FactoryBot.create(:user) + end + let(:member) do + FactoryBot.create(:member, + principal: principal, + project: project, + roles: [role]) + end + shared_let(:role) do + FactoryBot.create(:role, permissions: %i[view_work_packages] ) + end + + describe '#perform' do + # These are the only tests that include testing + # the ReplaceReferencesService. Most of the tests for this + # Service are handled within the matching spec file. + shared_examples_for 'work_package handling' do + let(:work_package) do + FactoryBot.create(:work_package, + assigned_to: principal, + responsible: principal) + end + + before do + work_package + job + end + + it 'resets assigned to to the deleted user' do + expect(work_package.reload.assigned_to) + .to eql(deleted_user) + end + + it 'resets assigned to in all journals to the deleted user' do + expect(Journal::WorkPackageJournal.pluck(:assigned_to_id)) + .to eql([deleted_user.id]) + end + + it 'resets responsible to to the deleted user' do + expect(work_package.reload.responsible) + .to eql(deleted_user) + end + + it 'resets responsible to in all journals to the deleted user' do + expect(Journal::WorkPackageJournal.pluck(:responsible_id)) + .to eql([deleted_user.id]) + end + end + + shared_examples_for 'labor_budget_item handling' do + let(:item) { FactoryBot.build(:labor_budget_item, user: principal) } + + before do + item.save! + + job + end + + it { expect(LaborBudgetItem.find_by_id(item.id)).to eq(item) } + it { expect(item.user_id).to eq(principal.id) } + end + + shared_examples_for 'cost_entry handling' do + let(:work_package) { FactoryBot.create(:work_package) } + let(:entry) do + FactoryBot.create(:cost_entry, + user: principal, + project: work_package.project, + units: 100.0, + spent_on: Date.today, + work_package: work_package, + comments: '') + end + + before do + FactoryBot.create(:member, + project: work_package.project, + user: principal, + roles: [FactoryBot.build(:role)]) + entry + + job + + entry.reload + end + + it { expect(entry.user_id).to eq(principal.id) } + end + + shared_examples_for 'member handling' do + before do + member + + job + end + + it 'removes that member' do + expect(Member.find_by(id: member.id)).to be_nil + end + + it 'leaves the role' do + expect(Role.find_by(id: role.id)).to eq(role) + end + + it 'leaves the project' do + expect(Project.find_by(id: project.id)).to eq(project) + end + end + + shared_examples_for 'hourly_rate handling' do + let(:hourly_rate) do + FactoryBot.build(:hourly_rate, + user: principal, + project: project) + end + + before do + hourly_rate.save! + job + end + + it { expect(HourlyRate.find_by_id(hourly_rate.id)).to eq(hourly_rate) } + it { expect(hourly_rate.reload.user_id).to eq(principal.id) } + end + + shared_examples_for 'watcher handling' do + let(:watched) { FactoryBot.create(:news, project: project) } + let(:watch) do + Watcher.create(user: principal, + watchable: watched) + end + + before do + member + watch + + job + end + + it { expect(Watcher.find_by(id: watch.id)).to be_nil } + end + + shared_examples_for 'token handling' do + let(:token) do + Token::RSS.new(user: principal, value: 'loremipsum') + end + + before do + token.save! + + job + end + + it { expect(Token::RSS.find_by(id: token.id)).to be_nil } + end + + shared_examples_for 'private query handling' do + let!(:query) do + FactoryBot.create(:private_query, user: principal) + end + + before do + job + end + + it { expect(Query.find_by(id: query.id)).to be_nil } + end + + shared_examples_for 'issue category handling' do + let(:category) do + FactoryBot.create(:category, + assigned_to: principal, + project: project) + end + + before do + member + category + job + end + + it 'does not remove the category' do + expect(Category.find_by(id: category.id)).to eq(category) + end + + it 'removes the assigned_to association to the principal' do + expect(category.reload.assigned_to).to be_nil + end + end + + shared_examples_for 'removes the principal' do + it 'deletes the principal' do + job + + expect(Principal.find_by(id: principal.id)) + .to be_nil + end + end + + shared_examples_for 'private cost_query handling' do + let!(:query) { FactoryBot.create(:private_cost_query, user: principal) } + + it 'removes the query' do + job + + expect(CostQuery.find_by_id(query.id)).to eq(nil) + end + end + + shared_examples_for 'public cost_query handling' do + let!(:query) { FactoryBot.create(:public_cost_query, user: principal) } + + before do + query + + job + end + + it 'leaves the query' do + expect(CostQuery.find_by_id(query.id)).to eq(query) + end + + it 'rewrites the user reference' do + expect(query.reload.user).to eq(deleted_user) + end + end + + shared_examples_for 'cost_query handling' do + let(:query) { FactoryBot.create(:cost_query) } + let(:other_user) { FactoryBot.create(:user) } + + shared_examples_for "public query rewriting" do + let(:filter_symbol) { filter.to_s.demodulize.underscore.to_sym } + + describe "with the filter has the deleted user as it's value" do + before do + query.filter(filter_symbol, values: [principal.id.to_s], operator: "=") + query.save! + + job + end + + it 'removes the filter' do + expect(CostQuery.find_by(id: query.id).deserialize.filters) + .not_to(be_any { |f| f.is_a?(filter) }) + end + end + + describe "with the filter has another user as it's value" do + before do + query.filter(filter_symbol, values: [other_user.id.to_s], operator: "=") + query.save! + + job + end + + it 'keeps the filter' do + expect(CostQuery.find_by(id: query.id).deserialize.filters) + .to(be_any { |f| f.is_a?(filter) }) + end + + it 'does not alter the filter values' do + expect(CostQuery.find_by(id: query.id).deserialize.filters.detect do |f| + f.is_a?(filter) + end.values).to eq([other_user.id.to_s]) + end + end + + describe "with the filter has the deleted user and another user as it's value" do + before do + query.filter(filter_symbol, values: [principal.id.to_s, other_user.id.to_s], operator: "=") + query.save! + + job + end + + it 'keeps the filter' do + expect(CostQuery.find_by(id: query.id).deserialize.filters) + .to(be_any { |f| f.is_a?(filter) }) + end + + it 'removes only the deleted user' do + expect(CostQuery.find_by(id: query.id).deserialize.filters.detect do |f| + f.is_a?(filter) + end.values).to eq([other_user.id.to_s]) + end + end + end + + describe "with the query has a user_id filter" do + let(:filter) { CostQuery::Filter::UserId } + + it_should_behave_like "public query rewriting" + end + + describe "with the query has a author_id filter" do + let(:filter) { CostQuery::Filter::AuthorId } + + it_should_behave_like "public query rewriting" + end + + describe "with the query has a assigned_to_id filter" do + let(:filter) { CostQuery::Filter::AssignedToId } + + it_should_behave_like "public query rewriting" + end + + describe "with the query has an responsible_id filter" do + let(:filter) { CostQuery::Filter::ResponsibleId } + + it_should_behave_like "public query rewriting" + end + end + + context 'with a user' do + it_behaves_like 'removes the principal' + it_behaves_like 'work_package handling' + it_behaves_like 'labor_budget_item handling' + it_behaves_like 'cost_entry handling' + it_behaves_like 'hourly_rate handling' + it_behaves_like 'member handling' + it_behaves_like 'watcher handling' + it_behaves_like 'token handling' + it_behaves_like 'private query handling' + it_behaves_like 'issue category handling' + it_behaves_like 'private cost_query handling' + it_behaves_like 'public cost_query handling' + it_behaves_like 'cost_query handling' + end + + context 'with a group' do + let(:principal) { FactoryBot.create(:group, members: group_members) } + let(:group_members) { [] } + + it_behaves_like 'removes the principal' + it_behaves_like 'work_package handling' + it_behaves_like 'member handling' + + context 'with user only in project through group' do + let(:user) do + FactoryBot.create(:user) + end + let(:group_members) { [user] } + let(:watched) { FactoryBot.create(:news, project: project) } + let(:watch) do + Watcher.create(user: user, + watchable: watched) + end + + it 'removes the watcher' do + job + + expect(watched.watchers.reload).to be_empty + end + end + end + + context 'with a placeholder user' do + let(:principal) { FactoryBot.create(:placeholder_user) } + + it_behaves_like 'removes the principal' + it_behaves_like 'work_package handling' + end + end +end