Skip to content

Commit

Permalink
Rewrite deleting again
Browse files Browse the repository at this point in the history
  • Loading branch information
segiddins committed Jan 24, 2024
1 parent dff62ae commit e504ded
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 17 deletions.
52 changes: 45 additions & 7 deletions app/jobs/delete_user_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ class DeleteUserJob < ApplicationJob
queue_as :default
queue_with_priority PRIORITIES.fetch(:profile_deletion)

ASSOCIATIONS = {
approved_ownership_requests: :destroy,
closed_ownership_calls: :destroy,
closed_ownership_requests: :destroy,
oidc_pending_trusted_publishers: :destroy,
ownership_calls: :destroy,
ownership_requests: :destroy,
ownerships: :destroy,
subscriptions: :destroy,
unconfirmed_ownerships: :destroy,
web_hooks: :destroy,
webauthn_credentials: :destroy,
webauthn_verification: :destroy,

api_keys: nil,
audits: nil,
deletions: nil,
oidc_api_key_roles: nil,
pushed_versions: nil
}.freeze

def perform(user:)
email = user.email
return if user.reload.deleted_at?
Expand All @@ -10,13 +31,8 @@ def perform(user:)
user.transaction do
user.yank_gems
user.api_keys.expire_all!
user.class.reflect_on_all_associations.each do |association|
next if association.name == :api_keys
case association.options[:dependent]
when :destroy, :destroy_async
user.association(association.name).handle_dependency
end
end
handle_associations(user)

user.update!(
deleted_at: Time.current, email: "deleted+#{user.id}@rubygems.org",
handle: nil, email_confirmed: false,
Expand All @@ -36,4 +52,26 @@ def perform(user:)
Mailer.deletion_failed(email).deliver_later
end
end

def handle_associations(user)
user.class.reflect_on_all_associations.each do |reflection|
next if reflection.through_reflection?

action = ASSOCIATIONS.fetch(reflection.name) do
raise ActiveRecord::DeleteRestrictionError, reflection.name

Check warning on line 61 in app/jobs/delete_user_job.rb

View check run for this annotation

Codecov / codecov/patch

app/jobs/delete_user_job.rb#L61

Added line #L61 was not covered by tests
end
next unless action
send(:"#{action}_#{reflection.class.name.tr(':', '_')}", user.association(reflection.name), reflection)
end
end

def destroy_ActiveRecord__Reflection__HasManyReflection(association, reflection) # rubocop:disable Naming/MethodName
# No point in executing the counter update since we're going to destroy the parent anyway
association.load_target.each { |t| t.destroyed_by_association = reflection }
association.destroy_all
end

def destroy_ActiveRecord__Reflection__HasOneReflection(association, _reflection) # rubocop:disable Naming/MethodName
association.delete
end
end
9 changes: 9 additions & 0 deletions test/integration/password_reset_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ def forgot_password_with(email)
assert_equal email, @user.email
end

test "resetting password of soft-deleted user" do
@user.update!(deleted_at: Time.zone.now, email: "deleted+#{@user.id}@rubygems.org")

forgot_password_with @user.email

assert_empty ActionMailer::Base.deliveries
page.assert_text "instructions for changing your password"
end

teardown do
@authenticator&.remove!
Capybara.reset_sessions!
Expand Down
36 changes: 27 additions & 9 deletions test/jobs/delete_user_job_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class DeleteUserJobTest < ActiveJob::TestCase
assert_delete user
assert_predicate version.reload, :yanked?
refute_predicate rubygem.reload, :indexed?

rubygem.validate!
version.validate!
end

test "sends deletion failed on failure" do
Expand Down Expand Up @@ -36,11 +39,14 @@ class DeleteUserJobTest < ActiveJob::TestCase
assert_delete user
assert_predicate user.reload, :deleted_at
assert_predicate api_key.reload, :expired?
assert_equal version.reload.pusher_api_key, api_key

User.unscoped do
assert_equal api_key.reload.owner, user
assert_equal version.reload.pusher, user
end
assert_equal version.reload.pusher_api_key, api_key

version.validate!
end

test "succeeds with version deletion" do
Expand All @@ -53,16 +59,24 @@ class DeleteUserJobTest < ActiveJob::TestCase
assert_delete user
assert_predicate user.reload, :deleted_at
assert_predicate api_key.reload, :expired?
User.unscoped do
assert_equal api_key.reload.owner, user
assert_equal version.reload.pusher, user
assert_equal deletion.reload.user, user
end
assert_nil api_key.reload.owner
assert_nil version.reload.pusher
assert_nil deletion.reload.user

assert_equal version.reload.pusher_api_key, api_key
assert_empty rubygem.reload.owners
assert_empty user.ownerships
assert_predicate version.reload, :yanked?
refute_predicate rubygem.reload, :indexed?

User.unscoped do
assert_equal api_key.reload.owner, user
assert_equal version.reload.pusher, user
assert_equal deletion.reload.user, user
end

version.validate!
deletion.validate!
end

test "succeeds with rubygem with shared ownership" do
Expand All @@ -80,6 +94,8 @@ class DeleteUserJobTest < ActiveJob::TestCase
assert_equal api_key.reload.owner, user
assert_equal version.reload.pusher, user
end
assert_nil api_key.reload.owner
assert_nil version.reload.pusher
assert_equal version.reload.pusher_api_key, api_key
assert_equal [other_user], rubygem.reload.owners
assert_empty user.ownerships
Expand All @@ -92,9 +108,11 @@ class DeleteUserJobTest < ActiveJob::TestCase
test "succeeds with webauthn credentials" do
user = create(:user)
user_credential = create(:webauthn_credential, user: user)
webauthn_verification = create(:webauthn_verification, user: user)

assert_delete user
assert_deleted user_credential
assert_deleted webauthn_verification
end

test "succeeds with subscription" do
Expand Down Expand Up @@ -132,15 +150,15 @@ class DeleteUserJobTest < ActiveJob::TestCase

def assert_delete(user)
Mailer.expects(:deletion_complete).with(user.email).returns(mock(deliver_later: nil))
Mailer.expects(:deletion_failed).never
DeleteUserJob.new(user:).perform(user:)

refute_predicate user, :destroyed?
assert_predicate user.reload, :deleted_at
user.validate!
end

def assert_deleted(record)
error = assert_raises(ActiveRecord::RecordNotFound) { assert_predicate record.reload, :destroyed? }

assert_equal record.class.name, error.model, "Expected #{record.class} to be class of not found error"
refute record.class.unscoped.exists?(record.id), "Expected #{record.class} with id #{record.id} to be deleted"
end
end
1 change: 0 additions & 1 deletion test/models/parallel_pusher_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class ParallelPusherTest < ActiveSupport::TestCase

teardown do
@user.destroy!
@api_key.destroy!
Rubygem.find_by(name: "hola").destroy!
GemDownload.delete_all
RubygemFs.mock!
Expand Down

0 comments on commit e504ded

Please sign in to comment.