Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-12 03:08:56 +00:00
parent 723dc8aced
commit f5703a054c
33 changed files with 602 additions and 86 deletions

View file

@ -25,8 +25,7 @@ gem 'marginalia', '~> 1.10.0'
# Authentication libraries
gem 'devise', '~> 4.7.2'
# TODO: verify ARM compile issue on 3.1.13+ version (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18828)
gem 'bcrypt', '3.1.12'
gem 'bcrypt', '~> 3.1', '>= 3.1.14'
gem 'doorkeeper', '~> 5.5.0.rc2'
gem 'doorkeeper-openid_connect', '~> 1.7.5'
gem 'omniauth', '~> 1.8'

View file

@ -128,7 +128,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (2.0.1)
bcrypt (3.1.12)
bcrypt (3.1.16)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
benchmark-memory (0.1.2)
@ -1346,7 +1346,7 @@ DEPENDENCIES
babosa (~> 1.0.2)
base32 (~> 0.3.0)
batch-loader (~> 2.0.1)
bcrypt (= 3.1.12)
bcrypt (~> 3.1, >= 3.1.14)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
benchmark-memory (~> 0.1)

View file

@ -2,30 +2,8 @@ import $ from 'jquery';
import { escape } from 'lodash';
import { __ } from '~/locale';
import Api from './api';
import axios from './lib/utils/axios_utils';
import { normalizeHeaders } from './lib/utils/common_utils';
import { loadCSSFile } from './lib/utils/css_utils';
const fetchGroups = (params) => {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
.then((res) => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
};
import { select2AxiosTransport } from './lib/utils/select2_utils';
const groupsSelect = () => {
loadCSSFile(gon.select2_css_path)
@ -51,9 +29,7 @@ const groupsSelect = () => {
url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
fetchGroups(params);
},
transport: select2AxiosTransport,
data(search, page) {
return {
search,
@ -63,8 +39,6 @@ const groupsSelect = () => {
};
},
results(data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter((group) => skipGroups.indexOf(group.id) === -1);

View file

@ -5,6 +5,7 @@ import Autosave from './autosave';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import { loadCSSFile } from './lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import { select2AxiosTransport } from './lib/utils/select2_utils';
import { queryToObject, objectToQuery } from './lib/utils/url_utility';
import UsersSelect from './users_select';
import ZenMode from './zen_mode';
@ -199,15 +200,16 @@ export default class IssuableForm {
search,
};
},
results(data) {
results({ results }) {
return {
// `data` keys are translated so we can't just access them with a string based key
results: data[Object.keys(data)[0]].map((name) => ({
results: results[Object.keys(results)[0]].map((name) => ({
id: name,
text: name,
})),
};
},
transport: select2AxiosTransport,
},
initSelection(el, callback) {
const val = el.val();

View file

@ -0,0 +1,25 @@
import axios from './axios_utils';
import { normalizeHeaders, parseIntPagination } from './common_utils';
// This is used in the select2 config to replace jQuery.ajax with axios
export const select2AxiosTransport = (params) => {
axios({
method: params.type?.toLowerCase() || 'get',
url: params.url,
params: params.data,
})
.then((res) => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const pagination = parseIntPagination(headers);
const more = pagination.nextPage > pagination.page;
params.success({
results,
pagination: {
more,
},
});
})
.catch(params.error);
};

View file

@ -96,6 +96,22 @@ class Environment < ApplicationRecord
end
scope :for_id, -> (id) { where(id: id) }
scope :stopped_review_apps, -> (before, limit) do
stopped
.in_review_folder
.where("created_at < ?", before)
.order("created_at ASC")
.limit(limit)
end
scope :scheduled_for_deletion, -> do
where.not(auto_delete_at: nil)
end
scope :not_scheduled_for_deletion, -> do
where(auto_delete_at: nil)
end
enum tier: {
production: 0,
staging: 1,
@ -147,6 +163,10 @@ class Environment < ApplicationRecord
self.state_machine.states.map(&:name)
end
def self.schedule_to_delete(at_time = 1.week.from_now)
update_all(auto_delete_at: at_time)
end
class << self
##
# This method returns stop actions (jobs) for multiple environments within one

View file

@ -0,0 +1,102 @@
# frozen_string_literal: true
module Environments
class ScheduleToDeleteReviewAppsService < ::BaseService
include ::Gitlab::ExclusiveLeaseHelpers
EXCLUSIVE_LOCK_KEY_BASE = 'environments:delete_review_apps:lock'
LOCK_TIMEOUT = 2.minutes
def execute
if validation_error = validate
return validation_error
end
mark_deletable_environments
end
private
def key
"#{EXCLUSIVE_LOCK_KEY_BASE}:#{project.id}"
end
def dry_run?
return true if params[:dry_run].nil?
params[:dry_run]
end
def validate
return if can?(current_user, :destroy_environment, project)
Result.new(error_message: "You do not have permission to destroy environments in this project", status: :unauthorized)
end
def mark_deletable_environments
in_lock(key, ttl: LOCK_TIMEOUT, retries: 1) do
unsafe_mark_deletable_environments
end
rescue FailedToObtainLockError
Result.new(error_message: "Another process is already processing a delete request. Please retry later.", status: :conflict)
end
def unsafe_mark_deletable_environments
result = Result.new
environments = project.environments
.not_scheduled_for_deletion
.stopped_review_apps(params[:before], params[:limit])
# Check if the actor has write permission to a potentially-protected environment.
deletable, failed = *environments.partition { |env| current_user.can?(:destroy_environment, env) }
if deletable.any? && failed.empty?
mark_for_deletion(deletable) unless dry_run?
result.set_status(:ok)
result.set_scheduled_entries(deletable)
else
result.set_status(
:bad_request,
error_message: "Failed to authorize deletions for some or all of the environments. Ask someone with more permissions to delete the environments."
)
result.set_unprocessable_entries(failed)
end
result
end
def mark_for_deletion(deletable_environments)
Environment.for_id(deletable_environments).schedule_to_delete
end
class Result
attr_accessor :scheduled_entries, :unprocessable_entries, :error_message, :status
def initialize(scheduled_entries: [], unprocessable_entries: [], error_message: nil, status: nil)
self.scheduled_entries = scheduled_entries
self.unprocessable_entries = unprocessable_entries
self.error_message = error_message
self.status = status
end
def success?
status == :ok
end
def set_status(status, error_message: nil)
self.status = status
self.error_message = error_message
end
def set_scheduled_entries(entries)
self.scheduled_entries = entries
end
def set_unprocessable_entries(entries)
self.unprocessable_entries = entries
end
end
end
end

View file

@ -0,0 +1,5 @@
---
title: Add API endpoint for deleting stale review envs
merge_request: 52224
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Update Pages template examples to default branch
merge_request: 56298
author:
type: other

View file

@ -62,25 +62,6 @@ their account.
If an administrator [increases](#set-the-user-cap-number) or [removes](#remove-the-user-cap) the
user cap, the users in pending approval state are automatically approved in a background job.
### Enable or disable User cap **(FREE SELF)**
User cap is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can opt to disable it.
To disable it:
```ruby
Feature.disable(:admin_new_user_signups_cap)
```
To enable it:
```ruby
Feature.enable(:admin_new_user_signups_cap)
```
### Set the user cap number
1. Go to **Admin Area > Settings > General**.

View file

@ -75,6 +75,33 @@ module API
end
end
desc "Delete multiple stopped review apps" do
detail "Remove multiple stopped review environments older than a specific age"
success Entities::Environment
end
params do
optional :before, type: Time, desc: "The timestamp before which environments can be deleted. Defaults to 30 days ago.", default: -> { 30.days.ago }
optional :limit, type: Integer, desc: "Maximum number of environments to delete. Defaults to 100.", default: 100, values: 1..1000
optional :dry_run, type: Boolean, desc: "If set, perform a dry run where no actual deletions will be performed. Defaults to true.", default: true
end
delete ":id/environments/review_apps" do
authorize! :read_environment, user_project
result = ::Environments::ScheduleToDeleteReviewAppsService.new(user_project, current_user, params).execute
response = {
scheduled_entries: Entities::Environment.represent(result.scheduled_entries),
unprocessable_entries: Entities::Environment.represent(result.unprocessable_entries)
}
if result.success?
status result.status
present response, current_user: current_user
else
render_api_error!(response.merge!(message: result.error_message), result.status)
end
end
desc 'Deletes an existing environment' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment

View file

@ -11,5 +11,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -9,5 +9,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -13,5 +13,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,5 +11,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -13,5 +13,5 @@ pages:
paths:
- node_modules
key: project
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -10,7 +10,8 @@ test:
script:
- hugo
except:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages:
script:
@ -19,4 +20,5 @@ pages:
paths:
- public
only:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,7 +11,8 @@ test:
- pip install hyde
- hyde gen
except:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages:
stage: deploy
@ -22,4 +23,5 @@ pages:
paths:
- public
only:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -18,7 +18,8 @@ test:
paths:
- test
except:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages:
stage: deploy
@ -28,4 +29,5 @@ pages:
paths:
- public
only:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -33,5 +33,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -12,5 +12,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -12,7 +12,8 @@ test:
- bundle install --path vendor
- bundle exec middleman build
except:
- master
variables:
- $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pages:
script:
@ -23,5 +24,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -8,5 +8,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -11,5 +11,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -25,5 +25,5 @@ pages:
artifacts:
paths:
- public
only:
- master
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

View file

@ -4666,6 +4666,9 @@ msgstr ""
msgid "Be careful. Renaming a project's repository can have unintended side effects."
msgstr ""
msgid "Before this can be merged, a Jira issue must be linked in the title or description"
msgstr ""
msgid "Begin with the selected commit"
msgstr ""

View file

@ -32,6 +32,8 @@ FactoryBot.define do
end
trait :with_review_app do |environment|
sequence(:name) { |n| "review/#{n}" }
transient do
ref { 'master' }
end

View file

@ -0,0 +1,100 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { select2AxiosTransport } from '~/lib/utils/select2_utils';
import 'select2/select2';
const TEST_URL = '/test/api/url';
const TEST_SEARCH_DATA = { extraSearch: 'test' };
const TEST_DATA = [{ id: 1 }];
const TEST_SEARCH = 'FOO';
describe('lib/utils/select2_utils', () => {
let mock;
let resultsSpy;
beforeEach(() => {
setHTMLFixture('<div><input id="root" /></div>');
mock = new MockAdapter(axios);
resultsSpy = jest.fn().mockReturnValue({ results: [] });
});
afterEach(() => {
mock.restore();
});
const setupSelect2 = (input) => {
input.select2({
ajax: {
url: TEST_URL,
quietMillis: 250,
transport: select2AxiosTransport,
data(search, page) {
return {
search,
page,
...TEST_SEARCH_DATA,
};
},
results: resultsSpy,
},
});
};
const setupSelect2AndSearch = async () => {
const $input = $('#root');
setupSelect2($input);
$input.select2('search', TEST_SEARCH);
jest.runOnlyPendingTimers();
await waitForPromises();
};
describe('select2AxiosTransport', () => {
it('uses axios to make request', async () => {
// setup mock response
const replySpy = jest.fn();
mock.onGet(TEST_URL).reply((...args) => replySpy(...args));
await setupSelect2AndSearch();
expect(replySpy).toHaveBeenCalledWith(
expect.objectContaining({
url: TEST_URL,
method: 'get',
params: {
page: 1,
search: TEST_SEARCH,
...TEST_SEARCH_DATA,
},
}),
);
});
it.each`
headers | pagination
${{}} | ${{ more: false }}
${{ 'X-PAGE': '1', 'x-next-page': 2 }} | ${{ more: true }}
`(
'passes results and pagination to results callback, with headers=$headers',
async ({ headers, pagination }) => {
mock.onGet(TEST_URL).reply(200, TEST_DATA, headers);
await setupSelect2AndSearch();
expect(resultsSpy).toHaveBeenCalledWith(
{ results: TEST_DATA, pagination },
1,
expect.anything(),
);
},
);
});
});

View file

@ -84,6 +84,62 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
end
end
describe ".stopped_review_apps" do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
let_it_be(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) }
let_it_be(:old_active_review_env) { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) }
let_it_be(:old_stopped_other_env) { create(:environment, :stopped, created_at: 31.days.ago, project: project) }
let_it_be(:new_stopped_other_env) { create(:environment, :stopped, project: project) }
let_it_be(:old_active_other_env) { create(:environment, :available, created_at: 31.days.ago, project: project) }
let(:before) { 30.days.ago }
let(:limit) { 1000 }
subject { project.environments.stopped_review_apps(before, limit) } # rubocop: disable RSpec/SingleLineHook
it { is_expected.to contain_exactly(old_stopped_review_env) }
context "current timestamp" do
let(:before) { Time.zone.now }
it { is_expected.to contain_exactly(old_stopped_review_env, new_stopped_review_env) }
end
end
describe "scheduled deletion" do
let_it_be(:deletable_environment) { create(:environment, auto_delete_at: Time.zone.now) }
let_it_be(:undeletable_environment) { create(:environment, auto_delete_at: nil) }
describe ".scheduled_for_deletion" do
subject { described_class.scheduled_for_deletion }
it { is_expected.to contain_exactly(deletable_environment) }
end
describe ".not_scheduled_for_deletion" do
subject { described_class.not_scheduled_for_deletion }
it { is_expected.to contain_exactly(undeletable_environment) }
end
describe ".schedule_to_delete" do
subject { described_class.for_id(deletable_environment).schedule_to_delete }
it "schedules the record for deletion" do
freeze_time do
subject
deletable_environment.reload
undeletable_environment.reload
expect(deletable_environment.auto_delete_at).to eq(1.week.from_now)
expect(undeletable_environment.auto_delete_at).to be_nil
end
end
end
end
describe 'state machine' do
it 'invalidates the cache after a change' do
expect(environment).to receive(:expire_etag_cache)

View file

@ -265,4 +265,76 @@ RSpec.describe API::Environments do
end
end
end
describe "DELETE /projects/:id/environments/review_apps" do
shared_examples "delete stopped review environments" do
around do |example|
freeze_time { example.run }
end
it "deletes the old stopped review apps" do
old_stopped_review_env = create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project)
new_stopped_review_env = create(:environment, :with_review_app, :stopped, project: project)
old_active_review_env = create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project)
old_stopped_other_env = create(:environment, :stopped, created_at: 31.days.ago, project: project)
new_stopped_other_env = create(:environment, :stopped, project: project)
old_active_other_env = create(:environment, :available, created_at: 31.days.ago, project: project)
delete api("/projects/#{project.id}/environments/review_apps", current_user), params: { dry_run: false }
project.environments.reload
expect(response).to have_gitlab_http_status(:ok)
expect(json_response["scheduled_entries"].size).to eq(1)
expect(json_response["scheduled_entries"].first["id"]).to eq(old_stopped_review_env.id)
expect(json_response["unprocessable_entries"].size).to eq(0)
expect(old_stopped_review_env.reload.auto_delete_at).to eq(1.week.from_now)
expect(new_stopped_review_env.reload.auto_delete_at).to be_nil
expect(old_active_review_env.reload.auto_delete_at).to be_nil
expect(old_stopped_other_env.reload.auto_delete_at).to be_nil
expect(new_stopped_other_env.reload.auto_delete_at).to be_nil
expect(old_active_other_env.reload.auto_delete_at).to be_nil
end
end
context "as a maintainer" do
it_behaves_like "delete stopped review environments" do
let(:current_user) { user }
end
end
context "as a developer" do
let(:developer) { create(:user) }
before do
project.add_developer(developer)
end
it_behaves_like "delete stopped review environments" do
let(:current_user) { developer }
end
end
context "as a reporter" do
let(:reporter) { create(:user) }
before do
project.add_reporter(reporter)
end
it "rejects the request" do
delete api("/projects/#{project.id}/environments/review_apps", reporter)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context "as a non member" do
it "rejects the request" do
delete api("/projects/#{project.id}/environments/review_apps", non_member)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View file

@ -0,0 +1,136 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Environments::ScheduleToDeleteReviewAppsService do
include ExclusiveLeaseHelpers
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project, :private, :repository, namespace: maintainer.namespace) }
let(:service) { described_class.new(project, current_user, before: 30.days.ago, dry_run: dry_run) }
let(:dry_run) { false }
let(:current_user) { maintainer }
before do
project.add_maintainer(maintainer)
project.add_developer(developer)
project.add_reporter(reporter)
end
describe "#execute" do
subject { service.execute }
shared_examples "can schedule for deletion" do
let!(:old_stopped_review_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project) }
let!(:new_stopped_review_env) { create(:environment, :with_review_app, :stopped, project: project) }
let!(:old_active_review_env) { create(:environment, :with_review_app, :available, created_at: 31.days.ago, project: project) }
let!(:old_stopped_other_env) { create(:environment, :stopped, created_at: 31.days.ago, project: project) }
let!(:new_stopped_other_env) { create(:environment, :stopped, project: project) }
let!(:old_active_other_env) { create(:environment, :available, created_at: 31.days.ago, project: project) }
let!(:already_deleting_env) { create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project, auto_delete_at: 1.day.from_now) }
let(:already_deleting_time) { already_deleting_env.reload.auto_delete_at }
context "live run" do
let(:dry_run) { false }
around do |example|
freeze_time { example.run }
end
it "marks the correct environment as scheduled_entries" do
expect(subject.success?).to be_truthy
expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
new_stopped_review_env.reload
old_active_review_env.reload
old_stopped_other_env.reload
new_stopped_other_env.reload
old_active_other_env.reload
already_deleting_env.reload
expect(old_stopped_review_env.auto_delete_at).to eq(1.week.from_now)
expect(new_stopped_review_env.auto_delete_at).to be_nil
expect(old_active_review_env.auto_delete_at).to be_nil
expect(old_stopped_other_env.auto_delete_at).to be_nil
expect(new_stopped_other_env.auto_delete_at).to be_nil
expect(old_active_other_env.auto_delete_at).to be_nil
expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
end
end
context "dry run" do
let(:dry_run) { true }
it "returns the same but doesn't update the record" do
expect(subject.success?).to be_truthy
expect(subject.scheduled_entries).to contain_exactly(old_stopped_review_env)
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
new_stopped_review_env.reload
old_active_review_env.reload
old_stopped_other_env.reload
new_stopped_other_env.reload
old_active_other_env.reload
already_deleting_env.reload
expect(old_stopped_review_env.auto_delete_at).to be_nil
expect(new_stopped_review_env.auto_delete_at).to be_nil
expect(old_active_review_env.auto_delete_at).to be_nil
expect(old_stopped_other_env.auto_delete_at).to be_nil
expect(new_stopped_other_env.auto_delete_at).to be_nil
expect(old_active_other_env.auto_delete_at).to be_nil
expect(already_deleting_env.auto_delete_at).to eq(already_deleting_time)
end
end
describe "execution in parallel" do
before do
stub_exclusive_lease_taken(service.send(:key))
end
it "does not execute unsafe_mark_scheduled_entries_environments" do
expect(service).not_to receive(:unsafe_mark_scheduled_entries_environments)
expect(subject.success?).to be_falsey
expect(subject.status).to eq(:conflict)
end
end
end
context "as a maintainer" do
let(:current_user) { maintainer }
it_behaves_like "can schedule for deletion"
end
context "as a developer" do
let(:current_user) { developer }
it_behaves_like "can schedule for deletion"
end
context "as a reporter" do
let(:current_user) { reporter }
it "fails to delete environments" do
old_stopped_review_env = create(:environment, :with_review_app, :stopped, created_at: 31.days.ago, project: project)
expect(subject.success?).to be_falsey
# Both of these should be empty as we fail before testing them
expect(subject.scheduled_entries).to be_empty
expect(subject.unprocessable_entries).to be_empty
old_stopped_review_env.reload
expect(old_stopped_review_env.auto_delete_at).to be_nil
end
end
end
end