summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-02-16 12:20:23 +0100
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-02-16 12:20:23 +0100
commit2ee20d5a4b9923bbe1eef82e370da45d7efae287 (patch)
tree79e41ce4d7998ac9adf73f7feb6197a049266ba6
parentd9a8d9f3de2705a3ab568532f3882dd23b3ce27a (diff)
parente5ecd9b196b200d4626c27d06a67fedafa87f119 (diff)
downloadgitlab-ce-2ee20d5a4b9923bbe1eef82e370da45d7efae287.tar.gz
Merge branch 'master' into backstage/gb/build-stages-catch-up-migration
* master: (38 commits)
-rw-r--r--.eslintrc2
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js1
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js4
-rw-r--r--app/assets/javascripts/commits.js72
-rw-r--r--app/assets/javascripts/dispatcher.js10
-rw-r--r--app/assets/javascripts/importer_status.js49
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue26
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js37
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js (renamed from app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js)2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue (renamed from app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue)0
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue (renamed from app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue)2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js (renamed from app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js)0
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js (renamed from app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js)0
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg (renamed from app/assets/javascripts/pipeline_schedules/icons/intro_illustration.svg)0
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js (renamed from app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js)10
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/charts/index.js56
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js (renamed from app/assets/javascripts/integrations/index.js)7
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js (renamed from app/assets/javascripts/users/activity_calendar.js)0
-rw-r--r--app/assets/javascripts/pages/users/index.js (renamed from app/assets/javascripts/users/index.js)2
-rw-r--r--app/assets/javascripts/pages/users/show/index.js3
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js (renamed from app/assets/javascripts/users/user_tabs.js)10
-rw-r--r--app/assets/javascripts/pipelines/pipelines_charts.js38
-rw-r--r--app/assets/javascripts/pipelines/pipelines_times.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal.vue106
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/models/identity.rb9
-rw-r--r--app/models/repository.rb10
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/issues/move_service.rb25
-rw-r--r--app/views/admin/jobs/index.html.haml7
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/_form.html.haml4
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml4
-rw-r--r--app/views/projects/pipelines/charts.html.haml3
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml3
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml3
-rw-r--r--app/views/projects/services/_form.html.haml3
-rw-r--r--app/views/projects/update.js.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml6
-rw-r--r--app/views/users/show.html.haml3
-rw-r--r--changelogs/unreleased/39607-fix-avatar--vertical-align.yml5
-rw-r--r--changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml4
-rw-r--r--changelogs/unreleased/42929-hide-new-variable-values.yml5
-rw-r--r--changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml5
-rw-r--r--changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml5
-rw-r--r--changelogs/unreleased/dm-escape-commit-message.yml5
-rw-r--r--changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml6
-rw-r--r--changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml5
-rw-r--r--changelogs/unreleased/jej-fix-slow-lfs-object-check.yml5
-rw-r--r--changelogs/unreleased/winh-new-modal-component.yml5
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/rack_attack_global.rb5
-rw-r--r--config/webpack.config.js8
-rw-r--r--doc/administration/integration/plantuml.md14
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md2
-rw-r--r--doc/administration/pages/index.md15
-rw-r--r--doc/development/fe_guide/components.md61
-rw-r--r--doc/development/fe_guide/dropdowns.md33
-rw-r--r--doc/development/fe_guide/img/gl-modal.pngbin0 -> 25893 bytes
-rw-r--r--doc/development/fe_guide/index.md9
-rw-r--r--doc/development/profiling.md11
-rw-r--r--doc/topics/autodevops/index.md4
-rw-r--r--doc/user/project/repository/index.md2
-rw-r--r--features/profile/profile.feature85
-rw-r--r--features/steps/profile/profile.rb226
-rw-r--r--features/support/env.rb7
-rw-r--r--lib/banzai/filter/html_entity_filter.rb2
-rw-r--r--lib/gitlab/checks/change_access.rb7
-rw-r--r--lib/gitlab/git_access.rb9
-rw-r--r--lib/gitlab/git_access_wiki.rb2
-rw-r--r--lib/gitlab/ldap/config.rb2
-rw-r--r--lib/gitlab/o_auth/user.rb8
-rw-r--r--lib/gitlab/profiler.rb1
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--spec/features/profiles/password_spec.rb76
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb58
-rw-r--r--spec/features/profiles/user_manages_applications_spec.rb39
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb25
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb52
-rw-r--r--spec/helpers/events_helper_spec.rb4
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js46
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js41
-rw-r--r--spec/javascripts/commits_spec.js20
-rw-r--r--spec/javascripts/importer_status_spec.js74
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js (renamed from spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js)2
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js (renamed from spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js)2
-rw-r--r--spec/javascripts/vue_shared/components/gl_modal_spec.js192
-rw-r--r--spec/lib/banzai/filter/html_entity_filter_spec.rb9
-rw-r--r--spec/lib/gitlab/git_access_spec.rb19
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb8
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb4
-rw-r--r--spec/lib/gitlab/profiler_spec.rb9
-rw-r--r--spec/models/identity_spec.rb33
-rw-r--r--spec/models/repository_spec.rb12
-rw-r--r--spec/models/user_spec.rb8
-rw-r--r--spec/requests/rack_attack_global_spec.rb10
-rw-r--r--spec/support/factory_bot.rb (renamed from spec/support/factory_girl.rb)0
-rw-r--r--spec/support/fixture_helpers.rb8
103 files changed, 1218 insertions, 668 deletions
diff --git a/.eslintrc b/.eslintrc
index ad5eaebccae..8f9cdfb14ac 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -36,7 +36,7 @@
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
- "no-underscore-dangle": ["error", { "allow": ["__"]}],
+ "no-underscore-dangle": ["error", { "allow": ["__", "_links"]}],
"vue/html-self-closing": ["error", {
"html": {
"void": "always",
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index 76f93e5c6bd..b33adff609f 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -75,6 +75,7 @@ export default class AjaxVariableList {
if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables);
+ this.variableList.hideValues();
} else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data);
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 3467e88119b..745f3404295 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -178,6 +178,10 @@ export default class VariableList {
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
}
+ hideValues() {
+ this.secretValues.updateDom(false);
+ }
+
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 4b2f75fffde..2be63bd8c76 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,52 +1,36 @@
-/* eslint-disable func-names, wrap-iife, consistent-return,
- no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
- prefer-template, object-shorthand, prefer-arrow-callback */
-
import { pluralize } from './lib/utils/text_utility';
import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager';
import axios from './lib/utils/axios_utils';
-export default (function () {
- const CommitsList = {};
-
- CommitsList.timer = null;
+export default class CommitsList {
+ constructor(limit = 0) {
+ this.timer = null;
- CommitsList.init = function (limit) {
this.$contentList = $('.content_list');
- $('body').on('click', '.day-commits-table li.commit', function (e) {
- if (e.target.nodeName !== 'A') {
- location.href = $(this).attr('url');
- e.stopPropagation();
- return false;
- }
- });
-
- Pager.init(parseInt(limit, 10), false, false, this.processCommits);
+ Pager.init(parseInt(limit, 10), false, false, this.processCommits.bind(this));
this.content = $('#commits-list');
this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val();
- return this.initSearch();
- };
+ this.initSearch();
+ }
- CommitsList.initSearch = function () {
+ initSearch() {
this.timer = null;
- return this.searchField.keyup((function (_this) {
- return function () {
- clearTimeout(_this.timer);
- return _this.timer = setTimeout(_this.filterResults, 500);
- };
- })(this));
- };
+ this.searchField.on('keyup', () => {
+ clearTimeout(this.timer);
+ this.timer = setTimeout(this.filterResults.bind(this), 500);
+ });
+ }
- CommitsList.filterResults = function () {
+ filterResults() {
const form = $('.commits-search-form');
- const search = CommitsList.searchField.val();
- if (search === CommitsList.lastSearch) return Promise.resolve();
- const commitsUrl = form.attr('action') + '?' + form.serialize();
- CommitsList.content.fadeTo('fast', 0.5);
+ const search = this.searchField.val();
+ if (search === this.lastSearch) return Promise.resolve();
+ const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
+ this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
[obj.name]: obj.value,
}), {});
@@ -55,9 +39,9 @@ export default (function () {
params,
})
.then(({ data }) => {
- CommitsList.lastSearch = search;
- CommitsList.content.html(data.html);
- CommitsList.content.fadeTo('fast', 1.0);
+ this.lastSearch = search;
+ this.content.html(data.html);
+ this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
history.replaceState({
@@ -65,16 +49,16 @@ export default (function () {
}, document.title, commitsUrl);
})
.catch(() => {
- CommitsList.content.fadeTo('fast', 1.0);
- CommitsList.lastSearch = null;
+ this.content.fadeTo('fast', 1.0);
+ this.lastSearch = null;
});
- };
+ }
// Prepare loaded data.
- CommitsList.processCommits = (data) => {
+ processCommits(data) {
let processedData = data;
const $processedData = $(processedData);
- const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last();
+ const $commitsHeadersLast = this.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
@@ -97,7 +81,5 @@ export default (function () {
localTimeAgo($processedData.find('.js-timeago'));
return processedData;
- };
-
- return CommitsList;
-})();
+ }
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index f8082c74943..8f708dde063 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -224,6 +224,11 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
+ case 'projects:services:edit':
+ import('./pages/projects/services/edit')
+ .then(callDefault)
+ .catch(fail);
+ break;
case 'projects:snippets:edit':
case 'projects:snippets:update':
import('./pages/projects/snippets/edit')
@@ -462,11 +467,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
- case 'users:show':
- import('./pages/users/show')
- .then(callDefault)
- .catch(fail);
- break;
case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show')
.then(callDefault)
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 134a503864e..35094f8e73b 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -59,29 +59,36 @@ class ImporterStatus {
.catch(() => flash(__('An error occurred while importing project')));
}
- setAutoUpdate() {
- return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => {
- const jobItem = $(`#project_${job.id}`);
- const statusField = jobItem.find('.job-status');
+ autoUpdate() {
+ return axios.get(this.jobsUrl)
+ .then(({ data = [] }) => {
+ data.forEach((job) => {
+ const jobItem = $(`#project_${job.id}`);
+ const statusField = jobItem.find('.job-status');
+
+ const spinner = '<i class="fa fa-spinner fa-spin"></i>';
- const spinner = '<i class="fa fa-spinner fa-spin"></i>';
+ switch (job.import_status) {
+ case 'finished':
+ jobItem.removeClass('active').addClass('success');
+ statusField.html('<span><i class="fa fa-check"></i> done</span>');
+ break;
+ case 'scheduled':
+ statusField.html(`${spinner} scheduled`);
+ break;
+ case 'started':
+ statusField.html(`${spinner} started`);
+ break;
+ default:
+ statusField.html(job.import_status);
+ break;
+ }
+ });
+ });
+ }
- switch (job.import_status) {
- case 'finished':
- jobItem.removeClass('active').addClass('success');
- statusField.html('<span><i class="fa fa-check"></i> done</span>');
- break;
- case 'scheduled':
- statusField.html(`${spinner} scheduled`);
- break;
- case 'started':
- statusField.html(`${spinner} started`);
- break;
- default:
- statusField.html(job.import_status);
- break;
- }
- })), 4000);
+ setAutoUpdate() {
+ setInterval(this.autoUpdate.bind(this), 4000);
}
}
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index 555725cbe12..ba1d8e4d8db 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,13 +1,13 @@
<script>
import axios from '~/lib/utils/axios_utils';
- import Flash from '~/flash';
- import modal from '~/vue_shared/components/modal.vue';
- import { s__ } from '~/locale';
+ import createFlash from '~/flash';
+ import GlModal from '~/vue_shared/components/gl_modal.vue';
import { redirectTo } from '~/lib/utils/url_utility';
+ import { s__ } from '~/locale';
export default {
components: {
- modal,
+ GlModal,
},
props: {
url: {
@@ -17,7 +17,7 @@
},
computed: {
text() {
- return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.');
+ return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
},
},
methods: {
@@ -28,7 +28,7 @@
redirectTo(response.request.responseURL);
})
.catch((error) => {
- Flash(s__('AdminArea|Stopping jobs failed'));
+ createFlash(s__('AdminArea|Stopping jobs failed'));
throw error;
});
},
@@ -37,11 +37,13 @@
</script>
<template>
- <modal
+ <gl-modal
id="stop-jobs-modal"
- :title="s__('AdminArea|Stop all jobs?')"
- :text="text"
- kind="danger"
- :primary-button-label="s__('AdminArea|Stop jobs')"
- @submit="onSubmit" />
+ :header-title-text="s__('AdminArea|Stop all jobs?')"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('AdminArea|Stop jobs')"
+ @submit="onSubmit"
+ >
+ {{ text }}
+ </gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 0e004bd9174..31d58eabaaf 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -8,22 +8,23 @@ Vue.use(Translate);
export default () => {
const stopJobsButton = document.getElementById('stop-jobs-button');
-
- // eslint-disable-next-line no-new
- new Vue({
- el: '#stop-jobs-modal',
- components: {
- stopJobsModal,
- },
- mounted() {
- stopJobsButton.classList.remove('disabled');
- },
- render(createElement) {
- return createElement('stop-jobs-modal', {
- props: {
- url: stopJobsButton.dataset.url,
- },
- });
- },
- });
+ if (stopJobsButton) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#stop-jobs-modal',
+ components: {
+ stopJobsModal,
+ },
+ mounted() {
+ stopJobsButton.classList.remove('disabled');
+ },
+ render(createElement) {
+ return createElement('stop-jobs-modal', {
+ props: {
+ url: stopJobsButton.dataset.url,
+ },
+ });
+ },
+ });
+ }
};
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index 90b5882a24f..6110fda17de 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -3,7 +3,7 @@ import GpgBadges from '~/gpg_badges';
import ShortcutsNavigation from '~/shortcuts_navigation';
export default () => {
- CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
+ new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
GpgBadges.fetch();
};
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js
new file mode 100644
index 00000000000..d65be6bc69e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js
@@ -0,0 +1,3 @@
+import initForm from '../shared/init_form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
new file mode 100644
index 00000000000..d65be6bc69e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -0,0 +1,3 @@
+import initForm from '../shared/init_form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index a6c945e22b0..544360dcd51 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
+import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
new file mode 100644
index 00000000000..d65be6bc69e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -0,0 +1,3 @@
+import initForm from '../shared/init_form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 2d18fa2044b..2d18fa2044b 100644
--- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index aa04a0ac47a..77508e62cef 100644
--- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -1,7 +1,7 @@
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
- import Translate from '../../vue_shared/translate';
+ import Translate from '../../../../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
index 0c3926d76b5..0c3926d76b5 100644
--- a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/target_branch_dropdown.js
diff --git a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 95ed9c7dc21..95ed9c7dc21 100644
--- a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
diff --git a/app/assets/javascripts/pipeline_schedules/icons/intro_illustration.svg b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg
index 26d1ff97b3e..26d1ff97b3e 100644
--- a/app/assets/javascripts/pipeline_schedules/icons/intro_illustration.svg
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index 0b1a81bae13..cfd30d6053f 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import Translate from '../vue_shared/translate';
-import GlFieldErrors from '../gl_field_errors';
+import Translate from '../../../../vue_shared/translate';
+import GlFieldErrors from '../../../../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
-import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list';
+import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
@@ -27,7 +27,7 @@ function initIntervalPatternInput() {
});
}
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
@@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => {
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
});
-});
+};
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js
new file mode 100644
index 00000000000..d65be6bc69e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js
@@ -0,0 +1,3 @@
+import initForm from '../shared/init_form';
+
+document.addEventListener('DOMContentLoaded', initForm);
diff --git a/app/assets/javascripts/pages/projects/pipelines/charts/index.js b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
new file mode 100644
index 00000000000..c1dafda0e24
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/charts/index.js
@@ -0,0 +1,56 @@
+import Chart from 'vendor/Chart';
+
+const options = {
+ scaleOverlay: true,
+ responsive: true,
+ maintainAspectRatio: false,
+};
+
+const buildChart = (chartScope) => {
+ const data = {
+ labels: chartScope.labels,
+ datasets: [{
+ fillColor: '#707070',
+ strokeColor: '#707070',
+ pointColor: '#707070',
+ pointStrokeColor: '#EEE',
+ data: chartScope.totalValues,
+ },
+ {
+ fillColor: '#1aaa55',
+ strokeColor: '#1aaa55',
+ pointColor: '#1aaa55',
+ pointStrokeColor: '#fff',
+ data: chartScope.successValues,
+ },
+ ],
+ };
+ const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
+
+ new Chart(ctx).Line(data, options);
+};
+
+document.addEventListener('DOMContentLoaded', () => {
+ const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
+ const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
+ const data = {
+ labels: chartTimesData.labels,
+ datasets: [{
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: chartTimesData.values,
+ }],
+ };
+
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8;
+ }
+
+ new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options);
+
+ chartsData.forEach(scope => buildChart(scope));
+});
diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index 10fe6bac0e8..434a7e44277 100644
--- a/app/assets/javascripts/integrations/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -1,7 +1,6 @@
-/* eslint-disable no-new */
-import IntegrationSettingsForm from './integration_settings_form';
+import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-$(() => {
+export default () => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
-});
+};
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 57306322aa4..57306322aa4 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/pages/users/index.js
index 9fd8452a2b6..899dcd42e37 100644
--- a/app/assets/javascripts/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,3 +1,4 @@
+import UserCallout from '~/user_callout';
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
+ new UserCallout(); // eslint-disable-line no-new
});
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
deleted file mode 100644
index f18f98b4e9a..00000000000
--- a/app/assets/javascripts/pages/users/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import UserCallout from '~/user_callout';
-
-export default () => new UserCallout();
diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index e13b9839a20..c1217623467 100644
--- a/app/assets/javascripts/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,9 +1,9 @@
-import axios from '../lib/utils/axios_utils';
-import Activities from '../activities';
+import axios from '~/lib/utils/axios_utils';
+import Activities from '~/activities';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import { __ } from '~/locale';
+import flash from '~/flash';
import ActivityCalendar from './activity_calendar';
-import { localTimeAgo } from '../lib/utils/datetime_utility';
-import { __ } from '../locale';
-import flash from '../flash';
/**
* UserTabs
diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js
deleted file mode 100644
index 821aa7e229f..00000000000
--- a/app/assets/javascripts/pipelines/pipelines_charts.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Chart from 'vendor/Chart';
-
-document.addEventListener('DOMContentLoaded', () => {
- const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
- const buildChart = (chartScope) => {
- const data = {
- labels: chartScope.labels,
- datasets: [{
- fillColor: '#707070',
- strokeColor: '#707070',
- pointColor: '#707070',
- pointStrokeColor: '#EEE',
- data: chartScope.totalValues,
- },
- {
- fillColor: '#1aaa55',
- strokeColor: '#1aaa55',
- pointColor: '#1aaa55',
- pointStrokeColor: '#fff',
- data: chartScope.successValues,
- },
- ],
- };
- const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
- const options = {
- scaleOverlay: true,
- responsive: true,
- maintainAspectRatio: false,
- };
- if (window.innerWidth < 768) {
- // Scale fonts if window width lower than 768px (iPad portrait)
- options.scaleFontSize = 8;
- }
- new Chart(ctx).Line(data, options);
- };
-
- chartData.forEach(scope => buildChart(scope));
-});
diff --git a/app/assets/javascripts/pipelines/pipelines_times.js b/app/assets/javascripts/pipelines/pipelines_times.js
deleted file mode 100644
index b5e7a0e53d9..00000000000
--- a/app/assets/javascripts/pipelines/pipelines_times.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Chart from 'vendor/Chart';
-
-document.addEventListener('DOMContentLoaded', () => {
- const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
- const data = {
- labels: chartData.labels,
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: chartData.values,
- }],
- };
- const ctx = $('#build_timesChart').get(0).getContext('2d');
- const options = {
- scaleOverlay: true,
- responsive: true,
- maintainAspectRatio: false,
- };
- if (window.innerWidth < 768) {
- // Scale fonts if window width lower than 768px (iPad portrait)
- options.scaleFontSize = 8;
- }
- new Chart(ctx).Bar(data, options);
-});
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue
new file mode 100644
index 00000000000..67c9181c7b1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue
@@ -0,0 +1,106 @@
+<script>
+ const buttonVariants = [
+ 'danger',
+ 'primary',
+ 'success',
+ 'warning',
+ ];
+
+ export default {
+ name: 'GlModal',
+
+ props: {
+ id: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ headerTitleText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ footerPrimaryButtonVariant: {
+ type: String,
+ required: false,
+ default: 'primary',
+ validator: value => buttonVariants.indexOf(value) !== -1,
+ },
+ footerPrimaryButtonText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ methods: {
+ emitCancel(event) {
+ this.$emit('cancel', event);
+ },
+ emitSubmit(event) {
+ this.$emit('submit', event);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ :id="id"
+ class="modal fade"
+ tabindex="-1"
+ role="dialog"
+ >
+ <div
+ class="modal-dialog"
+ role="document"
+ >
+ <div class="modal-content">
+ <div class="modal-header">
+ <slot name="header">
+ <button
+ type="button"
+ class="close"
+ data-dismiss="modal"
+ :aria-label="s__('Modal|Close')"
+ @click="emitCancel($event)"
+ >
+ <span aria-hidden="true">&times;</span>
+ </button>
+ <h4 class="modal-title">
+ <slot name="title">
+ {{ headerTitleText }}
+ </slot>
+ </h4>
+ </slot>
+ </div>
+
+ <div class="modal-body">
+ <slot></slot>
+ </div>
+
+ <div class="modal-footer">
+ <slot name="footer">
+ <button
+ type="button"
+ class="btn"
+ data-dismiss="modal"
+ @click="emitCancel($event)"
+ >
+ {{ s__('Modal|Cancel') }}
+ </button>
+ <button
+ type="button"
+ class="btn"
+ :class="`btn-${footerPrimaryButtonVariant}`"
+ data-dismiss="modal"
+ @click="emitSubmit($event)"
+ >
+ {{ footerPrimaryButtonText }}
+ </button>
+ </slot>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index dcd98cb522f..7e829826eba 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -255,8 +255,6 @@ ul.controls {
}
.author_link {
- display: inline-block;
-
.avatar-inline {
margin-left: 0;
margin-right: 0;
diff --git a/app/models/identity.rb b/app/models/identity.rb
index b3fa7d8176a..2b433e9b988 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -9,6 +9,7 @@ class Identity < ActiveRecord::Base
validates :user_id, uniqueness: { scope: :provider }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
+ after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
@@ -34,4 +35,12 @@ class Identity < ActiveRecord::Base
self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
end
+
+ def user_synced_attributes_metadata_from_provider?
+ user.user_synced_attributes_metadata&.provider == provider
+ end
+
+ def clear_user_synced_attributes
+ user.user_synced_attributes_metadata&.destroy
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1cf55fd4332..4f754b11da4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -593,7 +593,15 @@ class Repository
def license_key
return unless exists?
- Licensee.license(path).try(:key)
+ # The licensee gem creates a Rugged object from the path:
+ # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
+ begin
+ Licensee.license(path).try(:key)
+ # Normally we would rescue Rugged::Error, but that is banned by lint-rugged
+ # and we need to migrate this endpoint to Gitaly:
+ # https://gitlab.com/gitlab-org/gitaly/issues/1026
+ rescue
+ end
end
cache_method :license_key
diff --git a/app/models/user.rb b/app/models/user.rb
index 4097fe2b5dc..5e84d2da805 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -249,7 +249,7 @@ class User < ActiveRecord::Base
def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
- where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
+ where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
else
find_by(conditions)
end
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 2f511ab44b7..299b9c6215f 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -19,19 +19,10 @@ module Issues
# on rewriting notes (unfolding references)
#
ActiveRecord::Base.transaction do
- # New issue tasks
- #
@new_issue = create_new_issue
- rewrite_notes
- rewrite_issue_award_emoji
- add_note_moved_from
-
- # Old issue tasks
- #
- add_note_moved_to
- close_issue
- mark_as_moved
+ update_new_issue
+ update_old_issue
end
notify_participants
@@ -41,6 +32,18 @@ module Issues
private
+ def update_new_issue
+ rewrite_notes
+ rewrite_issue_award_emoji
+ add_note_moved_from
+ end
+
+ def update_old_issue
+ add_note_moved_to
+ close_issue
+ mark_as_moved
+ end
+
def create_new_issue
new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
milestone_id: cloneable_milestone_id,
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index a01676d82a8..4e3e2f7a475 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -7,10 +7,9 @@
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
- .nav-controls
- - if @all_builds.running_or_pending.any?
- #stop-jobs-modal
-
+ - if @all_builds.running_or_pending.any?
+ #stop-jobs-modal
+ .nav-controls
%button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#stop-jobs-modal',
url: cancel_all_admin_jobs_path } }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index e16d132f869..0931ceb1512 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -58,7 +58,7 @@
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
- = f.submit 'Save changes', class: "btn btn-success"
+ = f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
%section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml
index ff440e99042..160e325996a 100644
--- a/app/views/projects/pipeline_schedules/_form.html.haml
+++ b/app/views/projects/pipeline_schedules/_form.html.haml
@@ -1,7 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'schedule_form'
-
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 4fbdd1dd5e4..bcb6dddba1a 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,9 +1,5 @@
- breadcrumb_title _("Schedules")
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'schedules_index'
-
- @no_container = true
- page_title _("Pipeline Schedules")
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index ba55bc23add..a86cb14960a 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,9 +1,6 @@
- @no_container = true
- breadcrumb_title "CI / CD Charts"
- page_title _("Charts"), _("Pipelines")
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_d3')
- = page_specific_javascript_bundle_tag('graphs')
%div{ class: container_class }
.sub-header-block
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index a5dbd1b1532..510697c2ae9 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('pipelines_times')
-
%div
%p.light
= _("Commit duration in minutes for last 30 commits")
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 41dc2f6cf9d..2f4b6def155 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('pipelines_charts')
-
%h4= _("Pipelines charts")
%p
&nbsp;
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 0808b28a9df..17e804d682b 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('integrations')
-
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml
index 2c05ebe52ae..1a353953838 100644
--- a/app/views/projects/update.js.haml
+++ b/app/views/projects/update.js.haml
@@ -6,4 +6,4 @@
$(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide();
$('.project-edit-container').show();
- $('.edit-project .btn-save').enable();
+ $('.edit-project .js-btn-save-general-project-settings').enable();
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 1a259b679c7..8607be9cd06 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -23,11 +23,11 @@
- if show_archive_options
%li.divider
%li.js-filter-archived-projects
- = link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do
+ = link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects
%li.js-filter-archived-projects
- = link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
+ = link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
Show archived projects
%li.js-filter-archived-projects
- = link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
+ = link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
Show archived projects only
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index c9a77d668a2..a396d1007a7 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -4,9 +4,6 @@
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_d3'
- = webpack_bundle_tag 'users'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
diff --git a/changelogs/unreleased/39607-fix-avatar--vertical-align.yml b/changelogs/unreleased/39607-fix-avatar--vertical-align.yml
new file mode 100644
index 00000000000..4d9fee12f04
--- /dev/null
+++ b/changelogs/unreleased/39607-fix-avatar--vertical-align.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix user avatar's vertical align on the issues and merge requests pages"
+merge_request: 17072
+author: Laszlo Karpati
+type: fixed
diff --git a/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml b/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml
new file mode 100644
index 00000000000..543fd7c5e8d
--- /dev/null
+++ b/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml
@@ -0,0 +1,4 @@
+title: Fix 404 when listing archived projects in a group where all projects have been archived
+merge_request: 17077
+author: Ashley Dumaine
+type: fixed
diff --git a/changelogs/unreleased/42929-hide-new-variable-values.yml b/changelogs/unreleased/42929-hide-new-variable-values.yml
new file mode 100644
index 00000000000..68decd25b5a
--- /dev/null
+++ b/changelogs/unreleased/42929-hide-new-variable-values.yml
@@ -0,0 +1,5 @@
+---
+title: Hide CI secret variable values after saving
+merge_request: 17044
+author:
+type: changed
diff --git a/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml b/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml
new file mode 100644
index 00000000000..b527000332e
--- /dev/null
+++ b/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Allows project rename after validation error
+merge_request: 17150
+author:
+type: fixed
diff --git a/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml b/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml
new file mode 100644
index 00000000000..a51781396ee
--- /dev/null
+++ b/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml
@@ -0,0 +1,5 @@
+---
+title: Remove whitespace from the username/email sign in form field
+merge_request: 17020
+author: Peter lauck
+type: changed
diff --git a/changelogs/unreleased/dm-escape-commit-message.yml b/changelogs/unreleased/dm-escape-commit-message.yml
new file mode 100644
index 00000000000..89af2da3484
--- /dev/null
+++ b/changelogs/unreleased/dm-escape-commit-message.yml
@@ -0,0 +1,5 @@
+---
+title: Escape HTML entities in commit messages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml b/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml
new file mode 100644
index 00000000000..1e35f2b537d
--- /dev/null
+++ b/changelogs/unreleased/fj-37528-error-after-disabling-ldap.yml
@@ -0,0 +1,6 @@
+---
+title: Fixed error 500 when removing an identity with synced attributes and visiting
+ the profile page
+merge_request: 17054
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml b/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml
new file mode 100644
index 00000000000..cef339ef787
--- /dev/null
+++ b/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug with unauthenticated requests through git ssh
+merge_request: 17149
+author:
+type: fixed
diff --git a/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml b/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml
new file mode 100644
index 00000000000..09112fba85e
--- /dev/null
+++ b/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml
@@ -0,0 +1,5 @@
+---
+title: Only check LFS integrity for first ref in a push to avoid timeout
+merge_request: 17098
+author:
+type: performance
diff --git a/changelogs/unreleased/winh-new-modal-component.yml b/changelogs/unreleased/winh-new-modal-component.yml
new file mode 100644
index 00000000000..bcc0d489c88
--- /dev/null
+++ b/changelogs/unreleased/winh-new-modal-component.yml
@@ -0,0 +1,5 @@
+---
+title: Add new modal Vue component
+merge_request: 17108
+author:
+type: changed
diff --git a/config/application.rb b/config/application.rb
index c914e34b9c3..918bd4d57cf 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -69,6 +69,7 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
+ # - Secret variable values (:value)
config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i(
certificate
@@ -80,6 +81,7 @@ module Gitlab
sentry_dsn
trace
variables
+ value
)
# Enable escaping HTML in JSON.
diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb
index 9453df2ec5a..a90516eee7d 100644
--- a/config/initializers/rack_attack_global.rb
+++ b/config/initializers/rack_attack_global.rb
@@ -26,6 +26,7 @@ class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? &&
+ !req.api_internal_request? &&
req.ip
end
@@ -54,6 +55,10 @@ class Rack::Attack
path.start_with?('/api')
end
+ def api_internal_request?
+ path =~ %r{^/api/v\d+/internal/}
+ end
+
def web_request?
!api_request?
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index a4e6c64fce5..d6cb6a8892f 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -68,7 +68,6 @@ var config = {
help: './help/help.js',
how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
- integrations: './integrations',
job_details: './jobs/job_details_bundle.js',
locale: './locale/index.js',
main: './main.js',
@@ -79,9 +78,7 @@ var config = {
notes: './notes/index.js',
pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js',
- pipelines_charts: './pipelines/pipelines_charts.js',
pipelines_details: './pipelines/pipeline_details_bundle.js',
- pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
prometheus_metrics: './prometheus_metrics',
@@ -90,8 +87,6 @@ var config = {
registry_list: './registry/index.js',
ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js',
- schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
- schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
@@ -102,7 +97,6 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
- users: './users/index.js',
webpack_runtime: './webpack.js',
},
@@ -158,7 +152,7 @@ var config = {
include: /node_modules\/katex\/dist/,
use: [
{ loader: 'style-loader' },
- {
+ {
loader: 'css-loader',
options: {
name: '[name].[hash].[ext]'
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 65f59b72690..d978d1dca53 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -9,7 +9,19 @@ created in snippets, wikis, and repos.
## PlantUML Server
Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
-server that will generate the diagrams. Installing and configuring your
+server that will generate the diagrams.
+
+### Docker
+
+With Docker, you can just run a container like this:
+
+`docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:tomcat`
+
+The **PlantUML URL** will be the hostname of the server running the container.
+
+### Debian/Ubuntu
+
+Installing and configuring your
own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
First you need to create a `plantuml.war` file from the source code:
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index 9d1589d84aa..a795d5116ea 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -56,7 +56,7 @@ new one, and attempting to pull a repo.
> **Warning:** Do not disable writes until SSH is confirmed to be working
perfectly, because the file will quickly become out-of-date.
-In the case of lookup failures (which are not uncommon), the `authorized_keys`
+In the case of lookup failures (which are common), the `authorized_keys`
file will still be scanned. So git SSH performance will still be slow for many
users as long as a large file exists.
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 7d47aaac299..edb3e4c961e 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -61,6 +61,21 @@ Before proceeding with the Pages configuration, you will need to:
NOTE: **Note:**
If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network.
+### Add the domain to the Public Suffix List
+
+The [Public Suffix List](https://publicsuffix.org) is used by browsers to
+decide how to treat subdomains. If your GitLab instance allows members of the
+public to create GitLab Pages sites, it also allows those users to create
+subdomains on the pages domain (`example.io`). Adding the domain to the Public
+Suffix List prevents browsers from accepting
+[supercookies](https://en.wikipedia.org/wiki/HTTP_cookie#Supercookie),
+among other things.
+
+Follow [these instructions](https://publicsuffix.org/submit/) to submit your
+GitLab Pages subdomain. For instance, if your domain is `example.io`, you should
+request that `*.example.io` is added to the Public Suffix List. GitLab.com
+added `*.gitlab.io` [in 2016](https://gitlab.com/gitlab-com/infrastructure/issues/230).
+
### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider
diff --git a/doc/development/fe_guide/components.md b/doc/development/fe_guide/components.md
new file mode 100644
index 00000000000..66a8abe42f7
--- /dev/null
+++ b/doc/development/fe_guide/components.md
@@ -0,0 +1,61 @@
+# Components
+
+## Contents
+* [Dropdowns](#dropdowns)
+* [Modals](#modals)
+
+## Dropdowns
+
+See also the [corresponding UX guide](../ux_guide/components.md#dropdowns).
+
+### How to style a bootstrap dropdown
+1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
+1. Add a specific class to the top level `.dropdown` element
+
+
+ ```Haml
+ .dropdown.my-dropdown
+ %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
+ %span.dropdown-toggle-text
+ Toggle Dropdown
+ = icon('chevron-down')
+
+ %ul.dropdown-menu
+ %li
+ %a
+ item!
+ ```
+
+ Or use the helpers
+ ```Haml
+ .dropdown.my-dropdown
+ = dropdown_toggle('Toogle!', { toggle: 'dropdown' })
+ = dropdown_content
+ %li
+ %a
+ item!
+ ```
+
+[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
+
+## Modals
+
+See also the [corresponding UX guide](../ux_guide/components.md#modals).
+
+We have a reusable Vue component for modals: [vue_shared/components/gl-modal.vue](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/vue_shared/components/gl-modal.vue)
+
+Here is an example of how to use it:
+
+```html
+ <gl-modal
+ id="dogs-out-modal"
+ :header-title-text="s__('ModalExample|Let the dogs out?')"
+ footer-primary-button-variant="danger"
+ :footer-primary-button-text="s__('ModalExample|Let them out')"
+ @submit="letOut(theDogs)"
+ >
+ {{ s__('ModalExample|You’re about to let the dogs out.') }}
+ </gl-modal>
+```
+
+![example modal](img/gl-modal.png)
diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md
index 6314f8f38d2..e9d6244355c 100644
--- a/doc/development/fe_guide/dropdowns.md
+++ b/doc/development/fe_guide/dropdowns.md
@@ -1,32 +1 @@
-# Dropdowns
-
-
-## How to style a bootstrap dropdown
-1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
-1. Add a specific class to the top level `.dropdown` element
-
-
- ```Haml
- .dropdown.my-dropdown
- %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
- %span.dropdown-toggle-text
- Toggle Dropdown
- = icon('chevron-down')
-
- %ul.dropdown-menu
- %li
- %a
- item!
- ```
-
- Or use the helpers
- ```Haml
- .dropdown.my-dropdown
- = dropdown_toggle('Toogle!', { toggle: 'dropdown' })
- = dropdown_content
- %li
- %a
- item!
- ```
-
-[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
+This page has moved [here](components.md#dropdowns).
diff --git a/doc/development/fe_guide/img/gl-modal.png b/doc/development/fe_guide/img/gl-modal.png
new file mode 100644
index 00000000000..47302e857bc
--- /dev/null
+++ b/doc/development/fe_guide/img/gl-modal.png
Binary files differ
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 72cb557d054..12dfc10812b 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -21,6 +21,8 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
+We also use [Axios][axios] to handle all of our network requests.
+
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
@@ -77,8 +79,10 @@ Axios specific practices and gotchas.
## [Icons](icons.md)
How we use SVG for our Icons.
-## [Dropdowns](dropdowns.md)
-How we use dropdowns.
+## [Components](components.md)
+
+How we use UI components.
+
---
## Style Guides
@@ -122,6 +126,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the
[webpack]: https://webpack.js.org/
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
+[axios]: https://github.com/axios/axios
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 97c997e0568..11878b4009b 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user')
# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
```
+For routes that require authorization you will need to provide a user to
+`Gitlab::Profiler`. You can do this like so:
+
+```ruby
+Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first)
+```
+
+The user you provide will need to have a [personal access
+token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) in
+the GitLab instance.
+
Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send
ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source.
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 01bd925bd6f..5f5ba2b69bc 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -95,7 +95,9 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
-under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops).
+either under the project's CI/CD settings while
+[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
+the CI/CD section.
It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
A wildcard DNS A record matching the base domain is required, for example,
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index ce081cedd71..da3c30a8eaf 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -18,7 +18,7 @@ documentation.
> **Important:**
For security reasons, when using the command line, we strongly recommend
-you to [connect with GitLab via SSH](../../../ssh/README.md).
+that you [connect with GitLab via SSH](../../../ssh/README.md).
## Files
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
deleted file mode 100644
index 3263d3e212b..00000000000
--- a/features/profile/profile.feature
+++ /dev/null
@@ -1,85 +0,0 @@
-@profile
-Feature: Profile
- Background:
- Given I sign in as a user
-
- Scenario: I look at my profile
- Given I visit profile page
- Then I should see my profile info
-
- @javascript
- Scenario: I can see groups I belong to
- Given I have group with projects
- When I visit profile page
- And I click on my profile picture
- Then I should see my user page
- And I should see groups I belong to
-
- Scenario: I edit profile
- Given I visit profile page
- Then I change my profile info
- And I should see new profile info
-
- Scenario: I change my password without old one
- Given I visit profile password page
- When I try change my password w/o old one
- Then I should see a missing password error message
- And I should be redirected to password page
-
- Scenario: I change my password
- Given I visit profile password page
- Then I change my password
- And I should be redirected to sign in page
-
- Scenario: I edit my avatar
- Given I visit profile page
- Then I change my avatar
- And I should see new avatar
- And I should see the "Remove avatar" button
- And I should see the gravatar host link
-
- Scenario: I remove my avatar
- Given I visit profile page
- And I have an avatar
- When I remove my avatar
- Then I should see my gravatar
- And I should not see the "Remove avatar" button
- And I should see the gravatar host link
-
- Scenario: My password is expired
- Given my password is expired
- And I am not an ldap user
- Given I visit profile password page
- Then I redirected to expired password page
- And I submit new password
- And I redirected to sign in page
-
- Scenario: I unsuccessfully change my password
- Given I visit profile password page
- When I unsuccessfully change my password
- Then I should see a password error message
-
- Scenario: I visit history tab
- Given I logout
- And I sign in via the UI
- And I have activity
- When I visit Authentication log page
- Then I should see my activity
-
- Scenario: I visit my user page
- When I visit profile page
- And I click on my profile picture
- Then I should see my user page
-
- Scenario: I can manage application
- Given I visit profile applications page
- Then I should see application form
- Then I fill application form out and submit
- And I see application
- Then I click edit
- And I see edit application form
- Then I change name of application and submit
- And I see that application was changed
- Then I visit profile applications page
- And I click to remove application
- Then I see that application is removed
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
deleted file mode 100644
index d3b88ae8d2a..00000000000
--- a/features/steps/profile/profile.rb
+++ /dev/null
@@ -1,226 +0,0 @@
-class Spinach::Features::Profile < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
-
- step 'I should see my profile info' do
- expect(page).to have_content "This information will appear on your profile"
- end
-
- step 'I change my profile info' do
- fill_in 'user_skype', with: 'testskype'
- fill_in 'user_linkedin', with: 'testlinkedin'
- fill_in 'user_twitter', with: 'testtwitter'
- fill_in 'user_website_url', with: 'testurl'
- fill_in 'user_location', with: 'Ukraine'
- fill_in 'user_bio', with: 'I <3 GitLab'
- fill_in 'user_organization', with: 'GitLab'
- click_button 'Update profile settings'
- @user.reload
- end
-
- step 'I should see new profile info' do
- expect(@user.skype).to eq 'testskype'
- expect(@user.linkedin).to eq 'testlinkedin'
- expect(@user.twitter).to eq 'testtwitter'
- expect(@user.website_url).to eq 'testurl'
- expect(@user.bio).to eq 'I <3 GitLab'
- expect(@user.organization).to eq 'GitLab'
- expect(find('#user_location').value).to eq 'Ukraine'
- end
-
- step 'I change my avatar' do
- attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Update profile settings"
- @user.reload
- end
-
- step 'I should see new avatar' do
- expect(@user.avatar).to be_instance_of AvatarUploader
- expect(@user.avatar.url).to eq "/uploads/-/system/user/avatar/#{@user.id}/banana_sample.gif"
- end
-
- step 'I should see the "Remove avatar" button' do
- expect(page).to have_link("Remove avatar")
- end
-
- step 'I have an avatar' do
- attach_file(:user_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
- click_button "Update profile settings"
- @user.reload
- end
-
- step 'I remove my avatar' do
- click_link "Remove avatar"
- @user.reload
- end
-
- step 'I should see my gravatar' do
- expect(@user.avatar?).to eq false
- end
-
- step 'I should not see the "Remove avatar" button' do
- expect(page).not_to have_link("Remove avatar")
- end
-
- step 'I should see the gravatar host link' do
- expect(page).to have_link("gravatar.com")
- end
-
- step 'I try change my password w/o old one' do
- page.within '.update-password' do
- fill_in "user_password", with: "22233344"
- fill_in "user_password_confirmation", with: "22233344"
- click_button "Save password"
- end
- end
-
- step 'I change my password' do
- page.within '.update-password' do
- fill_in "user_current_password", with: "12345678"
- fill_in "user_password", with: "22233344"
- fill_in "user_password_confirmation", with: "22233344"
- click_button "Save password"
- end
- end
-
- step 'I unsuccessfully change my password' do
- page.within '.update-password' do
- fill_in "user_current_password", with: "12345678"
- fill_in "user_password", with: "password"
- fill_in "user_password_confirmation", with: "confirmation"
- click_button "Save password"
- end
- end
-
- step "I should see a missing password error message" do
- page.within ".flash-container" do
- expect(page).to have_content "You must provide a valid current password"
- end
- end
-
- step "I should see a password error message" do
- page.within '.alert-danger' do
- expect(page).to have_content "Password confirmation doesn't match"
- end
- end
-
- step 'I have activity' do
- create(:closed_issue_event, author: current_user)
- end
-
- step 'I should see my activity' do
- expect(page).to have_content "Signed in with standard authentication"
- end
-
- step 'my password is expired' do
- current_user.update_attributes(password_expires_at: Time.now - 1.hour)
- end
-
- step "I am not an ldap user" do
- current_user.identities.delete
- expect(current_user.ldap_user?).to eq false
- end
-
- step 'I redirected to expired password page' do
- expect(current_path).to eq new_profile_password_path
- end
-
- step 'I submit new password' do
- fill_in :user_current_password, with: '12345678'
- fill_in :user_password, with: '12345678'
- fill_in :user_password_confirmation, with: '12345678'
- click_button "Set new password"
- end
-
- step 'I redirected to sign in page' do
- expect(current_path).to eq new_user_session_path
- end
-
- step 'I should be redirected to password page' do
- expect(current_path).to eq edit_profile_password_path
- end
-
- step 'I should be redirected to account page' do
- expect(current_path).to eq profile_account_path
- end
-
- step 'I click on my profile picture' do
- find(:css, '.header-user-dropdown-toggle').click
-
- page.within ".header-user" do
- click_link "Profile"
- end
- end
-
- step 'I should see my user page' do
- page.within ".cover-block" do
- expect(page).to have_content current_user.name
- expect(page).to have_content current_user.username
- end
- end
-
- step 'I have group with projects' do
- @group = create(:group)
- @group.add_owner(current_user)
- @project = create(:project, :repository, namespace: @group)
- @event = create(:closed_issue_event, project: @project)
-
- @project.add_master(current_user)
- end
-
- step 'I should see groups I belong to' do
- page.within ".content" do
- click_link "Groups"
- end
-
- page.within "#groups" do
- expect(page).to have_content @group.name
- end
- end
-
- step 'I should see application form' do
- expect(page).to have_content "Add new application"
- end
-
- step 'I fill application form out and submit' do
- fill_in :doorkeeper_application_name, with: 'test'
- fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
- click_on "Save application"
- end
-
- step 'I see application' do
- expect(page).to have_content "Application: test"
- expect(page).to have_content "Application Id"
- expect(page).to have_content "Secret"
- end
-
- step 'I click edit' do
- click_on "Edit"
- end
-
- step 'I see edit application form' do
- expect(page).to have_content "Edit application"
- end
-
- step 'I change name of application and submit' do
- expect(page).to have_content "Edit application"
- fill_in :doorkeeper_application_name, with: 'test_changed'
- click_on "Save application"
- end
-
- step 'I see that application was changed' do
- expect(page).to have_content "test_changed"
- expect(page).to have_content "Application Id"
- expect(page).to have_content "Secret"
- end
-
- step 'I click to remove application' do
- page.within '.oauth-applications' do
- click_on "Destroy"
- end
- end
-
- step "I see that application is removed" do
- expect(page.find(".oauth-applications")).not_to have_content "test_changed"
- end
-end
diff --git a/features/support/env.rb b/features/support/env.rb
index 7f5b4c1c11b..15211995918 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -20,15 +20,16 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods
+ include ActiveJob::TestHelper
+ include FactoryBot::Syntax::Methods
+ include GitlabRoutingHelper
+
RSpec::Mocks.setup
TestEnv.init(mailer: false)
# skip pre-receive hook check so we can use
# web editor and merge
TestEnv.disable_pre_receive
-
- include FactoryBot::Syntax::Methods
- include GitlabRoutingHelper
end
Spinach.hooks.after_scenario do |scenario_data, step_definitions|
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
index f3bd587c28b..e008fd428b0 100644
--- a/lib/banzai/filter/html_entity_filter.rb
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -5,7 +5,7 @@ module Banzai
# Text filter that escapes these HTML entities: & " < >
class HtmlEntityFilter < HTML::Pipeline::TextFilter
def call
- ERB::Util.html_escape_once(text)
+ ERB::Util.html_escape(text)
end
end
end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index d75e73dac10..521680b8708 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -16,11 +16,11 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
- attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
+ attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize(
change, user_access:, project:, skip_authorization: false,
- protocol:
+ skip_lfs_integrity_check: false, protocol:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
@@ -28,6 +28,7 @@ module Gitlab
@user_access = user_access
@project = project
@skip_authorization = skip_authorization
+ @skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
end
@@ -37,7 +38,7 @@ module Gitlab
push_checks
branch_checks
tag_checks
- lfs_objects_exist_check
+ lfs_objects_exist_check unless skip_lfs_integrity_check
commits_check unless skip_commits_check
true
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 8ec3386184a..9ec3858b493 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -238,19 +238,22 @@ module Gitlab
changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied
- changes_list.each do |change|
+ changes_list.each.with_index do |change, index|
+ first_change = index == 0
+
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
- check_single_change_access(change)
+ check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end
end
- def check_single_change_access(change)
+ def check_single_change_access(change, skip_lfs_integrity_check: false)
Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
+ skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol
).exec
end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 1c9477e84b2..84d6e1490c3 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -13,7 +13,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
- def check_single_change_access(change)
+ def check_single_change_access(change, _options = {})
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 47b3fce3e7a..a6bea98d631 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def self.servers
- Gitlab.config.ldap.servers.values
+ Gitlab.config.ldap['servers']&.values || []
end
def self.available_servers
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index a3e1c66c19f..28ebac1776e 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -198,9 +198,11 @@ module Gitlab
end
def update_profile
+ clear_user_synced_attributes_metadata
+
return unless sync_profile_from_provider? || creating_linked_ldap_user?
- metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
+ metadata = gl_user.build_user_synced_attributes_metadata
if sync_profile_from_provider?
UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
@@ -221,6 +223,10 @@ module Gitlab
end
end
+ def clear_user_synced_attributes_metadata
+ gl_user&.user_synced_attributes_metadata&.destroy
+ end
+
def log
Gitlab::AppLogger
end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 95d94b3cc68..98a168b43bb 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -45,6 +45,7 @@ module Gitlab
if user
private_token ||= user.personal_access_tokens.active.pluck(:token).first
+ raise 'Your user must have a personal_access_token' unless private_token
end
headers['Private-Token'] = private_token if private_token
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index b603557f59c..0c7ad46d36b 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -45,6 +45,10 @@ module QA
end
def new_merge_request
+ wait(reload: true) do
+ has_css?(element_selector_css(:create_merge_request))
+ end
+
click_element :create_merge_request
end
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 4665626f114..1d7700b6767 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -1,6 +1,15 @@
require 'spec_helper'
describe 'Profile > Password' do
+ let(:user) { create(:user) }
+
+ def fill_passwords(password, confirmation)
+ fill_in 'New password', with: password
+ fill_in 'Password confirmation', with: confirmation
+
+ click_button 'Save password'
+ end
+
context 'Password authentication enabled' do
let(:user) { create(:user, password_automatically_set: true) }
@@ -9,13 +18,6 @@ describe 'Profile > Password' do
visit edit_profile_password_path
end
- def fill_passwords(password, confirmation)
- fill_in 'New password', with: password
- fill_in 'Password confirmation', with: confirmation
-
- click_button 'Save password'
- end
-
context 'User with password automatically set' do
describe 'User puts different passwords in the field and in the confirmation' do
it 'shows an error message' do
@@ -73,4 +75,64 @@ describe 'Profile > Password' do
end
end
end
+
+ context 'Change passowrd' do
+ before do
+ sign_in(user)
+ visit(edit_profile_password_path)
+ end
+
+ it 'does not change user passowrd without old one' do
+ page.within '.update-password' do
+ fill_passwords('22233344', '22233344')
+ end
+
+ page.within '.flash-container' do
+ expect(page).to have_content 'You must provide a valid current password'
+ end
+ end
+
+ it 'does not change password with invalid old password' do
+ page.within '.update-password' do
+ fill_in 'user_current_password', with: 'invalid'
+ fill_passwords('password', 'confirmation')
+ end
+
+ page.within '.flash-container' do
+ expect(page).to have_content 'You must provide a valid current password'
+ end
+ end
+
+ it 'changes user password' do
+ page.within '.update-password' do
+ fill_in "user_current_password", with: user.password
+ fill_passwords('22233344', '22233344')
+ end
+
+ expect(current_path).to eq new_user_session_path
+ end
+ end
+
+ context 'when password is expired' do
+ before do
+ sign_in(user)
+
+ user.update_attributes(password_expires_at: 1.hour.ago)
+ user.identities.delete
+ expect(user.ldap_user?).to eq false
+ end
+
+ it 'needs change user password' do
+ visit edit_profile_password_path
+
+ expect(current_path).to eq new_profile_password_path
+
+ fill_in :user_current_password, with: user.password
+ fill_in :user_password, with: '12345678'
+ fill_in :user_password_confirmation, with: '12345678'
+ click_button 'Set new password'
+
+ expect(current_path).to eq new_user_session_path
+ end
+ end
end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
new file mode 100644
index 00000000000..0b5eacbe916
--- /dev/null
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe 'User edit profile' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit(profile_path)
+ end
+
+ it 'changes user profile' do
+ fill_in 'user_skype', with: 'testskype'
+ fill_in 'user_linkedin', with: 'testlinkedin'
+ fill_in 'user_twitter', with: 'testtwitter'
+ fill_in 'user_website_url', with: 'testurl'
+ fill_in 'user_location', with: 'Ukraine'
+ fill_in 'user_bio', with: 'I <3 GitLab'
+ fill_in 'user_organization', with: 'GitLab'
+ click_button 'Update profile settings'
+
+ expect(user.reload).to have_attributes(
+ skype: 'testskype',
+ linkedin: 'testlinkedin',
+ twitter: 'testtwitter',
+ website_url: 'testurl',
+ bio: 'I <3 GitLab',
+ organization: 'GitLab'
+ )
+
+ expect(find('#user_location').value).to eq 'Ukraine'
+ expect(page).to have_content('Profile was successfully updated')
+ end
+
+ context 'user avatar' do
+ before do
+ attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
+ click_button 'Update profile settings'
+ end
+
+ it 'changes user avatar' do
+ expect(page).to have_link('Remove avatar')
+
+ user.reload
+ expect(user.avatar).to be_instance_of AvatarUploader
+ expect(user.avatar.url).to eq "/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif"
+ end
+
+ it 'removes user avatar' do
+ click_link 'Remove avatar'
+
+ user.reload
+
+ expect(user.avatar?).to eq false
+ expect(page).not_to have_link('Remove avatar')
+ expect(page).to have_link('gravatar.com')
+ end
+ end
+end
diff --git a/spec/features/profiles/user_manages_applications_spec.rb b/spec/features/profiles/user_manages_applications_spec.rb
new file mode 100644
index 00000000000..387584fef62
--- /dev/null
+++ b/spec/features/profiles/user_manages_applications_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'User manages applications' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit applications_profile_path
+ end
+
+ it 'manages applications' do
+ expect(page).to have_content 'Add new application'
+
+ fill_in :doorkeeper_application_name, with: 'test'
+ fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
+ click_on 'Save application'
+
+ expect(page).to have_content 'Application: test'
+ expect(page).to have_content 'Application Id'
+ expect(page).to have_content 'Secret'
+
+ click_on 'Edit'
+
+ expect(page).to have_content 'Edit application'
+ fill_in :doorkeeper_application_name, with: 'test_changed'
+ click_on 'Save application'
+
+ expect(page).to have_content 'test_changed'
+ expect(page).to have_content 'Application Id'
+ expect(page).to have_content 'Secret'
+
+ visit applications_profile_path
+
+ page.within '.oauth-applications' do
+ click_on 'Destroy'
+ end
+ expect(page.find('.oauth-applications')).not_to have_content 'test_changed'
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
index a50ebb29e01..0f419c3c2c0 100644
--- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -3,13 +3,28 @@ require 'spec_helper'
describe 'User visits the authentication log' do
let(:user) { create(:user) }
- before do
- sign_in(user)
+ context 'when user signed in' do
+ before do
+ sign_in(user)
+ end
- visit(audit_log_profile_path)
+ it 'shows correct menu item' do
+ visit(audit_log_profile_path)
+
+ expect(page).to have_active_navigation('Authentication log')
+ end
end
- it 'shows correct menu item' do
- expect(page).to have_active_navigation('Authentication log')
+ context 'when user has activity' do
+ before do
+ create(:closed_issue_event, author: user)
+ gitlab_sign_in(user)
+ end
+
+ it 'shows user activity' do
+ visit(audit_log_profile_path)
+
+ expect(page).to have_content 'Signed in with standard authentication'
+ end
end
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index a5d80439143..713112477c8 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -5,20 +5,58 @@ describe 'User visits their profile' do
before do
sign_in(user)
-
- visit(profile_path)
end
it 'shows correct menu item' do
+ visit(profile_path)
+
expect(page).to have_active_navigation('Profile')
end
- describe 'profile settings', :js do
- it 'saves updates' do
- fill_in 'user_bio', with: 'bio'
- click_button 'Update profile settings'
+ it 'shows profile info' do
+ visit(profile_path)
+
+ expect(page).to have_content "This information will appear on your profile"
+ end
+
+ context 'when user has groups' do
+ let(:group) do
+ create :group do |group|
+ group.add_owner(user)
+ end
+ end
+
+ let!(:project) do
+ create(:project, :repository, namespace: group) do |project|
+ create(:closed_issue_event, project: project)
+ project.add_master(user)
+ end
+ end
+
+ def click_on_profile_picture
+ find(:css, '.header-user-dropdown-toggle').click
+
+ page.within ".header-user" do
+ click_link "Profile"
+ end
+ end
+
+ it 'shows user groups', :js do
+ visit(profile_path)
+ click_on_profile_picture
+
+ page.within ".cover-block" do
+ expect(page).to have_content user.name
+ expect(page).to have_content user.username
+ end
+
+ page.within ".content" do
+ click_link "Groups"
+ end
- expect(page).to have_content('Profile was successfully updated')
+ page.within "#groups" do
+ expect(page).to have_content group.name
+ end
end
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 8a80b88da5d..fccde8b7eba 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -20,5 +20,9 @@ describe EventsHelper do
it 'handles nil values' do
expect(helper.event_commit_title(nil)).to eq('')
end
+
+ it 'does not escape HTML entities' do
+ expect(helper.event_commit_title("foo & bar")).to eq("foo & bar")
+ end
end
end
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index 5b9cdceee71..ee457a9c48c 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -1,8 +1,10 @@
+import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables';
+const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => {
preloadFixtures('projects/ci_cd_settings.html.raw');
@@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => {
const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon');
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
- expect(loadingIcon.classList.contains('hide')).toEqual(false);
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
return [200, {}];
});
- expect(loadingIcon.classList.contains('hide')).toEqual(true);
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
- expect(loadingIcon.classList.contains('hide')).toEqual(true);
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
})
.then(done)
.catch(done.fail);
@@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => {
it('hides any previous error box', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
- expect(errorBox.classList.contains('hide')).toEqual(true);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
- expect(errorBox.classList.contains('hide')).toEqual(true);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
})
.then(done)
.catch(done.fail);
@@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => {
.catch(done.fail);
});
+ it('hides secret values', (done) => {
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
+
+ const row = container.querySelector('.js-row:first-child');
+ const valueInput = row.querySelector('.js-ci-variable-input-value');
+ const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
+
+ valueInput.value = 'bar';
+ $(valueInput).trigger('input');
+
+ expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
+ expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
+
+ ajaxVariableList.onSaveClicked()
+ .then(() => {
+ expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
+ expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
it('shows error box with validation errors', (done) => {
const validationError = 'some validation error';
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [
validationError,
]);
- expect(errorBox.classList.contains('hide')).toEqual(true);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
- expect(errorBox.classList.contains('hide')).toEqual(false);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`);
})
.then(done)
@@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => {
it('shows flash message when request fails', (done) => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
- expect(errorBox.classList.contains('hide')).toEqual(true);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
ajaxVariableList.onSaveClicked()
.then(() => {
- expect(errorBox.classList.contains('hide')).toEqual(true);
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
})
.then(done)
.catch(done.fail);
@@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => {
const valueInput = row.querySelector('.js-ci-variable-input-value');
keyInput.value = 'foo';
- keyInput.dispatchEvent(new Event('input'));
+ $(keyInput).trigger('input');
valueInput.value = 'bar';
- valueInput.dispatchEvent(new Event('input'));
+ $(valueInput).trigger('input');
expect(idInput.value).toEqual('');
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
index 8acb346901f..cac785fd3c6 100644
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -1,6 +1,8 @@
import VariableList from '~/ci_variable_list/ci_variable_list';
import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+const HIDE_CLASS = 'hide';
+
describe('VariableList', () => {
preloadFixtures('pipeline_schedules/edit.html.raw');
preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
@@ -92,14 +94,14 @@ describe('VariableList', () => {
const $inputValue = $row.find('.js-ci-variable-input-value');
const $placeholder = $row.find('.js-secret-value-placeholder');
- expect($placeholder.hasClass('hide')).toBe(false);
- expect($inputValue.hasClass('hide')).toBe(true);
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
// Reveal values
$wrapper.find('.js-secret-value-reveal-button').click();
- expect($placeholder.hasClass('hide')).toBe(true);
- expect($inputValue.hasClass('hide')).toBe(false);
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
});
});
});
@@ -179,4 +181,35 @@ describe('VariableList', () => {
expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
});
});
+
+ describe('hideValues', () => {
+ beforeEach(() => {
+ loadFixtures('projects/ci_cd_settings.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should hide value input and show placeholder stars', () => {
+ const $row = $wrapper.find('.js-row');
+ const $inputValue = $row.find('.js-ci-variable-input-value');
+ const $placeholder = $row.find('.js-secret-value-placeholder');
+
+ $row.find('.js-ci-variable-input-value')
+ .val('foo')
+ .trigger('input');
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
+
+ variableList.hideValues();
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index 44ec9e4eabf..1daccc8dd02 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -4,6 +4,8 @@ import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
describe('Commits List', () => {
+ let commitsList;
+
beforeEach(() => {
setFixtures(`
<form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
@@ -11,6 +13,7 @@ describe('Commits List', () => {
</form>
<ol id="commits-list"></ol>
`);
+ commitsList = new CommitsList(25);
});
it('should be defined', () => {
@@ -19,7 +22,7 @@ describe('Commits List', () => {
describe('processCommits', () => {
it('should join commit headers', () => {
- CommitsList.$contentList = $(`
+ commitsList.$contentList = $(`
<div>
<li class="commit-header" data-day="2016-09-20">
<span class="day">20 Sep, 2016</span>
@@ -39,7 +42,7 @@ describe('Commits List', () => {
// The last commit header should be removed
// since the previous one has the same data-day value.
- expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0);
+ expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0);
});
});
@@ -48,8 +51,7 @@ describe('Commits List', () => {
let mock;
beforeEach(() => {
- CommitsList.init(25);
- CommitsList.searchField.val('');
+ commitsList.searchField.val('');
spyOn(history, 'replaceState').and.stub();
mock = new MockAdapter(axios);
@@ -66,11 +68,11 @@ describe('Commits List', () => {
});
it('should save the last search string', (done) => {
- CommitsList.searchField.val('GitLab');
- CommitsList.filterResults()
+ commitsList.searchField.val('GitLab');
+ commitsList.filterResults()
.then(() => {
expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
+ expect(commitsList.lastSearch).toEqual('GitLab');
done();
})
@@ -78,10 +80,10 @@ describe('Commits List', () => {
});
it('should not make ajax call if the input does not change', (done) => {
- CommitsList.filterResults()
+ commitsList.filterResults()
.then(() => {
expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
+ expect(commitsList.lastSearch).toEqual('');
done();
})
diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js
index bb49c576e91..71a2cd51f63 100644
--- a/spec/javascripts/importer_status_spec.js
+++ b/spec/javascripts/importer_status_spec.js
@@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
describe('Importer Status', () => {
+ let instance;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
describe('addToImport', () => {
- let instance;
- let mock;
const importUrl = '/import_url';
beforeEach(() => {
@@ -21,11 +30,6 @@ describe('Importer Status', () => {
spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
instance = new ImporterStatus('', importUrl);
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
});
it('sets table row to active after post request', (done) => {
@@ -44,4 +48,60 @@ describe('Importer Status', () => {
.catch(done.fail);
});
});
+
+ describe('autoUpdate', () => {
+ const jobsUrl = '/jobs_url';
+
+ beforeEach(() => {
+ const div = document.createElement('div');
+ div.innerHTML = `
+ <div id="project_1">
+ <div class="job-status">
+ </div>
+ </div>
+ `;
+
+ document.body.appendChild(div);
+
+ spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {});
+ spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {});
+ instance = new ImporterStatus(jobsUrl);
+ });
+
+ function setupMock(importStatus) {
+ mock.onGet(jobsUrl).reply(200, [{
+ id: 1,
+ import_status: importStatus,
+ }]);
+ }
+
+ function expectJobStatus(done, status) {
+ instance.autoUpdate()
+ .then(() => {
+ expect(document.querySelector('#project_1').innerText.trim()).toEqual(status);
+ done();
+ })
+ .catch(done.fail);
+ }
+
+ it('sets the job status to done', (done) => {
+ setupMock('finished');
+ expectJobStatus(done, 'done');
+ });
+
+ it('sets the job status to scheduled', (done) => {
+ setupMock('scheduled');
+ expectJobStatus(done, 'scheduled');
+ });
+
+ it('sets the job status to started', (done) => {
+ setupMock('started');
+ expectJobStatus(done, 'started');
+ });
+
+ it('sets the job status to custom status', (done) => {
+ setupMock('custom status');
+ expectJobStatus(done, 'custom status');
+ });
+ });
});
diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 040d14efed2..4655e29eed0 100644
--- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
-import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
Vue.use(Translate);
diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index ed481cb60a1..f95a7cef18a 100644
--- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
-import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue';
+import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js
new file mode 100644
index 00000000000..d6148cb785b
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js
@@ -0,0 +1,192 @@
+import Vue from 'vue';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const modalComponent = Vue.extend(GlModal);
+
+describe('GlModal', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('props', () => {
+ describe('with id', () => {
+ const props = {
+ id: 'my-modal',
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, props);
+ });
+
+ it('assigns the id to the modal', () => {
+ expect(vm.$el.id).toBe(props.id);
+ });
+ });
+
+ describe('without id', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, { });
+ });
+
+ it('does not add an id attribute to the modal', () => {
+ expect(vm.$el.hasAttribute('id')).toBe(false);
+ });
+ });
+
+ describe('with headerTitleText', () => {
+ const props = {
+ headerTitleText: 'my title text',
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, props);
+ });
+
+ it('sets the modal title', () => {
+ const modalTitle = vm.$el.querySelector('.modal-title');
+ expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText);
+ });
+ });
+
+ describe('with footerPrimaryButtonVariant', () => {
+ const props = {
+ footerPrimaryButtonVariant: 'danger',
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, props);
+ });
+
+ it('sets the primary button class', () => {
+ const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
+ expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`);
+ });
+ });
+
+ describe('with footerPrimaryButtonText', () => {
+ const props = {
+ footerPrimaryButtonText: 'my button text',
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, props);
+ });
+
+ it('sets the primary button text', () => {
+ const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type');
+ expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText);
+ });
+ });
+ });
+
+ it('works with data-toggle="modal"', (done) => {
+ setFixtures(`
+ <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button>
+ <div id="modal-container"></div>
+ `);
+
+ const modalContainer = document.getElementById('modal-container');
+ const modalButton = document.getElementById('modal-button');
+ vm = mountComponent(modalComponent, {
+ id: 'my-modal',
+ }, modalContainer);
+ $(vm.$el).on('shown.bs.modal', () => done());
+
+ modalButton.click();
+ });
+
+ describe('methods', () => {
+ const dummyEvent = 'not really an event';
+
+ beforeEach(() => {
+ vm = mountComponent(modalComponent, { });
+ spyOn(vm, '$emit');
+ });
+
+ describe('emitCancel', () => {
+ it('emits a cancel event', () => {
+ vm.emitCancel(dummyEvent);
+
+ expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent);
+ });
+ });
+
+ describe('emitSubmit', () => {
+ it('emits a submit event', () => {
+ vm.emitSubmit(dummyEvent);
+
+ expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent);
+ });
+ });
+ });
+
+ describe('slots', () => {
+ const slotContent = 'this should go into the slot';
+ const modalWithSlot = (slotName) => {
+ let template;
+ if (slotName) {
+ template = `
+ <gl-modal>
+ <template slot="${slotName}">${slotContent}</template>
+ </gl-modal>
+ `;
+ } else {
+ template = `<gl-modal>${slotContent}</gl-modal>`;
+ }
+
+ return Vue.extend({
+ components: {
+ GlModal,
+ },
+ template,
+ });
+ };
+
+ describe('default slot', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalWithSlot());
+ });
+
+ it('sets the modal body', () => {
+ const modalBody = vm.$el.querySelector('.modal-body');
+ expect(modalBody.innerHTML).toBe(slotContent);
+ });
+ });
+
+ describe('header slot', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalWithSlot('header'));
+ });
+
+ it('sets the modal header', () => {
+ const modalHeader = vm.$el.querySelector('.modal-header');
+ expect(modalHeader.innerHTML).toBe(slotContent);
+ });
+ });
+
+ describe('title slot', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalWithSlot('title'));
+ });
+
+ it('sets the modal title', () => {
+ const modalTitle = vm.$el.querySelector('.modal-title');
+ expect(modalTitle.innerHTML).toBe(slotContent);
+ });
+ });
+
+ describe('footer slot', () => {
+ beforeEach(() => {
+ vm = mountComponent(modalWithSlot('footer'));
+ });
+
+ it('sets the modal footer', () => {
+ const modalFooter = vm.$el.querySelector('.modal-footer');
+ expect(modalFooter.innerHTML).toBe(slotContent);
+ });
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
index 91e18d876d5..1d98fc0d5db 100644
--- a/spec/lib/banzai/filter/html_entity_filter_spec.rb
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -3,17 +3,12 @@ require 'spec_helper'
describe Banzai::Filter::HtmlEntityFilter do
include FilterSpecHelper
- let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' }
- let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' }
+ let(:unescaped) { 'foo <strike attr="foo">&&amp;&</strike>' }
+ let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;amp;&amp;&lt;/strike&gt;' }
it 'converts common entities to their HTML-escaped equivalents' do
output = filter(unescaped)
expect(output).to eq(escaped)
end
-
- it 'does not double-escape' do
- escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'")
- expect(filter(escaped)).to eq(escaped)
- end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 3c3697e7aa9..19d3f55501e 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -18,8 +18,9 @@ describe Gitlab::GitAccess do
redirected_path: redirected_path)
end
- let(:push_access_check) { access.check('git-receive-pack', '_any') }
- let(:pull_access_check) { access.check('git-upload-pack', '_any') }
+ let(:changes) { '_any' }
+ let(:push_access_check) { access.check('git-receive-pack', changes) }
+ let(:pull_access_check) { access.check('git-upload-pack', changes) }
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
@@ -646,6 +647,20 @@ describe Gitlab::GitAccess do
end
end
+ describe 'check LFS integrity' do
+ let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'checks LFS integrity only for first change' do
+ expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times
+
+ push_access_check
+ end
+ end
+
describe '#check_push_access!' do
before do
merge_into_protected_branch
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index ca2213cd112..e10837578a8 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -5,6 +5,14 @@ describe Gitlab::LDAP::Config do
let(:config) { described_class.new('ldapmain') }
+ describe '.servers' do
+ it 'returns empty array if no server information is available' do
+ allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false)
+
+ expect(described_class.servers).to eq []
+ end
+ end
+
describe '#initialize' do
it 'requires a provider' do
expect { described_class.new }.to raise_error ArgumentError
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 03e0a9e2a03..b8455403bdb 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -724,6 +724,10 @@ describe Gitlab::OAuth::User do
it "does not update the user location" do
expect(gl_user.location).not_to eq(info_hash[:address][:country])
end
+
+ it 'does not create associated user synced attributes metadata' do
+ expect(gl_user.user_synced_attributes_metadata).to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 4a43dbb2371..f02b1cf55fb 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -53,6 +53,15 @@ describe Gitlab::Profiler do
described_class.profile('/', user: user)
end
+ context 'when providing a user without a personal access token' do
+ it 'raises an error' do
+ user = double(:user)
+ allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([])
+
+ expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token')
+ end
+ end
+
it 'uses the private_token for auth if both it and user are set' do
user = double(:user)
user_token = 'user'
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index 7c66c98231b..a5ce245c21d 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -70,5 +70,38 @@ describe Identity do
end
end
end
+
+ context 'after_destroy' do
+ let!(:user) { create(:user) }
+ let(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', user: user) }
+ let(:ldap_user_synced_attributes) { { provider: 'ldapmain', name_synced: true, email_synced: true } }
+ let(:other_provider_user_synced_attributes) { { provider: 'other', name_synced: true, email_synced: true } }
+
+ describe 'if user synced attributes metadada provider' do
+ context 'matches the identity provider ' do
+ it 'removes the user synced attributes' do
+ user.create_user_synced_attributes_metadata(ldap_user_synced_attributes)
+
+ expect(user.user_synced_attributes_metadata.provider).to eq 'ldapmain'
+
+ ldap_identity.destroy
+
+ expect(user.reload.user_synced_attributes_metadata).to be_nil
+ end
+ end
+
+ context 'does not matche the identity provider' do
+ it 'does not remove the user synced attributes' do
+ user.create_user_synced_attributes_metadata(other_provider_user_synced_attributes)
+
+ expect(user.user_synced_attributes_metadata.provider).to eq 'other'
+
+ ldap_identity.destroy
+
+ expect(user.reload.user_synced_attributes_metadata.provider).to eq 'other'
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a6d48e369ac..0bc07dc7a85 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -873,6 +873,18 @@ describe Repository do
expect(repository.license_key).to be_nil
end
+ it 'returns nil when the commit SHA does not exist' do
+ allow(repository.head_commit).to receive(:sha).and_return('1' * 40)
+
+ expect(repository.license_key).to be_nil
+ end
+
+ it 'returns nil when master does not exist' do
+ repository.rm_branch(user, 'master')
+
+ expect(repository.license_key).to be_nil
+ end
+
it 'returns the license key' do
repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 76a6aef39cc..1815696a8a0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -893,6 +893,14 @@ describe User do
end
end
+ describe '.find_for_database_authentication' do
+ it 'strips whitespace from login' do
+ user = create(:user)
+
+ expect(described_class.find_for_database_authentication({ login: " #{user.username} " })).to eq user
+ end
+ end
+
describe '.find_by_any_email' do
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 0fec14d0cce..b18e922b063 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -22,6 +22,7 @@ describe 'Rack Attack global throttles' do
let(:url_that_does_not_require_authentication) { '/users/sign_in' }
let(:url_that_requires_authentication) { '/dashboard/snippets' }
+ let(:url_api_internal) { '/api/v4/internal/check' }
let(:api_partial_url) { '/todos' }
around do |example|
@@ -172,6 +173,15 @@ describe 'Rack Attack global throttles' do
get url_that_does_not_require_authentication
expect(response).to have_http_status 200
end
+
+ context 'when the request is to the api internal endpoints' do
+ it 'allows requests over the rate limit' do
+ (1 + requests_per_period).times do
+ get url_api_internal, secret_token: Gitlab::Shell.secret_token
+ expect(response).to have_http_status 200
+ end
+ end
+ end
end
context 'when the throttle is disabled' do
diff --git a/spec/support/factory_girl.rb b/spec/support/factory_bot.rb
index c7890e49c66..c7890e49c66 100644
--- a/spec/support/factory_girl.rb
+++ b/spec/support/factory_bot.rb
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
index 128aaaf25fe..8854382dc6b 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/fixture_helpers.rb
@@ -1,12 +1,12 @@
module FixtureHelpers
- def fixture_file(filename)
+ def fixture_file(filename, dir: '')
return '' if filename.blank?
- File.read(expand_fixture_path(filename))
+ File.read(expand_fixture_path(filename, dir: dir))
end
- def expand_fixture_path(filename)
- File.expand_path(Rails.root.join('spec/fixtures/', filename))
+ def expand_fixture_path(filename, dir: '')
+ File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end
end