Commit 2ee20d5a authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into backstage/gb/build-stages-catch-up-migration

* master: (38 commits)
parents d9a8d9f3 e5ecd9b1
......@@ -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",
......
......@@ -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;
})();
}
}
......@@ -224,6 +224,11 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'projects:services:edit':
import('./pages/projects/services/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:edit':
case 'projects:snippets:update':
import('./pages/projects/snippets/edit')
......@@ -462,11 +467,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'users:show':
import('./pages/users/show')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show')
.then(callDefault)
......
......@@ -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);
}
}
......
<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 initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
import Vue from 'vue';
import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
......
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
<script>
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../vue_shared/translate';
import Translate from '../../../../../vue_shared/translate';
import illustrationSvg from '../icons/intro_illustration.svg';
Vue.use(Translate);
......
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import GlFieldErrors from '../gl_field_errors';
import Translate from '../../../../vue_shared/translate';
import GlFieldErrors from '../../../../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list';
import setupNativeFormVariableList from '../../../../ci_variable_list/native_form_variable_list';
Vue.use(Translate);
......@@ -27,7 +27,7 @@ function initIntervalPatternInput() {
});
}
document.addEventListener('DOMContentLoaded', () => {
export default () => {
/* Most of the form is written in haml, but for fields with more complex behaviors,
* you should mount individual Vue components here. If at some point components need
* to share state, it may make sense to refactor the whole form to Vue */
......@@ -46,4 +46,4 @@ document.addEventListener('DOMContentLoaded', () => {
container: $('.js-ci-variable-list-section'),
formField: 'schedule',
});
});
};
import initForm from '../shared/init_form';
document.addEventListener('DOMContentLoaded', initForm);
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);
});
<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>
......@@ -255,8 +255,6 @@ ul.controls {
}
.author_link {
display: inline-block;
.avatar-inline {
margin-left: 0;
margin-right: 0;
......
......@@ -9,6 +9,7 @@ class Identity < ActiveRecord::Base
validates :user_id, uniqueness: { scope: :provider }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
......@@ -34,4 +35,12 @@ class Identity < ActiveRecord::Base
self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
end
def user_synced_attributes_metadata_from_provider?
user.user_synced_attributes_metadata&.provider == provider
end
def clear_user_synced_attributes
user.user_synced_attributes_metadata&.destroy
end
end
......@@ -593,7 +593,15 @@ class Repository
def license_key
return unless exists?
Licensee.license(path).try(:key)
# The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
begin
Licensee.license(path).try(:key)
# Normally we would rescue Rugged::Error, but that is banned by lint-rugged
# and we need to migrate this endpoint to Gitaly:
# https://gitlab.com/gitlab-org/gitaly/issues/1026
rescue
end
end
cache_method :license_key
......
......@@ -249,7 +249,7 @@ class User < ActiveRecord::Base
def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
else
find_by(conditions)
end
......
......@@ -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,
......
......@@ -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 } }
......
......@@ -58,7 +58,7 @@
- if @project.avatar?
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
= f.submit 'Save changes', class: "btn btn-success"
= f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
%section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedule_form'
= form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f|
= form_errors(@schedule)
.form-group
......
- breadcrumb_title _("Schedules")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'schedules_index'
- @no_container = true
- page_title _("Pipeline Schedules")
......
- @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")
......
---
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: 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: Only check LFS integrity for first ref in a push to avoid timeout
merge_request: 17098
author:
type: performance
---
title: Add new modal Vue component
merge_request: 17108
author:
type: changed
......@@ -69,6 +69,7 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
# - Secret variable values (:value)
config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i(
certificate
......@@ -80,6 +81,7 @@ module Gitlab
sentry_dsn
trace
variables
value
)
# Enable escaping HTML in JSON.
......
......@@ -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
......
......@@ -68,7 +68,6 @@ var config = {
help: './help/help.js',
how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
job_details: './jobs/job_details_bundle.js',
locale: './locale/index.js',
main: './main.js',
......@@ -79,9 +78,7 @@ var config = {
notes: './notes/index.js',
pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js',
pipelines_charts: './pipelines/pipelines_charts.js',
pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js',
prometheus_metrics: './prometheus_metrics',
......@@ -90,8 +87,6 @@ var config = {
registry_list: './registry/index.js',
ide: './ide/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
snippet: './snippet/snippet_bundle.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
......@@ -102,7 +97,6 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
webpack_runtime: './webpack.js',
},
......@@ -158,7 +152,7 @@ var config = {
include: /node_modules\/katex\/dist/,
use: [
{ loader: 'style-loader' },
{
{
loader: 'css-loader',
options: {
name: '[name].[hash].[ext]'
......
......@@ -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:
......
......@@ -56,7 +56,7 @@ new one, and attempting to pull a repo.
> **Warning:** Do not disable writes until SSH is confirmed to be working
perfectly, because the file will quickly become out-of-date.
In the case of lookup failures (which are not uncommon), the `authorized_keys`
In the case of lookup failures (which are common), the `authorized_keys`
file will still be scanned. So git SSH performance will still be slow for many
users as long as a large file exists.
......
......@@ -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
......
# 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
......
......@@ -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,
......
......@@ -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
......
@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
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
......@@ -20,15 +20,16 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods
include ActiveJob::TestHelper
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
RSpec::Mocks.setup
TestEnv.init(mailer: false)
# skip pre-receive hook check so we can use
# web editor and merge
TestEnv.disable_pre_receive
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
end
Spinach.hooks.after_scenario do |scenario_data, step_definitions|
......
......@@ -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
......
......@@ -16,11 +16,11 @@ module Gitlab
lfs_objects_missing: 'LFS objects are missing. Ensure LFS is properly set up or try a manual "git lfs push --all".'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
attr_reader :user_access, :project, :skip_authorization, :skip_lfs_integrity_check, :protocol, :oldrev, :newrev, :ref, :branch_name, :tag_name
def initialize(
change, user_access:, project:, skip_authorization: false,
protocol:
skip_lfs_integrity_check: false, protocol:
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
......@@ -28,6 +28,7 @@ module Gitlab
@user_access = user_access
@project = project
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol
end
......@@ -37,7 +38,7 @@ module Gitlab
push_checks
branch_checks
tag_checks
lfs_objects_exist_check
lfs_objects_exist_check unless skip_lfs_integrity_check
commits_check unless skip_commits_check
true
......
......@@ -238,19 +238,22 @@ module Gitlab
changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
changes_list.each.with_index do |change, index|
first_change = index == 0
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
check_single_change_access(change)
check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end
end
def check_single_change_access(change)
def check_single_change_access(change, skip_lfs_integrity_check: false)
Checks::ChangeAccess.new(
change,
user_access: user_access,
project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol
).exec
end
......
......@@ -13,7 +13,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end
def check_single_change_access(change)
def check_single_change_access(change, _options = {})
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
......
......@@ -15,7 +15,7 @@ module Gitlab
end
def self.servers
Gitlab.config.ldap.servers.values
Gitlab.config.ldap['servers']&.values || []
end
def self.available_servers
......
......@@ -198,9 +198,11 @@ module Gitlab
end
def update_profile
clear_user_synced_attributes_metadata
return unless sync_profile_from_provider? || creating_linked_ldap_user?
metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
metadata = gl_user.build_user_synced_attributes_metadata
if sync_profile_from_provider?
UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
......@@ -221,6 +223,10 @@ module Gitlab
end
end
def clear_user_synced_attributes_metadata
gl_user&.user_synced_attributes_metadata&.destroy
end
def log
Gitlab::AppLogger
end
......
......@@ -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
......
......@@ -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
......
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
......@@ -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
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');
});
});
});
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
Vue.use(Translate);
......
import Vue from 'vue';
import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
......
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);
});
});
});
});
......@@ -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
......@@ -18,8 +18,9 @@ describe Gitlab::GitAccess do
redirected_path: redirected_path)
end
let(:push_access_check) { access.check('git-receive-pack', '_any') }
let(:pull_access_check) { access.check('git-upload-pack', '_any') }
let(:changes) { '_any' }
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
......@@ -646,6 +647,20 @@ describe Gitlab::GitAccess do
end
end
describe 'check LFS integrity' do
let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] }
before do
project.add_developer(user)
end
it 'checks LFS integrity only for first change' do
expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times
push_access_check
end
end
describe '#check_push_access!' do
before do
merge_into_protected_branch
......
......@@ -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'
......
......@@ -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
......@@ -873,6 +873,18 @@ describe Repository do
expect(repository.license_key).to be_nil
end
it 'returns nil when the commit SHA does not exist' do
allow(repository.head_commit).to receive(:sha).and_return('1' * 40)
expect(repository.license_key).to be_nil
end
it 'returns nil when master does not exist' do
repository.rm_branch(user, 'master')
expect(repository.license_key).to be_nil
end
it 'returns the license key' do
repository.create_file(user, 'LICENSE',
Licensee::License.new('mit').content,
......
......@@ -893,6 +893,14 @@ describe User do
end
end
describe '.find_for_database_authentication' do
it 'strips whitespace from login' do
user = create(:user)
expect(described_class.find_for_database_authentication({ login: " #{user.username} " })).to eq user
end
end
describe '.find_by_any_email' do
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
......
This diff is collapsed.
This diff is collapsed.
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