[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 <jens.ulferts@googlemail.com>
pull/9015/head
Oliver Günther 4 years ago committed by GitHub
parent 36e229a461
commit f4dfd6c6c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      app/controllers/groups_controller.rb
  2. 10
      app/controllers/placeholder_users_controller.rb
  3. 1
      app/models/associations/groupable.rb
  4. 14
      app/models/group.rb
  5. 22
      app/models/user.rb
  6. 33
      app/services/journals/user_reference_update_service.rb
  7. 5
      app/services/placeholder_users/delete_service.rb
  8. 126
      app/services/principals/replace_references_service.rb
  9. 2
      app/services/users/delete_service.rb
  10. 85
      app/workers/principals/delete_job.rb
  11. 1
      config/locales/en.yml
  12. 22
      db/migrate/20210214205545_replace_invalid_principal_references.rb
  13. 10
      modules/budgets/app/models/budget.rb
  14. 9
      modules/costs/spec/factories/journal/budget_journal_factory.rb
  15. 32
      modules/costs/spec/factories/journal/time_entry_journal_factory.rb
  16. 208
      modules/costs/spec/models/user_deletion_spec.rb
  17. 32
      modules/documents/spec/factories/journal/document_journal_factory.rb
  18. 4
      modules/meeting/app/models/meeting.rb
  19. 4
      modules/meeting/app/models/meeting_content.rb
  20. 4
      modules/meeting/app/models/meeting_participant.rb
  21. 6
      modules/meeting/lib/open_project/meeting/engine.rb
  22. 207
      modules/meeting/spec/models/user_deletion_spec.rb
  23. 27
      modules/reporting/app/models/cost_query.rb
  24. 1
      modules/reporting/app/models/cost_query/filter.rb
  25. 41
      modules/reporting/app/models/cost_query/filter/responsible_id.rb
  26. 122
      modules/reporting/spec/models/cost_query/cost_query_spec.rb
  27. 3
      spec/controllers/groups_controller_spec.rb
  28. 32
      spec/factories/journal/attachment_journal_factory.rb
  29. 34
      spec/factories/journal/changeset_journal_factory.rb
  30. 32
      spec/factories/journal/customizable_journal_factory.rb
  31. 32
      spec/factories/journal/news_journal_facctory.rb
  32. 7
      spec/features/groups/groups_spec.rb
  33. 2
      spec/models/group_performance_spec.rb
  34. 56
      spec/models/group_spec.rb
  35. 464
      spec/models/user_deletion_spec.rb
  36. 2
      spec/requests/api/v3/user/user_resource_spec.rb
  37. 106
      spec/services/journals/user_reference_update_service_spec.rb
  38. 6
      spec/services/placeholder_users/delete_service_spec.rb
  39. 408
      spec/services/principals/replace_references_service_call_integration_spec.rb
  40. 6
      spec/services/users/delete_service_spec.rb
  41. 404
      spec/workers/principals/delete_job_integration_spec.rb

@ -80,9 +80,9 @@ class GroupsController < ApplicationController
end end
def destroy 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 redirect_to action: :index
end end

@ -120,11 +120,11 @@ class PlaceholderUsersController < ApplicationController
end end
def destroy def destroy
Users::DeleteService.new(user: User.current, PlaceholderUsers::DeleteService
model: @placeholder_user) .new(user: User.current, model: @placeholder_user)
.call .call
flash[:notice] = I18n.t('account.deleted') flash[:info] = I18n.t(:notice_deletion_scheduled)
respond_to do |format| respond_to do |format|
format.html do format.html do
@ -142,7 +142,7 @@ class PlaceholderUsersController < ApplicationController
end end
def check_if_deletion_allowed 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 end
protected protected

@ -31,6 +31,7 @@
module Associations::Groupable module Associations::Groupable
def self.included(base) def self.included(base)
base.has_and_belongs_to_many :groups, base.has_and_belongs_to_many :groups,
foreign_key: 'user_id',
join_table: "#{base.table_name_prefix}group_users#{base.table_name_suffix}", join_table: "#{base.table_name_prefix}group_users#{base.table_name_suffix}",
after_remove: ->(user, group) { group.user_removed(user) } after_remove: ->(user, group) { group.user_removed(user) }
end end

@ -36,8 +36,6 @@ class Group < Principal
acts_as_customizable acts_as_customizable
before_destroy :remove_references_before_destroy
alias_attribute(:groupname, :lastname) alias_attribute(:groupname, :lastname)
validates_presence_of :groupname validates_presence_of :groupname
validate :uniqueness_of_groupname validate :uniqueness_of_groupname
@ -94,18 +92,6 @@ class Group < Principal
private 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 def uniqueness_of_groupname
groups_with_name = Group.where('lastname = ? AND id <> ?', groupname, id || 0).count groups_with_name = Group.where('lastname = ? AND id <> ?', groupname, id || 0).count
if groups_with_name > 0 if groups_with_name > 0

@ -134,8 +134,6 @@ class User < Principal
after_save :update_password after_save :update_password
before_create :sanitize_mail_notification_setting before_create :sanitize_mail_notification_setting
before_destroy :delete_associated_private_queries
before_destroy :reassign_associated
scope :admin, -> { where(admin: true) } scope :admin, -> { where(admin: true) }
@ -733,26 +731,6 @@ class User < Principal
(passwords[keep_count..-1] || []).each(&:destroy) (passwords[keep_count..-1] || []).each(&:destroy)
end 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 # Brute force prevention - class methods
# #

@ -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

@ -29,4 +29,9 @@
#++ #++
class PlaceholderUsers::DeleteService < ::BaseServices::Delete class PlaceholderUsers::DeleteService < ::BaseServices::Delete
def destroy(placeholder)
::Principals::DeleteJob.perform_later(placeholder)
true
end
end end

@ -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

@ -40,7 +40,7 @@ module Users
# as destroying users is a lengthy process we handle it in the background # 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 # and lock the account now so that no action can be performed with it
user_object.lock! user_object.lock!
DeleteUserJob.perform_later(user_object) ::Principals::DeleteJob.perform_later(user_object)
logout! if self_delete? logout! if self_delete?

@ -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

@ -1906,6 +1906,7 @@ en:
notice_email_sent: "An email was sent to %{value}" 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_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_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_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." notice_forced_logout: "You have been automatically logged out after %{ttl_time} minutes of inactivity."

@ -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

@ -59,10 +59,6 @@ class Budget < ApplicationRecord
validates_length_of :subject, maximum: 255 validates_length_of :subject, maximum: 255
validates_length_of :subject, minimum: 1 validates_length_of :subject, minimum: 1
User.before_destroy do |user|
Budget.replace_author_with_deleted_user user
end
class << self class << self
def visible(user) def visible(user)
includes(:project) includes(:project)
@ -80,12 +76,6 @@ class Budget < ApplicationRecord
copy copy
end end
def replace_author_with_deleted_user(user)
substitute = DeletedUser.first
where(author_id: user.id).update_all(author_id: substitute.id)
end
protected protected
def copy_attributes(source) def copy_attributes(source)

@ -1,5 +1,3 @@
#-- encoding: UTF-8
#-- copyright #-- copyright
# OpenProject is an open source project management software. # OpenProject is an open source project management software.
# Copyright (C) 2012-2021 the OpenProject GmbH # Copyright (C) 2012-2021 the OpenProject GmbH
@ -28,10 +26,7 @@
# See docs/COPYRIGHT.rdoc for more details. # See docs/COPYRIGHT.rdoc for more details.
#++ #++
class DeleteUserJob < ApplicationJob FactoryBot.define do
queue_with_priority :low factory :journal_budget_journal, class: Journal::BudgetJournal do
def perform(user)
user.destroy
end end
end end

@ -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

@ -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

@ -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

@ -89,10 +89,6 @@ class Meeting < ApplicationRecord
after_initialize :set_initial_values 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 # Return the computed start_time when changed
def start_time def start_time

@ -54,10 +54,6 @@ class MeetingContent < ApplicationRecord
title: Proc.new { |o| "#{o.class.model_name.human}: #{o.meeting.title}" }, title: Proc.new { |o| "#{o.class.model_name.human}: #{o.meeting.title}" },
url: Proc.new { |o| { controller: '/meetings', action: 'show', id: o.meeting } } 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? def editable?
true true
end end

@ -33,10 +33,6 @@ class MeetingParticipant < ApplicationRecord
scope :invited, -> { where(invited: true) } scope :invited, -> { where(invited: true) }
scope :attended, -> { where(attended: 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 def name
user.present? ? user.name : name user.present? ? user.name : name
end end

@ -87,12 +87,6 @@ module OpenProject::Meeting
end end
config.to_prepare do 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) PermittedParams.permit(:search, :meetings)
end end

@ -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

@ -249,31 +249,4 @@ class CostQuery < ApplicationRecord
def private? def private?
!public? !public?
end 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 end

@ -44,6 +44,7 @@ class CostQuery::Filter < Report::Filter
CostQuery::Filter::OverriddenCosts, CostQuery::Filter::OverriddenCosts,
CostQuery::Filter::PriorityId, CostQuery::Filter::PriorityId,
CostQuery::Filter::ProjectId, CostQuery::Filter::ProjectId,
CostQuery::Filter::ResponsibleId,
CostQuery::Filter::SpentOn, CostQuery::Filter::SpentOn,
CostQuery::Filter::StartDate, CostQuery::Filter::StartDate,
CostQuery::Filter::StatusId, CostQuery::Filter::StatusId,

@ -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

@ -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

@ -82,6 +82,9 @@ describe GroupsController, type: :controller do
it 'should destroy' do it 'should destroy' do
delete :destroy, params: { id: group.id } delete :destroy, params: { id: group.id }
perform_enqueued_jobs
expect { group.reload }.to raise_error ActiveRecord::RecordNotFound expect { group.reload }.to raise_error ActiveRecord::RecordNotFound
expect(response).to redirect_to groups_path expect(response).to redirect_to groups_path

@ -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

@ -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

@ -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

@ -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

@ -44,6 +44,13 @@ feature 'group memberships through groups page', type: :feature do
expect(groups_page).to have_group "Bob's Team" expect(groups_page).to have_group "Bob's Team"
groups_page.delete_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" expect(groups_page).not_to have_group "Bob's Team"
end end
end end

@ -78,7 +78,7 @@ describe Group, type: :model do
puts "Destroying group ..." puts "Destroying group ..."
start = Time.now.to_i start = Time.now.to_i
group.destroy Principals::DeleteJob.perform_now group
@seconds = Time.now.to_i - start @seconds = Time.now.to_i - start
puts "Destroyed group in #{@seconds} seconds" puts "Destroyed group in #{@seconds} seconds"

@ -30,8 +30,6 @@ require 'spec_helper'
require_relative '../support/shared/become_member' require_relative '../support/shared/become_member'
describe Group, type: :model do describe Group, type: :model do
include BecomeMember
let(:group) { FactoryBot.create(:group) } let(:group) { FactoryBot.create(:group) }
let(:user) { FactoryBot.create(:user) } let(:user) { FactoryBot.create(:user) }
let(:watcher) { 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 it 'should roles removed when removing group membership' do
expect(user).to be_member_of project expect(user).to be_member_of project
member.destroy Principals::DeleteJob.perform_now group
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
user.reload user.reload
project.reload project.reload
expect(user).not_to be_member_of project expect(user).not_to be_member_of project
@ -117,50 +107,6 @@ describe Group, type: :model do
end end
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 '#create' do
describe 'group with empty group name' do describe 'group with empty group name' do
let(:group) { FactoryBot.build(:group, lastname: '') } let(:group) { FactoryBot.build(:group, lastname: '') }

@ -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

@ -249,7 +249,7 @@ describe 'API v3 User resource',
end end
it 'should lock the account and mark for deletion' do it 'should lock the account and mark for deletion' do
expect(DeleteUserJob) expect(Principals::DeleteJob)
.to have_been_enqueued .to have_been_enqueued
.with(user) .with(user)

@ -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

@ -40,7 +40,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do
shared_examples 'deletes the user' do shared_examples 'deletes the user' do
it do it do
expect(input_user).to receive(:lock!) 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 expect(subject).to eq true
end end
end end
@ -48,7 +48,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do
shared_examples 'does not delete the user' do shared_examples 'does not delete the user' do
it do it do
expect(input_user).not_to receive(:lock!) 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 expect(subject).to eq false
end end
end end
@ -76,7 +76,7 @@ describe ::PlaceholderUsers::DeleteService, type: :model do
it 'performs deletion' do it 'performs deletion' do
actor.run_given do actor.run_given do
expect(input_user).to receive(:lock!) 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 expect(subject).to eq true
end end
end end

@ -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

@ -39,7 +39,7 @@ describe ::Users::DeleteService, type: :model do
shared_examples 'deletes the user' do shared_examples 'deletes the user' do
it do it do
expect(input_user).to receive(:lock!) 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 expect(subject).to be_success
end end
end end
@ -47,7 +47,7 @@ describe ::Users::DeleteService, type: :model do
shared_examples 'does not delete the user' do shared_examples 'does not delete the user' do
it do it do
expect(input_user).not_to receive(:lock!) 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 expect(subject).not_to be_success
end end
end end
@ -75,7 +75,7 @@ describe ::Users::DeleteService, type: :model do
it 'performs deletion' do it 'performs deletion' do
actor.run_given do actor.run_given do
expect(input_user).to receive(:lock!) 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 expect(subject).to be_success
end end
end end

@ -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
Loading…
Cancel
Save