Commit 1d8a236a authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents e8336638 cb95c2f7
......@@ -45,6 +45,7 @@ export default () => {
new Vue({
el,
name: 'CycleAnalytics',
apolloProvider: {},
store,
render: (createElement) =>
createElement(CycleAnalytics, {
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import '~/profile/gl_crop';
import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings';
import initPasswordPrompt from './password_prompt';
// eslint-disable-next-line func-names
$(document).on('input.ssh_key', '#key_key', function () {
......@@ -19,3 +20,4 @@ $(document).on('input.ssh_key', '#key_key', function () {
new Profile(); // eslint-disable-line no-new
initSearchSettings();
initPasswordPrompt();
import { __, s__ } from '~/locale';
export const I18N_PASSWORD_PROMPT_TITLE = s__('PasswordPrompt|Confirm password to continue');
export const I18N_PASSWORD_PROMPT_FORM_LABEL = s__(
'PasswordPrompt|Please enter your password to confirm',
);
export const I18N_PASSWORD_PROMPT_ERROR_MESSAGE = s__('PasswordPrompt|Password is required');
export const I18N_PASSWORD_PROMPT_CONFIRM_BUTTON = s__('PasswordPrompt|Confirm password');
export const I18N_PASSWORD_PROMPT_CANCEL_BUTTON = __('Cancel');
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import PasswordPromptModal from './password_prompt_modal.vue';
Vue.use(Translate);
const emailFieldSelector = '#user_email';
const editFormSelector = '.js-password-prompt-form';
const passwordPromptFieldSelector = '.js-password-prompt-field';
const passwordPromptBtnSelector = '.js-password-prompt-btn';
const passwordPromptModalId = 'password-prompt-modal';
const getEmailValue = () => document.querySelector(emailFieldSelector).value.trim();
const passwordPromptButton = document.querySelector(passwordPromptBtnSelector);
const field = document.querySelector(passwordPromptFieldSelector);
const form = document.querySelector(editFormSelector);
const handleConfirmPassword = (pw) => {
// update the validation_password field
field.value = pw;
// submit the form
form.submit();
};
export default () => {
const passwordPromptModalEl = document.getElementById(passwordPromptModalId);
if (passwordPromptModalEl && field) {
return new Vue({
el: passwordPromptModalEl,
data() {
return {
initialEmail: '',
};
},
mounted() {
this.initialEmail = getEmailValue();
passwordPromptButton.addEventListener('click', this.handleSettingsUpdate);
},
methods: {
handleSettingsUpdate(ev) {
const email = getEmailValue();
if (email !== this.initialEmail) {
ev.preventDefault();
this.$root.$emit('bv::show::modal', passwordPromptModalId, passwordPromptBtnSelector);
}
},
},
render(createElement) {
return createElement(PasswordPromptModal, {
props: { handleConfirmPassword },
});
},
});
}
return null;
};
<script>
import { GlModal, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import {
I18N_PASSWORD_PROMPT_TITLE,
I18N_PASSWORD_PROMPT_FORM_LABEL,
I18N_PASSWORD_PROMPT_ERROR_MESSAGE,
I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
} from './constants';
export default {
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
},
props: {
handleConfirmPassword: {
type: Function,
required: true,
},
},
data() {
return {
passwordCheck: '',
};
},
computed: {
isValid() {
return Boolean(this.passwordCheck.length);
},
primaryProps() {
return {
text: I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.isValid }],
};
},
},
methods: {
onConfirmPassword() {
this.handleConfirmPassword(this.passwordCheck);
},
},
cancelProps: {
text: I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
},
i18n: {
title: I18N_PASSWORD_PROMPT_TITLE,
formLabel: I18N_PASSWORD_PROMPT_FORM_LABEL,
errorMessage: I18N_PASSWORD_PROMPT_ERROR_MESSAGE,
},
};
</script>
<template>
<gl-modal
data-testid="password-prompt-modal"
modal-id="password-prompt-modal"
:title="$options.i18n.title"
:action-primary="primaryProps"
:action-cancel="$options.cancelProps"
@primary="onConfirmPassword"
>
<gl-form @submit.prevent="onConfirmPassword">
<gl-form-group
:label="$options.i18n.formLabel"
label-for="password-prompt-confirmation"
:invalid-feedback="$options.i18n.errorMessage"
:state="isValid"
>
<gl-form-input
id="password-prompt-confirmation"
v-model="passwordCheck"
name="password-confirmation"
type="password"
data-testid="password-prompt-field"
/>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
......@@ -123,7 +123,7 @@ export default {
</script>
<template>
<div id="related-issues" class="related-issues-block">
<div id="related-issues" class="related-issues-block gl-mt-5">
<div class="card card-slim gl-overflow-hidden">
<div
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
......
......@@ -97,11 +97,7 @@ export default {
class="related-issues-token-body bordered-box bg-white"
:class="{ 'sortable-container': canReorder }"
>
<div
v-if="isFetching"
class="related-issues-loading-icon"
data-qa-selector="related_issues_loading_placeholder"
>
<div v-if="isFetching" class="gl-mb-2" data-qa-selector="related_issues_loading_placeholder">
<gl-loading-icon
ref="loadingIcon"
size="sm"
......
<script>
import { sprintf, s__ } from '~/locale';
import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
const timeSpent = s__('TimeTracking|%{spentStart}Spent: %{spentEnd}');
export default {
name: 'TimeTrackingSpentOnlyPane',
timeSpent,
components: {
GlSprintf,
},
props: {
timeSpentHumanReadable: {
type: String,
required: true,
},
},
computed: {
timeSpent() {
return sprintf(
s__('TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}'),
{
startTag: '<span class="gl-font-weight-bold">',
endTag: '</span>',
timeSpentHumanReadable: this.timeSpentHumanReadable,
},
false,
);
},
},
};
</script>
<template>
<div data-testid="spentOnlyPane" v-html="timeSpent /* eslint-disable-line vue/no-v-html */"></div>
<div data-testid="spentOnlyPane">
<gl-sprintf :message="$options.timeSpent">
<template #spent="{ content }">
<span class="gl-font-weight-bold">{{ content }}</span
>{{ timeSpentHumanReadable }}
</template>
</gl-sprintf>
</div>
</template>
......@@ -18,7 +18,7 @@ class ProfilesController < Profiles::ApplicationController
def update
respond_to do |format|
result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute(check_password: true)
if result[:status] == :success
message = s_("Profiles|Profile was successfully updated")
......@@ -129,6 +129,7 @@ class ProfilesController < Profiles::ApplicationController
:job_title,
:pronouns,
:pronunciation,
:validation_password,
status: [:emoji, :message, :availability]
]
end
......
......@@ -70,11 +70,16 @@ class MembersFinder
end
def project_invited_groups
invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy
.new(project.invited_groups)
.base_and_ancestors
.public_or_visible_to_user(current_user)
.select(:id)
invited_groups_and_ancestors = if ::Feature.enabled?(:linear_members_finder_ancestor_scopes, current_user, default_enabled: :yaml)
project.invited_groups
.self_and_ancestors
else
Gitlab::ObjectHierarchy
.new(project.invited_groups)
.base_and_ancestors
end
invited_groups_ids_including_ancestors = invited_groups_and_ancestors.public_or_visible_to_user(current_user).select(:id)
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end
......
......@@ -83,7 +83,7 @@ module Users
end
def lazy_user_availability(user)
BatchLoader.for(user.id).batch do |user_ids, loader|
BatchLoader.for(user.id).batch(replace_methods: false) do |user_ids, loader|
user_ids.each_slice(1_000) do |sliced_user_ids|
UserStatus
.select(:user_id, :availability)
......
......@@ -5,15 +5,18 @@ module Users
include NewUserNotifier
attr_reader :user, :identity_params
ATTRS_REQUIRING_PASSWORD_CHECK = %w[email].freeze
def initialize(current_user, params = {})
@current_user = current_user
@validation_password = params.delete(:validation_password)
@user = params.delete(:user)
@status_params = params.delete(:status)
@identity_params = params.slice(*identity_attributes)
@params = params.dup
end
def execute(validate: true, &block)
def execute(validate: true, check_password: false, &block)
yield(@user) if block_given?
user_exists = @user.persisted?
......@@ -21,6 +24,11 @@ module Users
discard_read_only_attributes
assign_attributes
if check_password && require_password_check? && !@user.valid_password?(@validation_password)
return error(s_("Profiles|Invalid password"))
end
assign_identity
build_canonical_email
......@@ -32,8 +40,8 @@ module Users
end
end
def execute!(*args, &block)
result = execute(*args, &block)
def execute!(*args, **kargs, &block)
result = execute(*args, **kargs, &block)
raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success
......@@ -42,6 +50,14 @@ module Users
private
def require_password_check?
return false unless @user.persisted?
return false if @user.password_automatically_set?
changes = @user.changed
ATTRS_REQUIRING_PASSWORD_CHECK.any? { |param| changes.include?(param) }
end
def build_canonical_email
return unless @user.email_changed?
......
......@@ -3,8 +3,11 @@
- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil)
- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user)
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
- password_automatically_set = @user.password_automatically_set?
= form.text_field :email, required: true, class: 'input-lg gl-form-input', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
- unless password_automatically_set
= hidden_field_tag 'user[validation_password]', :validation_password, class: 'js-password-prompt-field', help: s_("Profiles|Enter your password to confirm the email change")
= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2 input-lg', disabled: email_change_disabled
......
......@@ -5,7 +5,7 @@
- availability = availability_values
- custom_emoji = show_status_emoji?(@user.status)
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
= form_errors(@user)
.row.js-search-settings-section
......@@ -124,9 +124,11 @@
.help-block
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
%hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3'
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
#password-prompt-modal
.modal.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } }
.modal-dialog
.modal-content
......
......@@ -3,4 +3,3 @@
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "false" } }
- render('projects/issues/related_issues_block')
.related-issues-block
.card.card-slim
.card-header.panel-empty-heading.border-bottom-0
%h3.card-title.mt-0.mb-0.h5
= _('Linked issues')
......@@ -13,7 +13,8 @@
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
%li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= issuable.upvotes_count
%li.issuable-upvotes.gl-list-style-none
%span.has-tooltip{ title: _('Upvotes') }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= issuable.upvotes_count
%span.gl-text-gray-500= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
......@@ -26,8 +26,7 @@ module Gitlab
object = representation_class.from_json_hash(hash)
# To better express in the logs what object is being imported.
self.github_id = object.attributes.fetch(:github_id)
self.github_identifiers = object.github_identifiers
info(project.id, message: 'starting importer')
importer_class.new(object, project, client).execute
......@@ -35,10 +34,10 @@ module Gitlab
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
info(project.id, message: 'importer finished')
rescue KeyError => e
rescue NoMethodError => e
# This exception will be more useful in development when a new
# Representation is created but the developer forgot to add a
# `:github_id` field.
# `:github_identifiers` field.
Gitlab::Import::ImportFailureService.track(
project_id: project.id,
error_source: importer_class.name,
......@@ -72,7 +71,7 @@ module Gitlab
private
attr_accessor :github_id
attr_accessor :github_identifiers
def info(project_id, extra = {})
Logger.info(log_attributes(project_id, extra))
......@@ -82,7 +81,7 @@ module Gitlab
extra.merge(
project_id: project_id,
importer: importer_class.name,
github_id: github_id
github_identifiers: github_identifiers
)
end
end
......
---
name: linear_members_finder_ancestor_scopes
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70583
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341347
milestone: '14.4'
type: development
group: group::access
default_enabled: false
# frozen_string_literal: true
require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
require 'json'
VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/.freeze
module GettextI18nRails
class HamlParser
singleton_class.send(:alias_method, :old_convert_to_code, :convert_to_code)
# We need to convert text in Mustache format
# to a format that can be parsed by Gettext scripts.
# If we found a content like "{{ __('Stage') }}"
# in a HAML file we convert it to "= _('Stage')", that way
# it can be processed by the "rake gettext:find" script.
#
# Overwrites: https://github.com/grosser/gettext_i18n_rails/blob/8396387a431e0f8ead72fc1cd425cad2fa4992f2/lib/gettext_i18n_rails/haml_parser.rb#L9
def self.convert_to_code(text)
text.gsub!(VUE_TRANSLATE_REGEX, "\\2= \\3_(\\4)")
old_convert_to_code(text)
end
end
end
module GettextI18nRailsJs
module Parser
module Javascript
......
......@@ -21,6 +21,7 @@ You can use the following environment variables to override certain values:
|--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------|
| `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. |
| `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). |
| `EXTERNAL_URL` | string | Specify the external URL at the [time of installation](https://docs.gitlab.com/omnibus/settings/configuration.html#specifying-the-external-url-at-the-time-of-installation). |
| `EXTERNAL_VALIDATION_SERVICE_TIMEOUT` | integer | Timeout, in seconds, for an [external CI/CD pipeline validation service](external_pipeline_validation.md). Default is `5`. |
| `EXTERNAL_VALIDATION_SERVICE_URL` | string | URL to an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
| `EXTERNAL_VALIDATION_SERVICE_TOKEN` | string | The `X-Gitlab-Token` for authentication with an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
......
......@@ -64,7 +64,7 @@ build:
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
rules:
- if: $CI_COMMIT_TAG
......@@ -91,7 +91,7 @@ build:
- mkdir -p /kaniko/.docker
- |-
KANIKOPROXYBUILDARGS=""
KANIKOCFG="{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}"
KANIKOCFG="{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}"
if [ "x${http_proxy}" != "x" -o "x${https_proxy}" != "x" ]; then
KANIKOCFG="${KANIKOCFG}, \"proxies\": { \"default\": { \"httpProxy\": \"${http_proxy}\", \"httpsProxy\": \"${https_proxy}\", \"noProxy\": \"${no_proxy}\"}}"
KANIKOPROXYBUILDARGS="--build-arg http_proxy=${http_proxy} --build-arg https_proxy=${https_proxy} --build-arg no_proxy=${no_proxy}"
......@@ -120,7 +120,7 @@ store:
```yaml
before_script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64)\"}}}" > /kaniko/.docker/config.json
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$(echo -n ${CI_REGISTRY_USER}:${CI_REGISTRY_PASSWORD} | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
- |
echo "-----BEGIN CERTIFICATE-----
...
......
......@@ -71,6 +71,7 @@ topics and use cases. The most frequently required during database reviewing are
- [Migrations style guide](../migration_style_guide.md) for creating safe SQL migrations.
- [Avoiding downtime in migrations](../avoiding_downtime_in_migrations.md).
- [SQL guidelines](../sql.md) for working with SQL queries.
- [Guidelines for JiHu contributions with database migrations](https://about.gitlab.com/handbook/ceo/chief-of-staff-team/jihu-support/jihu-database-change-process.html)
## How to apply to become a database maintainer
......
......@@ -28,8 +28,8 @@ export default {
},
summaryDescription() {
const {
startDate,
endDate,
createdAfter,
createdBefore,
selectedProjectIds,
currentGroup: { name: groupName },
} = this.selectedTasksByTypeFilters;
......@@ -38,14 +38,14 @@ export default {
const str =
selectedProjectCount > 0
? s__(
"CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{startDate} to %{endDate}",
"CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{createdAfter} to %{createdBefore}",
)
: s__(
"CycleAnalytics|Showing data for group '%{groupName}' from %{startDate} to %{endDate}",
"CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}",
);
return sprintf(str, {
startDate: formattedDate(startDate),
endDate: formattedDate(endDate),
createdAfter: formattedDate(createdAfter),
createdBefore: formattedDate(createdBefore),
groupName,
selectedProjectCount,
});
......
......@@ -21,10 +21,6 @@ export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => {
export const tasksByTypeChartData = ({ data = [] } = {}, _, rootState = {}) => {
const { createdAfter = null, createdBefore = null } = rootState;
return data.length
? getTasksByTypeData({
data,
startDate: createdAfter,
endDate: createdBefore,
})
? getTasksByTypeData({ data, createdAfter, createdBefore })
: { groupBy: [], data: [] };
};
......@@ -214,20 +214,20 @@ export const flattenTaskByTypeSeries = (series = {}) =>
*
* @param {Object} obj
* @param {RawTasksByTypeData[]} obj.data - array of raw data, each element contains a label and series
* @param {Date} obj.startDate - start date in ISO date format
* @param {Date} obj.endDate - end date in ISO date format
* @param {Date} obj.createdAfter - start date in ISO date format
* @param {Date} obj.createdBefore - end date in ISO date format
*
* @returns {TransformedTasksByTypeData} The transformed data ready for use in charts
*/
export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null }) => {
if (!startDate || !endDate || !data.length) {
export const getTasksByTypeData = ({ data = [], createdAfter = null, createdBefore = null }) => {
if (!createdAfter || !createdBefore || !data.length) {
return {
groupBy: [],
data: [],
};
}
const groupBy = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate);
const groupBy = getDatesInRange(createdAfter, createdBefore, toYmd).sort(orderByDate);
const zeroValuesForEachDataPoint = groupBy.reduce(
(acc, date) => ({
...acc,
......
$token-spacing-bottom: 0.5em;
.related-issues-block {
margin-top: 3 * $gl-vert-padding;
}
.related-issues-header-help-icon {
margin-left: 0.25em;
color: $gl-text-color-secondary;
}
.related-issues-token-body {
padding: 0;
li .issuable-info-container {
padding-left: $gl-padding;
@include media-breakpoint-down(md) {
padding-left: $gl-padding-8;
}
}
a.issuable-main-info:hover {
text-decoration: none;
......@@ -29,25 +8,6 @@ $token-spacing-bottom: 0.5em;
}
}
.related-issues-loading-icon {
margin-bottom: $token-spacing-bottom;
line-height: 1.75;
}
.related-issues-token-list {
display: flex;
flex-wrap: wrap;
margin-bottom: 0;
padding-left: 0;
list-style: none;
}
.related-issues-token-list-item {
max-width: 100%;
margin-bottom: $token-spacing-bottom;
margin-right: 5px;
}
.issue-token-end {
order: 1;
}
......
......@@ -4,7 +4,10 @@
%h3.page-title= _('Upload License')
%p.light
To #{License.current ? "continue" : "start"} using GitLab Enterprise Edition, upload the <code>.gitlab-license</code> file or enter the license key you have received from GitLab Inc.
- if License.current
= _('To continue using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc.').html_safe % {codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe}
- else
= _('To start using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc.').html_safe % {codeOpen: '<code>'.html_safe, codeClose: '</code>'.html_safe}
%hr
= form_for @license, url: admin_license_path, html: { multipart: true, class: 'fieldset-form' } do |f|
......
......@@ -3,4 +3,3 @@
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "#{!!@project.feature_available?(:blocked_issues)}" } }
- render('projects/issues/related_issues_block')
......@@ -212,6 +212,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
expect(element.find('.js-daterange-picker-from input').value).to eq '2019-11-01'
expect(element.find('.js-daterange-picker-to input').value).to eq '2019-12-31'
expect(page.find('.js-tasks-by-type-chart')).to have_text(_("Showing data for group '%{group_name}' from Nov 1, 2019 to Dec 31, 2019") % { group_name: group.name })
end
end
end
......
......@@ -222,8 +222,8 @@ export const taskByTypeFilters = {
fullPath: 'gitlab-org',
},
selectedProjectIds: [],
startDate: new Date('2019-12-11'),
endDate: new Date('2020-01-10'),
createdAfter: new Date('2019-12-11'),
createdBefore: new Date('2020-01-10'),
subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
selectedLabelIds: [1, 2, 3],
};
......
......@@ -247,22 +247,20 @@ describe('Value Stream Analytics utils', () => {
});
it('will return blank arrays if given no data', () => {
[{ data: [], startDate: createdAfter, endDate: createdBefore }, [], {}].forEach(
(chartData) => {
transformed = getTasksByTypeData(chartData);
['data', 'groupBy'].forEach((key) => {
expect(transformed[key]).toEqual([]);
});
},
);
[{ data: [], createdAfter, createdBefore }, [], {}].forEach((chartData) => {
transformed = getTasksByTypeData(chartData);
['data', 'groupBy'].forEach((key) => {
expect(transformed[key]).toEqual([]);
});
});
});
describe('with data', () => {
beforeEach(() => {
transformed = getTasksByTypeData({
data: rawTasksByTypeData,
startDate: createdAfter,
endDate: createdBefore,
createdAfter,
createdBefore,
});
});
......
......@@ -1093,7 +1093,6 @@ module API
attrs = declared_params(include_missing: false)
service = ::UserPreferences::UpdateService.new(current_user, attrs).execute
if service.success?
present preferences, with: Entities::UserPreferences
else
......
......@@ -11,7 +11,7 @@ module Gitlab
expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path,
:diff_hunk, :author, :note, :created_at, :updated_at,
:github_id, :original_commit_id
:original_commit_id, :note_id
NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i.freeze
......@@ -40,7 +40,7 @@ module Gitlab
note: note.body,
created_at: note.created_at,
updated_at: note.updated_at,
github_id: note.id
note_id: note.id
}
new(hash)
......@@ -82,6 +82,14 @@ module Gitlab
new_file: false
}
end
def github_identifiers
{
note_id: note_id,
noteable_id: noteable_id,
noteable_type: noteable_type
}
end
end
end
end
......
......@@ -25,7 +25,6 @@ module Gitlab
hash = {
iid: issue.number,
github_id: issue.number,
title: issue.title,
description: issue.body,
milestone_number: issue.milestone&.number,
......@@ -75,6 +74,13 @@ module Gitlab
def issuable_type
pull_request? ? 'MergeRequest' : 'Issue'
end
def github_identifiers
{
iid: iid,
issuable_type: issuable_type
}
end
end
end
end
......
......@@ -16,8 +16,7 @@ module Gitlab
new(
oid: lfs_object.oid,
link: lfs_object.link,
size: lfs_object.size,
github_id: lfs_object.oid
size: lfs_object.size
)
end
......@@ -31,6 +30,12 @@ module Gitlab
def initialize(attributes)
@attributes = attributes
end
def github_identifiers
{
oid: oid
}
end
end
end
end
......
......@@ -10,7 +10,7 @@ module Gitlab
attr_reader :attributes
expose_attribute :noteable_id, :noteable_type, :author, :note,
:created_at, :updated_at, :github_id
:created_at, :updated_at, :note_id
NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i.freeze
......@@ -42,7 +42,7 @@ module Gitlab
note: note.body,
created_at: note.created_at,
updated_at: note.updated_at,
github_id: note.id
note_id: note.id
}
new(hash)
......@@ -64,6 +64,14 @@ module Gitlab
end
alias_method :issuable_type, :noteable_type
def github_identifiers
{
note_id: note_id,
noteable_id: noteable_id,
noteable_type: noteable_type
}
end
end
end
end
......
......@@ -25,7 +25,6 @@ module Gitlab
hash = {
iid: pr.number,
github_id: pr.number,
title: pr.title,
description: pr.body,
source_branch: pr.head.ref,
......@@ -108,6 +107,13 @@ module Gitlab
def issuable_type
'MergeRequest'
end
def github_identifiers
{
iid: iid,
issuable_type: issuable_type
}
end
end
end
end
......
......@@ -9,7 +9,7 @@ module Gitlab
attr_reader :attributes
expose_attribute :author, :note, :review_type, :submitted_at, :github_id, :merge_request_id
expose_attribute :author, :note, :review_type, :submitted_at, :merge_request_id, :review_id
def self.from_api_response(review)
user = Representation::User.from_api_response(review.user) if review.user
......@@ -20,7 +20,7 @@ module Gitlab
note: review.body,
review_type: review.state,
submitted_at: review.submitted_at,
github_id: review.id
review_id: review.id
)
end
......@@ -43,6 +43,13 @@ module Gitlab
def approval?
review_type == 'APPROVED'
end
def github_identifiers
{
review_id: review_id,
merge_request_id: merge_request_id
}
end
end
end
end
......
......@@ -17,7 +17,6 @@ module Gitlab
def self.from_api_response(user)
new(
id: user.id,
github_id: user.id,
login: user.login
)
end
......
......@@ -10198,10 +10198,10 @@ msgid_plural "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCo
msgstr[0] ""
msgstr[1] ""
msgid "CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{startDate} to %{endDate}"
msgid "CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{createdAfter} to %{createdBefore}"
msgstr ""
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{startDate} to %{endDate}"
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
msgstr ""
msgid "CycleAnalytics|Stages"
......@@ -24487,6 +24487,18 @@ msgstr ""
msgid "Password was successfully updated. Please sign in again."
msgstr ""
msgid "PasswordPrompt|Confirm password"
msgstr ""
msgid "PasswordPrompt|Confirm password to continue"
msgstr ""
msgid "PasswordPrompt|Password is required"
msgstr ""
msgid "PasswordPrompt|Please enter your password to confirm"
msgstr ""
msgid "Passwords should be unique and not used for any other sites or services."
msgstr ""
......@@ -25933,6 +25945,9 @@ msgstr ""
msgid "Profiles|Enter your name, so people you know can recognize you"
msgstr ""
msgid "Profiles|Enter your password to confirm the email change"
msgstr ""
msgid "Profiles|Enter your pronouns to let people know how to refer to you"
msgstr ""
......@@ -31274,6 +31289,9 @@ msgstr ""
msgid "Showing all issues"
msgstr ""
msgid "Showing data for group '%{group_name}' from Nov 1, 2019 to Dec 31, 2019"
msgstr ""
msgid "Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days."
msgstr ""
......@@ -35150,7 +35168,7 @@ msgstr ""
msgid "TimeTrackingEstimated|Est"
msgstr ""
msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
msgid "TimeTracking|%{spentStart}Spent: %{spentEnd}"
msgstr ""
msgid "TimeTracking|Estimated:"
......@@ -35388,6 +35406,9 @@ msgstr ""
msgid "To connect an SVN repository, check out %{svn_link}."
msgstr ""
msgid "To continue using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc."
msgstr ""
msgid "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select %{strongStart}Resend confirmation email.%{strongEnd}"
msgstr ""
......@@ -35493,6 +35514,9 @@ msgstr ""
msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there."
msgstr ""
msgid "To start using GitLab Enterprise Edition, upload the %{codeOpen}.gitlab-license%{codeClose} file or enter the license key you have received from GitLab Inc."
msgstr ""
msgid "To unsubscribe from this issue, please paste the following link into your browser:"
msgstr ""
......
......@@ -3,7 +3,8 @@
require('spec_helper')
RSpec.describe ProfilesController, :request_store do
let(:user) { create(:user) }
let(:password) { 'longsecret987!' }
let(:user) { create(:user, password: password) }
describe 'POST update' do
it 'does not update password' do
......@@ -23,7 +24,7 @@ RSpec.describe ProfilesController, :request_store do
sign_in(user)
put :update,
params: { user: { email: "john@gmail.com", name: "John" } }
params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
user.reload
......
......@@ -139,6 +139,8 @@ FactoryBot.define do
end
factory :omniauth_user do
password_automatically_set { true }
transient do
extern_uid { '123456' }
provider { 'ldapmain' }
......
......@@ -121,7 +121,7 @@ RSpec.describe 'Admin Mode Login' do
end
context 'when logging in via omniauth' do
let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: 'my-uid', provider: 'saml', password_automatically_set: false)}
let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml')
end
......
......@@ -19,6 +19,17 @@ RSpec.describe 'User edit profile' do
wait_for_requests if respond_to?(:wait_for_requests)
end
def update_user_email
fill_in 'user_email', with: 'new-email@example.com'
click_button 'Update profile settings'
end
def confirm_password(password)
fill_in 'password-confirmation', with: password
click_button 'Confirm password'
wait_for_requests if respond_to?(:wait_for_requests)
end
def visit_user
visit user_path(user)
wait_for_requests
......@@ -88,16 +99,42 @@ RSpec.describe 'User edit profile' do
expect(page).to have_content('Website url is not a valid URL')
end
describe 'when I change my email' do
describe 'when I change my email', :js do
before do
user.send_reset_password_instructions
end
it 'will prompt to confirm my password' do
expect(user.reset_password_token?).to be true
update_user_email
expect(page).to have_selector('[data-testid="password-prompt-modal"]')
end
context 'when prompted to confirm password' do
before do
update_user_email
end
it 'with the correct password successfully updates' do
confirm_password(user.password)
expect(page).to have_text("Profile was successfully updated")
end
it 'with the incorrect password fails to update' do
confirm_password("Fake password")
expect(page).to have_text("Invalid password")
end
end
it 'clears the reset password token' do
expect(user.reset_password_token?).to be true
fill_in 'user_email', with: 'new-email@example.com'
submit_settings
update_user_email
confirm_password(user.password)
user.reload
expect(user.confirmation_token).not_to be_nil
......
......@@ -875,7 +875,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
end
end
context 'when the user does not have an email configured' do
context 'when the user does not have an email configured', :js do
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
before do
......
......@@ -161,42 +161,54 @@ RSpec.describe MembersFinder, '#execute' do
end
context 'when :invited_groups is passed' do
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
shared_examples 'with invited_groups param' do
subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
let_it_be(:linked_group) { create(:group, :public) }
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
let_it_be(:linked_group) { create(:group, :public) }
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
it 'includes all the invited_groups members including members inherited from ancestor groups' do
create(:project_group_link, project: project, group: nested_linked_group)
it 'includes all the invited_groups members including members inherited from ancestor groups' do
create(:project_group_link, project: project, group: nested_linked_group)
expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
end
expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
end
it 'includes all the invited_groups members' do
create(:project_group_link, project: project, group: linked_group)
it 'includes all the invited_groups members' do
create(:project_group_link, project: project, group: linked_group)
expect(subject).to contain_exactly(linked_group_member)
end
expect(subject).to contain_exactly(linked_group_member)
end
it 'excludes group_members not visible to the user' do
create(:project_group_link, project: project, group: linked_group)
private_linked_group = create(:group, :private)
private_linked_group.add_developer(user3)
create(:project_group_link, project: project, group: private_linked_group)
it 'excludes group_members not visible to the user' do
create(:project_group_link, project: project, group: linked_group)
private_linked_group = create(:group, :private)
private_linked_group.add_developer(user3)
create(:project_group_link, project: project, group: private_linked_group)
expect(subject).to contain_exactly(linked_group_member)
expect(subject).to contain_exactly(linked_group_member)
end
context 'when the user is a member of invited group and ancestor groups' do
it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
nested_linked_group.add_developer(user1)
expect(subject.map(&:user)).to contain_exactly(user1, user2)
expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
end
end
end
context 'when the user is a member of invited group and ancestor groups' do
it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
nested_linked_group.add_developer(user1)
it_behaves_like 'with invited_groups param'
expect(subject.map(&:user)).to contain_exactly(user1, user2)
expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
context 'when feature flag :linear_members_finder_ancestor_scopes is disabled' do
before do
stub_feature_flags(linear_members_finder_ancestor_scopes: false)
end
it_behaves_like 'with invited_groups param'
end
end
end
import { GlModal } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
} from '~/pages/profiles/password_prompt/constants';
import PasswordPromptModal from '~/pages/profiles/password_prompt/password_prompt_modal.vue';
const createComponent = ({ props }) => {
return shallowMountExtended(PasswordPromptModal, {
propsData: {
...props,
},
});
};
describe('Password prompt modal', () => {
let wrapper;
const mockPassword = 'not+fake+shady+password';
const mockEvent = { preventDefault: jest.fn() };
const handleConfirmPasswordSpy = jest.fn();
const findField = () => wrapper.findByTestId('password-prompt-field');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmBtn = () => findModal().props('actionPrimary');
const findConfirmBtnDisabledState = () =>
findModal().props('actionPrimary').attributes[2].disabled;
const findCancelBtn = () => findModal().props('actionCancel');
const submitModal = () => findModal().vm.$emit('primary', mockEvent);
const setPassword = (newPw) => findField().vm.$emit('input', newPw);
beforeEach(() => {
wrapper = createComponent({
props: {
handleConfirmPassword: handleConfirmPasswordSpy,
},
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders the password field', () => {
expect(findField().exists()).toBe(true);
});
it('renders the confirm button', () => {
expect(findConfirmBtn().text).toEqual(I18N_PASSWORD_PROMPT_CONFIRM_BUTTON);
});
it('renders the cancel button', () => {
expect(findCancelBtn().text).toEqual(I18N_PASSWORD_PROMPT_CANCEL_BUTTON);
});
describe('confirm button', () => {
describe('with a valid password', () => {
it('calls the `handleConfirmPassword` method when clicked', async () => {
setPassword(mockPassword);
submitModal();
await wrapper.vm.$nextTick();
expect(handleConfirmPasswordSpy).toHaveBeenCalledTimes(1);
expect(handleConfirmPasswordSpy).toHaveBeenCalledWith(mockPassword);
});
it('enables the confirm button', async () => {
setPassword(mockPassword);
expect(findConfirmBtnDisabledState()).toBe(true);
await wrapper.vm.$nextTick();
expect(findConfirmBtnDisabledState()).toBe(false);
});
});
it('without a valid password is disabled', async () => {
setPassword('');
expect(findConfirmBtnDisabledState()).toBe(true);
await wrapper.vm.$nextTick();
expect(findConfirmBtnDisabledState()).toBe(true);
});
});
});
......@@ -51,7 +51,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
end
it 'includes the GitHub ID' do
expect(note.github_id).to eq(1)
expect(note.note_id).to eq(1)
end
it 'returns the noteable type' do
......@@ -106,7 +106,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world',
'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s,
'github_id' => 1
'note_id' => 1
}
end
......@@ -124,7 +124,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world',
'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s,
'github_id' => 1
'note_id' => 1
}
note = described_class.from_json_hash(hash)
......@@ -154,7 +154,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world',
'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s,
'github_id' => 1
'note_id' => 1
)
expect(note.diff_hash).to eq(
......@@ -167,4 +167,18 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
)
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
noteable_id: 42,
noteable_type: 'MergeRequest',
note_id: 1
}
other_attributes = { something_else: '_something_else_' }
note = described_class.new(github_identifiers.merge(other_attributes))
expect(note.github_identifiers).to eq(github_identifiers)
end
end
end
......@@ -181,4 +181,17 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do
expect(object.truncated_title).to eq('foo')
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
iid: 42,
issuable_type: 'MergeRequest'
}
other_attributes = { pull_request: true, something_else: '_something_else_' }
issue = described_class.new(github_identifiers.merge(other_attributes))
expect(issue.github_identifiers).to eq(github_identifiers)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Representation::LfsObject do
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
oid: 42
}
other_attributes = { something_else: '_something_else_' }
lfs_object = described_class.new(github_identifiers.merge(other_attributes))
expect(lfs_object.github_identifiers).to eq(github_identifiers)
end
end
end
......@@ -40,8 +40,8 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
expect(note.updated_at).to eq(updated_at)
end
it 'includes the GitHub ID' do
expect(note.github_id).to eq(1)
it 'includes the note ID' do
expect(note.note_id).to eq(1)
end
end
end
......@@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
'note' => 'Hello world',
'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s,
'github_id' => 1
'note_id' => 1
}
end
......@@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
'note' => 'Hello world',
'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s,
'github_id' => 1
'note_id' => 1
}
note = described_class.from_json_hash(hash)
......@@ -106,4 +106,18 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
expect(note.author).to be_nil
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
noteable_id: 42,
noteable_type: 'Issue',
note_id: 1
}
other_attributes = { something_else: '_something_else_' }
note = described_class.new(github_identifiers.merge(other_attributes))
expect(note.github_identifiers).to eq(github_identifiers)
end
end
end
......@@ -14,7 +14,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
expect(review.note).to eq('note')
expect(review.review_type).to eq('APPROVED')
expect(review.submitted_at).to eq(submitted_at)
expect(review.github_id).to eq(999)
expect(review.review_id).to eq(999)
expect(review.merge_request_id).to eq(42)
end
end
......@@ -50,7 +50,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
describe '.from_json_hash' do
let(:hash) do
{
'github_id' => 999,
'review_id' => 999,
'merge_request_id' => 42,
'note' => 'note',
'review_type' => 'APPROVED',
......@@ -75,4 +75,17 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
expect(review.submitted_at).to be_nil
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
review_id: 999,
merge_request_id: 42
}
other_attributes = { something_else: '_something_else_' }
review = described_class.new(github_identifiers.merge(other_attributes))
expect(review.github_identifiers).to eq(github_identifiers)
end
end
end
......@@ -288,4 +288,16 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do
expect(object.truncated_title).to eq('foo')
end
end
describe '#github_identifiers' do
it 'returns a hash with needed identifiers' do
github_identifiers = {
iid: 1
}
other_attributes = { something_else: '_something_else_' }
pr = described_class.new(github_identifiers.merge(other_attributes))
expect(pr.github_identifiers).to eq(github_identifiers.merge(issuable_type: 'MergeRequest'))
end
end
end
......@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Users::UpdateService do
let(:user) { create(:user) }
let(:password) { 'longsecret987!' }
let(:user) { create(:user, password: password, password_confirmation: password) }
describe '#execute' do
it 'updates time preferences' do
......@@ -18,7 +19,7 @@ RSpec.describe Users::UpdateService do
it 'returns an error result when record cannot be updated' do
result = {}
expect do
result = update_user(user, { email: 'invalid' })
result = update_user(user, { email: 'invalid', validation_password: password })
end.not_to change { user.reload.email }
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Email is invalid')
......@@ -65,7 +66,7 @@ RSpec.describe Users::UpdateService do
context 'updating canonical email' do
context 'if email was changed' do
subject do
update_user(user, email: 'user+extrastuff@example.com')
update_user(user, email: 'user+extrastuff@example.com', validation_password: password)
end
it 'calls canonicalize_email' do
......@@ -75,15 +76,68 @@ RSpec.describe Users::UpdateService do
subject
end
context 'when check_password is true' do
def update_user(user, opts)
described_class.new(user, opts.merge(user: user)).execute(check_password: true)
end
it 'returns error if no password confirmation was passed', :aggregate_failures do
result = {}
expect do
result = update_user(user, { email: 'example@example.com' })
end.not_to change { user.reload.unconfirmed_email }
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid password')
end
it 'returns error if wrong password confirmation was passed', :aggregate_failures do
result = {}
expect do
result = update_user(user, { email: 'example@example.com', validation_password: 'wrongpassword' })
end.not_to change { user.reload.unconfirmed_email }
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid password')
end
it 'does not require password if it was automatically set', :aggregate_failures do
user.update!(password_automatically_set: true)
result = {}
expect do
result = update_user(user, { email: 'example@example.com' })
end.to change { user.reload.unconfirmed_email }
expect(result[:status]).to eq(:success)
end
it 'does not require a password if the attribute changed does not require it' do
result = {}
expect do
result = update_user(user, { job_title: 'supreme leader of the universe' })
end.to change { user.reload.job_title }
expect(result[:status]).to eq(:success)
end
end
end
context 'if email was NOT changed' do
subject do
update_user(user, job_title: 'supreme leader of the universe')
context 'when check_password is left to false' do
it 'does not require a password check', :aggregate_failures do
result = {}
expect do
result = update_user(user, { email: 'example@example.com' })
end.to change { user.reload.unconfirmed_email }
expect(result[:status]).to eq(:success)
end
end
context 'if email was NOT changed' do
it 'skips update canonicalize email service call' do
expect { subject }.not_to change { user.user_canonical_email }
expect do
update_user(user, job_title: 'supreme leader of the universe')
end.not_to change { user.user_canonical_email }
end
end
end
......@@ -106,7 +160,7 @@ RSpec.describe Users::UpdateService do
it 'raises an error when record cannot be updated' do
expect do
update_user(user, email: 'invalid')
update_user(user, email: 'invalid', validation_password: password)
end.to raise_error(ActiveRecord::RecordInvalid)
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::ObjectImporter do
RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
let(:worker) do
Class.new do
def self.name
......@@ -26,9 +26,15 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
let(:importer_class) { double(:importer_class, name: 'klass_name') }
let(:importer_instance) { double(:importer_instance) }
let(:client) { double(:client) }
let(:github_identifiers) do
{
some_id: 1,
some_type: '_some_type_'
}
end
before do
stub_const('MockRepresantation', Class.new do
let(:representation_class) do
Class.new do
include Gitlab::GithubImport::Representation::ToHash
include Gitlab::GithubImport::Representation::ExposeAttribute
......@@ -41,7 +47,20 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
def initialize(attributes)
@attributes = attributes
end
end)
def github_identifiers
{
some_id: 1,
some_type: '_some_type_'
}
end
end
end
let(:stubbed_representation) { representation_class }
before do
stub_const('MockRepresantation', stubbed_representation)
end
describe '#import', :clean_gitlab_redis_cache do
......@@ -64,7 +83,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
github_id: 1,
github_identifiers: github_identifiers,
message: 'starting importer',
project_id: project.id,
importer: 'klass_name'
......@@ -73,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
github_id: 1,
github_identifiers: github_identifiers,
message: 'importer finished',
project_id: project.id,
importer: 'klass_name'
......@@ -101,7 +120,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger)
.to receive(:info)
.with(
github_id: 1,
github_identifiers: github_identifiers,
message: 'starting importer',
project_id: project.id,
importer: 'klass_name'
......@@ -125,21 +144,25 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(project.import_failures.last.exception_message).to eq('some error')
end
it 'logs error when representation does not have a github_id' do
expect(importer_class).not_to receive(:new)
context 'without github_identifiers defined' do
let(:stubbed_representation) { representation_class.instance_eval { undef_method :github_identifiers } }
expect(Gitlab::Import::ImportFailureService)
.to receive(:track)
.with(
project_id: project.id,
exception: a_kind_of(KeyError),
error_source: 'klass_name',
fail_import: true
)
.and_call_original
it 'logs error when representation does not have a github_id' do
expect(importer_class).not_to receive(:new)
expect { worker.import(project, client, { 'number' => 10 }) }
.to raise_error(KeyError, 'key not found: :github_id')
expect(Gitlab::Import::ImportFailureService)
.to receive(:track)
.with(
project_id: project.id,
exception: a_kind_of(NoMethodError),
error_source: 'klass_name',
fail_import: true
)
.and_call_original
expect { worker.import(project, client, { 'number' => 10 }) }
.to raise_error(NoMethodError, /^undefined method `github_identifiers/)
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