summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 15:09:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-14 15:09:44 +0000
commit874ead9c3a50de4c4ca4551eaf5b7eb976d26b50 (patch)
tree637ee9f2da5e251bc08ebf3e972209d51966bf7c /spec
parent2e4c4055181eec9186458dd5dd3219c937032ec7 (diff)
downloadgitlab-ce-874ead9c3a50de4c4ca4551eaf5b7eb976d26b50.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb42
-rw-r--r--spec/factories/ci/job_artifacts.rb2
-rw-r--r--spec/factories/diff_note_positions.rb10
-rw-r--r--spec/features/projects/environments/environments_spec.rb14
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js1
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js111
-rw-r--r--spec/frontend/smart_interval_spec.js197
-rw-r--r--spec/frontend/static_site_editor/components/static_site_editor_spec.js9
-rw-r--r--spec/frontend/static_site_editor/store/getters_spec.js12
-rw-r--r--spec/frontend/static_site_editor/store/mutations_spec.js1
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js19
-rw-r--r--spec/javascripts/smart_interval_spec.js234
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb20
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb31
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb31
-rw-r--r--spec/models/ci/job_artifact_spec.rb21
-rw-r--r--spec/models/diff_note_position_spec.rb22
-rw-r--r--spec/requests/api/group_clusters_spec.rb17
-rw-r--r--spec/requests/api/project_clusters_spec.rb22
-rw-r--r--spec/rubocop/cop/performance/ar_count_each_spec.rb62
-rw-r--r--spec/services/application_settings/update_service_spec.rb16
-rw-r--r--spec/services/clusters/create_service_spec.rb88
-rw-r--r--spec/services/clusters/management/validate_management_project_permissions_service_spec.rb88
-rw-r--r--spec/services/environments/auto_stop_service_spec.rb12
-rw-r--r--spec/services/notification_service_spec.rb1
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb8
-rw-r--r--spec/uploaders/records_uploads_spec.rb6
-rw-r--r--spec/views/projects/pages/show.html.haml_spec.rb2
28 files changed, 732 insertions, 367 deletions
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 74ed4a0f991..fdc8fe5f082 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1085,6 +1085,48 @@ describe Projects::IssuesController do
expect { subject }.to change(SentryIssue, :count)
end
end
+
+ context 'when the endpoint receives requests above the limit' do
+ before do
+ stub_application_setting(issues_create_limit: 5)
+ end
+
+ it 'prevents from creating more issues', :request_store do
+ 5.times { post_new_issue }
+
+ expect { post_new_issue }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1) # creates 1 projects and 0 issues
+
+ post_new_issue
+ expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'logs the event on auth.log' do
+ attributes = {
+ message: 'Application_Rate_Limiter_Request',
+ env: :issues_create_request_limit,
+ remote_ip: '0.0.0.0',
+ request_method: 'POST',
+ path: "/#{project.full_path}/-/issues",
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
+
+ project.add_developer(user)
+ sign_in(user)
+
+ 6.times do
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+ end
+ end
end
describe 'POST #mark_as_spam' do
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 82383cfa2b0..a259c5142fc 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
end
trait :remote_store do
- file_store { JobArtifactUploader::Store::REMOTE}
+ file_store { JobArtifactUploader::Store::REMOTE }
end
after :build do |artifact|
diff --git a/spec/factories/diff_note_positions.rb b/spec/factories/diff_note_positions.rb
new file mode 100644
index 00000000000..6e95e306d50
--- /dev/null
+++ b/spec/factories/diff_note_positions.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :diff_note_position do
+ association :note, factory: :diff_note_on_merge_request
+ line_code { note.line_code }
+ position { note.position }
+ diff_type { :head }
+ end
+end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index d7f12411a93..cee9b6d50ba 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -399,10 +399,12 @@ describe 'Environments page', :js do
describe 'environments folders' do
before do
- create(:environment, project: project,
+ create(:environment, :will_auto_stop,
+ project: project,
name: 'staging/review-1',
state: :available)
- create(:environment, project: project,
+ create(:environment, :will_auto_stop,
+ project: project,
name: 'staging/review-2',
state: :available)
end
@@ -420,6 +422,14 @@ describe 'Environments page', :js do
expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2'
+ within('.ci-table') do
+ within('.gl-responsive-table-row:nth-child(3)') do
+ expect(find('.js-auto-stop').text).not_to be_empty
+ end
+ within('.gl-responsive-table-row:nth-child(4)') do
+ expect(find('.js-auto-stop').text).not_to be_empty
+ end
+ end
end
end
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 3aad4c87237..870e47edde0 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -50,6 +50,7 @@ describe('Time series component', () => {
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
+ annotations: store.state.monitoringDashboard.annotations,
projectPath: `${mockHost}${mockProjectDir}`,
},
store,
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index d6faec29b65..c34a5afceb0 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -16,6 +16,7 @@ import {
fetchDeploymentsData,
fetchEnvironmentsData,
fetchDashboardData,
+ fetchAnnotations,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
@@ -24,10 +25,12 @@ import {
} from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
+import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
import storeState from '~/monitoring/stores/state';
import {
deploymentData,
environmentData,
+ annotationsData,
metricsDashboardResponse,
metricsDashboardViewModel,
dashboardGitResponse,
@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => {
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(
- Promise.resolve({
- data: {
- project: {
- data: {
- environments: [],
- },
+ jest.spyOn(gqClient, 'mutate').mockReturnValue({
+ data: {
+ project: {
+ data: {
+ environments: [],
},
},
- }),
- );
+ },
+ });
return testAction(
filterEnvironments,
@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataSuccess on success', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(
- Promise.resolve({
- data: {
- project: {
- data: {
- environments: environmentData,
- },
+ jest.spyOn(gqClient, 'mutate').mockResolvedValue({
+ data: {
+ project: {
+ data: {
+ environments: environmentData,
},
},
- }),
- );
+ },
+ });
return testAction(
fetchEnvironmentsData,
@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataFailure on error', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject());
+ jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
return testAction(
fetchEnvironmentsData,
@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => {
});
});
+ describe('fetchAnnotations', () => {
+ const { state } = store;
+ state.projectPath = 'gitlab-org/gitlab-test';
+ state.currentEnvironmentName = 'production';
+ state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardId: state.currentDashboard,
+ },
+ };
+
+ mockMutate.mockResolvedValue({
+ data: {
+ project: {
+ environment: {
+ metricDashboard: {
+ annotations: annotationsData,
+ },
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestAnnotations' },
+ { type: 'receiveAnnotationsSuccess', payload: annotationsData },
+ ],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardId: state.currentDashboard,
+ },
+ };
+
+ mockMutate.mockRejectedValue({});
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+ });
+
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
diff --git a/spec/frontend/smart_interval_spec.js b/spec/frontend/smart_interval_spec.js
new file mode 100644
index 00000000000..b32ac99e4e4
--- /dev/null
+++ b/spec/frontend/smart_interval_spec.js
@@ -0,0 +1,197 @@
+import $ from 'jquery';
+import { assignIn } from 'lodash';
+import waitForPromises from 'helpers/wait_for_promises';
+import SmartInterval from '~/smart_interval';
+
+jest.useFakeTimers();
+
+let interval;
+
+describe('SmartInterval', () => {
+ const DEFAULT_MAX_INTERVAL = 100;
+ const DEFAULT_STARTING_INTERVAL = 5;
+ const DEFAULT_INCREMENT_FACTOR = 2;
+
+ function createDefaultSmartInterval(config) {
+ const defaultParams = {
+ callback: () => Promise.resolve(),
+ startingInterval: DEFAULT_STARTING_INTERVAL,
+ maxInterval: DEFAULT_MAX_INTERVAL,
+ incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
+ lazyStart: false,
+ immediateExecution: false,
+ hiddenInterval: null,
+ };
+
+ if (config) {
+ assignIn(defaultParams, config);
+ }
+
+ return new SmartInterval(defaultParams);
+ }
+
+ afterEach(() => {
+ interval.destroy();
+ });
+
+ describe('Increment Interval', () => {
+ it('should increment the interval delay', () => {
+ interval = createDefaultSmartInterval();
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const intervalConfig = interval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations =
+ intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
+ });
+ });
+
+ it('should not increment past maxInterval', () => {
+ interval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const currentInterval = interval.getCurrentInterval();
+
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
+ });
+ });
+
+ it('does not increment while waiting for callback', () => {
+ interval = createDefaultSmartInterval({
+ callback: () => new Promise($.noop),
+ });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const oneInterval = interval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
+
+ expect(interval.getCurrentInterval()).toEqual(oneInterval);
+ });
+ });
+ });
+
+ describe('Public methods', () => {
+ beforeEach(() => {
+ interval = createDefaultSmartInterval();
+ });
+
+ it('should cancel an interval', () => {
+ jest.runOnlyPendingTimers();
+
+ interval.cancel();
+
+ return waitForPromises().then(() => {
+ const { intervalId } = interval.state;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
+
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
+ });
+ });
+
+ it('should resume an interval', () => {
+ jest.runOnlyPendingTimers();
+
+ interval.cancel();
+
+ interval.resume();
+
+ return waitForPromises().then(() => {
+ const { intervalId } = interval.state;
+
+ expect(intervalId).toBeTruthy();
+ });
+ });
+ });
+
+ describe('DOM Events', () => {
+ beforeEach(() => {
+ // This ensures DOM and DOM events are initialized for these specs.
+ setFixtures('<div></div>');
+
+ interval = createDefaultSmartInterval();
+ });
+
+ it('should pause when page is not visible', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+ });
+ });
+
+ it('should change to the hidden interval when page is not visible', () => {
+ interval.destroy();
+
+ const HIDDEN_INTERVAL = 1500;
+ interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(
+ interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
+ ).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ });
+ });
+
+ it('should resume when page is becomes visible at the previous interval', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'visible' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ });
+ });
+
+ it('should cancel on page unload', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ $(document).triggerHandler('beforeunload');
+
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ });
+ });
+
+ it('should execute callback before first interval', () => {
+ interval = createDefaultSmartInterval({ immediateExecution: true });
+
+ expect(interval.cfg.immediateExecution).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js
index a40f8edbeb2..2c4fa0e061a 100644
--- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js
+++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js
@@ -30,7 +30,6 @@ describe('StaticSiteEditor', () => {
store = new Vuex.Store({
state: createState(initialState),
getters: {
- isContentLoaded: () => false,
contentChanged: () => false,
...getters,
},
@@ -43,9 +42,11 @@ describe('StaticSiteEditor', () => {
};
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({
- initialState,
+ initialState: {
+ isContentLoaded: true,
+ ...initialState,
+ },
getters: {
- isContentLoaded: () => true,
...getters,
},
});
@@ -85,7 +86,7 @@ describe('StaticSiteEditor', () => {
const content = 'edit area content';
beforeEach(() => {
- buildStore({ initialState: { content }, getters: { isContentLoaded: () => true } });
+ buildContentLoadedStore({ initialState: { content } });
buildWrapper();
});
diff --git a/spec/frontend/static_site_editor/store/getters_spec.js b/spec/frontend/static_site_editor/store/getters_spec.js
index 1b482db9366..5793e344784 100644
--- a/spec/frontend/static_site_editor/store/getters_spec.js
+++ b/spec/frontend/static_site_editor/store/getters_spec.js
@@ -1,18 +1,8 @@
import createState from '~/static_site_editor/store/state';
-import { isContentLoaded, contentChanged } from '~/static_site_editor/store/getters';
+import { contentChanged } from '~/static_site_editor/store/getters';
import { sourceContent as content } from '../mock_data';
describe('Static Site Editor Store getters', () => {
- describe('isContentLoaded', () => {
- it('returns true when originalContent is not empty', () => {
- expect(isContentLoaded(createState({ originalContent: content }))).toBe(true);
- });
-
- it('returns false when originalContent is empty', () => {
- expect(isContentLoaded(createState({ originalContent: '' }))).toBe(false);
- });
- });
-
describe('contentChanged', () => {
it('returns true when content and originalContent are different', () => {
const state = createState({ content, originalContent: 'something else' });
diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js
index 1fd687eed4a..0b213c11a04 100644
--- a/spec/frontend/static_site_editor/store/mutations_spec.js
+++ b/spec/frontend/static_site_editor/store/mutations_spec.js
@@ -19,6 +19,7 @@ describe('Static Site Editor Store mutations', () => {
mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
+ ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index ef95cb1b8f2..e022f68fdec 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -273,25 +273,6 @@ describe('mrWidgetOptions', () => {
};
});
- it('should not tell service to check status if document is not visible', () => {
- Object.defineProperty(document, 'visibilityState', {
- value: 'hidden',
- configurable: true,
- });
- vm.checkStatus(cb);
-
- return vm.$nextTick().then(() => {
- expect(vm.service.checkStatus).not.toHaveBeenCalled();
- expect(vm.mr.setData).not.toHaveBeenCalled();
- expect(vm.handleNotification).not.toHaveBeenCalled();
- expect(isCbExecuted).toBeFalsy();
- Object.defineProperty(document, 'visibilityState', {
- value: 'visible',
- configurable: true,
- });
- });
- });
-
it('should tell service to check status if document is visible', () => {
vm.checkStatus(cb);
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
deleted file mode 100644
index 0dc9ee9d79a..00000000000
--- a/spec/javascripts/smart_interval_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import $ from 'jquery';
-import { assignIn } from 'lodash';
-import waitForPromises from 'spec/helpers/wait_for_promises';
-import SmartInterval from '~/smart_interval';
-
-describe('SmartInterval', function() {
- const DEFAULT_MAX_INTERVAL = 100;
- const DEFAULT_STARTING_INTERVAL = 5;
- const DEFAULT_SHORT_TIMEOUT = 75;
- const DEFAULT_INCREMENT_FACTOR = 2;
-
- function createDefaultSmartInterval(config) {
- const defaultParams = {
- callback: () => Promise.resolve(),
- startingInterval: DEFAULT_STARTING_INTERVAL,
- maxInterval: DEFAULT_MAX_INTERVAL,
- incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
- lazyStart: false,
- immediateExecution: false,
- hiddenInterval: null,
- };
-
- if (config) {
- assignIn(defaultParams, config);
- }
-
- return new SmartInterval(defaultParams);
- }
-
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- describe('Increment Interval', function() {
- it('should increment the interval delay', done => {
- const smartInterval = createDefaultSmartInterval();
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- const intervalConfig = smartInterval.cfg;
- const iterationCount = 4;
- const maxIntervalAfterIterations =
- intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
- const currentInterval = smartInterval.getCurrentInterval();
-
- // Provide some flexibility for performance of testing environment
- expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
- expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should not increment past maxInterval', done => {
- const smartInterval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
-
- jasmine.clock().tick(DEFAULT_STARTING_INTERVAL);
- jasmine.clock().tick(DEFAULT_STARTING_INTERVAL * DEFAULT_INCREMENT_FACTOR);
-
- waitForPromises()
- .then(() => {
- const currentInterval = smartInterval.getCurrentInterval();
-
- expect(currentInterval).toBe(smartInterval.cfg.maxInterval);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not increment while waiting for callback', done => {
- const smartInterval = createDefaultSmartInterval({
- callback: () => new Promise($.noop),
- });
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
-
- expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('Public methods', function() {
- beforeEach(function() {
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should cancel an interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- interval.cancel();
-
- waitForPromises()
- .then(() => {
- const { intervalId } = interval.state;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
-
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should resume an interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- interval.cancel();
-
- interval.resume();
-
- waitForPromises()
- .then(() => {
- const { intervalId } = interval.state;
-
- expect(intervalId).toBeTruthy();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('DOM Events', function() {
- beforeEach(function() {
- // This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
-
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should pause when page is not visible', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should change to the hidden interval when page is not visible', done => {
- const HIDDEN_INTERVAL = 1500;
- const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
- expect(
- interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
- interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
- ).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should resume when page is becomes visible at the previous interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
-
- expect(interval.state.intervalId).toBeTruthy();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should cancel on page unload', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- $(document).triggerHandler('beforeunload');
-
- expect(interval.state.intervalId).toBeUndefined();
- expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should execute callback before first interval', function() {
- const interval = createDefaultSmartInterval({ immediateExecution: true });
-
- expect(interval.cfg.immediateExecution).toBeFalsy();
- });
- });
-});
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index e11613b202d..6e8a8c03aad 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -113,28 +113,22 @@ describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_r
end
describe 'droppable?' do
- where(:idempotent, :duplicate, :feature_enabled) do
- # [true, false].repeated_permutation(3)
- [[true, true, true],
- [true, true, false],
- [true, false, true],
- [true, false, false],
- [false, true, true],
- [false, true, false],
- [false, false, true],
- [false, false, false]]
+ where(:idempotent, :duplicate) do
+ # [true, false].repeated_permutation(2)
+ [[true, true],
+ [true, false],
+ [false, true],
+ [false, false]]
end
with_them do
before do
allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent)
allow(duplicate_job).to receive(:duplicate?).and_return(duplicate)
- allow(Gitlab::SidekiqMiddleware::DuplicateJobs)
- .to receive(:drop_duplicates?).with(queue).and_return(feature_enabled)
end
it 'is droppable when all conditions are met' do
- if idempotent && duplicate && feature_enabled
+ if idempotent && duplicate
expect(duplicate_job).to be_droppable
else
expect(duplicate_job).not_to be_droppable
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb
deleted file mode 100644
index fa5938f470b..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::SidekiqMiddleware::DuplicateJobs do
- using RSpec::Parameterized::TableSyntax
-
- describe '.drop_duplicates?' do
- where(:global_feature_enabled, :selected_queue_enabled, :queue, :expected) do
- true | true | described_class::DROPPABLE_QUEUES.first | true
- true | true | "other_queue" | true
- true | false | described_class::DROPPABLE_QUEUES.first | true
- true | false | "other_queue" | true
- false | true | described_class::DROPPABLE_QUEUES.first | true
- false | true | "other_queue" | false
- false | false | described_class::DROPPABLE_QUEUES.first | false
- false | false | "other_queue" | false
- end
-
- with_them do
- before do
- stub_feature_flags(drop_duplicate_sidekiq_jobs: global_feature_enabled,
- drop_duplicate_sidekiq_jobs_for_queue: selected_queue_enabled)
- end
-
- it "allows dropping jobs when expected" do
- expect(described_class.drop_duplicates?(queue)).to be(expected)
- end
- end
- end
-end
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index 78887cef7ab..5029a17e4e5 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -23,13 +23,20 @@ describe Emails::PagesDomains do
is_expected.to have_subject(email_subject)
is_expected.to have_body_text(project.human_name)
is_expected.to have_body_text(domain.domain)
- is_expected.to have_body_text domain.url
is_expected.to have_body_text project_pages_domain_url(project, domain)
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
end
end
end
+ shared_examples 'a pages domain verification email' do
+ it_behaves_like 'a pages domain email'
+
+ it 'has the expected content' do
+ is_expected.to have_body_text domain.url
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
+ end
+ end
+
shared_examples 'notification about upcoming domain removal' do
context 'when domain is not scheduled for removal' do
it 'asks user to remove it' do
@@ -56,7 +63,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_enabled_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it { is_expected.to have_body_text 'has been enabled' }
end
@@ -67,7 +74,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_disabled_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it_behaves_like 'notification about upcoming domain removal'
@@ -80,7 +87,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_verification_succeeded_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it { is_expected.to have_body_text 'successfully verified' }
end
@@ -94,10 +101,18 @@ describe Emails::PagesDomains do
it_behaves_like 'a pages domain email'
it_behaves_like 'notification about upcoming domain removal'
+ end
+
+ describe '#pages_domain_auto_ssl_failed_email' do
+ let(:email_subject) { "#{project.path} | ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '#{domain.domain}'" }
+
+ subject { Notify.pages_domain_auto_ssl_failed_email(domain, user) }
+
+ it_behaves_like 'a pages domain email'
- it 'says verification has failed and when the domain is enabled until' do
- is_expected.to have_body_text 'Verification has failed'
- is_expected.to have_body_text domain.enabled_until.strftime('%F %T')
+ it 'says that we failed to obtain certificate' do
+ is_expected.to have_body_text "Something went wrong while obtaining the Let's Encrypt certificate."
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 6f6ff3704b4..80b619ed2b1 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -349,16 +349,13 @@ describe Ci::JobArtifact do
end
describe 'file is being stored' do
- subject { create(:ci_job_artifact, :archive) }
-
context 'when object has nil store' do
- before do
- subject.update_column(:file_store, nil)
- subject.reload
- end
-
it 'is stored locally' do
- expect(subject.file_store).to be(nil)
+ subject = build(:ci_job_artifact, :archive, file_store: nil)
+
+ subject.save
+
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
expect(subject.file).to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
end
@@ -366,6 +363,10 @@ describe Ci::JobArtifact do
context 'when existing object has local store' do
it 'is stored locally' do
+ subject = build(:ci_job_artifact, :archive)
+
+ subject.save
+
expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
expect(subject.file).to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
@@ -379,6 +380,10 @@ describe Ci::JobArtifact do
context 'when file is stored' do
it 'is stored remotely' do
+ subject = build(:ci_job_artifact, :archive)
+
+ subject.save
+
expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
expect(subject.file).not_to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
diff --git a/spec/models/diff_note_position_spec.rb b/spec/models/diff_note_position_spec.rb
new file mode 100644
index 00000000000..a00ba35feef
--- /dev/null
+++ b/spec/models/diff_note_position_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DiffNotePosition, type: :model do
+ it 'has a position attribute' do
+ diff_position = build(:diff_position)
+ line_code = 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
+ diff_note_position = build(:diff_note_position, line_code: line_code, position: diff_position)
+
+ expect(diff_note_position.position).to eq(diff_position)
+ expect(diff_note_position.line_code).to eq(line_code)
+ expect(diff_note_position.diff_content_type).to eq('text')
+ end
+
+ it 'unique by note_id and diff type' do
+ existing_diff_note_position = create(:diff_note_position)
+ diff_note_position = build(:diff_note_position, note: existing_diff_note_position.note)
+
+ expect { diff_note_position.save! }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+end
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index d3bd84f1604..fade54f6b11 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -157,6 +157,7 @@ describe API::GroupClusters do
let(:api_url) { 'https://kubernetes.example.com' }
let(:authorization_type) { 'rbac' }
+ let(:management_project_id) { create(:project, group: group).id }
let(:platform_kubernetes_attributes) do
{
@@ -171,7 +172,8 @@ describe API::GroupClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
@@ -203,6 +205,7 @@ describe API::GroupClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.management_project_id).to eq management_project_id
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.token).to eq('sample-token')
@@ -234,6 +237,18 @@ describe API::GroupClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with invalid params' do
let(:api_url) { 'invalid_api_url' }
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 648577dce8d..ed899e830e1 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -150,6 +150,12 @@ describe API::ProjectClusters do
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { project.path }
let(:authorization_type) { 'rbac' }
+ let(:management_project) { create(:project, namespace: project.namespace) }
+ let(:management_project_id) { management_project.id }
+
+ before do
+ management_project.add_maintainer(current_user)
+ end
let(:platform_kubernetes_attributes) do
{
@@ -165,7 +171,8 @@ describe API::ProjectClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
@@ -194,6 +201,7 @@ describe API::ProjectClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.management_project_id).to eq management_project_id
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.namespace).to eq(namespace)
@@ -227,6 +235,18 @@ describe API::ProjectClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
diff --git a/spec/rubocop/cop/performance/ar_count_each_spec.rb b/spec/rubocop/cop/performance/ar_count_each_spec.rb
new file mode 100644
index 00000000000..f934a1fde48
--- /dev/null
+++ b/spec/rubocop/cop/performance/ar_count_each_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/performance/ar_count_each.rb'
+
+describe RuboCop::Cop::Performance::ARCountEach do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ context 'when it is not haml file' do
+ it 'does not flag it as an offense' do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+
+ expect_no_offenses <<~SOURCE
+ show(@users.count)
+ @users.each { |user| display(user) }
+ SOURCE
+ end
+ end
+
+ context 'when it is haml file' do
+ before do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ end
+
+ context 'when the same object uses count and each' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ show(@users.count)
+ ^^^^^^^^^^^^ If @users is AR relation, avoid `@users.count ...; @users.each... `, this will trigger two queries. Use `@users.load.size ...; @users.each... ` instead. If @users is an array, try to use @users.size.
+ @users.each { |user| display(user) }
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARCountEach')
+ end
+ end
+
+ context 'when different object uses count and each' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ show(@emails.count)
+ @users.each { |user| display(user) }
+ SOURCE
+ end
+ end
+
+ context 'when just using count without each' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.count'
+ end
+ end
+
+ context 'when just using each without count' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.each { |user| display(user) }'
+ end
+ end
+ end
+end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 6e1fdb7aad0..069572e4dff 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -334,4 +334,20 @@ describe ApplicationSettings::UpdateService do
expect(application_settings.protected_paths).to eq(['/users/password', '/users/sign_in'])
end
end
+
+ context 'when issues_create_limit is passsed' do
+ let(:params) do
+ {
+ issues_create_limit: 600
+ }
+ end
+
+ it 'updates issues_create_limit value' do
+ subject.execute
+
+ application_settings.reload
+
+ expect(application_settings.issues_create_limit).to eq(600)
+ end
+ end
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index ecf0a9c9dce..3dd25be2a3d 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -59,4 +59,92 @@ describe Clusters::CreateService do
end
end
end
+
+ context 'when params includes :management_project_id' do
+ subject(:cluster) { described_class.new(user, params).execute(access_token: access_token) }
+
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a',
+ legacy_abac: 'true'
+ },
+ clusterable: clusterable,
+ management_project_id: management_project_id
+ }
+ end
+
+ let(:clusterable) { project }
+ let(:management_project_id) { management_project.id }
+ let(:management_project_namespace) { project.namespace }
+ let(:management_project) { create(:project, namespace: management_project_namespace) }
+
+ shared_examples 'invalid project or cluster permissions' do
+ it 'does not persist the cluster and adds errors' do
+ expect(cluster).not_to be_persisted
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+
+ shared_examples 'setting a management project' do
+ context 'when user is authorized to adminster manangement_project' do
+ before do
+ management_project.add_maintainer(user)
+ end
+
+ it 'persists the cluster' do
+ expect(cluster).to be_persisted
+
+ expect(cluster.management_project).to eq(management_project)
+ end
+ end
+
+ context 'when user is not authorized to adminster manangement_project' do
+ include_examples 'invalid project or cluster permissions'
+ end
+ end
+
+ shared_examples 'setting a management project outside of scope' do
+ context 'when manangement_project is outside of the namespace scope' do
+ let(:management_project_namespace) { create(:group) }
+
+ it 'does not persist the cluster' do
+ expect(cluster).not_to be_persisted
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ context 'management_project is non-existent' do
+ let(:management_project_id) { 0 }
+
+ include_examples 'invalid project or cluster permissions'
+ end
+
+ context 'project cluster' do
+ include_examples 'setting a management project'
+ include_examples 'setting a management project outside of scope'
+ end
+
+ context 'group cluster' do
+ let(:management_project_namespace) { create(:group) }
+ let(:clusterable) { management_project_namespace }
+
+ include_examples 'setting a management project'
+ include_examples 'setting a management project outside of scope'
+ end
+
+ context 'instance cluster' do
+ let(:clusterable) { Clusters::Instance.new }
+
+ include_examples 'setting a management project'
+ end
+ end
end
diff --git a/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
new file mode 100644
index 00000000000..1bcebe2e2ac
--- /dev/null
+++ b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Management::ValidateManagementProjectPermissionsService do
+ describe '#execute' do
+ subject { described_class.new(user).execute(cluster, management_project_id) }
+
+ let(:cluster) { build(:cluster, :project, projects: [create(:project)]) }
+ let(:user) { create(:user) }
+
+ context 'when management_project_id is nil' do
+ let(:management_project_id) { nil }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when management_project_id is not nil' do
+ let(:management_project_id) { management_project.id }
+ let(:management_project_namespace) { create(:group) }
+ let(:management_project) { create(:project, namespace: management_project_namespace) }
+
+ context 'when management_project does not exist' do
+ let(:management_project_id) { 0 }
+
+ it 'adds errors to the cluster and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+
+ shared_examples 'management project is in scope' do
+ context 'when user is authorized to administer manangement_project' do
+ before do
+ management_project.add_maintainer(user)
+ end
+
+ it 'adds no error and returns true' do
+ is_expected.to eq true
+
+ expect(cluster.errors).to be_empty
+ end
+ end
+
+ context 'when user is not authorized to adminster manangement_project' do
+ it 'adds an error and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ shared_examples 'management project is out of scope' do
+ context 'when manangement_project is outside of the namespace scope' do
+ let(:management_project_namespace) { create(:group) }
+
+ it 'adds an error and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ context 'project cluster' do
+ let(:cluster) { build(:cluster, :project, projects: [create(:project, namespace: management_project_namespace)]) }
+
+ include_examples 'management project is in scope'
+ include_examples 'management project is out of scope'
+ end
+
+ context 'group cluster' do
+ let(:cluster) { build(:cluster, :group, groups: [management_project_namespace]) }
+
+ include_examples 'management project is in scope'
+ include_examples 'management project is out of scope'
+ end
+
+ context 'instance cluster' do
+ let(:cluster) { build(:cluster, :instance) }
+
+ include_examples 'management project is in scope'
+ end
+ end
+ end
+end
diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb
index 3620bf8fe87..b34d15889d3 100644
--- a/spec/services/environments/auto_stop_service_spec.rb
+++ b/spec/services/environments/auto_stop_service_spec.rb
@@ -40,18 +40,6 @@ describe Environments::AutoStopService, :clean_gitlab_redis_shared_state do
expect(Ci::Build.where(name: 'stop_review_app').map(&:status).uniq).to eq(['pending'])
end
- context 'when auto_stop_environments feature flag is disabled' do
- before do
- stub_feature_flags(auto_stop_environments: false)
- end
-
- it 'does not execute Ci::StopEnvironmentsService' do
- expect(Ci::StopEnvironmentsService).not_to receive(:execute_in_batch)
-
- subject
- end
- end
-
context 'when the other sidekiq worker has already been running' do
before do
stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY)
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 86f37e9204c..163ca0b9bc3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2604,6 +2604,7 @@ describe NotificationService, :mailer do
pages_domain_disabled
pages_domain_verification_succeeded
pages_domain_verification_failed
+ pages_domain_auto_ssl_failed
].each do |sym|
describe "##{sym}" do
subject(:notify!) { notification.send(sym, domain) }
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 163276db7e6..63fd0978c97 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -180,5 +180,13 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
end
+
+ it 'sends notification' do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).to receive(:pages_domain_auto_ssl_failed).with(pages_domain)
+ end
+
+ service.execute
+ end
end
end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 71eff23c77c..140595e58ad 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -78,7 +78,8 @@ describe RecordsUploads do
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
model: build_stubbed(:user),
- uploader: uploader.class.to_s
+ uploader: uploader.class.to_s,
+ store: ::ObjectStorage::Store::LOCAL
)
uploader.upload = existing
@@ -98,7 +99,8 @@ describe RecordsUploads do
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
model: project,
- uploader: uploader.class.to_s
+ uploader: uploader.class.to_s,
+ store: ::ObjectStorage::Store::LOCAL
)
uploader.store!(upload_fixture('rails_sample.jpg'))
diff --git a/spec/views/projects/pages/show.html.haml_spec.rb b/spec/views/projects/pages/show.html.haml_spec.rb
index 80410e7bc32..39384484279 100644
--- a/spec/views/projects/pages/show.html.haml_spec.rb
+++ b/spec/views/projects/pages/show.html.haml_spec.rb
@@ -17,7 +17,7 @@ describe 'projects/pages/show' do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
- assign(:domains, [domain])
+ assign(:domains, project.pages_domains)
end
describe 'validation warning' do