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 () => { ...@@ -45,6 +45,7 @@ export default () => {
new Vue({ new Vue({
el, el,
name: 'CycleAnalytics', name: 'CycleAnalytics',
apolloProvider: {},
store, store,
render: (createElement) => render: (createElement) =>
createElement(CycleAnalytics, { createElement(CycleAnalytics, {
......
...@@ -2,6 +2,7 @@ import $ from 'jquery'; ...@@ -2,6 +2,7 @@ import $ from 'jquery';
import '~/profile/gl_crop'; import '~/profile/gl_crop';
import Profile from '~/profile/profile'; import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings'; import initSearchSettings from '~/search_settings';
import initPasswordPrompt from './password_prompt';
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
$(document).on('input.ssh_key', '#key_key', function () { $(document).on('input.ssh_key', '#key_key', function () {
...@@ -19,3 +20,4 @@ $(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 new Profile(); // eslint-disable-line no-new
initSearchSettings(); 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 { ...@@ -123,7 +123,7 @@ export default {
</script> </script>
<template> <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="card card-slim gl-overflow-hidden">
<div <div
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }" :class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
......
...@@ -97,11 +97,7 @@ export default { ...@@ -97,11 +97,7 @@ export default {
class="related-issues-token-body bordered-box bg-white" class="related-issues-token-body bordered-box bg-white"
:class="{ 'sortable-container': canReorder }" :class="{ 'sortable-container': canReorder }"
> >
<div <div v-if="isFetching" class="gl-mb-2" data-qa-selector="related_issues_loading_placeholder">
v-if="isFetching"
class="related-issues-loading-icon"
data-qa-selector="related_issues_loading_placeholder"
>
<gl-loading-icon <gl-loading-icon
ref="loadingIcon" ref="loadingIcon"
size="sm" size="sm"
......
<script> <script>
import { sprintf, s__ } from '~/locale'; import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
const timeSpent = s__('TimeTracking|%{spentStart}Spent: %{spentEnd}');
export default { export default {
name: 'TimeTrackingSpentOnlyPane', name: 'TimeTrackingSpentOnlyPane',
timeSpent,
components: {
GlSprintf,
},
props: { props: {
timeSpentHumanReadable: { timeSpentHumanReadable: {
type: String, type: String,
required: true, 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> </script>
<template> <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> </template>
...@@ -18,7 +18,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -18,7 +18,7 @@ class ProfilesController < Profiles::ApplicationController
def update def update
respond_to do |format| 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 if result[:status] == :success
message = s_("Profiles|Profile was successfully updated") message = s_("Profiles|Profile was successfully updated")
...@@ -129,6 +129,7 @@ class ProfilesController < Profiles::ApplicationController ...@@ -129,6 +129,7 @@ class ProfilesController < Profiles::ApplicationController
:job_title, :job_title,
:pronouns, :pronouns,
:pronunciation, :pronunciation,
:validation_password,
status: [:emoji, :message, :availability] status: [:emoji, :message, :availability]
] ]
end end
......
...@@ -70,11 +70,16 @@ class MembersFinder ...@@ -70,11 +70,16 @@ class MembersFinder
end end
def project_invited_groups def project_invited_groups
invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy invited_groups_and_ancestors = if ::Feature.enabled?(:linear_members_finder_ancestor_scopes, current_user, default_enabled: :yaml)
.new(project.invited_groups) project.invited_groups
.base_and_ancestors .self_and_ancestors
.public_or_visible_to_user(current_user) else
.select(:id) 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 GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end end
......
...@@ -83,7 +83,7 @@ module Users ...@@ -83,7 +83,7 @@ module Users
end end
def lazy_user_availability(user) 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| user_ids.each_slice(1_000) do |sliced_user_ids|
UserStatus UserStatus
.select(:user_id, :availability) .select(:user_id, :availability)
......
...@@ -5,15 +5,18 @@ module Users ...@@ -5,15 +5,18 @@ module Users
include NewUserNotifier include NewUserNotifier
attr_reader :user, :identity_params attr_reader :user, :identity_params
ATTRS_REQUIRING_PASSWORD_CHECK = %w[email].freeze
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@validation_password = params.delete(:validation_password)
@user = params.delete(:user) @user = params.delete(:user)
@status_params = params.delete(:status) @status_params = params.delete(:status)
@identity_params = params.slice(*identity_attributes) @identity_params = params.slice(*identity_attributes)
@params = params.dup @params = params.dup
end end
def execute(validate: true, &block) def execute(validate: true, check_password: false, &block)
yield(@user) if block_given? yield(@user) if block_given?
user_exists = @user.persisted? user_exists = @user.persisted?
...@@ -21,6 +24,11 @@ module Users ...@@ -21,6 +24,11 @@ module Users
discard_read_only_attributes discard_read_only_attributes
assign_attributes assign_attributes
if check_password && require_password_check? && !@user.valid_password?(@validation_password)
return error(s_("Profiles|Invalid password"))
end
assign_identity assign_identity
build_canonical_email build_canonical_email
...@@ -32,8 +40,8 @@ module Users ...@@ -32,8 +40,8 @@ module Users
end end
end end
def execute!(*args, &block) def execute!(*args, **kargs, &block)
result = execute(*args, &block) result = execute(*args, **kargs, &block)
raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success
...@@ -42,6 +50,14 @@ module Users ...@@ -42,6 +50,14 @@ module Users
private 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 def build_canonical_email
return unless @user.email_changed? return unless @user.email_changed?
......
...@@ -3,8 +3,11 @@ ...@@ -3,8 +3,11 @@
- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil) - 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) - 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 - 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 = 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), = 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") }, { 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 control_class: 'select2 input-lg', disabled: email_change_disabled
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
- availability = availability_values - availability = availability_values
- custom_emoji = show_status_emoji?(@user.status) - 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) = form_errors(@user)
.row.js-search-settings-section .row.js-search-settings-section
...@@ -124,9 +124,11 @@ ...@@ -124,9 +124,11 @@
.help-block .help-block
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information") = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
%hr %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' = 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.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } }
.modal-dialog .modal-dialog
.modal-content .modal-content
......
...@@ -3,4 +3,3 @@ ...@@ -3,4 +3,3 @@
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}", can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'), help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "false" } } 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 @@ ...@@ -13,7 +13,8 @@
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight) = highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right .col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0 - if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
%li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') } %li.issuable-upvotes.gl-list-style-none
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle") %span.has-tooltip{ title: _('Upvotes') }
= issuable.upvotes_count = 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 %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 ...@@ -26,8 +26,7 @@ module Gitlab
object = representation_class.from_json_hash(hash) object = representation_class.from_json_hash(hash)
# To better express in the logs what object is being imported. # 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') info(project.id, message: 'starting importer')
importer_class.new(object, project, client).execute importer_class.new(object, project, client).execute
...@@ -35,10 +34,10 @@ module Gitlab ...@@ -35,10 +34,10 @@ module Gitlab
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported) Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
info(project.id, message: 'importer finished') info(project.id, message: 'importer finished')
rescue KeyError => e rescue NoMethodError => e
# This exception will be more useful in development when a new # This exception will be more useful in development when a new
# Representation is created but the developer forgot to add a # Representation is created but the developer forgot to add a
# `:github_id` field. # `:github_identifiers` field.
Gitlab::Import::ImportFailureService.track( Gitlab::Import::ImportFailureService.track(
project_id: project.id, project_id: project.id,
error_source: importer_class.name, error_source: importer_class.name,
...@@ -72,7 +71,7 @@ module Gitlab ...@@ -72,7 +71,7 @@ module Gitlab
private private
attr_accessor :github_id attr_accessor :github_identifiers
def info(project_id, extra = {}) def info(project_id, extra = {})
Logger.info(log_attributes(project_id, extra)) Logger.info(log_attributes(project_id, extra))
...@@ -82,7 +81,7 @@ module Gitlab ...@@ -82,7 +81,7 @@ module Gitlab
extra.merge( extra.merge(
project_id: project_id, project_id: project_id,
importer: importer_class.name, importer: importer_class.name,
github_id: github_id github_identifiers: github_identifiers
) )
end end
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 # frozen_string_literal: true
require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript' require 'gettext_i18n_rails_js/parser/javascript'
require 'json' 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 GettextI18nRailsJs
module Parser module Parser
module Javascript module Javascript
......
...@@ -21,6 +21,7 @@ You can use the following environment variables to override certain values: ...@@ -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`. | | `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). | | `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_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_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). | | `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: ...@@ -64,7 +64,7 @@ build:
entrypoint: [""] entrypoint: [""]
script: script:
- mkdir -p /kaniko/.docker - 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 - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
...@@ -91,7 +91,7 @@ build: ...@@ -91,7 +91,7 @@ build:
- mkdir -p /kaniko/.docker - mkdir -p /kaniko/.docker
- |- - |-
KANIKOPROXYBUILDARGS="" 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 if [ "x${http_proxy}" != "x" -o "x${https_proxy}" != "x" ]; then
KANIKOCFG="${KANIKOCFG}, \"proxies\": { \"default\": { \"httpProxy\": \"${http_proxy}\", \"httpsProxy\": \"${https_proxy}\", \"noProxy\": \"${no_proxy}\"}}" 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}" KANIKOPROXYBUILDARGS="--build-arg http_proxy=${http_proxy} --build-arg https_proxy=${https_proxy} --build-arg no_proxy=${no_proxy}"
...@@ -120,7 +120,7 @@ store: ...@@ -120,7 +120,7 @@ store:
```yaml ```yaml
before_script: before_script:
- mkdir -p /kaniko/.docker - 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----- echo "-----BEGIN CERTIFICATE-----
... ...
......
...@@ -71,6 +71,7 @@ topics and use cases. The most frequently required during database reviewing are ...@@ -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. - [Migrations style guide](../migration_style_guide.md) for creating safe SQL migrations.
- [Avoiding downtime in migrations](../avoiding_downtime_in_migrations.md). - [Avoiding downtime in migrations](../avoiding_downtime_in_migrations.md).
- [SQL guidelines](../sql.md) for working with SQL queries. - [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 ## How to apply to become a database maintainer
......
...@@ -28,8 +28,8 @@ export default { ...@@ -28,8 +28,8 @@ export default {
}, },
summaryDescription() { summaryDescription() {
const { const {
startDate, createdAfter,
endDate, createdBefore,
selectedProjectIds, selectedProjectIds,
currentGroup: { name: groupName }, currentGroup: { name: groupName },
} = this.selectedTasksByTypeFilters; } = this.selectedTasksByTypeFilters;
...@@ -38,14 +38,14 @@ export default { ...@@ -38,14 +38,14 @@ export default {
const str = const str =
selectedProjectCount > 0 selectedProjectCount > 0
? s__( ? 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__( : s__(
"CycleAnalytics|Showing data for group '%{groupName}' from %{startDate} to %{endDate}", "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}",
); );
return sprintf(str, { return sprintf(str, {
startDate: formattedDate(startDate), createdAfter: formattedDate(createdAfter),
endDate: formattedDate(endDate), createdBefore: formattedDate(createdBefore),
groupName, groupName,
selectedProjectCount, selectedProjectCount,
}); });
......
...@@ -21,10 +21,6 @@ export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => { ...@@ -21,10 +21,6 @@ export const selectedTasksByTypeFilters = (state = {}, _, rootState = {}) => {
export const tasksByTypeChartData = ({ data = [] } = {}, _, rootState = {}) => { export const tasksByTypeChartData = ({ data = [] } = {}, _, rootState = {}) => {
const { createdAfter = null, createdBefore = null } = rootState; const { createdAfter = null, createdBefore = null } = rootState;
return data.length return data.length
? getTasksByTypeData({ ? getTasksByTypeData({ data, createdAfter, createdBefore })
data,
startDate: createdAfter,
endDate: createdBefore,
})
: { groupBy: [], data: [] }; : { groupBy: [], data: [] };
}; };
...@@ -214,20 +214,20 @@ export const flattenTaskByTypeSeries = (series = {}) => ...@@ -214,20 +214,20 @@ export const flattenTaskByTypeSeries = (series = {}) =>
* *
* @param {Object} obj * @param {Object} obj
* @param {RawTasksByTypeData[]} obj.data - array of raw data, each element contains a label and series * @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.createdAfter - start date in ISO date format
* @param {Date} obj.endDate - end 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 * @returns {TransformedTasksByTypeData} The transformed data ready for use in charts
*/ */
export const getTasksByTypeData = ({ data = [], startDate = null, endDate = null }) => { export const getTasksByTypeData = ({ data = [], createdAfter = null, createdBefore = null }) => {
if (!startDate || !endDate || !data.length) { if (!createdAfter || !createdBefore || !data.length) {
return { return {
groupBy: [], groupBy: [],
data: [], data: [],
}; };
} }
const groupBy = getDatesInRange(startDate, endDate, toYmd).sort(orderByDate); const groupBy = getDatesInRange(createdAfter, createdBefore, toYmd).sort(orderByDate);
const zeroValuesForEachDataPoint = groupBy.reduce( const zeroValuesForEachDataPoint = groupBy.reduce(
(acc, date) => ({ (acc, date) => ({
...acc, ...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 { .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 { a.issuable-main-info:hover {
text-decoration: none; text-decoration: none;
...@@ -29,25 +8,6 @@ $token-spacing-bottom: 0.5em; ...@@ -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 { .issue-token-end {
order: 1; order: 1;
} }
......
...@@ -4,7 +4,10 @@ ...@@ -4,7 +4,10 @@
%h3.page-title= _('Upload License') %h3.page-title= _('Upload License')
%p.light %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 %hr
= form_for @license, url: admin_license_path, html: { multipart: true, class: 'fieldset-form' } do |f| = form_for @license, url: admin_license_path, html: { multipart: true, class: 'fieldset-form' } do |f|
......
...@@ -3,4 +3,3 @@ ...@@ -3,4 +3,3 @@
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}", can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'), help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "#{!!@project.feature_available?(:blocked_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 ...@@ -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-from input').value).to eq '2019-11-01'
expect(element.find('.js-daterange-picker-to input').value).to eq '2019-12-31' 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 end
end end
......
...@@ -222,8 +222,8 @@ export const taskByTypeFilters = { ...@@ -222,8 +222,8 @@ export const taskByTypeFilters = {
fullPath: 'gitlab-org', fullPath: 'gitlab-org',
}, },
selectedProjectIds: [], selectedProjectIds: [],
startDate: new Date('2019-12-11'), createdAfter: new Date('2019-12-11'),
endDate: new Date('2020-01-10'), createdBefore: new Date('2020-01-10'),
subject: TASKS_BY_TYPE_SUBJECT_ISSUE, subject: TASKS_BY_TYPE_SUBJECT_ISSUE,
selectedLabelIds: [1, 2, 3], selectedLabelIds: [1, 2, 3],
}; };
......
...@@ -247,22 +247,20 @@ describe('Value Stream Analytics utils', () => { ...@@ -247,22 +247,20 @@ describe('Value Stream Analytics utils', () => {
}); });
it('will return blank arrays if given no data', () => { it('will return blank arrays if given no data', () => {
[{ data: [], startDate: createdAfter, endDate: createdBefore }, [], {}].forEach( [{ data: [], createdAfter, createdBefore }, [], {}].forEach((chartData) => {
(chartData) => { transformed = getTasksByTypeData(chartData);
transformed = getTasksByTypeData(chartData); ['data', 'groupBy'].forEach((key) => {
['data', 'groupBy'].forEach((key) => { expect(transformed[key]).toEqual([]);
expect(transformed[key]).toEqual([]); });
}); });
},
);
}); });
describe('with data', () => { describe('with data', () => {
beforeEach(() => { beforeEach(() => {
transformed = getTasksByTypeData({ transformed = getTasksByTypeData({
data: rawTasksByTypeData, data: rawTasksByTypeData,
startDate: createdAfter, createdAfter,
endDate: createdBefore, createdBefore,
}); });
}); });
......
...@@ -1093,7 +1093,6 @@ module API ...@@ -1093,7 +1093,6 @@ module API
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
service = ::UserPreferences::UpdateService.new(current_user, attrs).execute service = ::UserPreferences::UpdateService.new(current_user, attrs).execute
if service.success? if service.success?
present preferences, with: Entities::UserPreferences present preferences, with: Entities::UserPreferences
else else
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path, expose_attribute :noteable_type, :noteable_id, :commit_id, :file_path,
:diff_hunk, :author, :note, :created_at, :updated_at, :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 NOTEABLE_ID_REGEX = %r{/pull/(?<iid>\d+)}i.freeze
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
note: note.body, note: note.body,
created_at: note.created_at, created_at: note.created_at,
updated_at: note.updated_at, updated_at: note.updated_at,
github_id: note.id note_id: note.id
} }
new(hash) new(hash)
...@@ -82,6 +82,14 @@ module Gitlab ...@@ -82,6 +82,14 @@ module Gitlab
new_file: false new_file: false
} }
end end
def github_identifiers
{
note_id: note_id,
noteable_id: noteable_id,
noteable_type: noteable_type
}
end
end end
end end
end end
......
...@@ -25,7 +25,6 @@ module Gitlab ...@@ -25,7 +25,6 @@ module Gitlab
hash = { hash = {
iid: issue.number, iid: issue.number,
github_id: issue.number,
title: issue.title, title: issue.title,
description: issue.body, description: issue.body,
milestone_number: issue.milestone&.number, milestone_number: issue.milestone&.number,
...@@ -75,6 +74,13 @@ module Gitlab ...@@ -75,6 +74,13 @@ module Gitlab
def issuable_type def issuable_type
pull_request? ? 'MergeRequest' : 'Issue' pull_request? ? 'MergeRequest' : 'Issue'
end end
def github_identifiers
{
iid: iid,
issuable_type: issuable_type
}
end
end end
end end
end end
......
...@@ -16,8 +16,7 @@ module Gitlab ...@@ -16,8 +16,7 @@ module Gitlab
new( new(
oid: lfs_object.oid, oid: lfs_object.oid,
link: lfs_object.link, link: lfs_object.link,
size: lfs_object.size, size: lfs_object.size
github_id: lfs_object.oid
) )
end end
...@@ -31,6 +30,12 @@ module Gitlab ...@@ -31,6 +30,12 @@ module Gitlab
def initialize(attributes) def initialize(attributes)
@attributes = attributes @attributes = attributes
end end
def github_identifiers
{
oid: oid
}
end
end end
end end
end end
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
attr_reader :attributes attr_reader :attributes
expose_attribute :noteable_id, :noteable_type, :author, :note, 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 NOTEABLE_TYPE_REGEX = %r{/(?<type>(pull|issues))/(?<iid>\d+)}i.freeze
...@@ -42,7 +42,7 @@ module Gitlab ...@@ -42,7 +42,7 @@ module Gitlab
note: note.body, note: note.body,
created_at: note.created_at, created_at: note.created_at,
updated_at: note.updated_at, updated_at: note.updated_at,
github_id: note.id note_id: note.id
} }
new(hash) new(hash)
...@@ -64,6 +64,14 @@ module Gitlab ...@@ -64,6 +64,14 @@ module Gitlab
end end
alias_method :issuable_type, :noteable_type 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 end
end end
......
...@@ -25,7 +25,6 @@ module Gitlab ...@@ -25,7 +25,6 @@ module Gitlab
hash = { hash = {
iid: pr.number, iid: pr.number,
github_id: pr.number,
title: pr.title, title: pr.title,
description: pr.body, description: pr.body,
source_branch: pr.head.ref, source_branch: pr.head.ref,
...@@ -108,6 +107,13 @@ module Gitlab ...@@ -108,6 +107,13 @@ module Gitlab
def issuable_type def issuable_type
'MergeRequest' 'MergeRequest'
end end
def github_identifiers
{
iid: iid,
issuable_type: issuable_type
}
end
end end
end end
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
attr_reader :attributes 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) def self.from_api_response(review)
user = Representation::User.from_api_response(review.user) if review.user user = Representation::User.from_api_response(review.user) if review.user
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
note: review.body, note: review.body,
review_type: review.state, review_type: review.state,
submitted_at: review.submitted_at, submitted_at: review.submitted_at,
github_id: review.id review_id: review.id
) )
end end
...@@ -43,6 +43,13 @@ module Gitlab ...@@ -43,6 +43,13 @@ module Gitlab
def approval? def approval?
review_type == 'APPROVED' review_type == 'APPROVED'
end end
def github_identifiers
{
review_id: review_id,
merge_request_id: merge_request_id
}
end
end end
end end
end end
......
...@@ -17,7 +17,6 @@ module Gitlab ...@@ -17,7 +17,6 @@ module Gitlab
def self.from_api_response(user) def self.from_api_response(user)
new( new(
id: user.id, id: user.id,
github_id: user.id,
login: user.login login: user.login
) )
end end
......
...@@ -10198,10 +10198,10 @@ msgid_plural "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCo ...@@ -10198,10 +10198,10 @@ msgid_plural "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCo
msgstr[0] "" msgstr[0] ""
msgstr[1] "" 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 "" msgstr ""
msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{startDate} to %{endDate}" msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
msgstr "" msgstr ""
msgid "CycleAnalytics|Stages" msgid "CycleAnalytics|Stages"
...@@ -24487,6 +24487,18 @@ msgstr "" ...@@ -24487,6 +24487,18 @@ msgstr ""
msgid "Password was successfully updated. Please sign in again." msgid "Password was successfully updated. Please sign in again."
msgstr "" 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." msgid "Passwords should be unique and not used for any other sites or services."
msgstr "" msgstr ""
...@@ -25933,6 +25945,9 @@ msgstr "" ...@@ -25933,6 +25945,9 @@ msgstr ""
msgid "Profiles|Enter your name, so people you know can recognize you" msgid "Profiles|Enter your name, so people you know can recognize you"
msgstr "" 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" msgid "Profiles|Enter your pronouns to let people know how to refer to you"
msgstr "" msgstr ""
...@@ -31274,6 +31289,9 @@ msgstr "" ...@@ -31274,6 +31289,9 @@ msgstr ""
msgid "Showing all issues" msgid "Showing all issues"
msgstr "" 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." msgid "Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days."
msgstr "" msgstr ""
...@@ -35150,7 +35168,7 @@ msgstr "" ...@@ -35150,7 +35168,7 @@ msgstr ""
msgid "TimeTrackingEstimated|Est" msgid "TimeTrackingEstimated|Est"
msgstr "" msgstr ""
msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}" msgid "TimeTracking|%{spentStart}Spent: %{spentEnd}"
msgstr "" msgstr ""
msgid "TimeTracking|Estimated:" msgid "TimeTracking|Estimated:"
...@@ -35388,6 +35406,9 @@ msgstr "" ...@@ -35388,6 +35406,9 @@ msgstr ""
msgid "To connect an SVN repository, check out %{svn_link}." msgid "To connect an SVN repository, check out %{svn_link}."
msgstr "" 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}" 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 "" msgstr ""
...@@ -35493,6 +35514,9 @@ 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." 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 "" 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:" msgid "To unsubscribe from this issue, please paste the following link into your browser:"
msgstr "" msgstr ""
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
require('spec_helper') require('spec_helper')
RSpec.describe ProfilesController, :request_store do RSpec.describe ProfilesController, :request_store do
let(:user) { create(:user) } let(:password) { 'longsecret987!' }
let(:user) { create(:user, password: password) }
describe 'POST update' do describe 'POST update' do
it 'does not update password' do it 'does not update password' do
...@@ -23,7 +24,7 @@ RSpec.describe ProfilesController, :request_store do ...@@ -23,7 +24,7 @@ RSpec.describe ProfilesController, :request_store do
sign_in(user) sign_in(user)
put :update, put :update,
params: { user: { email: "john@gmail.com", name: "John" } } params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
user.reload user.reload
......
...@@ -139,6 +139,8 @@ FactoryBot.define do ...@@ -139,6 +139,8 @@ FactoryBot.define do
end end
factory :omniauth_user do factory :omniauth_user do
password_automatically_set { true }
transient do transient do
extern_uid { '123456' } extern_uid { '123456' }
provider { 'ldapmain' } provider { 'ldapmain' }
......
...@@ -121,7 +121,7 @@ RSpec.describe 'Admin Mode Login' do ...@@ -121,7 +121,7 @@ RSpec.describe 'Admin Mode Login' do
end end
context 'when logging in via omniauth' do 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 let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml') File.read('spec/fixtures/authentication/saml_response.xml')
end end
......
...@@ -19,6 +19,17 @@ RSpec.describe 'User edit profile' do ...@@ -19,6 +19,17 @@ RSpec.describe 'User edit profile' do
wait_for_requests if respond_to?(:wait_for_requests) wait_for_requests if respond_to?(:wait_for_requests)
end 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 def visit_user
visit user_path(user) visit user_path(user)
wait_for_requests wait_for_requests
...@@ -88,16 +99,42 @@ RSpec.describe 'User edit profile' do ...@@ -88,16 +99,42 @@ RSpec.describe 'User edit profile' do
expect(page).to have_content('Website url is not a valid URL') expect(page).to have_content('Website url is not a valid URL')
end end
describe 'when I change my email' do describe 'when I change my email', :js do
before do before do
user.send_reset_password_instructions user.send_reset_password_instructions
end 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 it 'clears the reset password token' do
expect(user.reset_password_token?).to be true expect(user.reset_password_token?).to be true
fill_in 'user_email', with: 'new-email@example.com' update_user_email
submit_settings confirm_password(user.password)
user.reload user.reload
expect(user.confirmation_token).not_to be_nil expect(user.confirmation_token).not_to be_nil
......
...@@ -875,7 +875,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -875,7 +875,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
end end
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') } let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
before do before do
......
...@@ -161,42 +161,54 @@ RSpec.describe MembersFinder, '#execute' do ...@@ -161,42 +161,54 @@ RSpec.describe MembersFinder, '#execute' do
end end
context 'when :invited_groups is passed' do 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(:linked_group) { create(:group, :public) }
let_it_be(:nested_linked_group) { create(:group, parent: linked_group) } 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(:linked_group_member) { linked_group.add_guest(user1) }
let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) } 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 it 'includes all the invited_groups members including members inherited from ancestor groups' do
create(:project_group_link, project: project, group: nested_linked_group) create(:project_group_link, project: project, group: nested_linked_group)
expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member) expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
end end
it 'includes all the invited_groups members' do it 'includes all the invited_groups members' do
create(:project_group_link, project: project, group: linked_group) create(:project_group_link, project: project, group: linked_group)
expect(subject).to contain_exactly(linked_group_member) expect(subject).to contain_exactly(linked_group_member)
end end
it 'excludes group_members not visible to the user' do it 'excludes group_members not visible to the user' do
create(:project_group_link, project: project, group: linked_group) create(:project_group_link, project: project, group: linked_group)
private_linked_group = create(:group, :private) private_linked_group = create(:group, :private)
private_linked_group.add_developer(user3) private_linked_group.add_developer(user3)
create(:project_group_link, project: project, group: private_linked_group) 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 end
context 'when the user is a member of invited group and ancestor groups' do it_behaves_like 'with invited_groups param'
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) context 'when feature flag :linear_members_finder_ancestor_scopes is disabled' do
expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER) before do
stub_feature_flags(linear_members_finder_ancestor_scopes: false)
end end
it_behaves_like 'with invited_groups param'
end end
end 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 ...@@ -51,7 +51,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
end end
it 'includes the GitHub ID' do it 'includes the GitHub ID' do
expect(note.github_id).to eq(1) expect(note.note_id).to eq(1)
end end
it 'returns the noteable type' do it 'returns the noteable type' do
...@@ -106,7 +106,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do ...@@ -106,7 +106,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world', 'note' => 'Hello world',
'created_at' => created_at.to_s, 'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s, 'updated_at' => updated_at.to_s,
'github_id' => 1 'note_id' => 1
} }
end end
...@@ -124,7 +124,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do ...@@ -124,7 +124,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world', 'note' => 'Hello world',
'created_at' => created_at.to_s, 'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s, 'updated_at' => updated_at.to_s,
'github_id' => 1 'note_id' => 1
} }
note = described_class.from_json_hash(hash) note = described_class.from_json_hash(hash)
...@@ -154,7 +154,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do ...@@ -154,7 +154,7 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
'note' => 'Hello world', 'note' => 'Hello world',
'created_at' => created_at.to_s, 'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s, 'updated_at' => updated_at.to_s,
'github_id' => 1 'note_id' => 1
) )
expect(note.diff_hash).to eq( expect(note.diff_hash).to eq(
...@@ -167,4 +167,18 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do ...@@ -167,4 +167,18 @@ RSpec.describe Gitlab::GithubImport::Representation::DiffNote do
) )
end end
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 end
...@@ -181,4 +181,17 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do ...@@ -181,4 +181,17 @@ RSpec.describe Gitlab::GithubImport::Representation::Issue do
expect(object.truncated_title).to eq('foo') expect(object.truncated_title).to eq('foo')
end end
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 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 ...@@ -40,8 +40,8 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
expect(note.updated_at).to eq(updated_at) expect(note.updated_at).to eq(updated_at)
end end
it 'includes the GitHub ID' do it 'includes the note ID' do
expect(note.github_id).to eq(1) expect(note.note_id).to eq(1)
end end
end end
end end
...@@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do ...@@ -84,7 +84,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
'note' => 'Hello world', 'note' => 'Hello world',
'created_at' => created_at.to_s, 'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s, 'updated_at' => updated_at.to_s,
'github_id' => 1 'note_id' => 1
} }
end end
...@@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do ...@@ -98,7 +98,7 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
'note' => 'Hello world', 'note' => 'Hello world',
'created_at' => created_at.to_s, 'created_at' => created_at.to_s,
'updated_at' => updated_at.to_s, 'updated_at' => updated_at.to_s,
'github_id' => 1 'note_id' => 1
} }
note = described_class.from_json_hash(hash) note = described_class.from_json_hash(hash)
...@@ -106,4 +106,18 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do ...@@ -106,4 +106,18 @@ RSpec.describe Gitlab::GithubImport::Representation::Note do
expect(note.author).to be_nil expect(note.author).to be_nil
end end
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 end
...@@ -14,7 +14,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do ...@@ -14,7 +14,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
expect(review.note).to eq('note') expect(review.note).to eq('note')
expect(review.review_type).to eq('APPROVED') expect(review.review_type).to eq('APPROVED')
expect(review.submitted_at).to eq(submitted_at) 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) expect(review.merge_request_id).to eq(42)
end end
end end
...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do ...@@ -50,7 +50,7 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
describe '.from_json_hash' do describe '.from_json_hash' do
let(:hash) do let(:hash) do
{ {
'github_id' => 999, 'review_id' => 999,
'merge_request_id' => 42, 'merge_request_id' => 42,
'note' => 'note', 'note' => 'note',
'review_type' => 'APPROVED', 'review_type' => 'APPROVED',
...@@ -75,4 +75,17 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do ...@@ -75,4 +75,17 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequestReview do
expect(review.submitted_at).to be_nil expect(review.submitted_at).to be_nil
end end
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 end
...@@ -288,4 +288,16 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do ...@@ -288,4 +288,16 @@ RSpec.describe Gitlab::GithubImport::Representation::PullRequest do
expect(object.truncated_title).to eq('foo') expect(object.truncated_title).to eq('foo')
end end
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 end
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Users::UpdateService do RSpec.describe Users::UpdateService do
let(:user) { create(:user) } let(:password) { 'longsecret987!' }
let(:user) { create(:user, password: password, password_confirmation: password) }
describe '#execute' do describe '#execute' do
it 'updates time preferences' do it 'updates time preferences' do
...@@ -18,7 +19,7 @@ RSpec.describe Users::UpdateService do ...@@ -18,7 +19,7 @@ RSpec.describe Users::UpdateService do
it 'returns an error result when record cannot be updated' do it 'returns an error result when record cannot be updated' do
result = {} result = {}
expect do expect do
result = update_user(user, { email: 'invalid' }) result = update_user(user, { email: 'invalid', validation_password: password })
end.not_to change { user.reload.email } end.not_to change { user.reload.email }
expect(result[:status]).to eq(:error) expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Email is invalid') expect(result[:message]).to eq('Email is invalid')
...@@ -65,7 +66,7 @@ RSpec.describe Users::UpdateService do ...@@ -65,7 +66,7 @@ RSpec.describe Users::UpdateService do
context 'updating canonical email' do context 'updating canonical email' do
context 'if email was changed' do context 'if email was changed' do
subject do subject do
update_user(user, email: 'user+extrastuff@example.com') update_user(user, email: 'user+extrastuff@example.com', validation_password: password)
end end
it 'calls canonicalize_email' do it 'calls canonicalize_email' do
...@@ -75,15 +76,68 @@ RSpec.describe Users::UpdateService do ...@@ -75,15 +76,68 @@ RSpec.describe Users::UpdateService do
subject subject
end 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 end
context 'if email was NOT changed' do context 'when check_password is left to false' do
subject do it 'does not require a password check', :aggregate_failures do
update_user(user, job_title: 'supreme leader of the universe') 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
end
context 'if email was NOT changed' do
it 'skips update canonicalize email service call' 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 end
end end
...@@ -106,7 +160,7 @@ RSpec.describe Users::UpdateService do ...@@ -106,7 +160,7 @@ RSpec.describe Users::UpdateService do
it 'raises an error when record cannot be updated' do it 'raises an error when record cannot be updated' do
expect do expect do
update_user(user, email: 'invalid') update_user(user, email: 'invalid', validation_password: password)
end.to raise_error(ActiveRecord::RecordInvalid) end.to raise_error(ActiveRecord::RecordInvalid)
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::GithubImport::ObjectImporter do RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures do
let(:worker) do let(:worker) do
Class.new do Class.new do
def self.name def self.name
...@@ -26,9 +26,15 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -26,9 +26,15 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
let(:importer_class) { double(:importer_class, name: 'klass_name') } let(:importer_class) { double(:importer_class, name: 'klass_name') }
let(:importer_instance) { double(:importer_instance) } let(:importer_instance) { double(:importer_instance) }
let(:client) { double(:client) } let(:client) { double(:client) }
let(:github_identifiers) do
{
some_id: 1,
some_type: '_some_type_'
}
end
before do let(:representation_class) do
stub_const('MockRepresantation', Class.new do Class.new do
include Gitlab::GithubImport::Representation::ToHash include Gitlab::GithubImport::Representation::ToHash
include Gitlab::GithubImport::Representation::ExposeAttribute include Gitlab::GithubImport::Representation::ExposeAttribute
...@@ -41,7 +47,20 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -41,7 +47,20 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
def initialize(attributes) def initialize(attributes)
@attributes = attributes @attributes = attributes
end 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 end
describe '#import', :clean_gitlab_redis_cache do describe '#import', :clean_gitlab_redis_cache do
...@@ -64,7 +83,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -64,7 +83,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger) expect(Gitlab::GithubImport::Logger)
.to receive(:info) .to receive(:info)
.with( .with(
github_id: 1, github_identifiers: github_identifiers,
message: 'starting importer', message: 'starting importer',
project_id: project.id, project_id: project.id,
importer: 'klass_name' importer: 'klass_name'
...@@ -73,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -73,7 +92,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger) expect(Gitlab::GithubImport::Logger)
.to receive(:info) .to receive(:info)
.with( .with(
github_id: 1, github_identifiers: github_identifiers,
message: 'importer finished', message: 'importer finished',
project_id: project.id, project_id: project.id,
importer: 'klass_name' importer: 'klass_name'
...@@ -101,7 +120,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -101,7 +120,7 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(Gitlab::GithubImport::Logger) expect(Gitlab::GithubImport::Logger)
.to receive(:info) .to receive(:info)
.with( .with(
github_id: 1, github_identifiers: github_identifiers,
message: 'starting importer', message: 'starting importer',
project_id: project.id, project_id: project.id,
importer: 'klass_name' importer: 'klass_name'
...@@ -125,21 +144,25 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do ...@@ -125,21 +144,25 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter do
expect(project.import_failures.last.exception_message).to eq('some error') expect(project.import_failures.last.exception_message).to eq('some error')
end end
it 'logs error when representation does not have a github_id' do context 'without github_identifiers defined' do
expect(importer_class).not_to receive(:new) let(:stubbed_representation) { representation_class.instance_eval { undef_method :github_identifiers } }
expect(Gitlab::Import::ImportFailureService) it 'logs error when representation does not have a github_id' do
.to receive(:track) expect(importer_class).not_to receive(:new)
.with(
project_id: project.id,
exception: a_kind_of(KeyError),
error_source: 'klass_name',
fail_import: true
)
.and_call_original
expect { worker.import(project, client, { 'number' => 10 }) } expect(Gitlab::Import::ImportFailureService)
.to raise_error(KeyError, 'key not found: :github_id') .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 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