Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

class CascadeDeleteWorkPackageSemanticAliases < ActiveRecord::Migration[8.1]
# Deleting a project cascades to its work_packages at the database level
# (work_packages.project_id has ON DELETE CASCADE). That database-level cascade
# bypasses the ActiveRecord `dependent: :delete_all` on the semantic_aliases
# association, so the restricting foreign key from work_package_semantic_aliases
# to work_packages blocked the delete. Cascade the aliases too.
def up
remove_foreign_key :work_package_semantic_aliases, :work_packages
add_foreign_key :work_package_semantic_aliases, :work_packages, on_delete: :cascade
end

def down
remove_foreign_key :work_package_semantic_aliases, :work_packages
add_foreign_key :work_package_semantic_aliases, :work_packages
end
end
31 changes: 31 additions & 0 deletions spec/models/work_package_semantic_alias_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,35 @@ def alias_for(identifier)
end
end
end

describe "work_package foreign key on delete",
with_settings: { work_packages_identifier: Setting::WorkPackageIdentifier::SEMANTIC } do
shared_let(:user) { create(:admin) }

# The path a user takes in the UI: Projects::DeleteProjectJob -> Projects::DeleteService.
it "deletes a parent project whose subproject holds a work package" do
parent_project = create(:project, identifier: "PARENT", name: "Parent")
child_project = create(:project, identifier: "CHILD", name: "Child", parent: parent_project)
child_work_package = create(:work_package, project: child_project, subject: "In the subproject")

expect(described_class.where(work_package_id: child_work_package.id).pluck(:identifier))
.to eq(["CHILD-1"])

service_call = Projects::DeleteService.new(user:, model: parent_project).call

expect(service_call).to be_success
expect(Project.where(id: [parent_project.id, child_project.id])).to be_empty
end

# Any caller that destroys a project without going through Projects::DeleteService,
# e.g. the console, seeders, or awesome_nested_set destroying descendants.
it "destroys a project holding a work package" do
project = create(:project, identifier: "SOLO", name: "Solo")
create(:work_package, project:, subject: "In the project")

project.destroy!

expect(Project.where(id: project.id)).to be_empty
end
end
end
Loading