Commit 35ca13e1 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 3776-ci-view-for-sast

* master: (123 commits)
  Add changelog entry
  Add tests for license expiration duration message
  Show license type in expiration only when applied license type is trial
  Replace "Kubernetes cluster" where appropriate in docs
  remove pipelines_charts.js
  Improve SAST message
  deleted pipelines_charts file
  Ignore `_links` property for no-underscore-dangle rule
  Improve error handling for Gitlab::Profiler and improve doc about providing a user
  Add axios to frontend docs as the default network request library
  Remove Webpack bundle tag for users:show
  Replace $.ajax with axios in members.js
  Remove duplicate imports
  Replace $.get with axios in importerStatus
  Refactor ee dispatcher groups:*
  Add scripts/ee-files-location-check to check wrong location of new EE-specific files
  Filter secret variable values from logs
  Resolve "Change icon and color of issue statuses in related issues and epic issues"
  Re-enable eslint in `commits.js` file
  Fixes Renaming repository (project[name]) fails on 2nd try whend the validation has failed
  ...
parents 0816d293 9090d0c2
......@@ -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",
......
......@@ -289,6 +289,14 @@ retrieve-tests-metadata:
- wget -O $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
ee-files-location-check:
<<: *dedicated-runner
stage: prepare
before_script: []
cache: {}
script:
- scripts/ee-files-location-check
update-tests-metadata:
<<: *tests-metadata-state
<<: *only-canonical-masters
......
......@@ -175,7 +175,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
~Geo, ~Gitaly, ~Platform, ~Monitoring, ~Release, and ~"UX".
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
......
......@@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
After 7th at 23:59 (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
Any release candidate that gets created after this date can become a final release,
hence the name release candidate.
### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration:
......@@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the
### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
you can ask for an exception to be made.
1. a Release Manager
2. an Engineering Lead
3. an Engineering Director, the VP of Engineering, or the CTO
Go to [Release tasks issue tracker](https://gitlab.com/gitlab-org/release/tasks/issues/new) and create an issue
using the `Exception-request` issue template.
You can find who is who on the [team page](https://about.gitlab.com/team/).
......
......@@ -50,10 +50,8 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', (e) => {
const $target = $(e.target);
if (!$target.closest('.emoji-menu-content').length) {
$('.js-awards-block.current').removeClass('current');
}
if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu'));
......
......@@ -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);
......
......@@ -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.
......
/* 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;
})();
}
}
......@@ -11,10 +11,8 @@ import UsersSelect from './users_select';
import UserCallout from './user_callout';
import ZenMode from './zen_mode';
import initGeoInfoModal from 'ee/init_geo_info_modal'; // eslint-disable-line import/first
import initGroupAnalytics from 'ee/init_group_analytics'; // eslint-disable-line import/first
import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first
import initApprovals from 'ee/approvals'; // eslint-disable-line import/first
import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line import/first
var Dispatcher;
......@@ -234,6 +232,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')
......@@ -473,11 +476,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)
......@@ -677,10 +675,14 @@ var Dispatcher;
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
case 'groups:analytics:show':
initGroupAnalytics();
import(/* webpackChunkName: "ee_groups_analytics_show" */ 'ee/pages/groups/analytics/show')
.then(callDefault)
.catch(fail);
break;
case 'groups:ldap_group_links:index':
initLDAPGroupsSelect();
import(/* webpackChunkName: "ee_groups_ldap_links" */ 'ee/pages/groups/ldap_group_links')
.then(callDefault)
.catch(fail);
break;
case 'admin:groups:edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
......
......@@ -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);
}
}
......
......@@ -7,8 +7,6 @@ import Sidebar from './right_sidebar';
import DueDateSelectors from './due_date_select';
import WeightSelect from 'ee/weight_select'; // eslint-disable-line import/first
export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
......@@ -16,7 +14,6 @@ export default () => {
full_path: sidebarOptions.fullPath,
});
new LabelsSelect();
new WeightSelect();
new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors();
Sidebar.initialize();
......
......@@ -63,7 +63,7 @@ const mixins = {
return this.title.length > 0;
},
iconName() {
return this.isOpen ? 'issue-open-m' : 'cut';
return this.isOpen ? 'issue-open' : 'issue-close';
},
iconClass() {
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
......
import Flash from './flash';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
export default class Members {
constructor() {
......@@ -50,10 +52,11 @@ export default class Members {
$toggle.disable();
$dateInput.disable();
this.overrideLdap($memberListItem, $link.data('endpoint'), false).fail(() => {
$toggle.enable();
$dateInput.enable();
});
this.overrideLdap($memberListItem, $link.data('endpoint'), false)
.catch(() => {
$toggle.enable();
$dateInput.enable();
});
}
},
});
......@@ -101,32 +104,31 @@ export default class Members {
$btn.disable();
// eslint-disable-next-line promise/catch-or-return
this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => {
this.showLDAPPermissionsWarning(e);
$toggle.enable();
$dateInput.enable();
}).fail((xhr) => {
$btn.enable();
if (xhr.status === 403) {
Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert');
} else {
Flash('An error occured whilst saving LDAP override status. Please try again.', 'alert');
}
});
this.overrideLdap($memberListItem, $btn.data('endpoint'), true)
.then(() => {
this.showLDAPPermissionsWarning(e);
$toggle.enable();
$dateInput.enable();
})
.catch((xhr) => {
$btn.enable();
if (xhr.status === 403) {
Flash(__('You do not have the correct permissions to override the settings from the LDAP group sync.'));
} else {
Flash(__('An error occurred while saving LDAP override status. Please try again.'));
}
});
}
// eslint-disable-next-line class-methods-use-this
overrideLdap($memberListitem, endpoint, override) {
return $.ajax({
url: endpoint,
type: 'PATCH',
data: {
group_member: {
override,
},
return axios.patch(endpoint, {
group_member: {
override,
},
}).then(() => {
})
.then(() => {
$memberListitem.toggleClass('is-overriden', override);
});
}
......
......@@ -69,20 +69,20 @@ export default class MirrorPull {
// Make backOff polling to get data
backOff((next, stop) => {
$.getJSON(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}`)
.done((res, statusText, header) => {
if (header.status === 204) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
axios.get(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}`)
.then(({ data, status }) => {
if (status === 204) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
stop(data);
}
})
.fail(stop);
} else {
stop(data);
}
})
.catch(stop);
})
.then((res) => {
$btnLoadSpinner.addClass('hidden');
......@@ -92,10 +92,11 @@ export default class MirrorPull {
this.showSSHInformation(res);
}
})
.catch((res) => {
.catch(({ response }) => {
// Show failure message when there's an error and re-enable Detect host keys button
const failureMessage = res.responseJSON ? res.responseJSON.message : 'Something went wrong on our end.';
const failureMessage = response.data ? response.data.message : __('An error occurred while detecting host keys');
Flash(failureMessage); // eslint-disable-line
$btnLoadSpinner.addClass('hidden');
this.$btnDetectHostKeys.enable();
});
......
<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>
......@@ -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,
},
});
},
});
}
};
......@@ -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();
};
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));
});
/* 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();
});
};
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
});
import UserCallout from '~/user_callout';
export default () => new UserCallout();
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
......
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));
});
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);
});
......@@ -149,6 +149,7 @@ export default {
},
handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
const label = data.pipeline.details.status.label;
const title = `Pipeline ${label}`;
......
<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>
......@@ -261,8 +261,6 @@ ul.controls {
}
.author_link {
display: inline-block;
.avatar-inline {
margin-left: 0;
margin-right: 0;
......
......@@ -321,11 +321,11 @@ ul.related-merge-requests > li {
}
.issue-token-state-icon-open {
color: $green-600;
color: $green-500;
}
.issue-token-state-icon-closed {
color: $red-600;
color: $blue-500;
}
.issue-token-title {
......
......@@ -39,12 +39,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
def verify_billing
case google_project_billing_status
when 'true'
return
when 'false'
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
when nil
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
when true
return
end
@cluster = ::Clusters::Cluster.new(create_params)
......@@ -81,9 +81,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end
def google_project_billing_status
Gitlab::Redis::SharedState.with do |redis|
redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
end
CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
end
def token_in_session
......
......@@ -44,12 +44,41 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
# This convoluted mess is because we need to handle two cases of
# artifact files during the migration. And a simple OR clause
# makes it impossible to optimize.
# Instead we want to use UNION ALL and do two carefully
# constructed disjoint queries. But Rails cannot handle UNION or
# UNION ALL queries so we do the query in a subquery and wrap it
# in an otherwise redundant WHERE IN query (IN is fine for
# non-null columns).
# This should all be ripped out when the migration is finished and
# replaced with just the new storage to avoid the extra work.
scope :with_artifacts, ->() do
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id'))
old = Ci::Build.select(:id).where(%q[artifacts_file <> ''])
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id'))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :with_artifacts_not_expired, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND (artifacts_expire_at IS NULL OR artifacts_expire_at > ?)], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND (expire_at IS NULL OR expire_at > ?)', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :with_expired_artifacts, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND artifacts_expire_at < ?], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND expire_at < ?', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
......
......@@ -102,10 +102,6 @@ class Event < ActiveRecord::Base
self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
# include this column, hence we're ignoring it as well.
ignore_column :data
class << self
def model_name
ActiveModel::Name.new(self, nil, 'event')
......
......@@ -39,7 +39,7 @@ class ExternalIssue
end
def to_reference(_from = nil, full: nil)
id
reference_link_text
end
def reference_link_text(from = nil)
......
......@@ -11,6 +11,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
......@@ -36,4 +37,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
......@@ -600,7 +600,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
......
......@@ -258,7 +258,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
......
......@@ -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,
......
......@@ -135,7 +135,7 @@ module MergeRequests
end
def append_closes_description
return unless issue
return unless issue&.to_reference.present?
closes_issue = "Closes #{issue.to_reference}"
......@@ -164,7 +164,7 @@ module MergeRequests
return if merge_request.title.present?
if issue_iid.present?
merge_request.title = "Resolve #{issue_iid}"
merge_request.title = "Resolve #{issue.to_reference}"
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title += " \"#{branch_title}\"" if branch_title.present?
end
......
......@@ -5,11 +5,15 @@ module Projects
end
def execute
params[:file] = Gitlab::ProjectTemplate.find(params[:template_name]).file
template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
params[:file] = file
GitlabProjectsImportService.new(current_user, params).execute
GitlabProjectsImportService.new(@current_user, @params).execute
ensure
params[:file]&.close
file&.close
end
end
end
......@@ -11,12 +11,14 @@ module Projects
def execute
FileUtils.mkdir_p(File.dirname(import_upload_path))
file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path)
Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id],
current_user,
import_upload_path,
params[:path]).execute
params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path
::Projects::CreateService.new(current_user, params).execute
end
private
......@@ -28,9 +30,5 @@ module Projects
def tmp_filename
SecureRandom.hex
end
def file
params[:file]
end
end
end
......@@ -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 } }
......
......@@ -65,7 +65,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
......
- @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
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_times')
%div
%p.light
= _("Commit duration in minutes for last 30 commits")
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_charts')
%h4= _("Pipelines charts")
%p
&nbsp;
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('integrations')
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
......
......@@ -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();
......@@ -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
......@@ -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")
......
......@@ -7,6 +7,7 @@ class CheckGcpProjectBillingWorker
LEASE_TIMEOUT = 3.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour
BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
def self.get_session_token(token_key)
Gitlab::Redis::SharedState.with do |redis|
......@@ -22,8 +23,11 @@ class CheckGcpProjectBillingWorker
end
end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
def self.get_billing_state(token)
Gitlab::Redis::SharedState.with do |redis|
value = redis.get(redis_shared_state_key_for(token))
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
end
end
def perform(token_key)
......@@ -33,12 +37,9 @@ class CheckGcpProjectBillingWorker
return unless token
return unless try_obtain_lease_for(token)
billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token)
Gitlab::Redis::SharedState.with do |redis|
redis.set(self.class.redis_shared_state_key_for(token),
!billing_enabled_projects.empty?,
ex: BILLING_TIMEOUT)
end
billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
self.class.set_billing_state(token, billing_enabled_state)
end
private
......@@ -51,9 +52,41 @@ class CheckGcpProjectBillingWorker
"gitlab:gcp:session:#{token_key}"
end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
end
def self.set_billing_state(token, value)
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
end
end
def try_obtain_lease_for(token)
Gitlab::ExclusiveLease
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
.try_obtain
end
def billing_changed_counter
@billing_changed_counter ||= Gitlab::Metrics.counter(
:gcp_billing_change_count,
"Counts the number of times a GCP project changed billing_enabled state from false to true",
BILLING_CHANGED_LABELS
)
end
def state_transition(previous_state, current_state)
if previous_state.nil? && !current_state
'no_billing'
elsif previous_state.nil? && current_state
'with_billing'
elsif !previous_state && current_state
'billing_configured'
end
end
def update_billing_change_counter(previous_state, current_state)
billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
end
end
---
title: Fix license expiration duration to show trial info only for trial license
merge_request: 4573
author:
type: fixed
---
title: Improve SAST description for no new vulnerabilities
merge_request:
author:
type: fixed
---
title: Prevent MR Widget error when no CI configured
merge_request:
author:
type: fixed
---
title: Fix Teleporting Emoji
merge_request: 16963
author: Jared Deckard <jared.deckard@gmail.com>
type: fixed
---
title: "Fix user avatar's vertical align on the issues and merge requests pages"
merge_request: 17072
author: Laszlo Karpati
type: fixed
title: Fix 404 when listing archived projects in a group where all projects have been archived
merge_request: 17077
author: Ashley Dumaine
type: fixed
---
title: Hide CI secret variable values after saving
merge_request: 17044
author:
type: changed
---
title: Allows project rename after validation error
merge_request: 17150
author:
type: fixed
---
title: Remove whitespace from the username/email sign in form field
merge_request: 17020
author: Peter lauck
type: changed
---
title: Escape HTML entities in commit messages
merge_request:
author:
type: fixed
---
title: Change SQL for expired artifacts to use new ci_job_artifacts.expire_at
merge_request: 16578
author:
type: performance
---
title: Respect description and visibility when creating project from template
merge_request: 16820
author: George Tsiolis
type: fixed
---
title: Fix validation of environment scope of variables
merge_request:
author:
type: fixed
---
title: Fixed error 500 when removing an identity with synced attributes and visiting
the profile page
merge_request: 17054
author:
type: fixed
---
title: Fixed bug with unauthenticated requests through git ssh
merge_request: 17149
author:
type: fixed
---
title: 'Ensure users cannot create environments with leading or trailing slashes (Fixes #39885)'
merge_request: 15273
author:
type: fixed
---
title: Add new modal Vue component
merge_request: 17108
author:
type: changed
......@@ -83,6 +83,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
......@@ -94,6 +95,7 @@ module Gitlab
sentry_dsn
trace
variables
value
)
# Enable escaping HTML in JSON.
......
......@@ -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
......
......@@ -75,7 +75,6 @@ var config = {
issues: './issues/issues_bundle.js',
how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
job_details: './jobs/job_details_bundle.js',
ldap_group_links: './groups/ldap_group_links.js',
locale: './locale/index.js',
......@@ -88,9 +87,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',
......@@ -115,7 +112,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',
},
......
......@@ -9,7 +9,6 @@ Gitlab::Seeder.quiet do
s.username = 'root'
s.password = '5iveL!fe'
s.admin = true
s.projects_limit = 100
s.confirmed_at = DateTime.now
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class OptimizeCiJobArtifacts < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
# job_id is just here to be a covering index for index only scans
# since we'll almost always be joining against ci_builds on job_id
add_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id])
add_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''")
end
def down
remove_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id])
remove_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''")
end
end
......@@ -370,6 +370,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do
t.integer "failure_reason"
end
add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
......@@ -411,6 +412,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do
t.integer "file_store"
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
......
......@@ -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:
......
......@@ -69,7 +69,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.
......
......@@ -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
......
......@@ -71,9 +71,10 @@ learn how to leverage its potential even more.
and status of each CI environment running on Kubernetes
- [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
- [Deploy Boards](../user/project/deploy_boards.md) - Check the current health
- [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or
more Kubernetes clusters to your project
- [Deploy Boards](../user/project/deploy_boards.md) - Check the current health
and status of each CI/CD environment running on Kubernetes
## GitLab CI/CD for Docker
......
# 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)
# 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).
......@@ -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
......
# Manage feature flags
Starting from GitLab 9.3 we support feature flags via
Starting from GitLab 9.3 we support feature flags for features in GitLab via
[Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature`
class (defined in `lib/feature.rb`) in your code to get, set and list feature
flags.
......@@ -19,3 +19,8 @@ dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
## Feature flags for user applications
GitLab does not yet support the use of feature flags in deployed user applications.
You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
\ No newline at end of file
......@@ -40,37 +40,12 @@ See [Translation guidelines](translation.md).
### Proof reading
Proof reading helps ensure the accuracy and consistency of translations.
All translations are proof read before being accepted.
If a translations requires changes, you will be notified with a comment explaining why.
Community assistance proof reading translations is encouraged and appreciated.
Requests to become a proof reader will be considered on the merits of previous translations.
- Bulgarian
- Chinese Simplified
- [Huang Tao](https://crowdin.com/profile/htve)
- Chinese Traditional
- [Huang Tao](https://crowdin.com/profile/htve)
- Chinese Traditional, Hong Kong
- [Huang Tao](https://crowdin.com/profile/htve)
- Dutch
- Esperanto
- French
- German
- Italian
- [Paolo Falomo](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- [Huang Tao](https://crowdin.com/profile/htve)
- Portuguese, Brazilian
- Russian
- [Alexy Lustin](https://crowdin.com/profile/lustin)
- [Nikita Grylov](https://crowdin.com/profile/nixel2007)
- Spanish
- Ukrainian
If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
Proof reading helps ensure the accuracy and consistency of translations. All
translations are proof read before being accepted. If a translations requires
changes, you will be notified with a comment explaining why.
See [Proofreading Translations](proofreader.md) for more information on who's
able to proofread and instructions on becoming a proofreader yourself.
## Release
......
# Proofread Translations
Most translations are contributed, reviewed, and accepted by the community. We
are very appreciative of the work done by translators and proofreaders!
## Proofreaders
- Bulgarian
- Chinese Simplified
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Chinese Traditional
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
- Esperanto
- French
- German
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Portuguese, Brazilian
- Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
- Russian
- Nikita Grylov - [GitLab](https://gitlab.com/nixel2007), [Crowdin](https://crowdin.com/profile/nixel2007)
- Alexy Lustin - [GitLab](https://gitlab.com/allustin), [Crowdin](https://crowdin.com/profile/lustin)
- Spanish
- Ukrainian
- Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13)
## Become a proofreader
> **Note:** Before requesting Proofreader permissions in Crowdin please make
> sure that you have a history of contributing translations to the GitLab
> project.
1. Once your translations have been accepted,
[open a merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/new)
to request Proofreader permissions and add yourself to the list above.
In the merge request description, please include links to any projects you
have previously translated.
1. Your request to become a proofreader will be considered on the merits of
your previous translations.
......@@ -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.
......
......@@ -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,
......
......@@ -5,20 +5,23 @@
Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes
cluster in a few steps.
With a cluster associated to your project, you can use Review Apps, deploy your
applications, run your pipelines, and much more, in an easy way.
## Overview
With a Kubernetes cluster associated to your project, you can use
[Review Apps](../../../ci/review_apps/index.md), deploy your applications, run
your pipelines, and much more, in an easy way.
There are two options when adding a new cluster to your project; either associate
your account with Google Kubernetes Engine (GKE) so that you can [create new
clusters](#adding-and-creating-a-new-gke-cluster-via-gitlab) from within GitLab,
or provide the credentials to an [existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster).
## Prerequisites
## Adding and creating a new GKE cluster via GitLab
In order to be able to manage your Kubernetes cluster through GitLab, the
following prerequisites must be met.
NOTE: **Note:**
You need Master [permissions] and above to access the Kubernetes page.
**For a cluster hosted on GKE:**
Before proceeding, make sure the following requirements are met:
- The [Google authentication integration](../../../integration/google.md) must
be enabled in GitLab at the instance level. If that's not the case, ask your
......@@ -28,30 +31,16 @@ following prerequisites must be met.
account](https://cloud.google.com/billing/docs/how-to/manage-billing-account)
must be set up and that you have to have permissions to access it.
- You must have Master [permissions] in order to be able to access the
**Cluster** page.
**Kubernetes** page.
- You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled
- You must have [Resource Manager
API](https://cloud.google.com/resource-manager/)
**For an existing Kubernetes cluster:**
- Since the cluster is already created, there are no prerequisites.
---
If all of the above requirements are met, you can proceed to add a new Kubernetes
cluster.
## Adding and creating a new GKE cluster via GitLab
NOTE: **Note:**
You need Master [permissions] and above to access the Clusters page.
Before proceeding, make sure all [prerequisites](#prerequisites) are met.
To add a new cluster hosted on GKE to your project:
If all of the above requirements are met, you can proceed to create and add a
new Kubernetes cluster that will be hosted on GKE to your project:
1. Navigate to your project's **CI/CD > Clusters** page.
1. Click on **Add cluster**.
1. Navigate to your project's **CI/CD > Kubernetes** page.
1. Click on **Add Kubernetes cluster**.
1. Click on **Create with GKE**.
1. Connect your Google account if you haven't done already by clicking the
**Sign in with Google** button.
......@@ -66,7 +55,7 @@ To add a new cluster hosted on GKE to your project:
- **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types)
of the Virtual Machine instance that the cluster will be based on.
- **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster.
1. Finally, click the **Create cluster** button.
1. Finally, click the **Create Kubernetes cluster** button.
After a few moments, your cluster should be created. If something goes wrong,
you will be notified.
......@@ -77,14 +66,14 @@ enable the Cluster integration.
## Adding an existing Kubernetes cluster
NOTE: **Note:**
You need Master [permissions] and above to access the Clusters page.
You need Master [permissions] and above to access the Kubernetes page.
To add an existing Kubernetes cluster to your project:
1. Navigate to your project's **CI/CD > Clusters** page.
1. Click on **Add cluster**.
1. Click on **Add an existing cluster** and fill in the details:
- **Cluster name** (required) - The name you wish to give the cluster.
1. Navigate to your project's **CI/CD > Kubernetes** page.
1. Click on **Add Kuberntes cluster**.
1. Click on **Add an existing Kubernetes cluster** and fill in the details:
- **Kubernetes cluster name** (required) - The name you wish to give the cluster.
- **Environment scope** (required)- The
[associated environment](#setting-the-environment-scope) to this cluster.
- **API URL** (required) -
......@@ -112,15 +101,13 @@ To add an existing Kubernetes cluster to your project:
- If you or someone created a secret specifically for the project, usually
with limited permissions, the secret's namespace and project namespace may
be the same.
1. Finally, click the **Create cluster** button.
The Kubernetes service takes the following parameters:
1. Finally, click the **Create Kuberntes cluster** button.
After a few moments, your cluster should be created. If something goes wrong,
you will be notified.
You can now proceed to install some pre-defined applications and then
enable the Cluster integration.
enable the Kubernetes cluster integration.
## Installing applications
......@@ -139,7 +126,7 @@ added directly to your configured cluster. Those applications are needed for
NOTE: **Note:**
You need a load balancer installed in your cluster in order to obtain the
external IP address with the following procedure. It can be deployed using the
**Ingress** application described in the previous section.
[**Ingress** application](#installing-appplications).
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
......@@ -153,7 +140,8 @@ the `gcloud` command in a local terminal or using the **Cloud Shell**.
If the cluster is not on GKE, follow the specific instructions for your
Kubernetes provider to configure `kubectl` with the right credentials.
If you installed the Ingress using the **Applications** section, run the following command:
If you installed the Ingress [via the **Applications**](#installing-applications),
run the following command:
```bash
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
......@@ -171,9 +159,14 @@ your deployed applications.
## Setting the environment scope
When adding more than one clusters, you need to differentiate them with an
environment scope. The environment scope associates clusters and
[environments](../../../ci/environments.md) in an 1:1 relationship similar to how the
NOTE: **Note:**
This is only available for [GitLab Premium][ee] where you can add more than
one Kubernetes cluster.
When adding more than one Kubernetes clusters to your project, you need to
differentiate them with an environment scope. The environment scope associates
clusters and [environments](../../../ci/environments.md) in an 1:1 relationship
similar to how the
[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables)
work.
......@@ -183,7 +176,7 @@ cluster in a project, and a validation error will occur if otherwise.
---
For example, let's say the following clusters exist in a project:
For example, let's say the following Kubernetes clusters exist in a project:
| Cluster | Environment scope |
| ---------- | ------------------- |
......@@ -231,8 +224,7 @@ With GitLab Premium, you can associate more than one Kubernetes clusters to your
project. That way you can have different clusters for different environments,
like dev, staging, production, etc.
To add another cluster, follow the same steps as described in [adding a
Kubernetes cluster](#adding-a-kubernetes-cluster) and make sure to
Simply add another cluster, like you did the first time, and make sure to
[set an environment scope](#setting-the-environment-scope) that will
differentiate the new cluster with the rest.
......@@ -240,45 +232,42 @@ differentiate the new cluster with the rest.
The Kubernetes cluster integration exposes the following
[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
GitLab CI/CD build environment:
- `KUBE_URL` - Equal to the API URL.
- `KUBE_TOKEN` - The Kubernetes token.
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
receive the default value.
- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
CA bundle would be embedded if specified.
## Enabling or disabling the Cluster integration
GitLab CI/CD build environment.
| Variable | Description |
| -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. |
| `KUBE_TOKEN` | The Kubernetes token. |
| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. |
| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
## Enabling or disabling the Kubernetes cluster integration
After you have successfully added your cluster information, you can enable the
Cluster integration:
Kubernetes cluster integration:
1. Click the "Enabled/Disabled" switch
1. Hit **Save** for the changes to take effect
You can now start using your Kubernetes cluster for your deployments.
To disable the Cluster integration, follow the same procedure.
To disable the Kubernetes cluster integration, follow the same procedure.
## Removing the Cluster integration
## Removing the Kubernetes cluster integration
NOTE: **Note:**
You need Master [permissions] and above to remove a cluster integration.
You need Master [permissions] and above to remove a Kubernetes cluster integration.
NOTE: **Note:**
When you remove a cluster, you only remove its relation to GitLab, not the
cluster itself. To remove the cluster, you can do so by visiting the GKE
dashboard or using `kubectl`.
To remove the Cluster integration from your project, simply click on the
To remove the Kubernetes cluster integration from your project, simply click on the
**Remove integration** button. You will then be able to follow the procedure
and [add a cluster](#adding-a-cluster) again.
and add a Kubernetes cluster again.
## What you can get with the Kubernetes integration
......
......@@ -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
......
import initLDAPGroupsSelect from 'ee/ldap_groups_select';
export default initLDAPGroupsSelect;
......@@ -65,7 +65,7 @@ export default {
};
},
showApproveButton() {
return this.userCanApprove && !this.userHasApproved;
return this.userCanApprove && !this.userHasApproved && this.mr.isOpen;
},
showSuggestedApprovers() {
return this.suggestedApprovers && this.suggestedApprovers.length;
......
......@@ -117,8 +117,8 @@ export default {
},
securityText() {
const { newIssues, resolvedIssues } = this.mr.securityReport;
return this.sastText(newIssues, resolvedIssues);
const { newIssues, resolvedIssues, allIssues } = this.mr.securityReport;
return this.sastText(newIssues, resolvedIssues, allIssues);
},
dockerText() {
......
......@@ -2,11 +2,13 @@ import { s__, n__, __, sprintf } from '~/locale';
export default {
methods: {
sastText(newIssues = [], resolvedIssues = []) {
sastText(newIssues = [], resolvedIssues = [], allIssues = []) {
const text = [];
if (!newIssues.length && !resolvedIssues.length) {
if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|SAST detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|SAST detected no new security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|SAST'));
}
......
......@@ -19,7 +19,7 @@
%span.light #{label}:
%strong= value
.panel.panel-default
.panel.panel-default.js-license-info-panel
.panel-heading
Details
%ul.well-list
......@@ -39,9 +39,11 @@
- else
Expires:
- if @license.will_expire? && @license.active?
%strong= time_ago_with_tooltip(@license.expires_at)
- if @license.trial?
%span Free trial will expire in #{pluralize(@license.remaining_days, 'day')}
%strong.has-tooltip{ title: @license.expires_at.to_formatted_s(:long), data: { placement: 'top' } }
Free trial will expire in #{pluralize(@license.remaining_days, 'day')}
- else
%strong= time_ago_with_tooltip(@license.expires_at)
- else
%strong Never
......
......@@ -27,6 +27,30 @@ feature "License Admin" do
end
end
context 'with a trial license' do
let!(:license) { create(:license, trial: true) }
it 'shows expiration duration with license type' do
visit admin_license_path
page.within '.js-license-info-panel' do
expect(page).to have_content('Expires: Free trial will expire in')
end
end
end
context 'with a regular license' do
let!(:license) { create(:license) }
it 'shows only expiration duration' do
visit admin_license_path
page.within '.js-license-info-panel' do
expect(page).not_to have_content('Expires: Free trial will expire in')
end
end
end
context 'with an expired trial license' do
let!(:license) { create(:license, trial: true, expired: true) }
......
......@@ -81,7 +81,7 @@ feature 'EE Clusters' do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true')
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true)
allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do
......
......@@ -32,10 +32,11 @@ describe 'User approves a merge request', :js do
before do
project.add_developer(user2)
project.add_developer(user3)
visit(merge_request_path(merge_request))
end
it 'shows multiple approvers beyond the needed count' do
visit(merge_request_path(merge_request))
click_button('Approve')
wait_for_requests
......@@ -47,6 +48,19 @@ describe 'User approves a merge request', :js do
expect(all('.js-approver-list-member').count).to eq(3)
end
it "doesn't show the approve additionally when a merge request is closed" do
merge_request_closed = create(:merge_request, :closed, source_project: project, target_project: project)
create(:approval, merge_request: merge_request_closed, user: user)
sign_in(user2)
visit(merge_request_path(merge_request_closed))
wait_for_requests
expect(page).not_to have_button('Approve')
expect(page).not_to have_button('Approve additionally')
end
def sign_in_visit_merge_request(user)
sign_in(user)
visit(merge_request_path(merge_request))
......
Feature: Group Members
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
Scenario: Search member by name
Given "Mary Jane" is guest of group "Guest"
And I visit group "Guest" members page
When I search for 'Mary' member
Then I should see user "Mary Jane" in team list
Then I should not see user "John Doe" in team list
Feature: Group Milestones
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
Scenario: I should see group "Owned" milestone index page with no milestones
When I visit group "Owned" page
And I click on group milestones
Then I should see group milestones index page has no milestones
Scenario: I should see group "Owned" milestone index page with milestones
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
Then I should see group milestones index page with milestones
Scenario: I should see group "Owned" milestone show page
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see group milestone with descriptions and expiry date
And I should see group milestone with all issues and MRs assigned to that milestone
Scenario: Create group milestones
Given I visit group "Owned" milestones page
And I click new milestone button
And I fill milestone name
When I press create mileston button
Then group milestone should be created
Scenario: I should see Issues listed with labels
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see the "bug" label
And I should see the "feature" label
And I should see the project name in the Issue row
@javascript
Scenario: I should see the Labels tab
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
And I click on the "Labels" tab
Then I should see the list of labels
@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
......@@ -9,14 +9,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
expect(group_members_list).to have_content("John Doe")
end
step 'I should not see user "John Doe" in team list' do
expect(group_members_list).not_to have_content("John Doe")
end
step 'I should see user "Mary Jane" in team list' do
expect(group_members_list).to have_content("Mary Jane")
end
step 'I should not see user "Mary Jane" in team list' do
expect(group_members_list).not_to have_content("Mary Jane")
end
......@@ -41,13 +33,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
# poltergeist always confirms popups.
end
step 'I search for \'Mary\' member' do
page.within '.member-search-form' do
fill_in 'search', with: 'Mary'
find('.member-search-btn').click
end
end
step 'I change the "Mary Jane" role to "Developer"' do
member = mary_jane_member
......
class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include WaitForRequests
include SharedAuthentication
include SharedPaths
include SharedGroup
include SharedUser
step 'I click on group milestones' do
visit group_milestones_path('owned')
end
step 'I should see group milestones index page has no milestones' do
expect(page).to have_content('No milestones to show')
end
step 'Group has projects with milestones' do
group_milestone
end
step 'I should see group milestones index page with milestones' do
expect(page).to have_content('Version 7.2')
expect(page).to have_content('GL-113')
expect(page).to have_link('3 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2"))
expect(page).to have_link('0 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113"))
end
step 'I click on one group milestone' do
milestones = Milestone.where(title: 'GL-113')
@global_milestone = GlobalMilestone.new('GL-113', milestones)
click_link 'GL-113'
end
step 'I should see group milestone with descriptions and expiry date' do
expect(page).to have_content('expires on Aug 20, 2114')
end
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
expect(page).to have_content('Milestone GL-113')
expect(page).to have_content('Issues 3 Open: 3 Closed: 0')
issue = Milestone.find_by(name: 'GL-113').issues.first
expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue))
end
step 'I fill milestone name' do
fill_in 'milestone_title', with: 'v2.9.0'
end
step 'I click new milestone button' do
page.within('.nav-controls') do
click_link "New milestone"
end
end
step 'I press create mileston button' do
click_button "Create milestone"
end
step 'group milestone should be created' do
group = Group.find_by(name: 'Owned')
expect(page).to have_content group.milestones.find_by_title('v2.9.0').title
end
step 'I should see the "bug" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the "feature" label' do
page.within('#tab-issues') do
expect(page).to have_content 'bug'
end
end
step 'I should see the project name in the Issue row' do
page.within('#tab-issues') do
@global_milestone.projects.each do |project|
expect(page).to have_content project.name
end
end
end
step 'I click on the "Labels" tab' do
page.within('.content .nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
end
step 'I should see the list of labels' do
wait_for_requests
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
end
private
def group_milestone
group = owned_group
%w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
project = create(:project, path: path, group: group)
milestone = create :milestone, title: "Version 7.2", project: project
create(:label, project: project, title: 'bug')
create(:label, project: project, title: 'feature')
create :issue,
project: project,
assignees: [current_user],
author: current_user,
milestone: milestone
milestone = create :milestone,
title: "GL-113",
project: project,
due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text'
issue = create :issue,
project: project,
assignees: [current_user],
author: current_user,
milestone: milestone
issue.labels << project.labels.find_by(title: 'bug')
issue.labels << project.labels.find_by(title: 'feature')
end
current_user.refresh_authorized_projects
end
end
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
......@@ -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
......
module Gitlab
module ImportExport
class ProjectCreator
def initialize(namespace_id, current_user, file, project_path)
@namespace_id = namespace_id
@current_user = current_user
@file = file
@project_path = project_path
end
def execute
::Projects::CreateService.new(
@current_user,
name: @project_path,
path: @project_path,
namespace_id: @namespace_id,
import_type: "gitlab_project",
import_source: @file
).execute
end
end
end
end
......@@ -19,9 +19,7 @@ module Gitlab
end
def self.servers
Gitlab.config.ldap.servers.values
rescue Settingslogic::MissingSetting
[]
Gitlab.config.ldap['servers']&.values || []
end
def self.available_servers
......
......@@ -200,9 +200,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|
......@@ -223,6 +225,10 @@ module Gitlab
end
end
def clear_user_synced_attributes_metadata
gl_user&.user_synced_attributes_metadata&.destroy
end
def log
Gitlab::AppLogger
end
......
......@@ -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
......
......@@ -41,12 +41,16 @@ module Gitlab
'a-zA-Z0-9_/\\$\\{\\}\\. \\-'
end
def environment_name_regex_chars_without_slash
'a-zA-Z0-9_\\$\\{\\}\\. -'
end
def environment_name_regex
@environment_name_regex ||= /\A[#{environment_name_regex_chars}]+\z/.freeze
@environment_name_regex ||= /\A[#{environment_name_regex_chars_without_slash}]([#{environment_name_regex_chars}]*[#{environment_name_regex_chars_without_slash}])?\z/.freeze
end
def environment_name_regex_message
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces"
"can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'"
end
def kubernetes_namespace_regex
......
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 212b6082e..98d6d79c0 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -12,7 +12,6 @@ import notificationsDropdown from './notifications_dropdown';
import groupAvatar from './group_avatar';
import GroupLabelSubscription from './group_label_subscription';
import LineHighlighter from './line_highlighter';
-import groupsSelect from './groups_select';
import Search from './search';
import initAdmin from './admin';
import NamespaceSelect from './namespace_select';
@@ -43,7 +42,6 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import ShortcutsWiki from './shortcuts_wiki';
-import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
import UsersSelect from './users_select';
@@ -400,23 +398,16 @@ import Activities from './activities';
break;
case 'projects:pipelines:new':
case 'projects:pipelines:create':
- new NewBranchForm($('.js-new-pipeline-form'));
+ import('./pages/projects/pipelines/new')
+ .then(callDefault)
+ .catch(fail);
break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
- const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
- const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
-
- new Pipelines({
- initTabs: true,
- pipelineStatusUrl,
- tabsOptions: {
- action: controllerAction,
- defaultAction: 'pipelines',
- parentEl: '.pipelines-tabs',
- },
- });
+ import('./pages/projects/pipelines/builds')
+ .then(callDefault)
+ .catch(fail);
break;
case 'groups:activity':
new Activities();
@@ -438,11 +429,9 @@ import Activities from './activities';
new UsersSelect();
break;
case 'projects:project_members:index':
- memberExpirationDate('.js-access-expiration-date-groups');
- groupsSelect();
- memberExpirationDate();
- new Members();
- new UsersSelect();
+ import('./pages/projects/project_members/')
+ .then(callDefault)
+ .catch(fail);
break;
case 'groups:new':
case 'admin:groups:new':
diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
new file mode 100644
index 000000000..060a78b42
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
@@ -0,0 +1,16 @@
+import Pipelines from '../../../../pipelines';
+
+export default () => {
+ const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
+ const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
+
+ new Pipelines({ // eslint-disable-line no-new
+ initTabs: true,
+ pipelineStatusUrl,
+ tabsOptions: {
+ action: controllerAction,
+ defaultAction: 'pipelines',
+ parentEl: '.pipelines-tabs',
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
new file mode 100644
index 000000000..c54cc62bf
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -0,0 +1,5 @@
+import NewBranchForm from '../../../../new_branch_form';
+
+export default () => {
+ new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new
+};
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
new file mode 100644
index 000000000..f4643e7db
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -0,0 +1,12 @@
+import memberExpirationDate from '../../../member_expiration_date';
+import UsersSelect from '../../../users_select';
+import groupsSelect from '../../../groups_select';
+import Members from '../../../members';
+
+export default () => {
+ memberExpirationDate('.js-access-expiration-date-groups');
+ groupsSelect();
+ memberExpirationDate();
+ new Members(); // eslint-disable-line no-new
+ new UsersSelect(); // eslint-disable-line no-new
+};
......@@ -33,7 +33,7 @@ module QA
end
def clone(opts = '')
`git clone #{opts} #{@uri.to_s} ./`
`git clone #{opts} #{@uri.to_s} ./ #{suppress_output}`
end
def shallow_clone
......@@ -61,12 +61,22 @@ module QA
end
def push_changes(branch = 'master')
`git push #{@uri.to_s} #{branch}`
`git push #{@uri.to_s} #{branch} #{suppress_output}`
end
def commits
`git log --oneline`.split("\n")
end
private
def suppress_output
# If we're running as the default user, it's probably a temporary
# instance and output can be useful for debugging
return if @username == Runtime::User.default_name
"&> #{File::NULL}"
end
end
end
end
......@@ -53,8 +53,8 @@ module QA
click_link 'LDAP'
fill_in :username, with: Runtime::User.name
fill_in :password, with: Runtime::User.password
fill_in :username, with: Runtime::User.ldap_username
fill_in :password, with: Runtime::User.ldap_password
click_button 'Sign in'
end
end
......
......@@ -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
......
......@@ -35,6 +35,14 @@ module QA
ENV['GITLAB_PASSWORD']
end
def ldap_username
ENV['GITLAB_LDAP_USERNAME']
end
def ldap_password
ENV['GITLAB_LDAP_PASSWORD']
end
def sandbox_name
ENV['GITLAB_SANDBOX_NAME']
end
......
......@@ -3,8 +3,12 @@ module QA
module User
extend self
def default_name
'root'
end
def name
Runtime::Env.user_username || 'root'
Runtime::Env.user_username || default_name
end
def password
......@@ -14,6 +18,14 @@ module QA
def ldap_user?
Runtime::Env.user_type == 'ldap'
end
def ldap_username
Runtime::Env.ldap_username || name
end
def ldap_password
Runtime::Env.ldap_password || password
end
end
end
end
#!/usr/bin/env ruby
WHITELIST = %w[
CHANGELOG-EE.md
app/assets/javascripts/**/*
changelogs/unreleased-ee/**/*
config/**/*
doc/**/*
scripts/*
spec/javascripts/**/*
].freeze
`git remote add canonical-ee https://gitlab.com/gitlab-org/gitlab-ee.git`
`git remote add canonical-ce https://gitlab.com/gitlab-org/gitlab-ce.git`
`git fetch canonical-ee master --quiet`
`git fetch canonical-ce master --quiet`
new_files_in_this_branch_not_at_the_ee_top_level =
`git diff canonical-ee/master...HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2`.lines.map(&:strip)
ee_specific_files_in_ce_master_not_at_the_ee_top_level =
`git diff canonical-ce/master...HEAD --name-status --diff-filter=A -- ./ ':!ee' | cut -f2`.lines.map(&:strip)
new_ee_specific_files_not_at_the_ee_top_level =
new_files_in_this_branch_not_at_the_ee_top_level & ee_specific_files_in_ce_master_not_at_the_ee_top_level
status = 0
new_ee_specific_files_not_at_the_ee_top_level.each do |file|
next if WHITELIST.any? { |pattern| Dir.glob(pattern).include?(file) }
puts
puts "* #{file} is EE-specific and should be moved to ee/#{file}:"
puts " => git mv #{file} ee/#{file}"
status = 1
end
`git remote remove canonical-ee`
`git remote remove canonical-ce`
exit(status)
......@@ -180,8 +180,8 @@ FactoryBot.define do
trait :artifacts do
after(:create) do |build|
create(:ci_job_artifact, :archive, job: build)
create(:ci_job_artifact, :metadata, job: build)
create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at)
create(:ci_job_artifact, :metadata, job: build, expire_at: build.artifacts_expire_at)
build.reload
end
end
......
require 'spec_helper'
describe 'Search group member' do
let(:user) { create :user }
let(:member) { create :user }
let!(:guest_group) do
create(:group) do |group|
group.add_guest(user)
group.add_guest(member)
end
end
before do
sign_in(user)
visit group_group_members_path(guest_group)
end
it 'renders member users' do
page.within '.member-search-form' do
fill_in 'search', with: member.name
find('.member-search-btn').click
end
group_members_list = find(".panel .content-list")
expect(group_members_list).to have_content(member.name)
expect(group_members_list).not_to have_content(user.name)
end
end
require 'rails_helper'
feature 'Group milestones', :js do
feature 'Group milestones' do
let(:group) { create(:group) }
let!(:project) { create(:project_empty_repo, group: group) }
let(:user) { create(:group_member, :master, user: create(:user), group: group ).user }
......@@ -13,7 +13,7 @@ feature 'Group milestones', :js do
sign_in(user)
end
context 'create a milestone' do
context 'create a milestone', :js do
before do
visit new_group_milestone_path(group)
end
......@@ -61,55 +61,132 @@ feature 'Group milestones', :js do
end
context 'milestones list' do
let!(:other_project) { create(:project_empty_repo, group: group) }
let!(:active_project_milestone1) { create(:milestone, project: project, state: 'active', title: 'v1.0') }
let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') }
let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') }
let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
before do
visit group_milestones_path(group)
context 'when no milestones' do
it 'renders no milestones text' do
visit group_milestones_path(group)
expect(page).to have_content('No milestones to show')
end
end
it 'counts milestones correctly' do
expect(find('.top-area .active .badge').text).to eq("2")
expect(find('.top-area .closed .badge').text).to eq("2")
expect(find('.top-area .all .badge').text).to eq("4")
end
context 'when milestones exists' do
let!(:other_project) { create(:project_empty_repo, group: group) }
let!(:active_project_milestone1) do
create(
:milestone,
project: project,
state: 'active',
title: 'v1.0',
due_date: '2114-08-20',
description: 'Lorem Ipsum is simply dummy text'
)
end
let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') }
let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
let!(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') }
let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
let!(:issue) do
create :issue, project: project, assignees: [user], author: user, milestone: active_project_milestone1
end
it 'lists legacy group milestones and group milestones' do
legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first
before do
visit group_milestones_path(group)
end
expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
it 'counts milestones correctly' do
expect(find('.top-area .active .badge').text).to eq("2")
expect(find('.top-area .closed .badge').text).to eq("2")
expect(find('.top-area .all .badge').text).to eq("4")
end
it 'updates milestone' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link('Edit')
it 'lists legacy group milestones and group milestones' do
legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first
expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
page.within('.milestone-form') do
fill_in 'milestone_title', with: 'new title'
click_button('Update milestone')
it 'updates milestone' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link('Edit')
end
page.within('.milestone-form') do
fill_in 'milestone_title', with: 'new title'
click_button('Update milestone')
end
expect(find('#content-body h2')).to have_content('new title')
end
expect(find('#content-body h2')).to have_content('new title')
end
it 'shows milestone detail and supports its edit' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
end
page.within('.detail-page-header') do
click_link('Edit')
end
it 'shows milestone detail and supports its edit' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
expect(page).to have_selector('.milestone-form')
end
page.within('.detail-page-header') do
click_link('Edit')
it 'renders milestones' do
expect(page).to have_content('v1.0')
expect(page).to have_content('GL-113')
expect(page).to have_link(
'1 Issue',
href: issues_group_path(group, milestone_title: 'v1.0')
)
expect(page).to have_link(
'0 Merge Requests',
href: merge_requests_group_path(group, milestone_title: 'v1.0')
)
end
expect(page).to have_selector('.milestone-form')
it 'renders group milestone details' do
click_link 'v1.0'
expect(page).to have_content('expires on Aug 20, 2114')
expect(page).to have_content('v1.0')
expect(page).to have_content('Issues 1 Open: 1 Closed: 0')
expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue))
end
describe 'labels' do
before do
create(:label, project: project, title: 'bug') do |label|
issue.labels << label
end
create(:label, project: project, title: 'feature') do |label|
issue.labels << label
end
end
it 'renders labels' do
click_link 'v1.0'
page.within('#tab-issues') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
end
it 'renders labels list', :js do
click_link 'v1.0'
page.within('.content .nav-links') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
end
end
end
end
end
end
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
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
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
......@@ -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
......@@ -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
......@@ -25,7 +25,7 @@ feature 'Gcp Cluster', :js do
context 'when user has a GCP project with billing enabled' do
before do
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true')
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true)
end
context 'when user does not have a cluster and visits cluster index page' do
......@@ -134,7 +134,7 @@ feature 'Gcp Cluster', :js do
context 'when user does not have a GCP project with billing enabled' do
before do
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('false')
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(false)
visit project_clusters_path(project)
......
......@@ -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
......@@ -20,7 +20,9 @@ import ApprovalsBody from 'ee/vue_merge_request_widget/components/approvals/appr
`);
this.initialData = {
mr: {},
mr: {
isOpen: true,
},
service: {},
suggestedApprovers: [{ name: 'Approver 1' }],
userCanApprove: false,
......
......@@ -79,7 +79,7 @@ import '~/lib/utils/common_utils';
return expect($emojiMenu.length).toBe(1);
});
});
return it('should remove emoji menu when body is clicked', function(done) {
it('should remove emoji menu when body is clicked', function(done) {
$('.js-add-award').eq(0).click();
return lazyAssert(done, function() {
var $emojiMenu;
......@@ -90,6 +90,17 @@ import '~/lib/utils/common_utils';
return expect($('.js-awards-block.current').length).toBe(0);
});
});
it('should not remove emoji menu when search is clicked', function(done) {
$('.js-add-award').eq(0).click();
return lazyAssert(done, function() {
var $emojiMenu;
$emojiMenu = $('.emoji-menu');
$('.emoji-search').click();
expect($emojiMenu.length).toBe(1);
expect($emojiMenu.hasClass('is-visible')).toBe(true);
return expect($('.js-awards-block.current').length).toBe(1);
});
});
});
describe('::addAwardToEmojiBar', function() {
it('should add emoji to votes block', function() {
......
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('');
......
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);
});
});
});
......@@ -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();
})
......
......@@ -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');
});
});
});
......@@ -17,6 +17,8 @@ import {
dockerReportParsed,
dast,
parsedDast,
sastBaseAllIssues,
sastHeadAllIssues,
} from '../vue_shared/security_reports/mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
......@@ -82,6 +84,31 @@ describe('ee merge request widget options', () => {
});
});
describe('with full report and no added or fixed issues', () => {
let mock;
beforeEach(() => {
mock = mock = new MockAdapter(axios);
mock.onGet('path.json').reply(200, sastBaseAllIssues);
mock.onGet('head_path.json').reply(200, sastHeadAllIssues);
vm = mountComponent(Component);
});
afterEach(() => {
mock.restore();
});
it('renders no new vulnerabilities message', (done) => {
setTimeout(() => {
expect(
vm.$el.querySelector('.js-sast-widget .js-code-text').textContent.trim(),
).toEqual('SAST detected no new security vulnerabilities');
done();
}, 0);
});
});
describe('with empty successful request', () => {
let mock;
......
......@@ -295,6 +295,15 @@ describe('mrWidgetOptions', () => {
expect(notify.notifyMe).not.toHaveBeenCalled();
});
it('should not notify if no pipeline provided', () => {
vm.handleNotification({
...data,
pipeline: undefined,
});
expect(notify.notifyMe).not.toHaveBeenCalled();
});
});
describe('resumePolling', () => {
......
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);
});
});
});
});
......@@ -17,7 +17,7 @@ describe('security report mixin', () => {
);
});
it('returns text for new issues', () => {
it('returns text for added issues', () => {
expect(mixin.methods.sastText(parsedSastIssuesHead, [])).toEqual(
'SAST degraded on 2 security vulnerabilities',
);
......@@ -28,6 +28,12 @@ describe('security report mixin', () => {
'SAST improved on 2 security vulnerabilities',
);
});
it('returns text for full report and no added or fixed issues', () => {
expect(mixin.methods.sastText([], [], parsedSastIssuesHead)).toEqual(
'SAST detected no new security vulnerabilities',
);
});
});
describe('translateText', () => {
......
......@@ -328,3 +328,88 @@ export const parsedDast = [
],
},
];
/**
* SAST report API response for no added & fixed issues but with security issues
*/
export const sastHeadAllIssues = [
{
tool: 'retire',
url: 'https://github.com/jquery/jquery/issues/2432',
file: '/builds/gonzoyumo/test-package-lock/node_modules/tinycolor2/demo/jquery-1.9.1.js',
priority: 'medium',
message: '3rd party CORS request may execute',
},
{
tool: 'retire',
url: 'https://bugs.jquery.com/ticket/11974',
file: '/builds/gonzoyumo/test-package-lock/node_modules/tinycolor2/demo/jquery-1.9.1.js',
priority: 'medium',
message: 'parseHTML() executes scripts in event handlers',
},
{
tool: 'retire',
url: 'https://nodesecurity.io/advisories/146',
priority: 'high',
message: 'growl_command-injection',
},
{
tool: 'retire',
url: 'https://nodesecurity.io/advisories/146',
priority: 'high',
message: 'growl_command-injection',
},
];
export const sastBaseAllIssues = [
{
tool: 'gemnasium',
message: 'Command Injection for growl',
url: 'https://github.com/tj/node-growl/pull/61',
file: 'package-lock.json',
},
{
tool: 'gemnasium',
message: 'Regular Expression Denial of Service for tough-cookie',
url: 'https://github.com/salesforce/tough-cookie/issues/92',
file: 'package-lock.json',
},
{
tool: 'gemnasium',
message: 'Regular Expression Denial of Service for string',
url: 'https://github.com/jprichardson/string.js/issues/212',
file: 'package-lock.json',
},
{
tool: 'gemnasium',
message: 'Regular Expression Denial of Service for debug',
url: 'https://nodesecurity.io/advisories/534',
file: 'package-lock.json',
},
{
tool: 'retire',
message: '3rd party CORS request may execute',
url: 'https://github.com/jquery/jquery/issues/2432',
file: '/code/node_modules/tinycolor2/demo/jquery-1.9.1.js',
priority: 'medium',
},
{
tool: 'retire',
message: 'parseHTML() executes scripts in event handlers',
url: 'https://bugs.jquery.com/ticket/11974',
file: '/code/node_modules/tinycolor2/demo/jquery-1.9.1.js',
priority: 'medium',
},
{
tool: 'retire',
message: 'growl_command-injection',
url: 'https://nodesecurity.io/advisories/146',
priority: 'high',
},
{
tool: 'retire',
message: 'growl_command-injection',
url: 'https://nodesecurity.io/advisories/146',
priority: 'high',
},
];
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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'
......
......@@ -18,6 +18,7 @@ describe Gitlab::Regex do
subject { described_class.environment_name_regex }
it { is_expected.to match('foo') }
it { is_expected.to match('a') }
it { is_expected.to match('foo-1') }
it { is_expected.to match('FOO') }
it { is_expected.to match('foo/1') }
......@@ -25,6 +26,10 @@ describe Gitlab::Regex do
it { is_expected.not_to match('9&foo') }
it { is_expected.not_to match('foo-^') }
it { is_expected.not_to match('!!()()') }
it { is_expected.not_to match('/foo') }
it { is_expected.not_to match('foo/') }
it { is_expected.not_to match('/foo/') }
it { is_expected.not_to match('/') }
end
describe '.environment_slug_regex' do
......
......@@ -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
......@@ -905,6 +905,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,
......
......@@ -921,6 +921,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')
......
......@@ -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
......
require 'spec_helper'
describe MergeRequests::BuildService do
using RSpec::Parameterized::TableSyntax
include RepoHelpers
let(:project) { create(:project, :repository) }
......@@ -111,6 +112,7 @@ describe MergeRequests::BuildService do
context 'one commit in the diff' do
let(:commits) { Commit.decorate([commit_1], project) }
let(:commit_description) { commit_1.safe_message.split(/\n+/, 2).last }
before do
stub_compare
......@@ -125,7 +127,7 @@ describe MergeRequests::BuildService do
end
it 'uses the description of the commit as the description of the merge request' do
expect(merge_request.description).to eq(commit_1.safe_message.split(/\n+/, 2).last)
expect(merge_request.description).to eq(commit_description)
end
context 'merge request already has a description set' do
......@@ -148,68 +150,32 @@ describe MergeRequests::BuildService do
end
end
context 'branch starts with issue IID followed by a hyphen' do
let(:source_branch) { "#{issue.iid}-fix-issue" }
it 'appends "Closes #$issue-iid" to the description' do
expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}")
context 'when the source branch matches an issue' do
where(:issue_tracker, :source_branch, :closing_message) do
:jira | 'FOO-123-fix-issue' | 'Closes FOO-123'
:jira | 'fix-issue' | nil
:custom_issue_tracker | '123-fix-issue' | 'Closes #123'
:custom_issue_tracker | 'fix-issue' | nil
:internal | '123-fix-issue' | 'Closes #123'
:internal | 'fix-issue' | nil
end
context 'merge request already has a description set' do
let(:description) { 'Merge request description' }
it 'appends "Closes #$issue-iid" to the description' do
expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}")
with_them do
before do
if issue_tracker == :internal
issue.update!(iid: 123)
else
create(:"#{issue_tracker}_service", project: project)
end
end
end
context 'commit has no description' do
let(:commits) { Commit.decorate([commit_2], project) }
it 'appends the closing description' do
expected_description = [commit_description, closing_message].compact.join("\n\n")
it 'sets the description to "Closes #$issue-iid"' do
expect(merge_request.description).to eq("Closes ##{issue.iid}")
expect(merge_request.description).to eq(expected_description)
end
end
end
context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
before do
allow(project).to receive(:external_issue_tracker).and_return(false)
allow(project).to receive(:issues_enabled?).and_return(false)
end
it 'uses the title of the commit as the title of the merge request' do
expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
end
it 'uses the description of the commit as the description of the merge request' do
commit_description = commit_1.safe_message.split(/\n+/, 2).last
expect(merge_request.description).to eq("#{commit_description}")
end
end
context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do
let(:source_branch) { 'EXMPL-12345-fix-issue' }
before do
allow(project).to receive(:external_issue_tracker).and_return(true)
allow(project).to receive(:issues_enabled?).and_return(false)
allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
end
it 'uses the title of the commit as the title of the merge request' do
expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
end
it 'uses the description of the commit as the description of the merge request and appends the closes text' do
commit_description = commit_1.safe_message.split(/\n+/, 2).last
expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345")
end
end
end
context 'more than one commit in the diff' do
......@@ -239,90 +205,62 @@ describe MergeRequests::BuildService do
end
end
context 'branch starts with GitLab issue IID followed by a hyphen' do
let(:source_branch) { "#{issue.iid}-fix-issue" }
it 'sets the title to: Resolves "$issue-title"' do
expect(merge_request.title).to eq("Resolve \"#{issue.title}\"")
context 'when the source branch matches an issue' do
where(:issue_tracker, :source_branch, :title, :closing_message) do
:jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123'
:jira | 'fix-issue' | 'Fix issue' | nil
:custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123'
:custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil
:internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123'
:internal | 'fix-issue' | 'Fix issue' | nil
:internal | '124-fix-issue' | '124 fix issue' | nil
end
context 'when issue is not accessible to user' do
with_them do
before do
project.team.truncate
end
it 'uses branch title as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid} fix issue")
if issue_tracker == :internal
issue.update!(iid: 123)
else
create(:"#{issue_tracker}_service", project: project)
end
end
end
context 'issue does not exist' do
let(:source_branch) { "#{issue.iid.succ}-fix-issue" }
it 'uses the title of the branch as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid.succ} fix issue")
it 'sets the correct title' do
expect(merge_request.title).to eq(title)
end
end
context 'issue is confidential' do
let(:issue_confidential) { true }
it 'uses the title of the branch as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid} fix issue")
it 'sets the closing description' do
expect(merge_request.description).to eq(closing_message)
end
end
end
context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
context 'when the issue is not accessible to user' do
let(:source_branch) { "#{issue.iid}-fix-issue" }
before do
allow(project).to receive(:external_issue_tracker).and_return(false)
allow(project).to receive(:issues_enabled?).and_return(false)
project.team.truncate
end
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('12345 fix issue')
it 'uses branch title as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid} fix issue")
end
end
describe 'with JIRA enabled' do
before do
allow(project).to receive(:external_issue_tracker).and_return(true)
allow(project).to receive(:issues_enabled?).and_return(false)
allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
it 'does not set a description' do
expect(merge_request.description).to be_nil
end
end
context 'branch does not start with JIRA-formatted external issue IID' do
let(:source_branch) { 'test-branch' }
context 'when the issue is confidential' do
let(:source_branch) { "#{issue.iid}-fix-issue" }
let(:issue_confidential) { true }
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Test branch')
end
it 'uses the title of the branch as the merge request title' do
expect(merge_request.title).to eq("#{issue.iid} fix issue")
end
context 'branch starts with JIRA-formatted external issue IID' do
let(:source_branch) { 'EXMPL-12345' }
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Resolve EXMPL-12345')
end
it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345')
end
context 'followed by hyphenated text' do
let(:source_branch) { 'EXMPL-12345-fix-issue' }
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"')
end
it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345')
end
end
it 'does not set a description' do
expect(merge_request.description).to be_nil
end
end
end
......
......@@ -4,8 +4,10 @@ describe Projects::CreateFromTemplateService do
let(:user) { create(:user) }
let(:project_params) do
{
path: user.to_param,
template_name: 'rails'
path: user.to_param,
template_name: 'rails',
description: 'project description',
visibility_level: Gitlab::VisibilityLevel::PRIVATE
}
end
......@@ -22,5 +24,7 @@ describe Projects::CreateFromTemplateService do
expect(project).to be_saved
expect(project.scheduled?).to be(true)
expect(project.description).to match('project description')
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end
......@@ -6,6 +6,11 @@ describe CheckGcpProjectBillingWorker do
subject { described_class.new.perform('token_key') }
before do
allow(described_class).to receive(:get_billing_state)
allow_any_instance_of(described_class).to receive(:update_billing_change_counter)
end
context 'when there is a token in redis' do
before do
allow(described_class).to receive(:get_session_token).and_return(token)
......@@ -23,11 +28,8 @@ describe CheckGcpProjectBillingWorker do
end
it 'stores billing status in redis' do
redis_double = double
expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
expect(redis_double).to receive(:set).with(described_class.redis_shared_state_key_for(token), anything, anything)
expect(described_class).to receive(:set_billing_state).with(token, true)
subject
end
......@@ -48,7 +50,7 @@ describe CheckGcpProjectBillingWorker do
context 'when there is no token in redis' do
before do
allow_any_instance_of(described_class).to receive(:get_session_token).and_return(nil)
allow(described_class).to receive(:get_session_token).and_return(nil)
end
it 'does not call the service' do
......@@ -58,4 +60,57 @@ describe CheckGcpProjectBillingWorker do
end
end
end
describe 'billing change counter' do
subject { described_class.new.perform('token_key') }
before do
allow(described_class).to receive(:get_session_token).and_return('bogustoken')
allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid')
allow(described_class).to receive(:set_billing_state)
end
context 'when previous state was false' do
before do
expect(described_class).to receive(:get_billing_state).and_return(false)
end
context 'when the current state is false' do
before do
expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([])
end
it 'increments the billing change counter' do
expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
subject
end
end
context 'when the current state is true' do
before do
expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
end
it 'increments the billing change counter' do
expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
subject
end
end
end
context 'when previous state was true' do
before do
expect(described_class).to receive(:get_billing_state).and_return(true)
expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
end
it 'increment the billing change counter' do
expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
subject
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment