summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJarka Kadlecová <jarka@gitlab.com>2018-07-16 20:30:17 +0200
committerJarka Kadlecová <jarka@gitlab.com>2018-07-30 13:29:18 +0200
commit501fb04ec65cadcd7dddc6376546db8d8f7f123c (patch)
tree9f984fc2b284239f03f10fa0daa1127c20b3fc59
parent2ca8219a20f16636b7a0ffa899a1a04ab8e84782 (diff)
downloadgitlab-ce-501fb04ec65cadcd7dddc6376546db8d8f7f123c.tar.gz
Delete todos when users loses target read permissions
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/members/destroy_service.rb8
-rw-r--r--app/services/projects/update_service.rb23
-rw-r--r--app/services/todos/destroy/base_service.rb33
-rw-r--r--app/services/todos/destroy/confidential_issue_service.rb30
-rw-r--r--app/services/todos/destroy/entity_leave_service.rb49
-rw-r--r--app/services/todos/destroy/project_private_service.rb30
-rw-r--r--app/workers/all_queues.yml4
-rw-r--r--app/workers/concerns/todos_destroyer_queue.rb12
-rw-r--r--app/workers/todos_destroyer/confidential_issue_worker.rb10
-rw-r--r--app/workers/todos_destroyer/entity_leave_worker.rb10
-rw-r--r--app/workers/todos_destroyer/project_private_worker.rb10
-rw-r--r--changelogs/unreleased/todos-visibility-change.yml5
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--spec/services/issues/update_service_spec.rb17
-rw-r--r--spec/services/members/destroy_service_spec.rb5
-rw-r--r--spec/services/projects/update_service_spec.rb22
-rw-r--r--spec/services/todos/destroy/confidential_issue_service_spec.rb39
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb96
-rw-r--r--spec/services/todos/destroy/project_private_service_spec.rb38
-rw-r--r--spec/workers/todos_destroyer/confidential_issue_worker_spec.rb12
-rw-r--r--spec/workers/todos_destroyer/entity_leave_worker_spec.rb12
-rw-r--r--spec/workers/todos_destroyer/project_private_worker_spec.rb12
23 files changed, 473 insertions, 7 deletions
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c02dddf67b2..faa4c8a5a4f 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -37,6 +37,8 @@ module Issues
end
if issue.previous_changes.include?('confidential')
+ # don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::ConfidentialIssueWorker.perform_in(1.hour, issue.id) if issue.confidential?
create_confidentiality_note(issue)
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index aca0ba66646..c186a5971dc 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -15,6 +15,8 @@ module Members
notification_service.decline_access_request(member)
end
+ enqeue_delete_todos(member)
+
after_execute(member: member)
member
@@ -22,6 +24,12 @@ module Members
private
+ def enqeue_delete_todos(member)
+ type = member.is_a?(GroupMember) ? 'Group' : 'Project'
+ # don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::EntityLeaveWorker.perform_in(1.hour, member.user_id, member.source_id, type)
+ end
+
def can_destroy_member?(member)
can?(current_user, destroy_member_permission(member), member)
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index d3dc11435fe..a3c688f8a14 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -25,13 +25,7 @@ module Projects
return validation_failed! if project.errors.any?
if project.update(params.except(:default_branch))
- if project.previous_changes.include?('path')
- project.rename_repo
- else
- system_hook_service.execute_hooks_for(project, :update)
- end
-
- update_pages_config if changing_pages_https_only?
+ after_update
success
else
@@ -47,6 +41,21 @@ module Projects
private
+ def after_update
+ if project.previous_changes.include?(:visibility_level) && project.private?
+ # don't enqueue immediately to prevent todos removal in case of a mistake
+ TodosDestroyer::ProjectPrivateWorker.perform_in(1.hour, project.id)
+ end
+
+ if project.previous_changes.include?('path')
+ project.rename_repo
+ else
+ system_hook_service.execute_hooks_for(project, :update)
+ end
+
+ update_pages_config if changing_pages_https_only?
+ end
+
def validation_failed!
model_errors = project.errors.full_messages.to_sentence
error_message = model_errors.presence || 'Project could not be updated!'
diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb
new file mode 100644
index 00000000000..71cd7921f32
--- /dev/null
+++ b/app/services/todos/destroy/base_service.rb
@@ -0,0 +1,33 @@
+module Todos
+ module Destroy
+ class BaseService
+ def execute
+ return unless todos_to_remove?
+
+ without_authorized(todos).delete_all
+ end
+
+ private
+
+ def without_authorized(items)
+ items.where('user_id NOT IN (?)', authorized_users)
+ end
+
+ def authorized_users
+ ProjectAuthorization.select(:user_id).where(project_id: project_ids)
+ end
+
+ def todos
+ # overridden in subclasses
+ end
+
+ def project_ids
+ # overridden in subclasses
+ end
+
+ def todos_to_remove?
+ # overridden in subclasses
+ end
+ end
+ end
+end
diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb
new file mode 100644
index 00000000000..06cf308a3cd
--- /dev/null
+++ b/app/services/todos/destroy/confidential_issue_service.rb
@@ -0,0 +1,30 @@
+module Todos
+ module Destroy
+ class ConfidentialIssueService < ::Todos::Destroy::BaseService
+ extend ::Gitlab::Utils::Override
+
+ attr_reader :issue
+
+ def initialize(issue_id)
+ @issue = Issue.find_by(id: issue_id)
+ end
+
+ private
+
+ override :todos
+ def todos
+ Todo.where(target: issue)
+ end
+
+ override :todos_to_remove?
+ def todos_to_remove?
+ issue&.confidential?
+ end
+
+ override :project_ids
+ def project_ids
+ issue.project_id
+ end
+ end
+ end
+end
diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb
new file mode 100644
index 00000000000..328a8b39e7b
--- /dev/null
+++ b/app/services/todos/destroy/entity_leave_service.rb
@@ -0,0 +1,49 @@
+module Todos
+ module Destroy
+ class EntityLeaveService < ::Todos::Destroy::BaseService
+ extend ::Gitlab::Utils::Override
+
+ attr_reader :user_id, :entity
+
+ def initialize(user_id, entity_id, entity_type)
+ unless %w(Group Project).include?(entity_type)
+ raise ArgumentError.new("#{entity_type} is not an entity user can leave")
+ end
+
+ @user_id = user_id
+ @entity = entity_type.constantize.find_by(id: entity_id)
+ end
+
+ private
+
+ override :todos
+ def todos
+ if entity.private?
+ Todo.where(project_id: project_ids, user_id: user_id)
+ else
+ Todo.where(target_id: confidential_issues.select(:id), target_type: Issue)
+ end
+ end
+
+ override :project_ids
+ def project_ids
+ if entity.is_a?(Project)
+ entity.id
+ else
+ Project.select(:id).where(namespace_id: entity.self_and_descendants.select(:id))
+ end
+ end
+
+ override :todos_to_remove?
+ def todos_to_remove?
+ return unless entity
+
+ entity.private? || confidential_issues.count > 0
+ end
+
+ def confidential_issues
+ Issue.where(project_id: project_ids, confidential: true)
+ end
+ end
+ end
+end
diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb
new file mode 100644
index 00000000000..171933e7cbc
--- /dev/null
+++ b/app/services/todos/destroy/project_private_service.rb
@@ -0,0 +1,30 @@
+module Todos
+ module Destroy
+ class ProjectPrivateService < ::Todos::Destroy::BaseService
+ extend ::Gitlab::Utils::Override
+
+ attr_reader :project
+
+ def initialize(project_id)
+ @project = Project.find_by(id: project_id)
+ end
+
+ private
+
+ override :todos
+ def todos
+ Todo.where(project_id: project_ids)
+ end
+
+ override :project_ids
+ def project_ids
+ project.id
+ end
+
+ override :todos_to_remove?
+ def todos_to_remove?
+ project&.private?
+ end
+ end
+ end
+end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 4de35b9bd06..4a6bee8af83 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -73,6 +73,10 @@
- repository_check:repository_check_batch
- repository_check:repository_check_single_repository
+- todos_destroyer:todos_destroyer_confidential_issue
+- todos_destroyer:todos_destroyer_entity_leave
+- todos_destroyer:todos_destroyer_project_private
+
- default
- mailers # ActionMailer::DeliveryJob.queue_name
diff --git a/app/workers/concerns/todos_destroyer_queue.rb b/app/workers/concerns/todos_destroyer_queue.rb
new file mode 100644
index 00000000000..8e2b1d30579
--- /dev/null
+++ b/app/workers/concerns/todos_destroyer_queue.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# Concern for setting Sidekiq settings for the various Todos Destroyers.
+#
+module TodosDestroyerQueue
+ extend ActiveSupport::Concern
+
+ included do
+ queue_namespace :todos_destroyer
+ end
+end
diff --git a/app/workers/todos_destroyer/confidential_issue_worker.rb b/app/workers/todos_destroyer/confidential_issue_worker.rb
new file mode 100644
index 00000000000..9d640c14963
--- /dev/null
+++ b/app/workers/todos_destroyer/confidential_issue_worker.rb
@@ -0,0 +1,10 @@
+module TodosDestroyer
+ class ConfidentialIssueWorker
+ include ApplicationWorker
+ include TodosDestroyerQueue
+
+ def perform(issue_id)
+ ::Todos::Destroy::ConfidentialIssueService.new(issue_id).execute
+ end
+ end
+end
diff --git a/app/workers/todos_destroyer/entity_leave_worker.rb b/app/workers/todos_destroyer/entity_leave_worker.rb
new file mode 100644
index 00000000000..e62d9876f4a
--- /dev/null
+++ b/app/workers/todos_destroyer/entity_leave_worker.rb
@@ -0,0 +1,10 @@
+module TodosDestroyer
+ class EntityLeaveWorker
+ include ApplicationWorker
+ include TodosDestroyerQueue
+
+ def perform(user_id, entity_id, entity_type)
+ ::Todos::Destroy::EntityLeaveService.new(user_id, entity_id, entity_type).execute
+ end
+ end
+end
diff --git a/app/workers/todos_destroyer/project_private_worker.rb b/app/workers/todos_destroyer/project_private_worker.rb
new file mode 100644
index 00000000000..7a853c36370
--- /dev/null
+++ b/app/workers/todos_destroyer/project_private_worker.rb
@@ -0,0 +1,10 @@
+module TodosDestroyer
+ class ProjectPrivateWorker
+ include ApplicationWorker
+ include TodosDestroyerQueue
+
+ def perform(project_id)
+ ::Todos::Destroy::ProjectPrivateService.new(project_id).execute
+ end
+ end
+end
diff --git a/changelogs/unreleased/todos-visibility-change.yml b/changelogs/unreleased/todos-visibility-change.yml
new file mode 100644
index 00000000000..b7632b94771
--- /dev/null
+++ b/changelogs/unreleased/todos-visibility-change.yml
@@ -0,0 +1,5 @@
+---
+title: Delete todos when user loses access to read the target
+merge_request: 20665
+author:
+type: other
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 70b584ff9e9..3c85cd07d46 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -45,6 +45,7 @@
- [github_import_advance_stage, 1]
- [project_service, 1]
- [delete_user, 1]
+ - [todos_destroyer, 1]
- [delete_merged_branches, 1]
- [authorized_projects, 1]
- [expire_build_instance_artifacts, 1]
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index fa98d05c61b..5bcfef46b75 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -55,6 +55,8 @@ describe Issues::UpdateService, :mailer do
end
it 'updates the issue with the given params' do
+ expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
+
update_issue(opts)
expect(issue).to be_valid
@@ -74,6 +76,21 @@ describe Issues::UpdateService, :mailer do
.to change { project.open_issues_count }.from(1).to(0)
end
+ it 'enqueues ConfidentialIssueWorker when an issue is made confidential' do
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(1.hour, issue.id)
+
+ update_issue(confidential: true)
+ end
+
+ it 'does not enqueue ConfidentialIssueWorker when an issue is made non confidential' do
+ # set confidentiality to true before the actual update
+ issue.update!(confidential: true)
+
+ expect(TodosDestroyer::ConfidentialIssueWorker).not_to receive(:perform_in)
+
+ update_issue(confidential: false)
+ end
+
it 'updates open issue counter for assignees when issue is reassigned' do
update_issue(assignee_ids: [user2.id])
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index ef47b0a450b..0a5220c7c61 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -20,6 +20,11 @@ describe Members::DestroyService do
end
shared_examples 'a service destroying a member' do
+ before do
+ type = member.is_a?(GroupMember) ? 'Group' : 'Project'
+ expect(TodosDestroyer::EntityLeaveWorker).to receive(:perform_in).with(1.hour, member.user_id, member.source_id, type)
+ end
+
it 'destroys the member' do
expect { described_class.new(current_user).execute(member, opts) }.to change { member.source.members_and_requesters.count }.by(-1)
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index ecf1ba05618..d1686e1007c 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -15,6 +15,8 @@ describe Projects::UpdateService do
context 'when changing visibility level' do
context 'when visibility_level is INTERNAL' do
it 'updates the project to internal' do
+ expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in)
+
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
expect(result).to eq({ status: :success })
@@ -24,12 +26,30 @@ describe Projects::UpdateService do
context 'when visibility_level is PUBLIC' do
it 'updates the project to public' do
+ expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in)
+
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
expect(result).to eq({ status: :success })
expect(project).to be_public
end
end
+ context 'when visibility_level is PRIVATE' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'updates the project to private' do
+ expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(1.hour, project.id)
+
+ result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+
+ expect(result).to eq({ status: :success })
+ expect(project).to be_private
+ end
+ end
+
context 'when visibility levels are restricted to PUBLIC only' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -38,6 +58,7 @@ describe Projects::UpdateService do
context 'when visibility_level is INTERNAL' do
it 'updates the project to internal' do
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+
expect(result).to eq({ status: :success })
expect(project).to be_internal
end
@@ -54,6 +75,7 @@ describe Projects::UpdateService do
context 'when updated by an admin' do
it 'updates the project to public' do
result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
expect(result).to eq({ status: :success })
expect(project).to be_public
end
diff --git a/spec/services/todos/destroy/confidential_issue_service_spec.rb b/spec/services/todos/destroy/confidential_issue_service_spec.rb
new file mode 100644
index 00000000000..5c214df49bc
--- /dev/null
+++ b/spec/services/todos/destroy/confidential_issue_service_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Todos::Destroy::ConfidentialIssueService do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:project_member) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+
+ let!(:todo_issue_non_member) { create(:todo, user: user, target: issue, project: project) }
+ let!(:todo_issue_member) { create(:todo, user: project_member, target: issue, project: project) }
+ let!(:todo_another_non_member) { create(:todo, user: user, project: project) }
+
+ describe '#execute' do
+ before do
+ project.add_developer(project_member)
+ end
+
+ subject { described_class.new(issue.id).execute }
+
+ context 'when provided issue is confidential' do
+ before do
+ issue.update!(confidential: true)
+ end
+
+ it 'removes issue todos for a user who is not a project member' do
+ expect { subject }.to change { Todo.count }.from(3).to(2)
+
+ expect(user.todos).to match_array([todo_another_non_member])
+ expect(project_member.todos).to match_array([todo_issue_member])
+ end
+ end
+
+ context 'when provided issue is not confidential' do
+ it 'does not remove any todos' do
+ expect { subject }.not_to change { Todo.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
new file mode 100644
index 00000000000..e5673383df8
--- /dev/null
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Todos::Destroy::EntityLeaveService do
+ let(:group) { create(:group, :private) }
+ let(:project) { create(:project, group: group) }
+ let(:user) { create(:user) }
+ let(:project_member) { create(:user) }
+ let(:issue) { create(:issue, :confidential, project: project) }
+
+ let!(:todo_non_member) { create(:todo, user: user, project: project) }
+ let!(:todo_conf_issue_non_member) { create(:todo, user: user, target: issue, project: project) }
+ let!(:todo_conf_issue_member) { create(:todo, user: project_member, target: issue, project: project) }
+
+ describe '#execute' do
+ before do
+ project.add_developer(project_member)
+ end
+
+ context 'when a user leaves a project' do
+ subject { described_class.new(user.id, project.id, 'Project').execute }
+
+ context 'when project is private' do
+ it 'removes todos for a user who is not a member' do
+ expect { subject }.to change { Todo.count }.from(3).to(1)
+
+ expect(user.todos).to be_empty
+ expect(project_member.todos).to match_array([todo_conf_issue_member])
+ end
+ end
+
+ context 'when project is not private' do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'removes only confidential issues todos' do
+ expect { subject }.to change { Todo.count }.from(3).to(2)
+ end
+ end
+ end
+
+ context 'when a user leaves a group' do
+ subject { described_class.new(user.id, group.id, 'Group').execute }
+
+ context 'when group is private' do
+ it 'removes todos for a user who is not a member' do
+ expect { subject }.to change { Todo.count }.from(3).to(1)
+
+ expect(user.todos).to be_empty
+ expect(project_member.todos).to match_array([todo_conf_issue_member])
+ end
+
+ context 'with nested groups', :nested_groups do
+ let(:subgroup) { create(:group, :private, parent: group) }
+ let(:subproject) { create(:project, group: subgroup) }
+
+ let!(:todo_subproject_non_member) { create(:todo, user: user, project: subproject) }
+ let!(:todo_subproject_member) { create(:todo, user: project_member, project: subproject) }
+
+ it 'removes todos for a user who is not a member' do
+ expect { subject }.to change { Todo.count }.from(5).to(2)
+
+ expect(user.todos).to be_empty
+ expect(project_member.todos)
+ .to match_array([todo_conf_issue_member, todo_subproject_member])
+ end
+ end
+ end
+
+ context 'when group is not private' do
+ before do
+ group.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'removes only confidential issues todos' do
+ expect { subject }.to change { Todo.count }.from(3).to(2)
+ end
+ end
+ end
+
+ context 'when entity type is not valid' do
+ it 'raises an exception' do
+ expect { described_class.new(user.id, group.id, 'GroupWrongly').execute }
+ .to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when entity was not found' do
+ it 'does not remove any todos' do
+ expect { described_class.new(user.id, 999999, 'Group').execute }
+ .not_to change { Todo.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/todos/destroy/project_private_service_spec.rb b/spec/services/todos/destroy/project_private_service_spec.rb
new file mode 100644
index 00000000000..badf3f913a5
--- /dev/null
+++ b/spec/services/todos/destroy/project_private_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Todos::Destroy::ProjectPrivateService do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:project_member) { create(:user) }
+
+ let!(:todo_issue_non_member) { create(:todo, user: user, project: project) }
+ let!(:todo_issue_member) { create(:todo, user: project_member, project: project) }
+ let!(:todo_another_non_member) { create(:todo, user: user, project: project) }
+
+ describe '#execute' do
+ before do
+ project.add_developer(project_member)
+ end
+
+ subject { described_class.new(project.id).execute }
+
+ context 'when a project set to private' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'removes issue todos for a user who is not a member' do
+ expect { subject }.to change { Todo.count }.from(3).to(1)
+
+ expect(user.todos).to be_empty
+ expect(project_member.todos).to match_array([todo_issue_member])
+ end
+ end
+
+ context 'when project is not private' do
+ it 'does not remove any todos' do
+ expect { subject }.not_to change { Todo.count }
+ end
+ end
+ end
+end
diff --git a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
new file mode 100644
index 00000000000..9d7c0b8f560
--- /dev/null
+++ b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe TodosDestroyer::ConfidentialIssueWorker do
+ it "calls the Todos::Destroy::ConfidentialIssueService with the params it was given" do
+ service = double
+
+ expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(100).and_return(service)
+ expect(service).to receive(:execute)
+
+ described_class.new.perform(100)
+ end
+end
diff --git a/spec/workers/todos_destroyer/entity_leave_worker_spec.rb b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
new file mode 100644
index 00000000000..955447906aa
--- /dev/null
+++ b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe TodosDestroyer::EntityLeaveWorker do
+ it "calls the Todos::Destroy::EntityLeaveService with the params it was given" do
+ service = double
+
+ expect(::Todos::Destroy::EntityLeaveService).to receive(:new).with(100, 5, 'Group').and_return(service)
+ expect(service).to receive(:execute)
+
+ described_class.new.perform(100, 5, 'Group')
+ end
+end
diff --git a/spec/workers/todos_destroyer/project_private_worker_spec.rb b/spec/workers/todos_destroyer/project_private_worker_spec.rb
new file mode 100644
index 00000000000..15d926fa9d5
--- /dev/null
+++ b/spec/workers/todos_destroyer/project_private_worker_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe TodosDestroyer::ProjectPrivateWorker do
+ it "calls the Todos::Destroy::ProjectPrivateService with the params it was given" do
+ service = double
+
+ expect(::Todos::Destroy::ProjectPrivateService).to receive(:new).with(100).and_return(service)
+ expect(service).to receive(:execute)
+
+ described_class.new.perform(100)
+ end
+end