Commit 906cba55 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '343577_simplify_recovery_of_accidentally_deleted_projects' into 'master'

Default to delayed deletion for projects not in personal namespace

See merge request gitlab-org/gitlab!80139
parents db0e7c8c 6b72603b
......@@ -24,11 +24,14 @@ class Projects::ApplicationController < ApplicationController
return unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
@project = find_routable!(Project, path, request.fullpath, extra_authorization_proc: auth_proc)
end
def auth_proc
->(project) { !project.pending_delete? }
end
def build_canonical_path(project)
params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param
......@@ -89,3 +92,5 @@ class Projects::ApplicationController < ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user)
end
end
Projects::ApplicationController.prepend_mod_with('Projects::ApplicationController')
......@@ -526,6 +526,7 @@ class Project < ApplicationRecord
# Scopes
scope :pending_delete, -> { where(pending_delete: true) }
scope :without_deleted, -> { where(pending_delete: false) }
scope :not_hidden, -> { where(hidden: false) }
scope :not_aimed_for_deletion, -> { where(marked_for_deletion_at: nil).without_deleted }
scope :with_storage_feature, ->(feature) do
......@@ -2805,6 +2806,10 @@ class Project < ApplicationRecord
end
end
def pending_delete_or_hidden?
pending_delete? || hidden?
end
private
# overridden in EE
......
# frozen_string_literal: true
class AddHiddenToProjects < Gitlab::Database::Migration[1.0]
DOWNTIME = false
enable_lock_retries!
def change
add_column :projects, :hidden, :boolean, default: false, null: false # rubocop: disable Migration/AddColumnsToWideTables
end
end
# frozen_string_literal: true
class UpdateApiIndexesForProjects < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
ARCHIVED_INDEX_NAME = 'idx_projects_api_created_at_id_for_archived'
OLD_ARCHIVED_INDEX_NAME = 'index_projects_api_created_at_id_for_archived'
PUBLIC_AND_ARCHIVED_INDEX_NAME = 'idx_projects_api_created_at_id_for_archived_vis20'
OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME = 'index_projects_api_created_at_id_for_archived_vis20'
INTERNAL_PROJECTS_INDEX_NAME = 'idx_projects_api_created_at_id_for_vis10'
OLD_INTERNAL_PROJECTS_INDEX_NAME = 'index_projects_api_created_at_id_for_vis10'
def up
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND pending_delete = false AND hidden = false",
name: ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND visibility_level = 20 AND pending_delete = false AND hidden = false",
name: PUBLIC_AND_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "visibility_level = 10 AND pending_delete = false AND hidden = false",
name: INTERNAL_PROJECTS_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, OLD_INTERNAL_PROJECTS_INDEX_NAME
end
def down
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND pending_delete = false",
name: OLD_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "archived = true AND visibility_level = 20 AND pending_delete = false",
name: OLD_PUBLIC_AND_ARCHIVED_INDEX_NAME
add_concurrent_index :projects, [:created_at, :id],
where: "visibility_level = 10 AND pending_delete = false",
name: OLD_INTERNAL_PROJECTS_INDEX_NAME
remove_concurrent_index_by_name :projects, ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, PUBLIC_AND_ARCHIVED_INDEX_NAME
remove_concurrent_index_by_name :projects, INTERNAL_PROJECTS_INDEX_NAME
end
end
02f7a38c7bc19b1a266ac1f1d6631f1922fc135518baeb19ee90bebd7c31c6b9
\ No newline at end of file
f63be8bd42cc1856c92f9073fdb39c58c45806b483d38b91db007a8661c49a97
\ No newline at end of file
......@@ -18918,7 +18918,8 @@ CREATE TABLE projects (
marked_for_deletion_by_user_id integer,
autoclose_referenced_issues boolean,
suggestion_commit_message character varying(255),
project_namespace_id bigint
project_namespace_id bigint,
hidden boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE projects_id_seq
......@@ -25452,6 +25453,12 @@ CREATE UNIQUE INDEX idx_project_id_payload_key_self_managed_prometheus_alert_eve
CREATE INDEX idx_project_repository_check_partial ON projects USING btree (repository_storage, created_at) WHERE (last_repository_check_at IS NULL);
CREATE INDEX idx_projects_api_created_at_id_for_archived ON projects USING btree (created_at, id) WHERE ((archived = true) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_api_created_at_id_for_archived_vis20 ON projects USING btree (created_at, id) WHERE ((archived = true) AND (visibility_level = 20) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_api_created_at_id_for_vis10 ON projects USING btree (created_at, id) WHERE ((visibility_level = 10) AND (pending_delete = false) AND (hidden = false));
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_false ON projects USING btree (id, created_at) WHERE ((disable_overriding_approvers_per_merge_request = false) OR (disable_overriding_approvers_per_merge_request IS NULL));
CREATE INDEX idx_projects_id_created_at_disable_overriding_approvers_true ON projects USING btree (id, created_at) WHERE (disable_overriding_approvers_per_merge_request = true);
......@@ -27568,12 +27575,6 @@ CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_f
CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC);
CREATE INDEX index_projects_api_created_at_id_for_archived ON projects USING btree (created_at, id) WHERE ((archived = true) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_for_archived_vis20 ON projects USING btree (created_at, id) WHERE ((archived = true) AND (visibility_level = 20) AND (pending_delete = false));
CREATE INDEX index_projects_api_created_at_id_for_vis10 ON projects USING btree (created_at, id) WHERE ((visibility_level = 10) AND (pending_delete = false));
CREATE INDEX index_projects_api_last_activity_at_id_desc ON projects USING btree (last_activity_at, id DESC);
CREATE INDEX index_projects_api_name_id_desc ON projects USING btree (name, id DESC);
......@@ -85,7 +85,10 @@ are included when cloning.
Top-level groups created after August 12, 2021 have delayed project deletion enabled by default.
Projects are permanently deleted after a seven-day delay.
You can disable this by changing the [group setting](../group/index.md#enable-delayed-project-deletion).
If you are on:
- Premium tier and above, you can disable this by changing the [group setting](../group/index.md#enable-delayed-project-deletion).
- Free tier, you cannot disable this setting or restore projects.
## Alternative SSH port
......
......@@ -38,7 +38,7 @@ module EE
end
def projects_pending_deletion_params
finder_params = { aimed_for_deletion: true }
finder_params = { aimed_for_deletion: true, include_hidden: true }
unless current_user.can_admin_all_resources?
finder_params[:min_access_level] = ::Gitlab::Access::OWNER
......
# frozen_string_literal: true
module EE
module Projects
module ApplicationController
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
private
override :auth_proc
def auth_proc
if params[:controller] == "projects" && params[:action] == "restore"
super
else
->(project) { !project.pending_delete_or_hidden? }
end
end
end
end
end
......@@ -38,17 +38,24 @@ module EE
override :destroy
def destroy
return super unless project.adjourned_deletion?
return super if project.marked_for_deletion? && params[:permanently_delete].present?
return super unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
return super unless project.adjourned_deletion_configured?
return super if project.marked_for_deletion_at? && params[:permanently_delete].present?
return access_denied! unless can?(current_user, :remove_project, project)
result = ::Projects::MarkForDeletionService.new(project, current_user, {}).execute
if result[:status] == :success
date = permanent_deletion_date(project.marked_for_deletion_at)
flash[:notice] = _("Project '%{project_name}' will be deleted on %{date}") % { date: date, project_name: project.full_name }
redirect_to(project_path(project), status: :found)
if project.licensed_feature_available?(:adjourned_deletion_for_projects_and_groups)
redirect_to(project_path(project), status: :found)
else
redirect_to dashboard_projects_path, status: :found
end
else
flash.now[:alert] = result[:message]
......
......@@ -8,6 +8,9 @@ module EE
# Added arguments:
# params:
# plans: string[]
# feature_available: string[]
# aimed_for_deletion: Symbol
# include_hidden: boolean
module ProjectsFinder
extend ::Gitlab::Utils::Override
......@@ -18,6 +21,7 @@ module EE
collection = super(collection)
collection = by_plans(collection)
collection = by_feature_available(collection)
collection = by_hidden(collection)
by_aimed_for_deletion(collection)
end
......@@ -44,5 +48,9 @@ module EE
items
end
end
def by_hidden(items)
params[:include_hidden].present? ? items : items.not_hidden
end
end
end
......@@ -109,7 +109,13 @@ module EE
def marked_for_removal_message(project)
date = permanent_deletion_date(Time.now.utc)
message = _("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains.")
message = if project.feature_available?(:adjourned_deletion_for_projects_and_groups)
_("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains.")
else
_("This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}")
end
html_escape(message) % remove_message_data(project).merge(date: date)
end
......
......@@ -728,13 +728,17 @@ module EE
def adjourned_deletion?
feature_available?(:adjourned_deletion_for_projects_and_groups) &&
::Gitlab::CurrentSettings.deletion_adjourned_period > 0 &&
adjourned_deletion_configured?
end
def adjourned_deletion_configured?
::Gitlab::CurrentSettings.deletion_adjourned_period > 0 &&
group_deletion_mode_configured?
end
def marked_for_deletion?
marked_for_deletion_at.present? &&
feature_available?(:adjourned_deletion_for_projects_and_groups)
License.feature_available?(:adjourned_deletion_for_projects_and_groups)
end
def ancestor_marked_for_deletion
......
......@@ -4,16 +4,12 @@ module Projects
class MarkForDeletionService < BaseService
def execute
return success if project.marked_for_deletion_at?
return error('Cannot mark project for deletion: feature not supported') unless project.feature_available?(:adjourned_deletion_for_projects_and_groups)
return error('Cannot mark project for deletion: feature not supported') unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
result = ::Projects::UpdateService.new(
project,
current_user,
{ archived: true,
name: "#{project.name}-deleted-#{project.id}",
path: "#{project.path}-deleted-#{project.id}",
marked_for_deletion_at: Time.current.utc,
deleting_user: current_user }
project_update_service_params
).execute
log_event if result[:status] == :success
log_error(result[:message]) if result[:status] == :error
......@@ -21,6 +17,8 @@ module Projects
result
end
private
def log_event
log_audit_event
log_info("User #{current_user.id} marked project #{project.full_path} for deletion")
......@@ -34,5 +32,17 @@ module Projects
custom_message: "Project marked for deletion"
).for_project.security_event
end
def project_update_service_params
params = {
archived: true,
name: "#{project.name}-deleted-#{project.id}",
path: "#{project.path}-deleted-#{project.id}",
marked_for_deletion_at: Time.current.utc,
deleting_user: current_user
}
params[:hidden] = true unless project.feature_available?(:adjourned_deletion_for_projects_and_groups)
params
end
end
end
......@@ -13,6 +13,7 @@ module Projects
project,
current_user,
{ archived: false,
hidden: false,
marked_for_deletion_at: nil,
deleting_user: nil,
name: updated_value(project.name),
......
- return unless can?(current_user, :remove_project, project)
- adjourned_deletion = project.adjourned_deletion?
- can_delay_project_deletions = License.feature_available?(:adjourned_deletion_for_projects_and_groups)
- adjourned_deletion = project.adjourned_deletion_configured?
- adjourned_date = adjourned_deletion ? permanent_deletion_date(Time.now.utc).to_s : nil
- restore_help_path = help_page_path('user/project/settings/index', anchor: 'restore-a-project')
- merge_requests_count = Projects::AllMergeRequestsCountService.new(project).count
......@@ -9,11 +10,14 @@
- unless project.marked_for_deletion?
.sub-section
%h4.danger-title= _('Delete this project')
- if adjourned_deletion
- if adjourned_deletion && can_delay_project_deletions
= render 'projects/settings/marked_for_removal'
#js-project-adjourned-delete-button{ data: { restore_help_path: restore_help_path, adjourned_removal_date: adjourned_date, form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
%p= permanent_delete_message(project)
- if project.feature_available?(:adjourned_deletion_for_projects_and_groups)
#js-project-adjourned-delete-button{ data: { restore_help_path: restore_help_path, adjourned_removal_date: adjourned_date, form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
#js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: delete_confirm_phrase(project), is_fork: project.forked?.to_s, issues_count: number_with_delimiter(issues_count), merge_requests_count: number_with_delimiter(merge_requests_count), forks_count: number_with_delimiter(forks_count), stars_count: number_with_delimiter(project.star_count) } }
- else
= render 'projects/settings/restore', project: project
......
- return unless @project.feature_available?(:adjourned_deletion_for_projects_and_groups)
- return unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
%p= marked_for_removal_message(@project)
......@@ -125,7 +125,8 @@ module EE
override :delete_project
def delete_project(user_project)
return super unless user_project.adjourned_deletion?
return super unless License.feature_available?(:adjourned_deletion_for_projects_and_groups)
return super unless user_project.adjourned_deletion_configured?
result = destroy_conditionally!(user_project) do
::Projects::MarkForDeletionService.new(user_project, current_user, {}).execute
......
......@@ -14,7 +14,7 @@ RSpec.describe Dashboard::ProjectsController do
before do
sign_in(user)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
allow(Kaminari.config).to receive(:default_per_page).and_return(2)
end
shared_examples 'returns not found' do
......@@ -32,6 +32,7 @@ RSpec.describe Dashboard::ProjectsController do
context 'for admin users', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
let_it_be(:hidden_project) { create(:project, :hidden, :archived, creator: user, marked_for_deletion_at: 2.days.ago) }
let_it_be(:projects) { create_list(:project, 2, :archived, creator: user, marked_for_deletion_at: 3.days.ago) }
it 'returns success' do
......@@ -43,7 +44,13 @@ RSpec.describe Dashboard::ProjectsController do
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
expect(assigns(:projects).count).to eq(2)
end
it 'returns projects marked for deletion' do
subject
expect(assigns(:projects)).to contain_exactly(hidden_project, projects.first)
end
end
......@@ -72,7 +79,7 @@ RSpec.describe Dashboard::ProjectsController do
it 'paginates the records' do
subject
expect(assigns(:projects).count).to eq(1)
expect(assigns(:projects).count).to eq(2)
end
context 'for should_check_namespace_plan' do
......
......@@ -679,11 +679,24 @@ RSpec.describe ProjectsController do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_truthy
expect(project.reload.hidden?).to be_falsey
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(project_path(project))
end
end
shared_examples 'marks free project for deletion' do
it do
delete :destroy, params: { namespace_id: project.namespace, id: project }
expect(project.reload.marked_for_deletion?).to be_truthy
expect(project.reload.marked_for_deletion_at).to be_truthy
expect(project.reload.hidden?).to be_truthy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(dashboard_projects_path)
end
end
context 'feature is available' do
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
......@@ -757,6 +770,18 @@ RSpec.describe ProjectsController do
it_behaves_like 'deletes project right away'
end
context 'when feature is not available for the project' do
before do
allow(group.namespace_settings).to receive(:delayed_project_removal?).and_return(true)
allow(project).to receive(:licensed_feature_available?).and_call_original
allow(project).to receive(:licensed_feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
allow(project).to receive(:feature_available?).and_call_original
allow(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
end
it_behaves_like 'marks free project for deletion'
end
context 'for projects in user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
......@@ -777,7 +802,6 @@ RSpec.describe ProjectsController do
let(:project) { create(:project, namespace: user.namespace) }
before do
controller.instance_variable_set(:@project, project)
sign_in(user)
end
......
......@@ -3,21 +3,20 @@
require 'spec_helper'
RSpec.describe ProjectsFinder do
describe '#execute' do
describe '#execute', :saas do
let_it_be(:user) { create(:user) }
let_it_be(:ultimate_project) { create_project(:ultimate_plan) }
let_it_be(:ultimate_project2) { create_project(:ultimate_plan) }
let_it_be(:premium_project) { create_project(:premium_plan) }
let_it_be(:no_plan_project) { create_project(nil) }
let(:project_ids_relation) { nil }
let(:finder) { described_class.new(current_user: user, params: params, project_ids_relation: project_ids_relation) }
subject { finder.execute }
describe 'filter by plans', :saas do
describe 'filter by plans' do
let(:params) { { plans: plans } }
let(:project_ids_relation) { nil }
let_it_be(:ultimate_project) { create_project(:ultimate_plan) }
let_it_be(:ultimate_project2) { create_project(:ultimate_plan) }
let_it_be(:premium_project) { create_project(:premium_plan) }
let_it_be(:no_plan_project) { create_project(nil) }
context 'with ultimate plan' do
let(:plans) { ['ultimate'] }
......@@ -48,28 +47,44 @@ RSpec.describe ProjectsFinder do
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
end
context 'filter by aimed for deletion' do
let_it_be(:params) { { aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
context 'filter by aimed for deletion' do
let_it_be(:params) { { aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
it { is_expected.to contain_exactly(aimed_for_deletion_project) }
end
context 'filter by not aimed for deletion' do
let_it_be(:params) { { not_aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
it { is_expected.to contain_exactly(aimed_for_deletion_project) }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
context 'filter by hidden' do
let_it_be(:hidden_project) { create(:project, :public, :hidden) }
context 'when include hidden is true' do
let_it_be(:params) { { include_hidden: true } }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project, hidden_project) }
end
context 'filter by not aimed for deletion' do
let_it_be(:params) { { not_aimed_for_deletion: true } }
let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) }
let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) }
context 'when include hidden is false' do
let_it_be(:params) { { include_hidden: false } }
it { is_expected.to contain_exactly(ultimate_project, ultimate_project2, premium_project, no_plan_project) }
end
end
private
private
def create_project(plan)
create(:project, :public, namespace: create(:namespace_with_plan, plan: plan))
end
def create_project(plan)
create(:project, :public, namespace: create(:namespace_with_plan, plan: plan))
end
end
end
......@@ -331,17 +331,26 @@ RSpec.describe ProjectsHelper do
subject { helper.marked_for_removal_message(project) }
before do
allow(project).to receive(:adjourned_deletion?).and_return(enabled)
allow(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(feature_available)
end
context 'when project has delayed deletion enabled' do
let(:enabled) { true }
context 'when project has delayed deletion feature' do
let(:feature_available) { true }
specify do
deletion_date = helper.permanent_deletion_date(Time.now.utc)
expect(subject).to eq "This action deletes <code>#{project.path_with_namespace}</code> on #{deletion_date} and everything this project contains."
end
end
context 'when project does not have delayed deletion feature' do
let(:feature_available) { false }
specify do
deletion_date = helper.permanent_deletion_date(Time.now.utc)
expect(subject).to eq "This action deletes <code>#{project.path_with_namespace}</code> on #{deletion_date} and everything this project contains. <strong>There is no going back.</strong>"
end
end
end
describe '#scheduled_for_deletion?' do
......
......@@ -3021,6 +3021,43 @@ RSpec.describe Project do
end
end
describe '#adjourned_deletion_configured?' do
subject { project.adjourned_deletion_configured? }
where(:feature_enabled_on_group?, :adjourned_period, :result) do
true | 0 | false
true | 1 | true
false | 0 | false
false | 1 | false
end
with_them do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
before do
stub_application_setting(deletion_adjourned_period: adjourned_period)
allow(group.namespace_settings).to receive(:delayed_project_removal?).and_return(feature_enabled_on_group?)
end
it { is_expected.to be result }
end
context 'when project belongs to user namespace' do
let_it_be(:user) { create(:user) }
let_it_be(:user_project) { create(:project, namespace: user.namespace) }
before do
stub_licensed_features(adjourned_deletion_for_projects_and_groups: true)
stub_application_setting(deletion_adjourned_period: 7)
end
it 'deletes immediately' do
expect(user_project.adjourned_deletion?).to be_falsey
end
end
end
describe 'calculate template repositories' do
let(:group1) { create(:group) }
let(:group2) { create(:group) }
......
......@@ -67,6 +67,7 @@ RSpec.describe Projects::MarkForDeletionService do
result = described_class.new(project, user).execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Cannot mark project for deletion: feature not supported')
expect(Project.all).to include(project)
expect(project.archived).to eq(false)
......@@ -75,4 +76,40 @@ RSpec.describe Projects::MarkForDeletionService do
end
end
end
describe "#project_update_service_params" do
subject { described_class.new(project, user) }
context 'when delayed deletion feature is not available' do
before do
expect(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(false)
end
it "creates the params for project update service" do
project_update_service_params = subject.send(:project_update_service_params)
expect(project_update_service_params[:marked_for_deletion_at]).not_to be_nil
expect(project_update_service_params[:archived]).to eq(true)
expect(project_update_service_params[:hidden]).to eq(true)
expect(project_update_service_params[:deleting_user]).to eq(user)
expect(project_update_service_params[:name]).to eq("test project xyz-deleted-#{project.id}")
end
end
context 'when delayed deletion feature is available' do
before do
expect(project).to receive(:feature_available?).with(:adjourned_deletion_for_projects_and_groups).and_return(true)
end
it "creates the params for project update service" do
project_update_service_params = subject.send(:project_update_service_params)
expect(project_update_service_params[:marked_for_deletion_at]).not_to be_nil
expect(project_update_service_params[:archived]).to eq(true)
expect(project_update_service_params.has_key?(:hidden)).to eq(false)
expect(project_update_service_params[:deleting_user]).to eq(user)
expect(project_update_service_params[:name]).to eq("test project xyz-deleted-#{project.id}")
end
end
end
end
......@@ -13,18 +13,20 @@ RSpec.describe Projects::RestoreService do
marked_for_deletion_at: 1.day.ago,
deleting_user: user,
archived: true,
hidden: true,
pending_delete: pending_delete)
end
context 'restoring project' do
subject { described_class.new(project, user).execute }
it 'marks project as unarchived and not marked for deletion' do
it 'marks project as not hidden, unarchived and not marked for deletion' do
subject
expect(Project.unscoped.all).to include(project)
expect(project.archived).to eq(false)
expect(project.hidden).to eq(false)
expect(project.marked_for_deletion_at).to be_nil
expect(project.deleting_user).to eq(nil)
end
......
......@@ -119,7 +119,7 @@ module API
def find_project(id)
return unless id
projects = Project.without_deleted
projects = Project.without_deleted.not_hidden
if id.is_a?(Integer) || id =~ /^\d+$/
projects.find_by(id: id)
......
......@@ -37176,6 +37176,9 @@ msgstr ""
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains."
msgstr ""
msgid "This action deletes %{codeOpen}%{project_path_with_namespace}%{codeClose} on %{date} and everything this project contains. %{strongOpen}There is no going back.%{strongClose}"
msgstr ""
msgid "This action will %{strongOpen}permanently remove%{strongClose} %{codeOpen}%{group}%{codeClose} %{strongOpen}immediately%{strongClose}."
msgstr ""
......
......@@ -154,6 +154,10 @@ FactoryBot.define do
archived { true }
end
trait :hidden do
hidden { true }
end
trait :last_repository_check_failed do
last_repository_check_failed { true }
end
......
......@@ -109,6 +109,26 @@ RSpec.describe API::Helpers do
end
end
end
context 'when project is pending delete' do
let(:project_pending_delete) { create(:project, pending_delete: true) }
it 'does not return the project pending delete' do
expect(Project).not_to receive(:find_by_full_path)
expect(subject.find_project(project_pending_delete.id)).to be_nil
end
end
context 'when project is hidden' do
let(:hidden_project) { create(:project, :hidden) }
it 'does not return the hidden project' do
expect(Project).not_to receive(:find_by_full_path)
expect(subject.find_project(hidden_project.id)).to be_nil
end
end
end
describe '#find_project!' do
......
......@@ -8001,6 +8001,37 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '.not_hidden' do
it 'lists projects that are not hidden' do
project = create(:project)
hidden_project = create(:project, :hidden)
expect(described_class.not_hidden).to contain_exactly(project)
expect(described_class.not_hidden).not_to include(hidden_project)
end
end
describe '#pending_delete_or_hidden?' do
let_it_be(:project) { create(:project, name: 'test-project') }
where(:pending_delete, :hidden, :expected_result) do
true | false | true
true | true | true
false | true | true
false | false | false
end
with_them do
it 'returns true if project is pending delete or hidden' do
project.pending_delete = pending_delete
project.hidden = hidden
project.save!
expect(project.pending_delete_or_hidden?).to eq(expected_result)
end
end
end
private
def finish_job(export_job)
......
......@@ -9,6 +9,7 @@ itself: # project
- external_webhook_token
- has_external_issue_tracker
- has_external_wiki
- hidden
- import_source
- import_type
- import_url
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment