summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 15:07:53 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 15:07:53 +0000
commita5ab3467a705b62911feacc3cf627fdbb00aa198 (patch)
tree65143ce13405efccb922fc428624ad57c38b6efa
parenteb30dd6e28f6fc9eb8021d205f6ed84511f001e2 (diff)
downloadgitlab-ce-a5ab3467a705b62911feacc3cf627fdbb00aa198.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index/index.js3
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue84
-rw-r--r--app/assets/javascripts/registry/settings/store/getters.js8
-rw-r--r--app/assets/javascripts/registry/settings/store/index.js2
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js5
-rw-r--r--app/assets/javascripts/registry/settings/store/state.js4
-rw-r--r--app/assets/javascripts/registry/settings/utils.js6
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js26
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb65
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb2
-rw-r--r--app/controllers/groups/group_links_controller.rb24
-rw-r--r--app/controllers/groups/group_members_controller.rb41
-rw-r--r--app/finders/group_members_finder.rb44
-rw-r--r--app/graphql/mutations/snippets/create.rb2
-rw-r--r--app/helpers/application_settings_helper.rb6
-rw-r--r--app/helpers/groups/group_members_helper.rb4
-rw-r--r--app/helpers/selects_helper.rb3
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/group_group_link.rb4
-rw-r--r--app/models/project_group_link.rb2
-rw-r--r--app/views/admin/application_settings/_gitaly.html.haml3
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml22
-rw-r--r--app/views/groups/group_members/index.html.haml42
-rw-r--r--app/views/projects/project_members/_groups.html.haml4
-rw-r--r--app/views/projects/project_members/_team.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml8
-rw-r--r--app/views/projects/registry/settings/_index.haml5
-rw-r--r--app/views/shared/members/_group.html.haml9
-rw-r--r--app/views/shared/members/_invite_group.html.haml (renamed from app/views/projects/project_members/_new_project_group.html.haml)15
-rw-r--r--app/views/shared/members/_invite_member.html.haml (renamed from app/views/projects/project_members/_new_project_member.html.haml)17
-rw-r--r--changelogs/unreleased/37359-cablett-support-envelope-to.yml5
-rw-r--r--changelogs/unreleased/bvl-gitaly-dynamic-deadline.yml5
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/lograge.rb2
-rw-r--r--config/initializers/request_context.rb2
-rw-r--r--config/routes/admin.rb2
-rw-r--r--doc/administration/raketasks/uploads/migrate.md12
-rw-r--r--doc/raketasks/README.md4
-rw-r--r--doc/update/README.md4
-rw-r--r--lib/gitlab/auth/unique_ips_limiter.rb2
-rw-r--r--lib/gitlab/email/receiver.rb10
-rw-r--r--lib/gitlab/gitaly_client.rb23
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb4
-rw-r--r--lib/gitlab/middleware/request_context.rb27
-rw-r--r--lib/gitlab/request_context.rb41
-rw-r--r--locale/gitlab.pot43
-rw-r--r--qa/qa/page/group/sub_menus/members.rb9
-rw-r--r--qa/qa/page/project/settings/members.rb12
-rw-r--r--spec/controllers/groups/group_links_controller_spec.rb96
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb6
-rw-r--r--spec/controllers/health_check_controller_spec.rb8
-rw-r--r--spec/controllers/metrics_controller_spec.rb8
-rw-r--r--spec/features/admin/admin_groups_spec.rb10
-rw-r--r--spec/features/admin/admin_projects_spec.rb4
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb89
-rw-r--r--spec/features/groups/members/manage_members_spec.rb8
-rw-r--r--spec/features/groups/members/search_members_spec.rb2
-rw-r--r--spec/features/projects/members/list_spec.rb4
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb4
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb2
-rw-r--r--spec/finders/group_members_finder_spec.rb40
-rw-r--r--spec/fixtures/emails/envelope_to_header.eml32
-rw-r--r--spec/fixtures/emails/forwarded_new_issue.eml16
-rw-r--r--spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap134
-rw-r--r--spec/frontend/registry/settings/components/settings_form_spec.js33
-rw-r--r--spec/frontend/registry/settings/mock_data.js12
-rw-r--r--spec/frontend/registry/settings/store/mutations_spec.js10
-rw-r--r--spec/frontend/vuex_shared/bindings_spec.js80
-rw-r--r--spec/helpers/application_settings_helper_spec.rb14
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb19
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb53
-rw-r--r--spec/lib/gitlab/middleware/request_context_spec.rb78
-rw-r--r--spec/lib/gitlab/request_context_spec.rb67
-rw-r--r--spec/models/application_setting_spec.rb5
-rw-r--r--spec/models/group_group_link_spec.rb8
-rw-r--r--spec/models/group_spec.rb51
-rw-r--r--spec/requests/self_monitoring_project_spec.rb108
-rw-r--r--spec/support/shared_examples/email_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb2
-rw-r--r--spec/support/shared_examples/unique_ip_check_shared_examples.rb7
83 files changed, 1333 insertions, 373 deletions
diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js
index e77a7cf8e0a..0c732922e81 100644
--- a/app/assets/javascripts/pages/groups/group_members/index/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index/index.js
@@ -3,9 +3,12 @@
import Members from 'ee_else_ce/members';
import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
+import groupsSelect from '~/groups_select';
document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate();
+ memberExpirationDate('.js-access-expiration-date-groups');
new Members();
+ groupsSelect();
new UsersSelect();
});
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 402763e2e21..55a6a1ace55 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,6 +1,6 @@
<script>
-import { mapActions } from 'vuex';
-import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputed } from '~/vuex_shared/bindings';
@@ -12,19 +12,25 @@ export default {
GlFormSelect,
GlFormTextarea,
GlButton,
+ GlCard,
},
labelsConfig: {
cols: 3,
align: 'right',
},
computed: {
- ...mapComputed('settings', 'updateSettings', [
- 'enabled',
- 'cadence',
- 'older_than',
- 'keep_n',
- 'name_regex',
- ]),
+ ...mapState(['formOptions']),
+ ...mapComputed(
+ [
+ 'enabled',
+ { key: 'cadence', getter: 'getCadence' },
+ { key: 'older_than', getter: 'getOlderThan' },
+ { key: 'keep_n', getter: 'getKeepN' },
+ 'name_regex',
+ ],
+ 'updateSettings',
+ 'settings',
+ ),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
@@ -66,12 +72,12 @@ export default {
</script>
<template>
- <div class="card">
- <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
- <div class="card-header">
+ <form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
+ <gl-card>
+ <template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }}
- </div>
- <div class="card-body">
+ </template>
+ <template>
<gl-form-group
id="expiration-policy-toggle-group"
:label-cols="$options.labelsConfig.cols"
@@ -92,9 +98,10 @@ export default {
label-for="expiration-policy-interval"
:label="s__('ContainerRegistry|Expiration interval:')"
>
- <gl-form-select id="expiration-policy-interval" v-model="older_than">
- <option value="1">{{ __('Option 1') }}</option>
- <option value="2">{{ __('Option 2') }}</option>
+ <gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled">
+ <option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
+ {{ option.label }}
+ </option>
</gl-form-select>
</gl-form-group>
@@ -105,9 +112,10 @@ export default {
label-for="expiration-policy-schedule"
:label="s__('ContainerRegistry|Expiration schedule:')"
>
- <gl-form-select id="expiration-policy-schedule" v-model="cadence">
- <option value="1">{{ __('Option 1') }}</option>
- <option value="2">{{ __('Option 2') }}</option>
+ <gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled">
+ <option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
+ {{ option.label }}
+ </option>
</gl-form-select>
</gl-form-group>
@@ -118,9 +126,10 @@ export default {
label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Expiration latest:')"
>
- <gl-form-select id="expiration-policy-latest" v-model="keep_n">
- <option value="1">{{ __('Option 1') }}</option>
- <option value="2">{{ __('Option 2') }}</option>
+ <gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
+ <option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
+ {{ option.label }}
+ </option>
</gl-form-select>
</gl-form-group>
@@ -140,19 +149,30 @@ export default {
v-model="name_regex"
:placeholder="nameRegexPlaceholder"
:state="nameRegexState"
+ :disabled="!enabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
- </div>
- <div class="card-footer text-right">
- <gl-button ref="cancel-button" type="reset">{{ __('Cancel') }}</gl-button>
- <gl-button ref="save-button" type="submit" :disabled="formIsValid" variant="success">
- {{ __('Save Expiration Policy') }}
- </gl-button>
- </div>
- </form>
- </div>
+ </template>
+ <template #footer>
+ <div class="d-flex justify-content-end">
+ <gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{
+ __('Cancel')
+ }}</gl-button>
+ <gl-button
+ ref="save-button"
+ type="submit"
+ :disabled="formIsValid"
+ variant="success"
+ class="d-block"
+ >
+ {{ __('Save expiration policy') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-card>
+ </form>
</template>
diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js
new file mode 100644
index 00000000000..fc32a9f08e4
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/store/getters.js
@@ -0,0 +1,8 @@
+import { findDefaultOption } from '../utils';
+
+export const getCadence = state =>
+ state.settings.cadence || findDefaultOption(state.formOptions.cadence);
+export const getKeepN = state =>
+ state.settings.keep_n || findDefaultOption(state.formOptions.keepN);
+export const getOlderThan = state =>
+ state.settings.older_than || findDefaultOption(state.formOptions.olderThan);
diff --git a/app/assets/javascripts/registry/settings/store/index.js b/app/assets/javascripts/registry/settings/store/index.js
index 91a35aac149..c2500454d8e 100644
--- a/app/assets/javascripts/registry/settings/store/index.js
+++ b/app/assets/javascripts/registry/settings/store/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
+import * as getters from './getters';
import state from './state';
Vue.use(Vuex);
@@ -11,6 +12,7 @@ export const createStore = () =>
state,
actions,
mutations,
+ getters,
});
export default createStore();
diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js
index b8384fd4a45..25a67cc6973 100644
--- a/app/assets/javascripts/registry/settings/store/mutations.js
+++ b/app/assets/javascripts/registry/settings/store/mutations.js
@@ -3,6 +3,11 @@ import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
state.projectId = initialState.projectId;
+ state.formOptions = {
+ cadence: JSON.parse(initialState.cadenceOptions),
+ keepN: JSON.parse(initialState.keepNOptions),
+ olderThan: JSON.parse(initialState.olderThanOptions),
+ };
},
[types.UPDATE_SETTINGS](state, settings) {
state.settings = { ...state.settings, ...settings };
diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js
index c3a26083c9f..50c882e1839 100644
--- a/app/assets/javascripts/registry/settings/store/state.js
+++ b/app/assets/javascripts/registry/settings/store/state.js
@@ -23,4 +23,8 @@ export default () => ({
* Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel'
*/
original: {},
+ /*
+ * Contains the options used to populate the form selects
+ */
+ formOptions: {},
});
diff --git a/app/assets/javascripts/registry/settings/utils.js b/app/assets/javascripts/registry/settings/utils.js
new file mode 100644
index 00000000000..75af401e96d
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/utils.js
@@ -0,0 +1,6 @@
+export const findDefaultOption = options => {
+ const item = options.find(o => o.default);
+ return item ? item.key : null;
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index 51035ebc8a8..817a90f8149 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -1,9 +1,29 @@
-export const mapComputed = (root, updateFn, list) => {
+/**
+ * Returns computed properties two way bound to vuex
+ *
+ * @param {(string[]|Object[])} list - list of string matching state keys or list objects
+ * @param {string} list[].key - the key matching the key present in the vuex state
+ * @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
+ * @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
+ * @param {string} defaultUpdateFn - the default function to dispatch
+ * @param {string} root - the key of the state where to search fo they keys described in list
+ * @returns {Object} a dictionary with all the computed properties generated
+ */
+export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {};
- list.forEach(key => {
+ list.forEach(item => {
+ const [getter, key, updateFn] =
+ typeof item === 'string'
+ ? [false, item, defaultUpdateFn]
+ : [item.getter, item.key, item.updateFn || defaultUpdateFn];
result[key] = {
get() {
- return this.$store.state[root][key];
+ if (getter) {
+ return this.$store.getters[getter];
+ } else if (root) {
+ return this.$store.state[root][key];
+ }
+ return this.$store.state[key];
},
set(value) {
this.$store.dispatch(updateFn, { [key]: value });
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index ae92a2fbd7b..54bca80194f 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -3,7 +3,7 @@
border-bottom: 1px solid $border-color;
}
-.users-project-form {
+.invite-users-form {
.btn-success {
margin-right: 10px;
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 58f088c6335..3047ee02680 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -5,6 +5,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data]
+ before_action :validate_self_monitoring_feature_flag_enabled, only: [
+ :create_self_monitoring_project,
+ :status_create_self_monitoring_project,
+ :delete_self_monitoring_project,
+ :status_delete_self_monitoring_project
+ ]
before_action do
push_frontend_feature_flag(:self_monitoring_project)
@@ -74,8 +80,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def create_self_monitoring_project
- return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
-
job_id = SelfMonitoringProjectCreateWorker.perform_async
render status: :accepted, json: {
@@ -85,8 +89,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def status_create_self_monitoring_project
- return self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
-
job_id = params[:job_id].to_s
unless job_id.length <= PARAM_JOB_ID_MAX_SIZE
@@ -97,23 +99,66 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
if Gitlab::CurrentSettings.instance_administration_project_id.present?
- render status: :ok, json: self_monitoring_data
+ return render status: :ok, json: self_monitoring_data
elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000)
- render status: :accepted, json: { message: _('Job is in progress') }
+ return render status: :accepted, json: {
+ message: _('Job to create self-monitoring project is in progress')
+ }
+ end
+
+ render status: :bad_request, json: {
+ message: _('Self-monitoring project does not exist. Please check logs ' \
+ 'for any error messages')
+ }
+ end
+
+ def delete_self_monitoring_project
+ job_id = SelfMonitoringProjectDeleteWorker.perform_async
+
+ render status: :accepted, json: {
+ job_id: job_id,
+ monitor_status: status_delete_self_monitoring_project_admin_application_settings_path
+ }
+ end
+
+ def status_delete_self_monitoring_project
+ job_id = params[:job_id].to_s
+
+ unless job_id.length <= PARAM_JOB_ID_MAX_SIZE
+ return render status: :bad_request, json: {
+ message: _('Parameter "job_id" cannot exceed length of %{job_id_max_size}' %
+ { job_id_max_size: PARAM_JOB_ID_MAX_SIZE })
+ }
+ end
- else
- render status: :bad_request, json: {
- message: _('Self-monitoring project does not exist. Please check logs ' \
- 'for any error messages')
+ if Gitlab::CurrentSettings.instance_administration_project_id.nil?
+ return render status: :ok, json: {
+ message: _('Self-monitoring project has been successfully deleted')
+ }
+
+ elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
+ ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
+ return render status: :accepted, json: {
+ message: _('Job to delete self-monitoring project is in progress')
}
end
+
+ render status: :bad_request, json: {
+ message: _('Self-monitoring project was not deleted. Please check logs ' \
+ 'for any error messages')
+ }
end
private
+ def validate_self_monitoring_feature_flag_enabled
+ self_monitoring_project_not_implemented unless Feature.enabled?(:self_monitoring_project)
+ end
+
def self_monitoring_data
{
project_id: Gitlab::CurrentSettings.instance_administration_project_id,
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index 2e9905997db..c92b1cecaaa 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -18,7 +18,7 @@ module RequiresWhitelistedMonitoringClient
# debugging purposes
return true if Rails.env.development? && request.local?
- ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.client_ip) }
+ ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.instance.client_ip) }
end
def ip_whitelist
diff --git a/app/controllers/groups/group_links_controller.rb b/app/controllers/groups/group_links_controller.rb
index 7965311c5f1..6796a862c00 100644
--- a/app/controllers/groups/group_links_controller.rb
+++ b/app/controllers/groups/group_links_controller.rb
@@ -3,6 +3,7 @@
class Groups::GroupLinksController < Groups::ApplicationController
before_action :check_feature_flag!
before_action :authorize_admin_group!
+ before_action :group_link, only: [:update, :destroy]
def create
shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present?
@@ -22,12 +23,35 @@ class Groups::GroupLinksController < Groups::ApplicationController
redirect_to group_group_members_path(group)
end
+ def update
+ @group_link.update(group_link_params)
+ end
+
+ def destroy
+ Groups::GroupLinks::DestroyService.new(nil, nil).execute(@group_link)
+
+ respond_to do |format|
+ format.html do
+ redirect_to group_group_members_path(group), status: :found
+ end
+ format.js { head :ok }
+ end
+ end
+
private
+ def group_link
+ @group_link ||= group.shared_with_group_links.find(params[:id])
+ end
+
def group_link_create_params
params.permit(:shared_group_access, :expires_at)
end
+ def group_link_params
+ params.require(:group_link).permit(:group_access, :expires_at)
+ end
+
def check_feature_flag!
render_404 unless Feature.enabled?(:share_group_with_group)
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index dcdf9aced1a..d1eed85fde6 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -20,28 +20,17 @@ class Groups::GroupMembersController < Groups::ApplicationController
:override
def index
- can_manage_members = can?(current_user, :admin_group_member, @group)
-
@sort = params[:sort].presence || sort_value_name
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = find_members
if can_manage_members
- @invited_members = @members.invite
- @invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
- @invited_members = present_members(@invited_members.page(params[:invited_members_page]).per(MEMBER_PER_PAGE_LIMIT))
+ @skip_groups = @group.related_group_ids
+ @invited_members = present_invited_members(@members)
end
@members = @members.non_invite
- @members = @members.search(params[:search]) if params[:search].present?
- @members = @members.sort_by_attribute(@sort)
-
- if can_manage_members && params[:two_factor].present?
- @members = @members.filter_by_2fa(params[:two_factor])
- end
-
- @members = @members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
- @members = present_members(@members)
+ @members = present_group_members(@members)
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user))
@@ -54,8 +43,30 @@ class Groups::GroupMembersController < Groups::ApplicationController
private
+ def present_invited_members(members)
+ invited_members = members.invite
+
+ if params[:search_invited].present?
+ invited_members = invited_members.search_invite_email(params[:search_invited])
+ end
+
+ present_members(invited_members
+ .page(params[:invited_members_page])
+ .per(MEMBER_PER_PAGE_LIMIT))
+ end
+
def find_members
- GroupMembersFinder.new(@group).execute(include_relations: requested_relations)
+ filter_params = params.slice(:two_factor, :search).merge(sort: @sort)
+ GroupMembersFinder.new(@group, current_user).execute(include_relations: requested_relations, params: filter_params)
+ end
+
+ def can_manage_members
+ can?(current_user, :admin_group_member, @group)
+ end
+
+ def present_group_members(original_members)
+ members = original_members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
+ present_members(members)
end
end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index d8739c350e4..05151b4f1f3 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -1,38 +1,64 @@
# frozen_string_literal: true
class GroupMembersFinder < UnionFinder
- def initialize(group)
+ # Params can be any of the following:
+ # two_factor: string. 'enabled' or 'disabled' are returning different set of data, other values are not effective.
+ # sort: string
+ # search: string
+
+ def initialize(group, user = nil)
@group = group
+ @user = user
end
# rubocop: disable CodeReuse/ActiveRecord
- def execute(include_relations: [:inherited, :direct])
- group_members = @group.members
+ def execute(include_relations: [:inherited, :direct], params: {})
+ group_members = group.members
relations = []
return group_members if include_relations == [:direct]
relations << group_members if include_relations.include?(:direct)
- if include_relations.include?(:inherited) && @group.parent
+ if include_relations.include?(:inherited) && group.parent
parents_members = GroupMember.non_request
- .where(source_id: @group.ancestors.select(:id))
- .where.not(user_id: @group.users.select(:id))
+ .where(source_id: group.ancestors.select(:id))
+ .where.not(user_id: group.users.select(:id))
relations << parents_members
end
if include_relations.include?(:descendants)
descendant_members = GroupMember.non_request
- .where(source_id: @group.descendants.select(:id))
- .where.not(user_id: @group.users.select(:id))
+ .where(source_id: group.descendants.select(:id))
+ .where.not(user_id: group.users.select(:id))
relations << descendant_members
end
- find_union(relations, GroupMember)
+ members = find_union(relations, GroupMember)
+ filter_members(members, params)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ attr_reader :user, :group
+
+ def filter_members(members, params)
+ members = members.search(params[:search]) if params[:search].present?
+ members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
+
+ if can_manage_members && params[:two_factor].present?
+ members = members.filter_by_2fa(params[:two_factor])
+ end
+
+ members
+ end
+
+ def can_manage_members
+ Ability.allowed?(user, :admin_group_member, group)
+ end
end
GroupMembersFinder.prepend_if_ee('EE::GroupMembersFinder')
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index fe1f543ea1a..c439a6cfc2f 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -42,7 +42,7 @@ module Mutations
if project_path.present?
project = find_project!(project_path: project_path)
elsif !can_create_personal_snippet?
- raise_resource_not_avaiable_error!
+ raise_resource_not_available_error!
end
snippet = CreateSnippetService.new(project,
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ffea7d53b14..7115fd834fd 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -344,6 +344,12 @@ module ApplicationSettingsHelper
'status_create_self_monitoring_project_path' =>
status_create_self_monitoring_project_admin_application_settings_path,
+ 'delete_self_monitoring_project_path' =>
+ delete_self_monitoring_project_admin_application_settings_path,
+
+ 'status_delete_self_monitoring_project_path' =>
+ status_delete_self_monitoring_project_admin_application_settings_path,
+
'self_monitoring_project_exists' =>
Gitlab::CurrentSettings.instance_administration_project.present?,
diff --git a/app/helpers/groups/group_members_helper.rb b/app/helpers/groups/group_members_helper.rb
index a8f6c974bbd..1952325c504 100644
--- a/app/helpers/groups/group_members_helper.rb
+++ b/app/helpers/groups/group_members_helper.rb
@@ -4,6 +4,10 @@ module Groups::GroupMembersHelper
def group_member_select_options
{ multiple: true, class: 'input-clamp qa-member-select-field ', scope: :all, email_user: true }
end
+
+ def render_invite_member_for_group(group, default_access_level)
+ render 'shared/members/invite_member', submit_url: group_group_members_path(group), access_levels: GroupMember.access_level_roles, default_access_level: default_access_level
+ end
end
Groups::GroupMembersHelper.prepend_if_ee('EE::Groups::GroupMembersHelper')
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 90c54123597..4d0f9e530fb 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -85,7 +85,8 @@ module SelectsHelper
first_user: opts[:first_user] && current_user ? current_user.username : false,
current_user: opts[:current_user] || false,
author_id: opts[:author_id] || '',
- skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil
+ skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
+ qa_selector: opts[:qa_selector] || ''
}
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3858adfc71f..a0a600a340e 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -169,7 +169,11 @@ class ApplicationSetting < ApplicationRecord
validates :gitaly_timeout_default,
presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ numericality: {
+ only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: Settings.gitlab.max_request_duration_seconds
+ }
validates :gitaly_timeout_medium,
presence: true,
diff --git a/app/models/group.rb b/app/models/group.rb
index 8289d4f099c..186253619fe 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -420,6 +420,12 @@ class Group < Namespace
GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
end
+ def related_group_ids
+ [id,
+ *ancestors.pluck(:id),
+ *shared_with_group_links.pluck(:shared_with_group_id)]
+ end
+
def hashed_storage?(_feature)
false
end
diff --git a/app/models/group_group_link.rb b/app/models/group_group_link.rb
index 4b279b7af5b..5a0d9b08cb0 100644
--- a/app/models/group_group_link.rb
+++ b/app/models/group_group_link.rb
@@ -20,4 +20,8 @@ class GroupGroupLink < ApplicationRecord
def self.default_access
Gitlab::Access::DEVELOPER
end
+
+ def human_access
+ Gitlab::Access.human_access(self.group_access)
+ end
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index 0d3a2d4e398..b70c07a8386 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -21,6 +21,8 @@ class ProjectGroupLink < ApplicationRecord
after_commit :refresh_group_members_authorized_projects
+ alias_method :shared_with_group, :group
+
def self.access_options
Gitlab::Access.options
end
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
index 1da02de0461..fac2de8811f 100644
--- a/app/views/admin/application_settings/_gitaly.html.haml
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -8,6 +8,9 @@
.form-text.text-muted
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
for git fetch/push operations or Sidekiq jobs.
+ This timeout should be less than the worker timeout. If a Gitaly call timeout would exceed the
+ worker timeout, the remaining time from the worker timeout would be used to avoid having to terminate
+ the worker.
.form-group
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-bold'
= f.number_field :gitaly_timeout_fast, class: 'form-control'
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
deleted file mode 100644
index 93dd8f48a60..00000000000
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-= form_for @group_member, url: group_group_members_path(@group), html: { class: 'users-project-form users-group-form' } do |f|
- .row
- .col-md-4.col-lg-6
- = users_select_tag(:user_ids, group_member_select_options)
- .form-text.text-muted.append-bottom-10
- Search for members by name, username, or email, or invite new ones using their email address.
-
- .col-md-3.col-lg-2
- = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select"
- .form-text.text-muted.append-bottom-10
- = link_to "Read more", help_page_path("user/permissions")
- about role permissions
-
- .col-md-3.col-lg-2
- .clearable-input
- = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
- %i.clear-icon.js-clear-input
- .form-text.text-muted.append-bottom-10
- On this date, the member(s) will automatically lose access to this group and all of its projects.
-
- .col-md-2
- = f.submit 'Add to group', class: "btn btn-success btn-block", data: { qa_selector: 'add_to_group_button' }
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 882fcc79421..6eb8a8947cc 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,17 +1,28 @@
-- page_title _("Members")
+- page_title _("Group members")
- can_manage_members = can?(current_user, :admin_group_member, @group)
- show_invited_members = can_manage_members && @invited_members.exists?
- pending_active = params[:search_invited].present?
+- total_count = @members.count + @group.shared_with_group_links.count
.project-members-page.prepend-top-default
%h4
- = _("Members")
+ = _("Group members")
%hr
- if can_manage_members
- .project-members-new.append-bottom-default
- %p.clearfix
- = _("Add new member to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
- = render "new_group_member"
+ - if Feature.enabled?(:share_group_with_group)
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
+ .tab-content.gitlab-tab-content
+ .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
+ = render_invite_member_for_group(@group, @group_member.access_level)
+ - if Feature.enabled?(:share_group_with_group)
+ .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
+ = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
+ - else
+ = render_invite_member_for_group(@group, @group_member.access_level)
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
@@ -19,10 +30,10 @@
%ul.nav-links.mobile-separator.nav.nav-tabs.clearfix
%li.nav-item
- = link_to "#existing_members", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
+ = link_to "#existing_shares", class: ["nav-link", ("active" unless pending_active)] , 'data-toggle' => 'tab' do
%span
- = _("Existing")
- %span.badge.badge-pill= @members.total_count
+ = _("Existing shares")
+ %span.badge.badge-pill= total_count
- if show_invited_members
%li.nav-item
= link_to "#invited_members", class: ["nav-link", ("active" if pending_active)], 'data-toggle' => 'tab' do
@@ -31,7 +42,16 @@
%span.badge.badge-pill= @invited_members.total_count
.tab-content
- #existing_members.tab-pane{ :class => ("active" unless pending_active) }
+ #existing_shares.tab-pane{ :class => ("active" unless pending_active) }
+ - if @group.shared_with_group_links.any?
+ .card.card-without-border
+ .d-flex.flex-column.flex-md-row.row-content-block.second-block
+ %span.flex-grow-1.align-self-md-center.col-form-label
+ = _("Groups with access to %{strong_start}%{group_name}%{strong_end}").html_safe % { group_name: @group.name, strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ %ul.content-list.members-list{ data: { qa_selector: "groups_list" } }
+ - can_admin_member = can?(current_user, :admin_group_member, @group)
+ - @group.shared_with_group_links.each do |group_link|
+ = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: group_group_link_path(@group, group_link)
.card.card-without-border
.d-flex.flex-column.flex-md-row.row-content-block.second-block
%span.flex-grow-1.align-self-md-center.col-form-label
@@ -46,7 +66,7 @@
= label_tag '2fa', '2FA', class: 'col-form-label label-bold pr-md-2'
= render 'shared/members/filter_2fa_dropdown'
= render 'shared/members/sort_dropdown'
- %ul.content-list.members-list
+ %ul.content-list.members-list{ data: { qa_selector: "members_list" } }
= render partial: 'shared/members/member', collection: @members, as: :member
= paginate @members, theme: 'gitlab'
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 00321014f91..353c36d0fed 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -3,4 +3,6 @@
= _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize(@project.name, tags: []) }
%span.badge.badge-pill= group_links.size
%ul.content-list.members-list
- = render partial: 'shared/members/group', collection: group_links, as: :group_link
+ - can_admin_member = can?(current_user, :admin_project_member, @project)
+ - @group_links.each do |group_link|
+ = render 'shared/members/group', group_link: group_link, can_admin_member: can_admin_member, group_link_path: project_group_link_path(@project, group_link)
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 5310c1fad01..5d8005b2e2a 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -13,5 +13,5 @@
%button.user-search-btn{ type: "submit", "aria-label" => _("Submit search") }
= icon("search")
= render 'shared/members/sort_dropdown'
- %ul.content-list.members-list.qa-members-list
+ %ul.content-list.members-list{ data: { qa_selector: 'members_list' } }
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 24fe583a9b5..c24a9061146 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -23,13 +23,13 @@
.tab-content.gitlab-tab-content
.tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render 'projects/project_members/new_project_member', tab_title: _('Invite member')
+ = render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
.tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
- = render 'projects/project_members/new_project_group', tab_title: _('Invite group')
+ = render 'shared/members/invite_group', submit_url: project_group_links_path(@project), access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, group_link_field: 'link_group_id', group_access_field: 'link_group_access'
- elsif !membership_locked?
- .invite-member= render 'projects/project_members/new_project_member', tab_title: _('Invite member')
+ .invite-member= render 'shared/members/invite_member', submit_url: project_project_members_path(@project), access_levels: ProjectMember.access_level_roles, default_access_level: @project_member.access_level, can_import_members?: can_import_members?, import_path: import_project_project_members_path(@project)
- elsif @project.allowed_to_share_with_group?
- .invite-group= render 'projects/project_members/new_project_group', tab_title: _('Invite group')
+ .invite-group= render 'shared/members/invite_group', access_levels: ProjectGroupLink.access_options, default_access_level: ProjectGroupLink.default_access, submit_url: project_group_links_path(@project), group_link_field: 'link_group_id', group_access_field: 'link_group_access'
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.clearfix
diff --git a/app/views/projects/registry/settings/_index.haml b/app/views/projects/registry/settings/_index.haml
index d023a083445..0e0341a9923 100644
--- a/app/views/projects/registry/settings/_index.haml
+++ b/app/views/projects/registry/settings/_index.haml
@@ -1 +1,4 @@
-#js-registry-settings{ data: { project_id: @project.id, } }
+#js-registry-settings{ data: { project_id: @project.id,
+ cadence_options: cadence_options.to_json,
+ keep_n_options: keep_n_options.to_json,
+ older_than_options: older_than_options.to_json} }
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 18368ecc9ff..4aeeac87f3c 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -1,6 +1,7 @@
- group_link = local_assigns[:group_link]
-- group = group_link.group
-- can_admin_member = can?(current_user, :admin_project_member, @project)
+- group = group_link.shared_with_group
+- can_admin_member = local_assigns[:can_admin_member]
+- group_link_path = local_assigns[:group_link_path]
- dom_id = "group_member_#{group_link.id}"
-# Note this is just for groups. For individual members please see shared/members/_member
@@ -17,7 +18,7 @@
%span{ class: ('text-warning' if group_link.expires_soon?) }
= _("Expires in %{expires_at}").html_safe % { expires_at: distance_of_time_in_words_to_now(group_link.expires_at) }
.controls.member-controls.align-items-center
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
+ = form_tag group_link_path, method: :put, remote: true, class: 'js-edit-member-form form-group d-sm-flex' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.mr-sm-2.d-sm-inline-block
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
@@ -39,7 +40,7 @@
= text_field_tag 'group_link[expires_at]', group_link.expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: _('Expiration date'), id: "member_expires_at_#{group.id}", disabled: !can_admin_member
%i.clear-icon.js-clear-input
- if can_admin_member
- = link_to project_group_link_path(@project, group_link),
+ = link_to group_link_path,
method: :delete,
data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, qa_selector: 'delete_group_access_link' },
class: 'btn btn-remove m-0 ml-sm-2 align-self-center' do
diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/shared/members/_invite_group.html.haml
index d413048ca10..27c930bcbb5 100644
--- a/app/views/projects/project_members/_new_project_group.html.haml
+++ b/app/views/shared/members/_invite_group.html.haml
@@ -1,13 +1,18 @@
+- access_levels = local_assigns[:access_levels]
+- default_access_level = local_assigns[:default_access_level]
+- submit_url = local_assigns[:submit_url]
+- group_link_field = local_assigns[:group_link_field]
+- group_access_field = local_assigns[:group_access_field]
.row
.col-sm-12
- = form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do
+ = form_tag submit_url, class: 'invite-group-form js-requires-input', method: :post do
.form-group
- = label_tag :link_group_id, _("Select a group to invite"), class: "label-bold"
- = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp qa-group-select-field", required: true)
+ = label_tag group_link_field, _("Select a group to invite"), class: "label-bold"
+ = groups_select_tag(group_link_field, data: { skip_groups: @skip_groups }, class: 'input-clamp qa-group-select-field', required: true)
.form-group
- = label_tag :link_group_access, _("Max access level"), class: "label-bold"
+ = label_tag group_access_field, _("Max access level"), class: "label-bold"
.select-wrapper
- = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
+ = select_tag group_access_field, options_for_select(access_levels, default_access_level), data: { qa_selector: 'group_access_field' }, class: "form-control select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions')
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/shared/members/_invite_member.html.haml
index 149b0d6cddd..d3a1c85e285 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/shared/members/_invite_member.html.haml
@@ -1,13 +1,18 @@
+- access_levels = local_assigns[:access_levels]
+- default_access_level = local_assigns[:default_access_level]
+- submit_url = local_assigns[:submit_url]
+- can_import_members = local_assigns[:can_import_members?]
+- import_path = local_assigns[:import_path]
.row
.col-sm-12
- = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f|
+ = form_tag submit_url, class: 'invite-users-form', method: :post do
.form-group
= label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold"
- = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
+ = users_select_tag(:user_ids, multiple: true, class: 'input-clamp qa-member-select-field', scope: :all, email_user: true, placeholder: 'Search for members to update or invite')
.form-group
= label_tag :access_level, _("Choose a role permission"), class: "label-bold"
.select-wrapper
- = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control"
+ = select_tag :access_level, options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control"
= icon('chevron-down')
.form-text.text-muted.append-bottom-10
- permissions_docs_path = help_page_path('user/permissions')
@@ -18,6 +23,6 @@
= label_tag :expires_at, _('Access expiration date'), class: 'label-bold'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
- = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button"
- - if can_import_members?
- = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
+ = submit_tag _("Invite"), class: "btn btn-success", data: { qa_selector: 'invite_member_button' }
+ - if can_import_members
+ = link_to _("Import"), import_path, class: "btn btn-default", title: _("Import members from another project")
diff --git a/changelogs/unreleased/37359-cablett-support-envelope-to.yml b/changelogs/unreleased/37359-cablett-support-envelope-to.yml
new file mode 100644
index 00000000000..510ebe1c32d
--- /dev/null
+++ b/changelogs/unreleased/37359-cablett-support-envelope-to.yml
@@ -0,0 +1,5 @@
+---
+title: Accept `Envelope-To` as possible location for Service Desk key
+merge_request: 22354
+author: Max Winterstein
+type: added
diff --git a/changelogs/unreleased/bvl-gitaly-dynamic-deadline.yml b/changelogs/unreleased/bvl-gitaly-dynamic-deadline.yml
new file mode 100644
index 00000000000..e44127ccf60
--- /dev/null
+++ b/changelogs/unreleased/bvl-gitaly-dynamic-deadline.yml
@@ -0,0 +1,5 @@
+---
+title: Don't let Gitaly calls exceed a request time of 55 seconds
+merge_request: 21492
+author:
+type: performance
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 6613afdfed2..dc66b2adb70 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -33,6 +33,9 @@ production: &base
host: localhost
port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
+ # The maximum time unicorn/puma can spend on the request. This needs to be smaller than the worker timeout.
+ # Default is 95% of the worker timeout
+ max_request_duration: 57
# Uncomment this line below if your ssh host is different from HTTP/HTTPS one
# (you'd obviously need to replace ssh.host_example.com with your own host).
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ce45dfe06f8..d7d4bd9d3a1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -209,6 +209,7 @@ Settings.gitlab['content_security_policy'] ||= Gitlab::ContentSecurityPolicy::Co
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
+Settings.gitlab['max_request_duration_seconds'] ||= 57
Gitlab.ee do
Settings.gitlab['mirror_max_delay'] ||= 300
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 0acbe6a9258..dc064a76033 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -37,7 +37,7 @@ unless Gitlab::Runtime.sidekiq?
payload[:response] = event.payload[:response] if event.payload[:response]
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
- if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.start_thread_cpu_time)
+ if cpu_s = Gitlab::Metrics::System.thread_cpu_duration(::Gitlab::RequestContext.instance.start_thread_cpu_time)
payload[:cpu_s] = cpu_s
end
diff --git a/config/initializers/request_context.rb b/config/initializers/request_context.rb
index 0b485fc1adc..f79f1f32d70 100644
--- a/config/initializers/request_context.rb
+++ b/config/initializers/request_context.rb
@@ -1,3 +1,3 @@
Rails.application.configure do |config|
- config.middleware.insert_after RequestStore::Middleware, Gitlab::RequestContext
+ config.middleware.insert_after RequestStore::Middleware, Gitlab::Middleware::RequestContext
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index c58b74ecfec..45cca1d3191 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -119,6 +119,8 @@ namespace :admin do
post :create_self_monitoring_project
get :status_create_self_monitoring_project
+ delete :delete_self_monitoring_project
+ get :status_delete_self_monitoring_project
end
resources :labels
diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md
index 26c811ca54d..aef15e3f388 100644
--- a/doc/administration/raketasks/uploads/migrate.md
+++ b/doc/administration/raketasks/uploads/migrate.md
@@ -1,4 +1,4 @@
-# Uploads Migrate Rake Task
+# Uploads Migrate Rake Tasks
## Migrate to Object Storage
@@ -110,7 +110,15 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeReque
To migrate all uploads created by legacy uploaders, run:
-```shell
+**Omnibus Installation**
+
+```bash
+gitlab-rake gitlab:uploads:legacy:migrate
+```
+
+**Source Installation**
+
+```bash
bundle exec rake gitlab:uploads:legacy:migrate
```
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index ad86555fc17..a9ba44f82c6 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -14,5 +14,5 @@ comments: false
- [Webhooks](web_hooks.md)
- [Import](import.md) of Git repositories in bulk
- [Rebuild authorized_keys file](../administration/raketasks/maintenance.md#rebuild-authorized_keys-file) task for administrators
-- [Migrate Uploads](../administration/raketasks/uploads/migrate.md)
-- [Sanitize Uploads](../administration/raketasks/uploads/sanitize.md)
+- [Uploads Migrate](../administration/raketasks/uploads/migrate.md)
+- [Uploads Sanitize](../administration/raketasks/uploads/sanitize.md)
diff --git a/doc/update/README.md b/doc/update/README.md
index e9db0a73ac5..f23716f3df8 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -134,8 +134,8 @@ Please follow the [Upgrade Recommendations](../policy/maintenance.md#upgrade-rec
to identify the ideal upgrade path.
Before upgrading to a new major version, you should ensure that any background
-migration jobs from previous releases have been completed. The number of remaining
-migrations jobs can be found by running the following command:
+migration jobs from previous releases have been completed. To see the current size
+of the `background_migration` queue, [check for background migrations before upgrading](#checking-for-background-migrations-before-upgrading).
## Upgrading between editions
diff --git a/lib/gitlab/auth/unique_ips_limiter.rb b/lib/gitlab/auth/unique_ips_limiter.rb
index 97e78ecf094..74f7fdfc180 100644
--- a/lib/gitlab/auth/unique_ips_limiter.rb
+++ b/lib/gitlab/auth/unique_ips_limiter.rb
@@ -8,7 +8,7 @@ module Gitlab
class << self
def limit_user_id!(user_id)
if config.unique_ips_limit_enabled
- ip = RequestContext.client_ip
+ ip = RequestContext.instance.client_ip
unique_ips = update_and_return_ips_count(user_id, ip)
raise TooManyIps.new(user_id, ip, unique_ips) if unique_ips > config.unique_ips_limit_per_user
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 847260b2e0f..f028102da9b 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -66,7 +66,8 @@ module Gitlab
def key_from_additional_headers(mail)
find_key_from_references(mail) ||
- find_key_from_delivered_to_header(mail)
+ find_key_from_delivered_to_header(mail) ||
+ find_key_from_envelope_to_header(mail)
end
def ensure_references_array(references)
@@ -96,6 +97,13 @@ module Gitlab
end
end
+ def find_key_from_envelope_to_header(mail)
+ Array(mail[:envelope_to]).find do |header|
+ key = Gitlab::IncomingEmail.key_from_address(header.value)
+ break key if key
+ end
+ end
+
def ignore_auto_reply!(mail)
if auto_submitted?(mail) || auto_replied?(mail)
raise AutoGeneratedEmailError
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 9636e75aba1..262a1ef653f 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -160,6 +160,7 @@ module Gitlab
def self.execute(storage, service, rpc, request, remote_storage:, timeout:)
enforce_gitaly_request_limits(:call)
+ Gitlab::RequestContext.instance.ensure_deadline_not_exceeded!
kwargs = request_kwargs(storage, timeout: timeout.to_f, remote_storage: remote_storage)
kwargs = yield(kwargs) if block_given?
@@ -234,12 +235,28 @@ module Gitlab
metadata['gitaly-session-id'] = session_id
metadata.merge!(Feature::Gitaly.server_feature_flags)
- result = { metadata: metadata }
+ deadline_info = request_deadline(timeout)
+ metadata.merge!(deadline_info.slice(:deadline_type))
- result[:deadline] = real_time + timeout if timeout > 0
- result
+ { metadata: metadata, deadline: deadline_info[:deadline] }
end
+ def self.request_deadline(timeout)
+ # timeout being 0 means the request is allowed to run indefinitely.
+ # We can't allow that inside a request, but this won't count towards Gitaly
+ # error budgets
+ regular_deadline = real_time.to_i + timeout if timeout > 0
+
+ return { deadline: regular_deadline } if Sidekiq.server?
+ return { deadline: regular_deadline } unless Gitlab::RequestContext.instance.request_deadline
+
+ limited_deadline = [regular_deadline, Gitlab::RequestContext.instance.request_deadline].compact.min
+ limited = limited_deadline < regular_deadline
+
+ { deadline: limited_deadline, deadline_type: limited ? "limited" : "regular" }
+ end
+ private_class_method :request_deadline
+
def self.session_id
Gitlab::SafeRequestStore[:gitaly_session_id] ||= SecureRandom.uuid
end
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index 26e8c53032f..94871498cf8 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -40,7 +40,7 @@ module Gitlab
def authorize!(object)
unless authorized_resource?(object)
- raise_resource_not_avaiable_error!
+ raise_resource_not_available_error!
end
end
@@ -63,7 +63,7 @@ module Gitlab
end
end
- def raise_resource_not_avaiable_error!
+ def raise_resource_not_available_error!
raise Gitlab::Graphql::Errors::ResourceNotAvailable, RESOURCE_ACCESS_ERROR
end
end
diff --git a/lib/gitlab/middleware/request_context.rb b/lib/gitlab/middleware/request_context.rb
new file mode 100644
index 00000000000..953423b371c
--- /dev/null
+++ b/lib/gitlab/middleware/request_context.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Middleware
+ class RequestContext
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ # We should be using ActionDispatch::Request instead of
+ # Rack::Request to be consistent with Rails, but due to a Rails
+ # bug described in
+ # https://gitlab.com/gitlab-org/gitlab-foss/issues/58573#note_149799010
+ # hosts behind a load balancer will only see 127.0.0.1 for the
+ # load balancer's IP.
+ req = Rack::Request.new(env)
+
+ Gitlab::RequestContext.instance.client_ip = req.ip
+ Gitlab::RequestContext.instance.start_thread_cpu_time = Gitlab::Metrics::System.thread_cpu_time
+ Gitlab::RequestContext.instance.request_start_time = Gitlab::Metrics::System.real_time
+
+ @app.call(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index 13187836e02..49c2c0c982c 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -2,34 +2,37 @@
module Gitlab
class RequestContext
- class << self
- def client_ip
- Gitlab::SafeRequestStore[:client_ip]
- end
+ include Singleton
+
+ RequestDeadlineExceeded = Class.new(StandardError)
+
+ attr_accessor :client_ip, :start_thread_cpu_time, :request_start_time
- def start_thread_cpu_time
- Gitlab::SafeRequestStore[:start_thread_cpu_time]
+ class << self
+ def instance
+ Gitlab::SafeRequestStore[:request_context] ||= new
end
end
- def initialize(app)
- @app = app
+ def request_deadline
+ return unless request_start_time
+ return unless Feature.enabled?(:request_deadline)
+
+ @request_deadline ||= request_start_time + max_request_duration_seconds
end
- def call(env)
- # We should be using ActionDispatch::Request instead of
- # Rack::Request to be consistent with Rails, but due to a Rails
- # bug described in
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/58573#note_149799010
- # hosts behind a load balancer will only see 127.0.0.1 for the
- # load balancer's IP.
- req = Rack::Request.new(env)
+ def ensure_deadline_not_exceeded!
+ return unless request_deadline
+ return if Gitlab::Metrics::System.real_time < request_deadline
- Gitlab::SafeRequestStore[:client_ip] = req.ip
+ raise RequestDeadlineExceeded,
+ "Request takes longer than #{max_request_duration_seconds}"
+ end
- Gitlab::SafeRequestStore[:start_thread_cpu_time] = Gitlab::Metrics::System.thread_cpu_time
+ private
- @app.call(env)
+ def max_request_duration_seconds
+ Settings.gitlab.max_request_duration_seconds
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b8fa6e845cb..5b4db60d020 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1068,9 +1068,6 @@ msgstr ""
msgid "Add new directory"
msgstr ""
-msgid "Add new member to %{strong_start}%{group_name}%{strong_end}"
-msgstr ""
-
msgid "Add or subtract spent time"
msgstr ""
@@ -1095,9 +1092,6 @@ msgstr ""
msgid "Add to merge train when pipeline succeeds"
msgstr ""
-msgid "Add to project"
-msgstr ""
-
msgid "Add to review"
msgstr ""
@@ -7458,10 +7452,10 @@ msgstr ""
msgid "Excluding merge commits. Limited to 6,000 commits."
msgstr ""
-msgid "Existing"
+msgid "Existing members and groups"
msgstr ""
-msgid "Existing members and groups"
+msgid "Existing shares"
msgstr ""
msgid "Expand"
@@ -9122,6 +9116,9 @@ msgstr ""
msgid "Group maintainers can register group runners in the %{link}"
msgstr ""
+msgid "Group members"
+msgstr ""
+
msgid "Group name"
msgstr ""
@@ -9392,6 +9389,9 @@ msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
+msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}"
+msgstr ""
+
msgid "Groups with access to <strong>%{project_name}</strong>"
msgstr ""
@@ -10314,9 +10314,6 @@ msgstr ""
msgid "Job has wrong arguments format."
msgstr ""
-msgid "Job is in progress"
-msgstr ""
-
msgid "Job is missing the `model_type` argument."
msgstr ""
@@ -10326,6 +10323,12 @@ msgstr ""
msgid "Job logs and artifacts"
msgstr ""
+msgid "Job to create self-monitoring project is in progress"
+msgstr ""
+
+msgid "Job to delete self-monitoring project is in progress"
+msgstr ""
+
msgid "Job was retried"
msgstr ""
@@ -12639,12 +12642,6 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
-msgid "Option 1"
-msgstr ""
-
-msgid "Option 2"
-msgstr ""
-
msgid "Optional"
msgstr ""
@@ -15784,9 +15781,6 @@ msgstr ""
msgid "Save Changes"
msgstr ""
-msgid "Save Expiration Policy"
-msgstr ""
-
msgid "Save anyway"
msgstr ""
@@ -15802,6 +15796,9 @@ msgstr ""
msgid "Save comment"
msgstr ""
+msgid "Save expiration policy"
+msgstr ""
+
msgid "Save password"
msgstr ""
@@ -16373,6 +16370,12 @@ msgstr ""
msgid "Self-monitoring project does not exist. Please check logs for any error messages"
msgstr ""
+msgid "Self-monitoring project has been successfully deleted"
+msgstr ""
+
+msgid "Self-monitoring project was not deleted. Please check logs for any error messages"
+msgstr ""
+
msgid "Send a separate email notification to Developers."
msgstr ""
diff --git a/qa/qa/page/group/sub_menus/members.rb b/qa/qa/page/group/sub_menus/members.rb
index c8b3f5bb422..33c4caaddcb 100644
--- a/qa/qa/page/group/sub_menus/members.rb
+++ b/qa/qa/page/group/sub_menus/members.rb
@@ -7,12 +7,9 @@ module QA
class Members < Page::Base
include Page::Component::UsersSelect
- view 'app/views/groups/group_members/_new_group_member.html.haml' do
- element :add_to_group_button
- end
-
- view 'app/helpers/groups/group_members_helper.rb' do
+ view 'app/views/shared/members/_invite_member.html.haml' do
element :member_select_field
+ element :invite_member_button
end
view 'app/views/shared/members/_member.html.haml' do
@@ -24,7 +21,7 @@ module QA
def add_member(username)
select_user :member_select_field, username
- click_element :add_to_group_button
+ click_element :invite_member_button
end
def update_access_level(username, access_level)
diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb
index 2ef018fd983..fd3e0add2a6 100644
--- a/qa/qa/page/project/settings/members.rb
+++ b/qa/qa/page/project/settings/members.rb
@@ -8,9 +8,9 @@ module QA
include Page::Component::UsersSelect
include QA::Page::Component::Select2
- view 'app/views/projects/project_members/_new_project_member.html.haml' do
- element :member_select_input
- element :add_member_button
+ view 'app/views/shared/members/_invite_member.html.haml' do
+ element :member_select_field
+ element :invite_member_button
end
view 'app/views/projects/project_members/_team.html.haml' do
@@ -21,7 +21,7 @@ module QA
element :invite_group_tab
end
- view 'app/views/projects/project_members/_new_project_group.html.haml' do
+ view 'app/views/shared/members/_invite_group.html.haml' do
element :group_select_field
element :invite_group_button
end
@@ -43,8 +43,8 @@ module QA
end
def add_member(username)
- select_user :member_select_input, username
- click_element :add_member_button
+ select_user :member_select_field, username
+ click_element :invite_member_button
end
def remove_group(group_name)
diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb
index 8f04822fee6..04f2e33b26a 100644
--- a/spec/controllers/groups/group_links_controller_spec.rb
+++ b/spec/controllers/groups/group_links_controller_spec.rb
@@ -111,4 +111,100 @@ describe Groups::GroupLinksController do
end
end
end
+
+ describe '#update' do
+ let!(:link) do
+ create(:group_group_link, { shared_group: shared_group,
+ shared_with_group: shared_with_group })
+ end
+
+ let(:expiry_date) { 1.month.from_now.to_date }
+
+ subject do
+ post(:update, params: { group_id: shared_group,
+ id: link.id,
+ group_link: { group_access: Gitlab::Access::GUEST,
+ expires_at: expiry_date } })
+ end
+
+ context 'when user has admin access to the shared group' do
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'updates existing link' do
+ expect(link.group_access).to eq(Gitlab::Access::DEVELOPER)
+ expect(link.expires_at).to be_nil
+
+ subject
+
+ link.reload
+
+ expect(link.group_access).to eq(Gitlab::Access::GUEST)
+ expect(link.expires_at).to eq(expiry_date)
+ end
+ end
+
+ context 'when user does not have admin access to the shared group' do
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe '#destroy' do
+ let!(:link) do
+ create(:group_group_link, { shared_group: shared_group,
+ shared_with_group: shared_with_group })
+ end
+
+ subject do
+ post(:destroy, params: { group_id: shared_group,
+ id: link.id })
+ end
+
+ context 'when user has admin access to the shared group' do
+ before do
+ shared_group.add_owner(user)
+ end
+
+ it 'deletes existing link' do
+ expect { subject }.to change(GroupGroupLink, :count).by(-1)
+ end
+ end
+
+ context 'when user does not have admin access to the shared group' do
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ it 'renders 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index a144d9e0786..1c8a2bd160d 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -31,6 +31,12 @@ describe Groups::GroupMembersController do
expect(assigns(:invited_members).map(&:invite_email)).to match_array(invited.map(&:invite_email))
end
+ it 'assigns skip groups' do
+ get :index, params: { group_id: group }
+
+ expect(assigns(:skip_groups)).to match_array(group.related_group_ids)
+ end
+
it 'restricts search to one email' do
get :index, params: { group_id: group, search_invited: invited.first.invite_email }
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index b48b7dc86e0..cbcda5d0dc7 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe HealthCheckController do
+describe HealthCheckController, :request_store do
include StubENV
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
@@ -18,7 +18,7 @@ describe HealthCheckController do
describe 'GET #index' do
context 'when services are up but accessed from outside whitelisted ips' do
before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
end
it 'returns a not found page' do
@@ -48,7 +48,7 @@ describe HealthCheckController do
context 'when services are up and accessed from whitelisted ips' do
before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end
it 'supports successful plaintext response' do
@@ -95,7 +95,7 @@ describe HealthCheckController do
before do
allow(HealthCheck::Utils).to receive(:process_checks).with(['standard']).and_return('The server is on fire')
allow(HealthCheck::Utils).to receive(:process_checks).with(['email']).and_return('Email is on fire')
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end
it 'supports failure plaintext response' do
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 1d378b9b9dc..331eafba0d3 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe MetricsController do
+describe MetricsController, :request_store do
include StubENV
let(:metrics_multiproc_dir) { @metrics_multiproc_dir }
@@ -53,7 +53,7 @@ describe MetricsController do
context 'accessed from whitelisted ip' do
before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
end
it_behaves_like 'endpoint providing metrics'
@@ -61,7 +61,7 @@ describe MetricsController do
context 'accessed from ip in whitelisted range' do
before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip_in_whitelisted_range)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_in_whitelisted_range)
end
it_behaves_like 'endpoint providing metrics'
@@ -69,7 +69,7 @@ describe MetricsController do
context 'accessed from not whitelisted ip' do
before do
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(not_whitelisted_ip)
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
end
it 'returns the expected error response' do
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 257e5cb8bf0..9a4889a0335 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -167,14 +167,14 @@ describe 'Admin Groups' do
it 'adds admin a to a group as developer', :js do
visit group_group_members_path(group)
- page.within '.users-group-form' do
+ page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
- click_button 'Add to group'
+ click_button 'Invite'
- page.within '.content-list' do
+ page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
@@ -187,7 +187,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group)
- page.within '.content-list' do
+ page.within '[data-qa-selector="members_list"]' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
@@ -196,7 +196,7 @@ describe 'Admin Groups' do
visit group_group_members_path(group)
- page.within '.content-list' do
+ page.within '[data-qa-selector="members_list"]' do
expect(page).not_to have_content(current_user.name)
expect(page).not_to have_content('Developer')
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 7c40ac5bde3..d1889d3a89a 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -98,12 +98,12 @@ describe "Admin::Projects" do
it 'adds admin a to a project as developer', :js do
visit project_project_members_path(project)
- page.within '.users-project-form' do
+ page.within '.invite-users-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
- click_button 'Add to project'
+ click_button 'Invite'
page.within '.content-list' do
expect(page).to have_content(current_user.name)
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
new file mode 100644
index 00000000000..55f9418521f
--- /dev/null
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Groups > Members > Manage groups', :js do
+ include Select2Helper
+ include Spec::Support::Helpers::Features::ListRowsHelpers
+
+ let(:user) { create(:user) }
+ let(:shared_with_group) { create(:group) }
+ let(:shared_group) { create(:group) }
+
+ before do
+ shared_group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'with share groups with groups feature flag' do
+ before do
+ stub_feature_flags(shared_with_group: true)
+ end
+
+ it 'add group to group' do
+ visit group_group_members_path(shared_group)
+
+ add_group(shared_with_group.id, 'Reporter')
+
+ page.within(first_row) do
+ expect(page).to have_content(shared_with_group.name)
+ expect(page).to have_content('Reporter')
+ end
+ end
+
+ it 'remove user from group' do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
+
+ visit group_group_members_path(shared_group)
+
+ expect(page).to have_content(shared_with_group.name)
+
+ accept_confirm do
+ find(:css, '#existing_shares li', text: shared_with_group.name).find(:css, 'a.btn-remove').click
+ end
+
+ wait_for_requests
+
+ expect(page).not_to have_content(shared_with_group.name)
+ end
+
+ it 'update group to owner level' do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
+
+ visit group_group_members_path(shared_group)
+
+ page.within(first_row) do
+ click_button('Developer')
+ click_link('Maintainer')
+
+ wait_for_requests
+
+ expect(page).to have_button('Maintainer')
+ end
+ end
+
+ def add_group(id, role)
+ page.click_link 'Invite group'
+ page.within ".invite-group-form" do
+ select2(id, from: "#shared_with_group_id")
+ select(role, from: "shared_group_access")
+ click_button "Invite"
+ end
+ end
+ end
+
+ context 'without share groups with groups feature flag' do
+ before do
+ stub_feature_flags(share_group_with_group: false)
+ end
+
+ it 'does not render invitation form and tabs' do
+ visit group_group_members_path(shared_group)
+
+ expect(page).not_to have_link('Invite member')
+ expect(page).not_to have_link('Invite group')
+ end
+ end
+end
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index cdd16ae9441..e4ba3022d8b 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -113,7 +113,8 @@ describe 'Groups > Members > Manage members' do
visit group_group_members_path(group)
- expect(page).not_to have_button 'Add to group'
+ expect(page).not_to have_selector '.invite-users-form'
+ expect(page).not_to have_selector '.invite-group-form'
page.within(second_row) do
# Can not modify user2 role
@@ -125,11 +126,10 @@ describe 'Groups > Members > Manage members' do
end
def add_user(id, role)
- page.within ".users-group-form" do
+ page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level")
+ click_button "Invite"
end
-
- click_button "Add to group"
end
end
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index 9c17aac09e8..fda129ce422 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -24,7 +24,7 @@ describe 'Search group member' do
find('.user-search-btn').click
end
- group_members_list = find(".card .content-list")
+ group_members_list = find('[data-qa-selector="members_list"]')
expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name)
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 6d92c777033..84000ef73ce 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -87,12 +87,12 @@ describe 'Project members list' do
end
def add_user(id, role)
- page.within ".users-project-form" do
+ page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true)
select(role, from: "access_level")
end
- click_button "Add to project"
+ click_button "Invite"
end
def visit_members_page
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
index 501dd05300a..cbcd03b33ce 100644
--- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -20,10 +20,10 @@ describe 'Projects > Members > Maintainer adds member with expiration date', :js
date = 4.days.from_now
visit project_project_members_path(project)
- page.within '.users-project-form' do
+ page.within '.invite-users-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: date.to_s(:medium) + "\n"
- click_on 'Add to project'
+ click_on 'Invite'
end
page.within "#project_member_#{new_member.project_members.first.id}" do
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 6d94388a6e2..705c60f15ee 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -37,7 +37,7 @@ describe 'Projects > Settings > User manages project members' do
visit(project_project_members_path(project))
- page.within('.users-project-form') do
+ page.within('.invite-users-form') do
click_link('Import')
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index f161a1df9c3..5636b5db84f 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -10,6 +10,7 @@ describe GroupMembersFinder, '#execute' do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:user4) { create(:user) }
+ let(:user5) { create(:user, :two_factor_via_otp) }
it 'returns members for top-level group' do
member1 = group.add_maintainer(user1)
@@ -67,4 +68,43 @@ describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member2, member3, member4])
end
+
+ it 'returns searched members if requested' do
+ group.add_maintainer(user2)
+ nested_group.add_maintainer(user2)
+ nested_group.add_maintainer(user3)
+ nested_group.add_maintainer(user4)
+ member = group.add_maintainer(user1)
+
+ result = described_class.new(group).execute(include_relations: [:direct, :descendants], params: { search: user1.name })
+
+ expect(result.to_a).to match_array([member])
+ end
+
+ it 'returns members with two-factor auth if requested by owner' do
+ group.add_owner(user2)
+ group.add_maintainer(user1)
+ nested_group.add_maintainer(user2)
+ nested_group.add_maintainer(user3)
+ nested_group.add_maintainer(user4)
+ member = group.add_maintainer(user5)
+
+ result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'enabled' })
+
+ expect(result.to_a).to contain_exactly(member)
+ end
+
+ it 'returns members without two-factor auth if requested by owner' do
+ member1 = group.add_owner(user2)
+ member2 = group.add_maintainer(user1)
+ nested_group.add_maintainer(user2)
+ member3 = nested_group.add_maintainer(user3)
+ member4 = nested_group.add_maintainer(user4)
+ member_with_2fa = group.add_maintainer(user5)
+
+ result = described_class.new(group, user2).execute(include_relations: [:direct, :descendants], params: { two_factor: 'disabled' })
+
+ expect(result.to_a).not_to include(member_with_2fa)
+ expect(result.to_a).to match_array([member1, member2, member3, member4])
+ end
end
diff --git a/spec/fixtures/emails/envelope_to_header.eml b/spec/fixtures/emails/envelope_to_header.eml
new file mode 100644
index 00000000000..4b6418d4c06
--- /dev/null
+++ b/spec/fixtures/emails/envelope_to_header.eml
@@ -0,0 +1,32 @@
+Return-Path: <jake@example.com>
+Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@example.com>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by myserver.example.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+From: "jake@example.com" <jake@example.com>
+To: "support@example.com" <support@example.com>
+Subject: Insert hilarious subject line here
+Date: Tue, 26 Nov 2019 14:22:41 +0000
+Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT>
+Accept-Language: de-DE, en-US
+Content-Language: de-DE
+X-MS-Has-Attach:
+X-MS-TNEF-Correlator:
+x-ms-exchange-transport-fromentityheader: Hosted
+x-originating-ip: [62.96.54.178]
+Content-Type: multipart/alternative;
+ boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_"
+MIME-Version: 1.0
+Envelope-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com
+
+--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Look, a message with some alternate headers! We should really support them.
diff --git a/spec/fixtures/emails/forwarded_new_issue.eml b/spec/fixtures/emails/forwarded_new_issue.eml
index 258106bb897..e3688697651 100644
--- a/spec/fixtures/emails/forwarded_new_issue.eml
+++ b/spec/fixtures/emails/forwarded_new_issue.eml
@@ -1,13 +1,13 @@
-Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
-Return-Path: <jake@adventuretime.ooo>
-Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
-Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
-Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Delivered-To: incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com
+Return-Path: <jake@example.com>
+Received: from iceking.example.com ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.example.com>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
-From: Jake the Dog <jake@adventuretime.ooo>
-Delivered-To: support@adventuretime.ooo
-To: support@adventuretime.ooo
+From: Jake the Dog <jake@example.com>
+Delivered-To: support@example.com
+To: support@example.com
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
Subject: New Issue by email
Mime-Version: 1.0
diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
index 9724033f3c9..bef4674bd8b 100644
--- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -1,10 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Form renders 1`] = `
-<div
- class="card"
->
- <form>
+<form>
+ <div
+ class="card"
+ >
+ <!---->
<div
class="card-header"
>
@@ -12,11 +13,13 @@ exports[`Settings Form renders 1`] = `
Tag expiration policy
</div>
-
<div
class="card-body"
>
- <gl-form-group-stub
+ <!---->
+ <!---->
+
+ <glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
@@ -26,7 +29,7 @@ exports[`Settings Form renders 1`] = `
<div
class="d-flex align-items-start"
>
- <gl-toggle-stub
+ <gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
@@ -41,81 +44,96 @@ exports[`Settings Form renders 1`] = `
</strong>
</span>
</div>
- </gl-form-group-stub>
+ </glformgroup-stub>
- <gl-form-group-stub
+ <glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
- <gl-form-select-stub
+ <glformselect-stub
+ disabled="true"
id="expiration-policy-interval"
+ value="bar"
>
<option
- value="1"
+ value="foo"
>
- Option 1
+
+ Foo
+
</option>
-
<option
- value="2"
+ value="bar"
>
- Option 2
+
+ Bar
+
</option>
- </gl-form-select-stub>
- </gl-form-group-stub>
+ </glformselect-stub>
+ </glformgroup-stub>
- <gl-form-group-stub
+ <glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
- <gl-form-select-stub
+ <glformselect-stub
+ disabled="true"
id="expiration-policy-schedule"
+ value="bar"
>
<option
- value="1"
+ value="foo"
>
- Option 1
+
+ Foo
+
</option>
-
<option
- value="2"
+ value="bar"
>
- Option 2
+
+ Bar
+
</option>
- </gl-form-select-stub>
- </gl-form-group-stub>
+ </glformselect-stub>
+ </glformgroup-stub>
- <gl-form-group-stub
+ <glformgroup-stub
id="expiration-policy-latest-group"
label="Expiration latest:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
- <gl-form-select-stub
+ <glformselect-stub
+ disabled="true"
id="expiration-policy-latest"
+ value="bar"
>
<option
- value="1"
+ value="foo"
>
- Option 1
+
+ Foo
+
</option>
-
<option
- value="2"
+ value="bar"
>
- Option 2
+
+ Bar
+
</option>
- </gl-form-select-stub>
- </gl-form-group-stub>
+ </glformselect-stub>
+ </glformgroup-stub>
- <gl-form-group-stub
+ <glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Expire Docker tags with name matching:"
@@ -123,33 +141,41 @@ exports[`Settings Form renders 1`] = `
label-cols="3"
label-for="expiration-policy-name-matching"
>
- <gl-form-textarea-stub
+ <glformtextarea-stub
+ disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
- </gl-form-group-stub>
+ </glformgroup-stub>
+
</div>
-
<div
- class="card-footer text-right"
+ class="card-footer"
>
- <gl-button-stub
- type="reset"
- >
- Cancel
- </gl-button-stub>
-
- <gl-button-stub
- type="submit"
- variant="success"
+ <div
+ class="d-flex justify-content-end"
>
+ <glbutton-stub
+ class="mr-2 d-block"
+ type="reset"
+ >
+ Cancel
+ </glbutton-stub>
+
+ <glbutton-stub
+ class="d-block"
+ type="submit"
+ variant="success"
+ >
+
+ Save expiration policy
- Save Expiration Policy
-
- </gl-button-stub>
+ </glbutton-stub>
+ </div>
</div>
- </form>
-</div>
+ <!---->
+ </div>
+</form>
`;
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index b944e5a2cea..bd733e965a4 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -1,8 +1,10 @@
import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
+import stubChildren from 'helpers/stub_children';
import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/';
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
+import { stringifiedFormOptions } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -13,7 +15,6 @@ describe('Settings Form', () => {
let saveSpy;
let resetSpy;
- const helpPagePath = 'foo';
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
@@ -23,7 +24,11 @@ describe('Settings Form', () => {
const mountComponent = (options = {}) => {
saveSpy = jest.fn();
resetSpy = jest.fn();
- wrapper = shallowMount(component, {
+ wrapper = mount(component, {
+ stubs: {
+ ...stubChildren(component),
+ GlCard: false,
+ },
store,
methods: {
saveSettings: saveSpy,
@@ -35,7 +40,7 @@ describe('Settings Form', () => {
beforeEach(() => {
store = createStore();
- store.dispatch('setInitialState', { helpPagePath });
+ store.dispatch('setInitialState', stringifiedFormOptions);
mountComponent();
});
@@ -48,13 +53,13 @@ describe('Settings Form', () => {
});
describe.each`
- elementName | modelName | value
- ${'toggle'} | ${'enabled'} | ${true}
- ${'interval'} | ${'older_than'} | ${'foo'}
- ${'schedule'} | ${'cadence'} | ${'foo'}
- ${'latest'} | ${'keep_n'} | ${'foo'}
- ${'name-matching'} | ${'name_regex'} | ${'foo'}
- `('%s form element', ({ elementName, modelName, value }) => {
+ elementName | modelName | value | disabledByToggle
+ ${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
+ ${'interval'} | ${'older_than'} | ${'foo'} | ${'disabled'}
+ ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
+ ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
+ ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
+ `('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => {
let formGroup;
beforeEach(() => {
formGroup = findFormGroup(elementName);
@@ -89,6 +94,12 @@ describe('Settings Form', () => {
expect(wrapper.vm[modelName]).toBe(value);
});
});
+
+ it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
+ store.dispatch('updateSettings', { enabled: false });
+ const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
+ expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
+ });
});
describe('form actions', () => {
diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js
new file mode 100644
index 00000000000..411363c2c95
--- /dev/null
+++ b/spec/frontend/registry/settings/mock_data.js
@@ -0,0 +1,12 @@
+export const options = [{ key: 'foo', label: 'Foo' }, { key: 'bar', label: 'Bar', default: true }];
+export const stringifiedOptions = JSON.stringify(options);
+export const stringifiedFormOptions = {
+ cadenceOptions: stringifiedOptions,
+ keepNOptions: stringifiedOptions,
+ olderThanOptions: stringifiedOptions,
+};
+export const formOptions = {
+ cadence: options,
+ keepN: options,
+ olderThan: options,
+};
diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js
index a8c7ed3bafa..1a0effbe125 100644
--- a/spec/frontend/registry/settings/store/mutations_spec.js
+++ b/spec/frontend/registry/settings/store/mutations_spec.js
@@ -1,6 +1,7 @@
import mutations from '~/registry/settings/store/mutations';
import * as types from '~/registry/settings/store/mutation_types';
import createState from '~/registry/settings/store/state';
+import { formOptions, stringifiedFormOptions } from '../mock_data';
describe('Mutations Registry Store', () => {
let mockState;
@@ -11,11 +12,14 @@ describe('Mutations Registry Store', () => {
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
- const payload = { helpPagePath: 'foo', projectId: 'bar' };
- const expectedState = { ...mockState, ...payload };
- mutations[types.SET_INITIAL_STATE](mockState, payload);
+ const expectedState = { ...mockState, projectId: 'foo', formOptions };
+ mutations[types.SET_INITIAL_STATE](mockState, {
+ projectId: 'foo',
+ ...stringifiedFormOptions,
+ });
expect(mockState.projectId).toEqual(expectedState.projectId);
+ expect(mockState.formOptions).toEqual(expectedState.formOptions);
});
});
diff --git a/spec/frontend/vuex_shared/bindings_spec.js b/spec/frontend/vuex_shared/bindings_spec.js
index 022d9e8e84c..0f91a09018f 100644
--- a/spec/frontend/vuex_shared/bindings_spec.js
+++ b/spec/frontend/vuex_shared/bindings_spec.js
@@ -3,49 +3,77 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
- const dummyComponent = {
+ const defaultArgs = [['baz'], 'bar', 'foo'];
+
+ const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: {
- ...mapComputed('foo', 'bar', ['baz']),
+ ...mapComputed(...mapComputedArgs),
},
render() {
return null;
},
+ });
+
+ const mocks = {
+ $store: {
+ state: {
+ baz: 2,
+ foo: {
+ baz: 1,
+ },
+ },
+ getters: {
+ getBaz: 'foo',
+ },
+ dispatch: jest.fn(),
+ },
};
- it('returns an object with keys equal to the last fn parameter ', () => {
+
+ it('returns an object with keys equal to the first fn parameter ', () => {
const keyList = ['foo1', 'foo2'];
- const result = mapComputed('foo', 'bar', keyList);
+ const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList);
});
+
it('returned object has set and get function', () => {
- const result = mapComputed('foo', 'bar', ['baz']);
+ const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined();
expect(result.baz.get).toBeDefined();
});
- it('set function invokes $store.dispatch', () => {
- const context = shallowMount(dummyComponent, {
- mocks: {
- $store: {
- dispatch: jest.fn(),
- },
- },
+ describe('set function', () => {
+ it('invokes $store.dispatch', () => {
+ const context = shallowMount(createDummy(), { mocks });
+ context.vm.baz = 'a';
+ expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
+ });
+ it('uses updateFn in list object mode if updateFn exists', () => {
+ const context = shallowMount(createDummy([[{ key: 'foo', updateFn: 'baz' }]]), { mocks });
+ context.vm.foo = 'b';
+ expect(context.vm.$store.dispatch).toHaveBeenCalledWith('baz', { foo: 'b' });
+ });
+ it('in list object mode defaults to defaultUpdateFn if updateFn do not exists', () => {
+ const context = shallowMount(createDummy([[{ key: 'foo' }], 'defaultFn']), { mocks });
+ context.vm.foo = 'c';
+ expect(context.vm.$store.dispatch).toHaveBeenCalledWith('defaultFn', { foo: 'c' });
});
- context.vm.baz = 'a';
- expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
});
- it('get function returns $store.state[root][key]', () => {
- const context = shallowMount(dummyComponent, {
- mocks: {
- $store: {
- state: {
- foo: {
- baz: 1,
- },
- },
- },
- },
+
+ describe('get function', () => {
+ it('if root is set returns $store.state[root][key]', () => {
+ const context = shallowMount(createDummy(), { mocks });
+ expect(context.vm.baz).toBe(mocks.$store.state.foo.baz);
+ });
+
+ it('if root is not set returns $store.state[key]', () => {
+ const context = shallowMount(createDummy([['baz'], 'bar']), { mocks });
+ expect(context.vm.baz).toBe(mocks.$store.state.baz);
+ });
+
+ it('when using getters it invoke the appropriate getter', () => {
+ const context = shallowMount(createDummy([[{ getter: 'getBaz', key: 'baz' }]]), { mocks });
+ expect(context.vm.baz).toBe(mocks.$store.getters.getBaz);
});
- expect(context.vm.baz).toBe(1);
});
});
});
diff --git a/spec/helpers/application_settings_helper_spec.rb b/spec/helpers/application_settings_helper_spec.rb
index 629ac34edc1..e5deeb990a1 100644
--- a/spec/helpers/application_settings_helper_spec.rb
+++ b/spec/helpers/application_settings_helper_spec.rb
@@ -76,6 +76,20 @@ describe ApplicationSettingsHelper do
)
end
+ it 'returns delete_self_monitoring_project_path' do
+ expect(helper.self_monitoring_project_data).to include(
+ 'delete_self_monitoring_project_path' =>
+ delete_self_monitoring_project_admin_application_settings_path
+ )
+ end
+
+ it 'returns status_delete_self_monitoring_project_path' do
+ expect(helper.self_monitoring_project_data).to include(
+ 'status_delete_self_monitoring_project_path' =>
+ status_delete_self_monitoring_project_admin_application_settings_path
+ )
+ end
+
it 'returns self_monitoring_project_exists false' do
expect(helper.self_monitoring_project_data).to include(
'self_monitoring_project_exists' => false
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 43c73242f5f..018219e5647 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -5,22 +5,27 @@ require 'spec_helper'
describe Gitlab::Email::Receiver do
include_context :email_shared_context
- context "when the email contains a valid email address in a Delivered-To header" do
- let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
+ context 'when the email contains a valid email address in a header' do
let(:handler) { double(:handler) }
before do
- stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
-
allow(handler).to receive(:execute)
allow(handler).to receive(:metrics_params)
allow(handler).to receive(:metrics_event)
+
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
+ end
+
+ context 'when in a Delivered-To header' do
+ let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
+
+ it_behaves_like 'correctly finds the mail key'
end
- it "finds the mail key" do
- expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
+ context 'when in an Envelope-To header' do
+ let(:email_raw) { fixture_file('emails/envelope_to_header.eml') }
- receiver.execute
+ it_behaves_like 'correctly finds the mail key'
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 0d9719a5663..ebf56c0ae66 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -229,6 +229,59 @@ describe Gitlab::GitalyClient do
end
end
end
+
+ context 'deadlines', :request_store do
+ let(:request_deadline) { real_time + 10.0 }
+
+ before do
+ allow(Gitlab::RequestContext.instance).to receive(:request_deadline).and_return(request_deadline)
+ end
+
+ it 'includes the deadline information' do
+ kword_args = described_class.request_kwargs('default', timeout: 2)
+
+ expect(kword_args[:deadline])
+ .to be_within(1).of(real_time + 2)
+ expect(kword_args[:metadata][:deadline_type]).to eq("regular")
+ end
+
+ it 'limits the deadline do the request deadline if that is closer', :aggregate_failures do
+ kword_args = described_class.request_kwargs('default', timeout: 15)
+
+ expect(kword_args[:deadline]).to eq(request_deadline)
+ expect(kword_args[:metadata][:deadline_type]).to eq("limited")
+ end
+
+ it 'does not limit calls in sidekiq' do
+ expect(Sidekiq).to receive(:server?).and_return(true)
+
+ kword_args = described_class.request_kwargs('default', timeout: 6.hours.to_i)
+
+ expect(kword_args[:deadline]).to be_within(1).of(real_time + 6.hours.to_i)
+ expect(kword_args[:metadata][:deadline_type]).to be_nil
+ end
+
+ it 'does not limit calls in sidekiq when allowed unlimited' do
+ expect(Sidekiq).to receive(:server?).and_return(true)
+
+ kword_args = described_class.request_kwargs('default', timeout: 0)
+
+ expect(kword_args[:deadline]).to be_nil
+ expect(kword_args[:metadata][:deadline_type]).to be_nil
+ end
+
+ it 'includes only the deadline specified by the timeout when there was no deadline' do
+ allow(Gitlab::RequestContext.instance).to receive(:request_deadline).and_return(nil)
+ kword_args = described_class.request_kwargs('default', timeout: 6.hours.to_i)
+
+ expect(kword_args[:deadline]).to be_within(1).of(Gitlab::Metrics::System.real_time + 6.hours.to_i)
+ expect(kword_args[:metadata][:deadline_type]).to be_nil
+ end
+
+ def real_time
+ Gitlab::Metrics::System.real_time
+ end
+ end
end
describe 'enforce_gitaly_request_limits?' do
diff --git a/spec/lib/gitlab/middleware/request_context_spec.rb b/spec/lib/gitlab/middleware/request_context_spec.rb
new file mode 100644
index 00000000000..1ed06a97c1e
--- /dev/null
+++ b/spec/lib/gitlab/middleware/request_context_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+require 'fast_spec_helper'
+require 'rack'
+require 'request_store'
+require_relative '../../../support/helpers/next_instance_of'
+
+describe Gitlab::Middleware::RequestContext do
+ include NextInstanceOf
+
+ let(:app) { -> (env) {} }
+ let(:env) { {} }
+
+ around do |example|
+ RequestStore.begin!
+ example.run
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
+ describe '#call' do
+ context 'setting the client ip' do
+ subject { Gitlab::RequestContext.instance.client_ip }
+
+ context 'with X-Forwarded-For headers' do
+ let(:load_balancer_ip) { '1.2.3.4' }
+ let(:headers) do
+ {
+ 'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
+ 'REMOTE_ADDR' => '127.0.0.1'
+ }
+ end
+
+ let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
+
+ it 'returns the load balancer IP' do
+ endpoint = proc do
+ [200, {}, ["Hello"]]
+ end
+
+ described_class.new(endpoint).call(env)
+
+ expect(subject).to eq(load_balancer_ip)
+ end
+ end
+
+ context 'request' do
+ let(:ip) { '192.168.1.11' }
+
+ before do
+ allow_next_instance_of(Rack::Request) do |instance|
+ allow(instance).to receive(:ip).and_return(ip)
+ end
+ described_class.new(app).call(env)
+ end
+
+ it { is_expected.to eq(ip) }
+ end
+
+ context 'before RequestContext middleware run' do
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ context 'setting the thread cpu time' do
+ it 'sets the `start_thread_cpu_time`' do
+ expect { described_class.new(app).call(env) }
+ .to change { Gitlab::RequestContext.instance.start_thread_cpu_time }.from(nil).to(Float)
+ end
+ end
+
+ context 'setting the request start time' do
+ it 'sets the `request_start_time`' do
+ expect { described_class.new(app).call(env) }
+ .to change { Gitlab::RequestContext.instance.request_start_time }.from(nil).to(Float)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index 87b8029de2e..1290071549d 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -2,59 +2,44 @@
require 'spec_helper'
-describe Gitlab::RequestContext do
- describe '#client_ip' do
- subject { described_class.client_ip }
+describe Gitlab::RequestContext, :request_store do
+ subject { described_class.instance }
- let(:app) { -> (env) {} }
- let(:env) { Hash.new }
+ it { is_expected.to have_attributes(client_ip: nil, start_thread_cpu_time: nil, request_start_time: nil) }
- context 'with X-Forwarded-For headers', :request_store do
- let(:load_balancer_ip) { '1.2.3.4' }
- let(:headers) do
- {
- 'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
- 'REMOTE_ADDR' => '127.0.0.1'
- }
- end
+ describe '#request_deadline' do
+ let(:request_start_time) { 1575982156.206008 }
- let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
+ it "sets the time to #{Settings.gitlab.max_request_duration_seconds} seconds in the future" do
+ allow(subject).to receive(:request_start_time).and_return(request_start_time)
- it 'returns the load balancer IP' do
- client_ip = nil
-
- endpoint = proc do
- client_ip = Gitlab::SafeRequestStore[:client_ip]
- [200, {}, ["Hello"]]
- end
+ expect(subject.request_deadline).to eq(1575982156.206008 + Settings.gitlab.max_request_duration_seconds)
+ expect(subject.request_deadline).to be_a(Float)
+ end
- described_class.new(endpoint).call(env)
+ it 'returns nil if there is no start time' do
+ allow(subject).to receive(:request_start_time).and_return(nil)
- expect(client_ip).to eq(load_balancer_ip)
- end
+ expect(subject.request_deadline).to be_nil
end
+ end
- context 'when RequestStore::Middleware is used' do
- around do |example|
- RequestStore::Middleware.new(-> (env) { example.run }).call({})
- end
+ describe '#ensure_request_deadline_not_exceeded!' do
+ it 'does not raise an error when there was no deadline' do
+ expect(subject).to receive(:request_deadline).and_return(nil)
+ expect { subject.ensure_deadline_not_exceeded! }.not_to raise_error
+ end
- context 'request' do
- let(:ip) { '192.168.1.11' }
+ it 'does not raise an error if the deadline is in the future' do
+ allow(subject).to receive(:request_deadline).and_return(Gitlab::Metrics::System.real_time + 10)
- before do
- allow_next_instance_of(Rack::Request) do |instance|
- allow(instance).to receive(:ip).and_return(ip)
- end
- described_class.new(app).call(env)
- end
+ expect { subject.ensure_deadline_not_exceeded! }.not_to raise_error
+ end
- it { is_expected.to eq(ip) }
- end
+ it 'raises an error when the deadline is in the past' do
+ allow(subject).to receive(:request_deadline).and_return(Gitlab::Metrics::System.real_time - 10)
- context 'before RequestContext middleware run' do
- it { is_expected.to be_nil }
- end
+ expect { subject.ensure_deadline_not_exceeded! }.to raise_error(described_class::RequestDeadlineExceeded)
end
end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 52e60a69a52..bbd50f1c0ef 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -319,6 +319,11 @@ describe ApplicationSetting do
end
context 'gitaly timeouts' do
+ it "validates that the default_timeout is lower than the max_request_duration" do
+ is_expected.to validate_numericality_of(:gitaly_timeout_default)
+ .is_less_than_or_equal_to(Settings.gitlab.max_request_duration_seconds)
+ end
+
[:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name|
it do
is_expected.to validate_presence_of(timeout_name)
diff --git a/spec/models/group_group_link_spec.rb b/spec/models/group_group_link_spec.rb
index e4ad5703a10..a877cc803dd 100644
--- a/spec/models/group_group_link_spec.rb
+++ b/spec/models/group_group_link_spec.rb
@@ -33,4 +33,12 @@ describe GroupGroupLink do
validate_inclusion_of(:group_access).in_array(Gitlab::Access.values))
end
end
+
+ describe '#human_access' do
+ it 'delegates to Gitlab::Access' do
+ expect(Gitlab::Access).to receive(:human_access).with(group_group_link.group_access)
+
+ group_group_link.human_access
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 842acf92c2a..3531c695236 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1003,6 +1003,57 @@ describe Group do
end
end
+ describe '#related_group_ids' do
+ let(:nested_group) { create(:group, parent: group) }
+ let(:shared_with_group) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, shared_group: nested_group,
+ shared_with_group: shared_with_group)
+ end
+
+ subject(:related_group_ids) { nested_group.related_group_ids }
+
+ it 'returns id' do
+ expect(related_group_ids).to include(nested_group.id)
+ end
+
+ it 'returns ancestor id' do
+ expect(related_group_ids).to include(group.id)
+ end
+
+ it 'returns shared with group id' do
+ expect(related_group_ids).to include(shared_with_group.id)
+ end
+
+ context 'with more than one ancestor group' do
+ let(:ancestor_group) { create(:group) }
+
+ before do
+ group.update(parent: ancestor_group)
+ end
+
+ it 'returns all ancestor group ids' do
+ expect(related_group_ids).to(
+ include(group.id, ancestor_group.id))
+ end
+ end
+
+ context 'with more than one shared with group' do
+ let(:another_shared_with_group) { create(:group, parent: group) }
+
+ before do
+ create(:group_group_link, shared_group: nested_group,
+ shared_with_group: another_shared_with_group)
+ end
+
+ it 'returns all shared with group ids' do
+ expect(related_group_ids).to(
+ include(shared_with_group.id, another_shared_with_group.id))
+ end
+ end
+ end
+
context 'with uploads' do
it_behaves_like 'model with uploads', true do
let(:model_object) { create(:group, :with_avatar) }
diff --git a/spec/requests/self_monitoring_project_spec.rb b/spec/requests/self_monitoring_project_spec.rb
index f3ffa8e0ea4..d562a34aec4 100644
--- a/spec/requests/self_monitoring_project_spec.rb
+++ b/spec/requests/self_monitoring_project_spec.rb
@@ -60,7 +60,7 @@ describe 'Self-Monitoring project requests' do
end
it_behaves_like 'sets polling header and returns accepted' do
- let(:in_progress_message) { 'Job is in progress' }
+ let(:in_progress_message) { 'Job to create self-monitoring project is in progress' }
end
end
@@ -115,4 +115,110 @@ describe 'Self-Monitoring project requests' do
end
end
end
+
+ describe 'DELETE #delete_self_monitoring_project' do
+ let(:worker_class) { SelfMonitoringProjectDeleteWorker }
+
+ subject { delete delete_self_monitoring_project_admin_application_settings_path }
+
+ it_behaves_like 'not accessible to non-admin users'
+
+ context 'with admin user' do
+ before do
+ login_as(admin)
+ end
+
+ context 'with feature flag disabled' do
+ it_behaves_like 'not accessible if feature flag is disabled'
+ end
+
+ context 'with feature flag enabled' do
+ let(:status_api) { status_delete_self_monitoring_project_admin_application_settings_path }
+
+ it_behaves_like 'triggers async worker, returns sidekiq job_id with response accepted'
+ end
+ end
+ end
+
+ describe 'GET #status_delete_self_monitoring_project' do
+ let(:worker_class) { SelfMonitoringProjectDeleteWorker }
+ let(:job_id) { 'job_id' }
+
+ subject do
+ get status_delete_self_monitoring_project_admin_application_settings_path,
+ params: { job_id: job_id }
+ end
+
+ it_behaves_like 'not accessible to non-admin users'
+
+ context 'with admin user' do
+ before do
+ login_as(admin)
+ end
+
+ context 'with feature flag disabled' do
+ it_behaves_like 'not accessible if feature flag is disabled'
+ end
+
+ context 'with feature flag enabled' do
+ it_behaves_like 'handles invalid job_id'
+
+ context 'when job is in progress' do
+ before do
+ allow(worker_class).to receive(:in_progress?)
+ .with(job_id)
+ .and_return(true)
+
+ stub_application_setting(instance_administration_project_id: 1)
+ end
+
+ it_behaves_like 'sets polling header and returns accepted' do
+ let(:in_progress_message) { 'Job to delete self-monitoring project is in progress' }
+ end
+ end
+
+ context 'when self-monitoring project exists and job does not exist' do
+ before do
+ stub_application_setting(instance_administration_project_id: 1)
+ end
+
+ it 'returns bad_request' do
+ subject
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq(
+ 'message' => 'Self-monitoring project was not deleted. Please check logs ' \
+ 'for any error messages'
+ )
+ end
+ end
+ end
+
+ context 'when self-monitoring project does not exist' do
+ it 'does not need job_id' do
+ get status_delete_self_monitoring_project_admin_application_settings_path
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq(
+ 'message' => 'Self-monitoring project has been successfully deleted'
+ )
+ end
+ end
+
+ it 'returns success with job_id' do
+ subject
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq(
+ 'message' => 'Self-monitoring project has been successfully deleted'
+ )
+ end
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/email_shared_examples.rb b/spec/support/shared_examples/email_shared_examples.rb
new file mode 100644
index 00000000000..634a2504766
--- /dev/null
+++ b/spec/support/shared_examples/email_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+shared_examples_for 'correctly finds the mail key' do
+ specify do
+ expect(Gitlab::Email::Handler).to receive(:for).with(an_instance_of(Mail::Message), 'gitlabhq/gitlabhq+auth_token').and_return(handler)
+
+ receiver.execute
+ end
+end
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index 8e1d24c4be2..98010150e65 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
expect_no_visible_access_request(entity, user)
- page.within('.members-list') do
+ page.within('[data-qa-selector="members_list"]') do
expect(page).to have_content user.name
end
end
diff --git a/spec/support/shared_examples/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
index 65d86ddee9e..9bdfa762fc8 100644
--- a/spec/support/shared_examples/unique_ip_check_shared_examples.rb
+++ b/spec/support/shared_examples/unique_ip_check_shared_examples.rb
@@ -2,6 +2,8 @@
shared_context 'unique ips sign in limit' do
include StubENV
+ let(:request_context) { Gitlab::RequestContext.instance }
+
before do
Gitlab::Redis::Cache.with(&:flushall)
Gitlab::Redis::Queues.with(&:flushall)
@@ -15,10 +17,13 @@ shared_context 'unique ips sign in limit' do
unique_ips_limit_enabled: true,
unique_ips_limit_time_window: 10000
)
+
+ # Make sure we're working with the same reqeust context everywhere
+ allow(Gitlab::RequestContext).to receive(:instance).and_return(request_context)
end
def change_ip(ip)
- allow(Gitlab::RequestContext).to receive(:client_ip).and_return(ip)
+ allow(request_context).to receive(:client_ip).and_return(ip)
end
def request_from_ip(ip)