Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-29 15:11:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-29 15:11:22 +0300
commit5f8d4d631d241c993c2cac54db3494f474b21dc1 (patch)
treead82966503efbc7d8dc37601ff498af85d76b495
parentc724e639a91a4d112b7f0a05b3c6a0ffa6baa7a4 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml1
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js1
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/notifications/constants.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js1
-rw-r--r--app/assets/javascripts/pages/profiles/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/password_prompt/constants.js9
-rw-r--r--app/assets/javascripts/pages/profiles/password_prompt/index.js58
-rw-r--r--app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue82
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js24
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue2
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js7
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss14
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/finders/members_finder.rb15
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/create.rb69
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/delete.rb35
-rw-r--r--app/graphql/mutations/clusters/agents/create.rb38
-rw-r--r--app/graphql/mutations/clusters/agents/delete.rb39
-rw-r--r--app/graphql/types/container_expiration_policy_older_than_enum.rb1
-rw-r--r--app/graphql/types/mutation_type.rb4
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/models/audit_event.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/avatarable.rb2
-rw-r--r--app/models/container_expiration_policy.rb1
-rw-r--r--app/models/project.rb4
-rw-r--r--app/services/clusters/agent_tokens/create_service.rb31
-rw-r--r--app/services/clusters/agents/create_service.rb29
-rw-r--r--app/services/clusters/agents/delete_service.rb23
-rw-r--r--app/services/users/update_service.rb22
-rw-r--r--app/views/devise/mailer/_confirmation_instructions_secondary.text.erb2
-rw-r--r--app/views/profiles/_email_settings.html.haml3
-rw-r--r--app/views/profiles/show.html.haml6
-rw-r--r--app/views/projects/issues/_related_issues.html.haml1
-rw-r--r--app/views/projects/issues/_related_issues_block.html.haml5
-rw-r--r--app/views/search/results/_issuable.html.haml7
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb11
-rw-r--r--config/feature_flags/development/linear_members_finder_ancestor_scopes.yml8
-rw-r--r--config/initializers/batch_loader.rb10
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb22
-rw-r--r--config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml22
-rw-r--r--config/routes/project.rb2
-rw-r--r--doc/administration/environment_variables.md1
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/docker/using_kaniko.md6
-rw-r--r--doc/development/code_review.md1
-rw-r--r--doc/development/contributing/design.md112
-rw-r--r--doc/development/contributing/issue_workflow.md17
-rw-r--r--doc/development/contributing/merge_request_workflow.md4
-rw-r--r--doc/development/database/database_reviewer_guidelines.md1
-rw-r--r--lib/api/entities/issuable_entity.rb2
-rw-r--r--lib/api/users.rb1
-rw-r--r--lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb2
-rw-r--r--lib/gitlab/email/message/in_product_marketing/trial.rb2
-rw-r--r--lib/gitlab/github_import/representation/diff_note.rb12
-rw-r--r--lib/gitlab/github_import/representation/issue.rb8
-rw-r--r--lib/gitlab/github_import/representation/lfs_object.rb9
-rw-r--r--lib/gitlab/github_import/representation/note.rb12
-rw-r--r--lib/gitlab/github_import/representation/pull_request.rb8
-rw-r--r--lib/gitlab/github_import/representation/pull_request_review.rb11
-rw-r--r--lib/gitlab/github_import/representation/user.rb1
-rw-r--r--lib/gitlab/pagination/keyset/iterator.rb11
-rw-r--r--locale/gitlab.pot143
-rw-r--r--spec/controllers/profiles_controller_spec.rb5
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/features/admin/admin_mode/login_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb43
-rw-r--r--spec/features/users/login_spec.rb2
-rw-r--r--spec/finders/members_finder_spec.rb62
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap6
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap2
-rw-r--r--spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js92
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/index_spec.js7
-rw-r--r--spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js36
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js46
-rw-r--r--spec/frontend/vue_mr_widget/test_extension.js36
-rw-r--r--spec/graphql/mutations/clusters/agent_tokens/create_spec.rb61
-rw-r--r--spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb52
-rw-r--r--spec/graphql/mutations/clusters/agents/create_spec.rb50
-rw-r--r--spec/graphql/mutations/clusters/agents/delete_spec.rb51
-rw-r--r--spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb2
-rw-r--r--spec/helpers/container_expiration_policies_helper_spec.rb1
-rw-r--r--spec/helpers/search_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/representation/diff_note_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/representation/issue_spec.rb13
-rw-r--r--spec/lib/gitlab/github_import/representation/lfs_object_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/representation/note_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb17
-rw-r--r--spec/lib/gitlab/github_import/representation/pull_request_spec.rb12
-rw-r--r--spec/lib/gitlab/pagination/keyset/iterator_spec.rb31
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb7
-rw-r--r--spec/models/namespace_spec.rb6
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb42
-rw-r--r--spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb43
-rw-r--r--spec/requests/api/graphql/project/cluster_agents_spec.rb108
-rw-r--r--spec/services/clusters/agent_tokens/create_service_spec.rb64
-rw-r--r--spec/services/clusters/agents/create_service_spec.rb52
-rw-r--r--spec/services/clusters/agents/delete_service_spec.rb35
-rw-r--r--spec/services/users/update_service_spec.rb70
-rw-r--r--spec/support/helpers/usage_data_helpers.rb1
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb63
113 files changed, 2062 insertions, 316 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 41c8525c0a4..62fb7c75cca 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -653,7 +653,6 @@
.rails:rules:decomposed-databases:
rules:
- <<: *if-merge-request-run-decomposed
- allow_failure: true
.rails:rules:ee-and-foss-migration:
rules:
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index a1911585f80..a548b283142 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -81,7 +81,7 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
})
.catch(() =>
createFlash({
- message: __('An error occurred while fetching markdown preview'),
+ message: __('An error occurred while fetching Markdown preview'),
}),
);
};
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 620da0104e0..34ef03409b8 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -45,6 +45,7 @@ export default () => {
new Vue({
el,
name: 'CycleAnalytics',
+ apolloProvider: {},
store,
render: (createElement) =>
createElement(CycleAnalytics, {
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 051ab710e5f..7acb5549273 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -25,7 +25,7 @@ export default {
lazy: true,
},
translations: {
- cronPlaceholder: __('* * * * *'),
+ cronPlaceholder: '* * * * *',
cronSyntaxInstructions: __(
'Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}',
),
diff --git a/app/assets/javascripts/notifications/constants.js b/app/assets/javascripts/notifications/constants.js
index 4f875977d78..f5891c9acb5 100644
--- a/app/assets/javascripts/notifications/constants.js
+++ b/app/assets/javascripts/notifications/constants.js
@@ -31,7 +31,7 @@ export const i18n = {
title: __('Custom notification events'),
bodyTitle: __('Notification events'),
bodyMessage: __(
- 'Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}.',
+ 'Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart}notification emails%{notificationLinkEnd}.',
),
},
eventNames: {
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index 165c4aae3cb..4d477fbd05d 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -73,6 +73,7 @@ export const OLDER_THAN_OPTIONS = [
{ key: 'SEVEN_DAYS', variable: 7, default: false },
{ key: 'FOURTEEN_DAYS', variable: 14, default: false },
{ key: 'THIRTY_DAYS', variable: 30, default: false },
+ { key: 'SIXTY_DAYS', variable: 60, default: false },
{ key: 'NINETY_DAYS', variable: 90, default: true },
];
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 80bc32dd43f..6afb3636998 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -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();
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/constants.js b/app/assets/javascripts/pages/profiles/password_prompt/constants.js
new file mode 100644
index 00000000000..99b8442c928
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/constants.js
@@ -0,0 +1,9 @@
+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');
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/index.js b/app/assets/javascripts/pages/profiles/password_prompt/index.js
new file mode 100644
index 00000000000..20645112893
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/index.js
@@ -0,0 +1,58 @@
+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;
+};
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue b/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue
new file mode 100644
index 00000000000..44728ea9cdf
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue
@@ -0,0 +1,82 @@
+<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>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
index 02baa76f627..d8f15cfde91 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
@@ -2,51 +2,51 @@ import { s__ } from '~/locale';
export const PIPELINE_SOURCES = [
{
- text: s__('Pipeline|Source|Push'),
+ text: s__('PipelineSource|Push'),
value: 'push',
},
{
- text: s__('Pipeline|Source|Web'),
+ text: s__('PipelineSource|Web'),
value: 'web',
},
{
- text: s__('Pipeline|Source|Trigger'),
+ text: s__('PipelineSource|Trigger'),
value: 'trigger',
},
{
- text: s__('Pipeline|Source|Schedule'),
+ text: s__('PipelineSource|Schedule'),
value: 'schedule',
},
{
- text: s__('Pipeline|Source|API'),
+ text: s__('PipelineSource|API'),
value: 'api',
},
{
- text: s__('Pipeline|Source|External'),
+ text: s__('PipelineSource|External'),
value: 'external',
},
{
- text: s__('Pipeline|Source|Pipeline'),
+ text: s__('PipelineSource|Pipeline'),
value: 'pipeline',
},
{
- text: s__('Pipeline|Source|Chat'),
+ text: s__('PipelineSource|Chat'),
value: 'chat',
},
{
- text: s__('Pipeline|Source|Web IDE'),
+ text: s__('PipelineSource|Web IDE'),
value: 'webide',
},
{
- text: s__('Pipeline|Source|Merge Request'),
+ text: s__('PipelineSource|Merge Request'),
value: 'merge_request_event',
},
{
- text: s__('Pipeline|Source|External Pull Request'),
+ text: s__('PipelineSource|External Pull Request'),
value: 'external_pull_request_event',
},
{
- text: s__('Pipeline|Source|Parent Pipeline'),
+ text: s__('PipelineSource|Parent Pipeline'),
value: 'parent_pipeline',
},
];
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index c042f0eef5f..7d23c7033f3 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -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 }"
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
index 8f486fb1b07..a21e294a34a 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -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"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
index db2197ec65e..4564a48fa2d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -1,30 +1,31 @@
<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>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index ba97bb767e2..08f4837a5cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -1,7 +1,8 @@
<script>
import { GlButton, GlLoadingIcon, GlIcon, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
-import StatusIcon from '../mr_widget_status_icon.vue';
+import { EXTENSION_ICON_CLASS } from '../../constants';
+import StatusIcon from './status_icon.vue';
export const LOADING_STATES = {
collapsedLoading: 'collapsedLoading',
@@ -45,14 +46,6 @@ export default {
return true;
},
statusIconName() {
- if (this.isLoadingSummary) {
- return 'loading';
- }
-
- if (this.loadingState === LOADING_STATES.collapsedError) {
- return 'warning';
- }
-
return this.statusIcon(this.collapsedData);
},
},
@@ -96,13 +89,18 @@ export default {
});
},
},
+ EXTENSION_ICON_CLASS,
};
</script>
<template>
- <section class="media-section mr-widget-border-top">
+ <section class="media-section mr-widget-border-top" data-testid="widget-extension">
<div class="media gl-p-5">
- <status-icon :status="statusIconName" class="align-self-center" />
+ <status-icon
+ :name="$options.name"
+ :is-loading="isLoadingSummary"
+ :icon-name="statusIconName"
+ />
<div class="media-body d-flex flex-align-self-center align-items-center">
<div class="code-text">
<template v-if="isLoadingSummary">
@@ -114,13 +112,18 @@ export default {
v-if="isCollapsible"
size="small"
class="float-right align-self-center"
+ data-testid="toggle-button"
@click="toggleCollapsed"
>
{{ isCollapsed ? __('Expand') : __('Collapse') }}
</gl-button>
</div>
</div>
- <div v-if="!isCollapsed" class="mr-widget-grouped-section">
+ <div
+ v-if="!isCollapsed"
+ class="mr-widget-grouped-section"
+ data-testid="widget-extension-collapsed-section"
+ >
<div v-if="isLoadingExpanded" class="report-block-container">
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
index 2fd1e17aaa6..46046d16fcf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
@@ -1,4 +1,4 @@
-import { extensions } from './index';
+import { registeredExtensions } from './index';
export default {
props: {
@@ -8,6 +8,8 @@ export default {
},
},
render(h) {
+ const { extensions } = registeredExtensions;
+
if (extensions.length === 0) return null;
return h('div', {}, [
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index 9796bb44939..bb40e22fe3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -1,12 +1,13 @@
+import Vue from 'vue';
import ExtensionBase from './base.vue';
// Holds all the currently registered extensions
-export const extensions = [];
+export const registeredExtensions = Vue.observable({ extensions: [] });
export const registerExtension = (extension) => {
// Pushes into the extenions array a dynamically created Vue component
// that gets exteneded from `base.vue`
- extensions.push({
+ registeredExtensions.extensions.push({
extends: ExtensionBase,
name: extension.name,
props: extension.props,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
new file mode 100644
index 00000000000..ed6aa8be1d5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ iconAriaLabel() {
+ const statusLabel = Object.keys(EXTENSION_ICONS).find(
+ (k) => EXTENSION_ICONS[k] === this.iconName,
+ );
+
+ return `${capitalizeFirstCharacter(statusLabel)} ${this.name}`;
+ },
+ },
+ EXTENSION_ICON_CLASS,
+};
+</script>
+
+<template>
+ <div
+ :class="[$options.EXTENSION_ICON_CLASS[iconName], { 'mr-widget-extension-icon': !isLoading }]"
+ class="align-self-center gl-rounded-full gl-mr-3 gl-relative gl-p-2"
+ >
+ <gl-loading-icon v-if="isLoading" size="md" inline class="gl-display-block" />
+ <gl-icon
+ v-else
+ :name="iconName"
+ :size="16"
+ :aria-label="iconAriaLabel"
+ class="gl-display-block"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 5edd4684529..c02783be385 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -91,4 +91,19 @@ export const stateToTransitionMap = {
export const stateToComponentMap = {
[states.MERGING]: classStateMap[stateKey.merging],
};
+
+export const EXTENSION_ICONS = {
+ failed: 'status-failed',
+ warning: 'status-alert',
+ success: 'status-success',
+ neutral: 'status-neutral',
+};
+
+export const EXTENSION_ICON_CLASS = {
+ [EXTENSION_ICONS.failed]: 'gl-text-red-500',
+ [EXTENSION_ICONS.warning]: 'gl-text-orange-500',
+ [EXTENSION_ICONS.success]: 'gl-text-green-500',
+ [EXTENSION_ICONS.neutral]: 'gl-text-gray-400',
+};
+
export { STATE_MACHINE };
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index 6c6f5e7fc73..5eda5c0778f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -1,11 +1,12 @@
/* eslint-disable */
+import { EXTENSION_ICONS } from '../constants';
import issuesCollapsedQuery from './issues_collapsed.query.graphql';
import issuesQuery from './issues.query.graphql';
export default {
// Give the extension a name
// Make it easier to track in Vue dev tools
- name: 'WidgetIssues',
+ name: 'Issues',
// Add an array of props
// These then get mapped to values stored in the MR Widget store
props: ['targetProjectFullPath'],
@@ -14,12 +15,12 @@ export default {
// Small summary text to be displayed in the collapsed state
// Receives the collapsed data as an argument
summary(count) {
- return `<strong>${count}</strong> open issue`;
+ return 'Summary text';
},
// Status icon to be used next to the summary text
// Receives the collapsed data as an argument
statusIcon(count) {
- return count > 0 ? 'warning' : 'success';
+ return EXTENSION_ICONS.warning;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 7b88b36aa0f..ea507017caa 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -97,7 +97,7 @@ export default {
});
})
.catch(() => {
- this.previewContent = __('An error occurred while fetching markdown preview');
+ this.previewContent = __('An error occurred while fetching Markdown preview');
this.isLoading = false;
});
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 77730ada9bb..86f04c78ebe 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -254,7 +254,7 @@ export default {
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
createFlash({
- message: __('Error rendering markdown preview'),
+ message: __('Error rendering Markdown preview'),
}),
);
},
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 071a5be073f..59520e629ab 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -1040,3 +1040,17 @@ $tabs-holder-z-index: 250;
margin-bottom: 1px;
}
}
+
+.mr-widget-extension-icon::before {
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-left-0;
+ @include gl-top-0;
+ @include gl-opacity-3;
+ @include gl-border-solid;
+ @include gl-border-4;
+ @include gl-rounded-full;
+
+ width: 24px;
+ height: 24px;
+}
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 29ae268ef67..69257081cc9 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -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
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index ea101cf1dcd..0faafa6df9c 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -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
diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb
new file mode 100644
index 00000000000..07bf2536065
--- /dev/null
+++ b/app/graphql/mutations/clusters/agent_tokens/create.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module AgentTokens
+ class Create < BaseMutation
+ graphql_name 'ClusterAgentTokenCreate'
+
+ authorize :create_cluster
+
+ ClusterAgentID = ::Types::GlobalIDType[::Clusters::Agent]
+
+ argument :cluster_agent_id,
+ ClusterAgentID,
+ required: true,
+ description: 'Global ID of the cluster agent that will be associated with the new token.'
+
+ argument :description,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Description of the token.'
+
+ argument :name,
+ GraphQL::Types::String,
+ required: true,
+ description: 'Name of the token.'
+
+ field :secret,
+ GraphQL::Types::String,
+ null: true,
+ description: "Token secret value. Make sure you save it - you won't be able to access it again."
+
+ field :token,
+ Types::Clusters::AgentTokenType,
+ null: true,
+ description: 'Token created after mutation.'
+
+ def resolve(args)
+ cluster_agent = authorized_find!(id: args[:cluster_agent_id])
+
+ result = ::Clusters::AgentTokens::CreateService
+ .new(
+ container: cluster_agent.project,
+ current_user: current_user,
+ params: args.merge(agent_id: cluster_agent.id)
+ )
+ .execute
+
+ payload = result.payload
+
+ {
+ secret: payload[:secret],
+ token: payload[:token],
+ errors: Array.wrap(result.message)
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ClusterAgentID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agent_tokens/delete.rb b/app/graphql/mutations/clusters/agent_tokens/delete.rb
new file mode 100644
index 00000000000..603b6b30910
--- /dev/null
+++ b/app/graphql/mutations/clusters/agent_tokens/delete.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module AgentTokens
+ class Delete < BaseMutation
+ graphql_name 'ClusterAgentTokenDelete'
+
+ authorize :admin_cluster
+
+ TokenID = ::Types::GlobalIDType[::Clusters::AgentToken]
+
+ argument :id, TokenID,
+ required: true,
+ description: 'Global ID of the cluster agent token that will be deleted.'
+
+ def resolve(id:)
+ token = authorized_find!(id: id)
+ token.destroy
+
+ { errors: errors_on_object(token) }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = TokenID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agents/create.rb b/app/graphql/mutations/clusters/agents/create.rb
new file mode 100644
index 00000000000..0896cc7b203
--- /dev/null
+++ b/app/graphql/mutations/clusters/agents/create.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module Agents
+ class Create < BaseMutation
+ include FindsProject
+
+ authorize :create_cluster
+
+ graphql_name 'CreateClusterAgent'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the associated project for this cluster agent.'
+
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Name of the cluster agent.'
+
+ field :cluster_agent,
+ Types::Clusters::AgentType,
+ null: true,
+ description: 'Cluster agent created after mutation.'
+
+ def resolve(project_path:, name:)
+ project = authorized_find!(project_path)
+ result = ::Clusters::Agents::CreateService.new(project, current_user).execute(name: name)
+
+ {
+ cluster_agent: result[:cluster_agent],
+ errors: Array.wrap(result[:message])
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agents/delete.rb b/app/graphql/mutations/clusters/agents/delete.rb
new file mode 100644
index 00000000000..9ada1f31f60
--- /dev/null
+++ b/app/graphql/mutations/clusters/agents/delete.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module Agents
+ class Delete < BaseMutation
+ graphql_name 'ClusterAgentDelete'
+
+ authorize :admin_cluster
+
+ AgentID = ::Types::GlobalIDType[::Clusters::Agent]
+
+ argument :id, AgentID,
+ required: true,
+ description: 'Global ID of the cluster agent that will be deleted.'
+
+ def resolve(id:)
+ cluster_agent = authorized_find!(id: id)
+ result = ::Clusters::Agents::DeleteService
+ .new(container: cluster_agent.project, current_user: current_user)
+ .execute(cluster_agent)
+
+ {
+ errors: Array.wrap(result.message)
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = AgentID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/container_expiration_policy_older_than_enum.rb b/app/graphql/types/container_expiration_policy_older_than_enum.rb
index 7364910f8cd..9c32d767caf 100644
--- a/app/graphql/types/container_expiration_policy_older_than_enum.rb
+++ b/app/graphql/types/container_expiration_policy_older_than_enum.rb
@@ -6,6 +6,7 @@ module Types
'7d': 'SEVEN_DAYS',
'14d': 'FOURTEEN_DAYS',
'30d': 'THIRTY_DAYS',
+ '60d': 'SIXTY_DAYS',
'90d': 'NINETY_DAYS'
}.freeze
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ea50af1c554..b512bfe8dd6 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -31,6 +31,10 @@ module Types
mount_mutation Mutations::Boards::Lists::Update
mount_mutation Mutations::Boards::Lists::Destroy
mount_mutation Mutations::Branches::Create, calls_gitaly: true
+ mount_mutation Mutations::Clusters::Agents::Create
+ mount_mutation Mutations::Clusters::Agents::Delete
+ mount_mutation Mutations::Clusters::AgentTokens::Create
+ mount_mutation Mutations::Clusters::AgentTokens::Delete
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::CustomEmoji::Destroy, feature_flag: :custom_emoji
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index b8e58e3afb1..cb28025c900 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -87,9 +87,9 @@ module SearchHelper
def search_entries_info_template(collection)
if collection.total_pages > 1
- s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}").html_safe
+ s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element}").html_safe
else
- s_("SearchResults|Showing %{count} %{scope} for%{term_element}").html_safe
+ s_("SearchResults|Showing %{count} %{scope} for %{term_element}").html_safe
end
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index f17fff742fe..a1c6793607f 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -71,7 +71,7 @@ class AuditEvent < ApplicationRecord
end
def lazy_author
- BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader|
+ BatchLoader.for(author_id).batch do |author_ids, loader|
User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
loader.call(user.id, user)
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6c8b4ae1139..553681ee960 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -133,7 +133,7 @@ class Commit
end
def lazy(container, oid)
- BatchLoader.for({ container: container, oid: oid }).batch(replace_methods: false) do |items, loader|
+ BatchLoader.for({ container: container, oid: oid }).batch do |items, loader|
items_by_container = items.group_by { |i| i[:container] }
items_by_container.each do |container, commit_ids|
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 84a74386ff7..53cc6aabcce 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -110,7 +110,7 @@ module Avatarable
def retrieve_upload_from_batch(identifier)
BatchLoader.for(identifier: identifier, model: self)
- .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args|
+ .batch(key: self.class) do |upload_params, loader, args|
model_class = args[:key]
paths = upload_params.flat_map do |params|
params[:model].upload_paths(params[:identifier])
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index 9bacd9a0edf..aecb47f7a03 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -74,6 +74,7 @@ class ContainerExpirationPolicy < ApplicationRecord
'7d': _('%{days} days until tags are automatically removed') % { days: 7 },
'14d': _('%{days} days until tags are automatically removed') % { days: 14 },
'30d': _('%{days} days until tags are automatically removed') % { days: 30 },
+ '60d': _('%{days} days until tags are automatically removed') % { days: 60 },
'90d': _('%{days} days until tags are automatically removed') % { days: 90 }
}
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a05a21b6807..126fcc8d7e6 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1791,7 +1791,7 @@ class Project < ApplicationRecord
def open_issues_count(current_user = nil)
return Projects::OpenIssuesCountService.new(self, current_user).count unless current_user.nil?
- BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
+ BatchLoader.for(self).batch do |projects, loader|
issues_count_per_project = ::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache_and_retrieve_data
issues_count_per_project.each do |project, count|
@@ -2256,7 +2256,7 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def forks_count
- BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
+ BatchLoader.for(self).batch do |projects, loader|
fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data
fork_count_per_project.each do |project, count|
diff --git a/app/services/clusters/agent_tokens/create_service.rb b/app/services/clusters/agent_tokens/create_service.rb
new file mode 100644
index 00000000000..ae2617f510b
--- /dev/null
+++ b/app/services/clusters/agent_tokens/create_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Clusters
+ module AgentTokens
+ class CreateService < ::BaseContainerService
+ ALLOWED_PARAMS = %i[agent_id description name].freeze
+
+ def execute
+ return error_no_permissions unless current_user.can?(:create_cluster, container)
+
+ token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
+
+ if token.save
+ ServiceResponse.success(payload: { secret: token.token, token: token })
+ else
+ ServiceResponse.error(message: token.errors.full_messages)
+ end
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: s_('ClusterAgent|User has insufficient permissions to create a token for this project'))
+ end
+
+ def filtered_params
+ params.slice(*ALLOWED_PARAMS)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/create_service.rb b/app/services/clusters/agents/create_service.rb
new file mode 100644
index 00000000000..568f168d63b
--- /dev/null
+++ b/app/services/clusters/agents/create_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class CreateService < BaseService
+ def execute(name:)
+ return error_no_permissions unless cluster_agent_permissions?
+
+ agent = ::Clusters::Agent.new(name: name, project: project, created_by_user: current_user)
+
+ if agent.save
+ success.merge(cluster_agent: agent)
+ else
+ error(agent.errors.full_messages)
+ end
+ end
+
+ private
+
+ def cluster_agent_permissions?
+ current_user.can?(:admin_pipeline, project) && current_user.can?(:create_cluster, project)
+ end
+
+ def error_no_permissions
+ error(s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project'))
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/delete_service.rb b/app/services/clusters/agents/delete_service.rb
new file mode 100644
index 00000000000..2132dffa606
--- /dev/null
+++ b/app/services/clusters/agents/delete_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class DeleteService < ::BaseContainerService
+ def execute(cluster_agent)
+ return error_no_permissions unless current_user.can?(:admin_cluster, cluster_agent)
+
+ if cluster_agent.destroy
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: cluster_agent.errors.full_messages)
+ end
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: s_('ClusterAgent|You have insufficient permissions to delete this cluster agent'))
+ end
+ end
+ end
+end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index 23c67231a29..c3df9b153a1 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -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?
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
index ab46aaaca1a..32e88047a9c 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
@@ -1,4 +1,4 @@
-<%= _(" %{name}, confirm your email address now! ") % { name: @resource.user.name } %>
+<%= _("%{name}, confirm your email address now!") % { name: @resource.user.name } %>
<%= _("Use the link below to confirm your email address (%{email})") % { email: @resource.email } %>
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml
index bc678c2c429..1057e96f442 100644
--- a/app/views/profiles/_email_settings.html.haml
+++ b/app/views/profiles/_email_settings.html.haml
@@ -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
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 0bb4859dd1e..ad8cde689f9 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -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
diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml
index d131d20f079..bab37609c20 100644
--- a/app/views/projects/issues/_related_issues.html.haml
+++ b/app/views/projects/issues/_related_issues.html.haml
@@ -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')
diff --git a/app/views/projects/issues/_related_issues_block.html.haml b/app/views/projects/issues/_related_issues_block.html.haml
deleted file mode 100644
index 8d986b64b1d..00000000000
--- a/app/views/projects/issues/_related_issues_block.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.related-issues-block
- .card.card-slim
- .card-header.panel-empty-heading.border-bottom-0
- %h3.card-title.mt-0.mb-0.h5
- = _('Linked issues')
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index 5645fbfb238..41058034d6f 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -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
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index a377b7a2000..e1f404b250d 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -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
diff --git a/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml b/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml
new file mode 100644
index 00000000000..6bd5e164324
--- /dev/null
+++ b/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+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
diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb
index d88b43fbcea..e542a26cb81 100644
--- a/config/initializers/batch_loader.rb
+++ b/config/initializers/batch_loader.rb
@@ -1,3 +1,13 @@
# frozen_string_literal: true
Rails.application.config.middleware.use(BatchLoader::Middleware)
+
+# Disables replace_methods by default.
+# See https://github.com/exAspArk/batch-loader#replacing-methods for more information.
+module BatchLoaderWithoutMethodReplacementByDefault
+ def batch(replace_methods: false, **kw_args, &batch_block)
+ super
+ end
+end
+
+BatchLoader.prepend(BatchLoaderWithoutMethodReplacementByDefault)
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index c23049e93c9..3c994516b2d 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -1,30 +1,8 @@
# 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
diff --git a/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml b/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml
new file mode 100644
index 00000000000..1a9fe349a90
--- /dev/null
+++ b/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml
@@ -0,0 +1,22 @@
+---
+data_category: optional
+key_path: counts.projects_with_expiration_policy_enabled_with_older_than_set_to_60d
+description: A count of projects with the cleanup policy set delete tags older than
+ 60 days
+product_section: ops
+product_stage: package
+product_group: group::package
+product_category: container registry
+value_type: number
+status: active
+time_frame: all
+data_source: database
+distribution:
+- ee
+- ce
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/routes/project.rb b/config/routes/project.rb
index cbd2f5ac839..bfb7802b3de 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -298,6 +298,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ resources :cluster_agents, only: [:show], param: :name
+
concerns :clusterable
namespace :serverless do
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 057abce0ed5..3af80363916 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -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). |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 33a1d39a911..3ebaa3f159b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -15460,6 +15460,7 @@ Conan file types.
| <a id="containerexpirationpolicyolderthanenumfourteen_days"></a>`FOURTEEN_DAYS` | 14 days until tags are automatically removed. |
| <a id="containerexpirationpolicyolderthanenumninety_days"></a>`NINETY_DAYS` | 90 days until tags are automatically removed. |
| <a id="containerexpirationpolicyolderthanenumseven_days"></a>`SEVEN_DAYS` | 7 days until tags are automatically removed. |
+| <a id="containerexpirationpolicyolderthanenumsixty_days"></a>`SIXTY_DAYS` | 60 days until tags are automatically removed. |
| <a id="containerexpirationpolicyolderthanenumthirty_days"></a>`THIRTY_DAYS` | 30 days until tags are automatically removed. |
### `ContainerRepositoryCleanupStatus`
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
index 6886899a54b..853b1aaca42 100644
--- a/doc/ci/docker/using_kaniko.md
+++ b/doc/ci/docker/using_kaniko.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-----
...
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 8fa7d1be04c..89516c2168b 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -106,6 +106,7 @@ with [domain expertise](#domain-experts).
1. If your merge request includes user-facing changes (*3*), it must be
**approved by a [Product Designer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_reviewers_UX)**,
based on assignments in the appropriate [DevOps stage group](https://about.gitlab.com/handbook/product/categories/#devops-stages).
+ See the [design and user interface guidelines](contributing/design.md) for details.
1. If your merge request includes adding a new JavaScript library (*1*)...
- If the library significantly increases the
[bundle size](https://gitlab.com/gitlab-org/frontend/playground/webpack-memory-metrics/-/blob/master/doc/report.md), it must
diff --git a/doc/development/contributing/design.md b/doc/development/contributing/design.md
index 9e8375fcbdd..6831fb186d4 100644
--- a/doc/development/contributing/design.md
+++ b/doc/development/contributing/design.md
@@ -5,34 +5,102 @@ group: Development
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Implement design & UI elements
+# Design and user interface changes
-For guidance on UX implementation at GitLab, please refer to our [Design System](https://design.gitlab.com/).
+Follow these guidelines when contributing or reviewing design and user interface
+(UI) changes. Refer to our [code review guide](../code_review.md) for broader
+advice and best practices for code review in general.
-The UX team uses labels to manage their workflow.
+The basis for most of these guidelines is [Pajamas](https://design.gitlab.com/),
+GitLab design system. We encourage you to [contribute to Pajamas](https://design.gitlab.com/get-started/contribute)
+with additions and improvements.
-The `~UX` label on an issue is a signal to the UX team that it will need UX attention.
-To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux/) of the handbook.
+## Merge request reviews
-Once an issue has been worked on and is ready for development, a UXer removes the `~UX` label and applies the `~"UX ready"` label to that issue.
+As a merge request (MR) author, you must include _Before_ and _After_
+screenshots (or videos) of your changes in the description, as explained in our
+[MR workflow](merge_request_workflow.md). These screenshots/videos are very helpful
+for all reviewers and can speed up the review process, especially if the changes
+are small.
-There is a special type label called `~"product discovery"` intended for UX (user experience),
-PM (product manager), FE (frontend), and BE (backend). It represents a discovery issue to discuss the problem and
-potential solutions. The final output for this issue could be a doc of
-requirements, a design artifact, or even a prototype. The solution will be
-developed in a subsequent milestone.
+## Checklist
-`~"product discovery"` issues are like any other issue and should contain a milestone label, `~Deliverable` or `~Stretch`, when scheduled in the current milestone.
+Check these aspects both when _designing_ and _reviewing_ UI changes.
-The initial issue should be about the problem we are solving. If a separate [product discovery issue](https://about.gitlab.com/handbook/engineering/ux/ux-department-workflow/#how-we-use-labels)
-is needed for additional research and design work, it will be created by a PM or UX person.
-Assign the `~UX`, `~"product discovery"` and `~Deliverable` labels, add a milestone and
-use a title that makes it clear that the scheduled issue is product discovery
-(for example, `Product discovery for XYZ`).
+### Writing
-In order to complete a product discovery issue in a release, you must complete the following:
+- Follow [Pajamas](https://design.gitlab.com/content/punctuation/) as the primary
+ guidelines for UI text and [documentation style guide](../documentation/styleguide/index.md)
+ as the secondary.
+- Use clear and consistent [terminology](https://design.gitlab.com/content/terminology).
+- Check grammar and spelling.
+- Consider help content and follow its [guidelines](https://design.gitlab.com/usability/helping-users).
+- Request review from the [appropriate Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers),
+ indicating any specific files or lines they should review, and how to preview
+ or understand the location/context of the text from the user's perspective.
-1. UXer removes the `~UX` label, adds the `~"UX ready"` label.
-1. Modify the issue description in the product discovery issue to contain the final design. If it makes sense, the original information indicating the need for the design can be moved to a lower "Original Information" section.
-1. Copy the design to the description of the delivery issue for which the product discovery issue was created. Do not simply refer to the product discovery issue as a separate source of truth.
-1. In some cases, a product discovery issue also identifies future enhancements that will not go into the issue that originated the product discovery issue. For these items, create new issues containing the designs to ensure they are not lost. Put the issues in the backlog if they are agreed upon as good ideas. Otherwise leave them for triage.
+### Patterns
+
+- Consider similar patterns used in the product and justify in the issue when diverging
+ from them.
+- Use appropriate [components](https://design.gitlab.com/components/overview)
+ and [data visualizations](https://design.gitlab.com/data-visualization/overview).
+
+### States
+
+- Account for all applicable states ([error](https://design.gitlab.com/content/error-messages),
+ rest, loading, focus, hover, selected, disabled).
+- Account for states dependent on data size ([empty](https://design.gitlab.com/regions/empty-states),
+ some data, and lots of data).
+- Account for states dependent on user role, user preferences, and subscription.
+- Consider animations and transitions, and follow their [guidelines](https://design.gitlab.com/product-foundations/motion).
+
+### Visual design
+
+- Use recommended [colors](https://design.gitlab.com/product-foundations/colors)
+ and [typography](https://design.gitlab.com/product-foundations/type-fundamentals).
+- Follow [layout guidelines](https://design.gitlab.com/layout/grid).
+- Use existing [icons](http://gitlab-org.gitlab.io/gitlab-svgs/) and [illustrations](http://gitlab-org.gitlab.io/gitlab-svgs/illustrations)
+ or propose new ones according to [iconography](https://design.gitlab.com/product-foundations/iconography)
+ and [illustration](https://design.gitlab.com/product-foundations/illustration)
+ guidelines.
+- _Optionally_ consider [dark mode](../../user/profile/preferences.md#dark-mode). [^1]
+
+ [^1]: You're not required to design for [dark mode](../../user/profile/preferences.md#dark-mode) while the feature is in [alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha). The [UX Foundations team](https://about.gitlab.com/direction/ecosystem/foundations/) plans to improve the dark mode in the future. Until we integrate [Pajamas](https://design.gitlab.com/) components into the product and the underlying design strategy is in place to support dark mode, we cannot guarantee that we won't introduce bugs and debt to this mode. At your discretion, evaluate the need to create dark mode patches.
+
+### Responsive
+
+- Account for resizing, collapsing, moving, or wrapping of elements across
+ all breakpoints (even if larger viewports are prioritized).
+- Provide the same information and actions in all breakpoints.
+
+### Accessibility
+
+- Conform to level AA of the World Wide Web Consortium (W3C) [Web Content Accessibility Guidelines 2.1](https://www.w3.org/TR/WCAG21/),
+ according to our [statement of compliance](https://design.gitlab.com/accessibility/a11y).
+- Follow accessibility [best practices](https://design.gitlab.com/accessibility/best-practices)
+ and [checklist](../fe_guide/accessibility.md#quick-checklist).
+
+### Handoff
+
+- Share design specifications in the related issue, preferably through a [Figma link](https://help.figma.com/hc/en-us/articles/360040531773-Share-Files-with-anyone-using-Link-Sharing#Copy_links)
+ link or [GitLab Designs feature](../../user/project/issues/design_management.md#the-design-management-section).
+ See [when you should use each tool](https://about.gitlab.com/handbook/engineering/ux/product-designer/#deliver).
+- Document user flow and states (for example, using [Mermaid flowcharts in Markdown](../../user/markdown.md#mermaid)).
+- Document animations and transitions.
+- Document responsive behaviors.
+- Document non-evident behaviors (for example, field is auto-focused).
+- Document accessibility behaviors (for example, using [accessibility annotations in Figma](https://www.figma.com/file/g7QtDbfxF3pCdWiyskIr0X/Accessibility-bluelines)).
+- Contribute new icons or illustrations to the [GitLab SVGs](https://gitlab.com/gitlab-org/gitlab-svgs)
+ project.
+
+### Follow-ups
+
+- Contribute [issues to Pajamas](https://design.gitlab.com/get-started/contribute#contribute-an-issue)
+ for additions or enhancements to the design system.
+- Create issues with the [`~UX debt`](issue_workflow.md#technical-and-ux-debt)
+ label for intentional deviations from the agreed-upon UX requirements due to
+ time or feasibility challenges, linking back to the corresponding issue(s) or
+ MR(s).
+- Create issues for [feature additions or enhancements](issue_workflow.md#feature-proposals)
+ outside the agreed-upon UX requirements to avoid scope creep.
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 6ab51067b0d..d85d733e74c 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -342,19 +342,22 @@ To create a feature proposal, open an issue on the
[issue tracker](https://gitlab.com/gitlab-org/gitlab/-/issues).
In order to help track the feature proposals, we have created a
-[`feature`](https://gitlab.com/gitlab-org/gitlab/-/issues?label_name=feature) label. For the time being, users that are not members
-of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
-members to add the label ~feature to the issue or add the following
+[`feature`](https://gitlab.com/gitlab-org/gitlab/-/issues?label_name=feature) label.
+For the time being, users that are not members of the project cannot add labels.
+You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/)
+members to add the label `~feature` to the issue or add the following
code snippet right after your description in a new line: `~feature`.
Please keep feature proposals as small and simple as possible, complex ones
might be edited to make them small and simple.
-Please submit Feature Proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) provided on the issue tracker.
+Please submit feature proposals using the ['Feature Proposal' issue template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal%20-%20detailed.md) provided on the issue tracker.
-For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
-be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
-need to ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label, if you do not have permissions to do it by yourself.
+For changes to the user interface (UI), follow our [design and UI guidelines](design.md),
+and include a visual example (screenshot, wireframe, or mockup). Such issues should
+be given the `~UX"` label for the Product Design team to provide input and guidance.
+You may need to ask one of the [core team](https://about.gitlab.com/community/core-team/)
+members to add the label, if you do not have permissions to do it by yourself.
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 25561764bd6..6d3da7ef863 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -18,8 +18,8 @@ in order to ensure the work is finished before the release date.
If you want to add a new feature that is not labeled, it is best to first create
an issue (if there isn't one already) and leave a comment asking for it
-to be marked as `Accepting Merge Requests`. Please include screenshots or
-wireframes of the proposed feature if it will also change the UI.
+to be marked as `Accepting merge requests`. See the [feature proposals](issue_workflow.md#feature-proposals)
+section.
Merge requests should be submitted to the appropriate project at GitLab.com, for example
[GitLab](https://gitlab.com/gitlab-org/gitlab/-/merge_requests),
diff --git a/doc/development/database/database_reviewer_guidelines.md b/doc/development/database/database_reviewer_guidelines.md
index 59653c6dde3..bc18e606f21 100644
--- a/doc/development/database/database_reviewer_guidelines.md
+++ b/doc/development/database/database_reviewer_guidelines.md
@@ -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
diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb
index fd5d6c8137f..e2c674c0b8b 100644
--- a/lib/api/entities/issuable_entity.rb
+++ b/lib/api/entities/issuable_entity.rb
@@ -24,7 +24,7 @@ module API
# entity according to the current top-level entity options, such
# as the current_user.
def lazy_issuable_metadata
- BatchLoader.for(object).batch(key: [current_user, :issuable_metadata], replace_methods: false) do |models, loader, args|
+ BatchLoader.for(object).batch(key: [current_user, :issuable_metadata]) do |models, loader, args|
current_user = args[:key].first
issuable_metadata = Gitlab::IssuableMetadata.new(current_user, models)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c82bcc53cb2..1cab668a248 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -1085,7 +1085,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
diff --git a/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb b/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb
index dc31f995ae0..909bf10341a 100644
--- a/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb
+++ b/lib/gitlab/background_migration/populate_finding_uuid_for_vulnerability_feedback.rb
@@ -38,7 +38,7 @@ module Gitlab
end
def vulnerability_finding
- BatchLoader.for(finding_key).batch(replace_methods: false) do |finding_keys, loader|
+ BatchLoader.for(finding_key).batch do |finding_keys, loader|
project_ids = finding_keys.map { |key| key[:project_id] }
categories = finding_keys.map { |key| key[:category] }
fingerprints = finding_keys.map { |key| key[:project_fingerprint] }
diff --git a/lib/gitlab/email/message/in_product_marketing/trial.rb b/lib/gitlab/email/message/in_product_marketing/trial.rb
index 222046a3966..11a799886ab 100644
--- a/lib/gitlab/email/message/in_product_marketing/trial.rb
+++ b/lib/gitlab/email/message/in_product_marketing/trial.rb
@@ -15,7 +15,7 @@ module Gitlab
def tagline
[
- s_('InProductMarketing|Start a free trial of GitLab Ultimate – no CC required'),
+ s_('InProductMarketing|Start a free trial of GitLab Ultimate – no credit card required'),
s_('InProductMarketing|Improve app security with a 30-day trial'),
s_('InProductMarketing|Start with a GitLab Ultimate free trial')
][series]
diff --git a/lib/gitlab/github_import/representation/diff_note.rb b/lib/gitlab/github_import/representation/diff_note.rb
index d0584cc6255..1d6dfbead5f 100644
--- a/lib/gitlab/github_import/representation/diff_note.rb
+++ b/lib/gitlab/github_import/representation/diff_note.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/issue.rb b/lib/gitlab/github_import/representation/issue.rb
index 0e04b5ad57f..db4a8188c03 100644
--- a/lib/gitlab/github_import/representation/issue.rb
+++ b/lib/gitlab/github_import/representation/issue.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/lfs_object.rb b/lib/gitlab/github_import/representation/lfs_object.rb
index 41723759645..18737bfcde3 100644
--- a/lib/gitlab/github_import/representation/lfs_object.rb
+++ b/lib/gitlab/github_import/representation/lfs_object.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/note.rb b/lib/gitlab/github_import/representation/note.rb
index 5b98ce7d5ed..bcdb1a5459b 100644
--- a/lib/gitlab/github_import/representation/note.rb
+++ b/lib/gitlab/github_import/representation/note.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/pull_request.rb b/lib/gitlab/github_import/representation/pull_request.rb
index e4f54fcc833..82bcdee8b2b 100644
--- a/lib/gitlab/github_import/representation/pull_request.rb
+++ b/lib/gitlab/github_import/representation/pull_request.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/pull_request_review.rb b/lib/gitlab/github_import/representation/pull_request_review.rb
index 08b3160fc4c..70c1e51ffdd 100644
--- a/lib/gitlab/github_import/representation/pull_request_review.rb
+++ b/lib/gitlab/github_import/representation/pull_request_review.rb
@@ -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
diff --git a/lib/gitlab/github_import/representation/user.rb b/lib/gitlab/github_import/representation/user.rb
index d97b90b6291..fac8920a3f2 100644
--- a/lib/gitlab/github_import/representation/user.rb
+++ b/lib/gitlab/github_import/representation/user.rb
@@ -17,7 +17,6 @@ module Gitlab
def self.from_api_response(user)
new(
id: user.id,
- github_id: user.id,
login: user.login
)
end
diff --git a/lib/gitlab/pagination/keyset/iterator.rb b/lib/gitlab/pagination/keyset/iterator.rb
index 14807fa37c4..f54ade6ca66 100644
--- a/lib/gitlab/pagination/keyset/iterator.rb
+++ b/lib/gitlab/pagination/keyset/iterator.rb
@@ -6,10 +6,11 @@ module Gitlab
class Iterator
UnsupportedScopeOrder = Class.new(StandardError)
- def initialize(scope:, use_union_optimization: true, in_operator_optimization_options: nil)
+ def initialize(scope:, cursor: {}, use_union_optimization: true, in_operator_optimization_options: nil)
@scope, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope)
raise(UnsupportedScopeOrder, 'The order on the scope does not support keyset pagination') unless success
+ @cursor = cursor
@order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
@use_union_optimization = in_operator_optimization_options ? false : use_union_optimization
@in_operator_optimization_options = in_operator_optimization_options
@@ -17,11 +18,9 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def each_batch(of: 1000)
- cursor_attributes = {}
-
loop do
current_scope = scope.dup
- relation = order.apply_cursor_conditions(current_scope, cursor_attributes, keyset_options)
+ relation = order.apply_cursor_conditions(current_scope, cursor, keyset_options)
relation = relation.reorder(order) unless @in_operator_optimization_options
relation = relation.limit(of)
@@ -30,14 +29,14 @@ module Gitlab
last_record = relation.last
break unless last_record
- cursor_attributes = order.cursor_attributes_for_node(last_record)
+ @cursor = order.cursor_attributes_for_node(last_record)
end
end
# rubocop: enable CodeReuse/ActiveRecord
private
- attr_reader :scope, :order
+ attr_reader :scope, :cursor, :order
def keyset_options
{
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 63f50708d23..9eb4895385a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16,9 +16,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-msgid " %{name}, confirm your email address now! "
-msgstr ""
-
msgid " %{start} to %{end}"
msgstr ""
@@ -1135,9 +1132,6 @@ msgstr ""
msgid "(we need your current password to confirm your changes)"
msgstr ""
-msgid "* * * * *"
-msgstr ""
-
msgid "+ %{amount} more"
msgstr ""
@@ -3587,6 +3581,9 @@ msgstr ""
msgid "An error occurred while enabling Service Desk."
msgstr ""
+msgid "An error occurred while fetching Markdown preview"
+msgstr ""
+
msgid "An error occurred while fetching ancestors"
msgstr ""
@@ -3617,9 +3614,6 @@ msgstr ""
msgid "An error occurred while fetching label colors."
msgstr ""
-msgid "An error occurred while fetching markdown preview"
-msgstr ""
-
msgid "An error occurred while fetching participants"
msgstr ""
@@ -7345,9 +7339,6 @@ msgstr ""
msgid "ClusterAgents|You will need to create a token to connect to your agent"
msgstr ""
-msgid "ClusterAgent|This feature is only available for premium plans"
-msgstr ""
-
msgid "ClusterAgent|User has insufficient permissions to create a token for this project"
msgstr ""
@@ -9999,7 +9990,7 @@ msgstr ""
msgid "Custom notification events"
msgstr ""
-msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart} notification emails%{notificationLinkEnd}."
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notificationLinkStart}notification emails%{notificationLinkEnd}."
msgstr ""
msgid "Custom project templates"
@@ -10198,10 +10189,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"
@@ -13366,7 +13357,7 @@ msgstr ""
msgid "Error parsing CSV file. Please make sure it has"
msgstr ""
-msgid "Error rendering markdown preview"
+msgid "Error rendering Markdown preview"
msgstr ""
msgid "Error saving label update."
@@ -17606,7 +17597,7 @@ msgstr ""
msgid "InProductMarketing|Start a GitLab Ultimate trial today in less than one minute, no credit card required."
msgstr ""
-msgid "InProductMarketing|Start a free trial of GitLab Ultimate – no CC required"
+msgid "InProductMarketing|Start a free trial of GitLab Ultimate – no credit card required"
msgstr ""
msgid "InProductMarketing|Start a trial"
@@ -24487,6 +24478,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 ""
@@ -24862,6 +24865,48 @@ msgstr ""
msgid "PipelineSchedules|Variables"
msgstr ""
+msgid "PipelineSource|API"
+msgstr ""
+
+msgid "PipelineSource|Chat"
+msgstr ""
+
+msgid "PipelineSource|External"
+msgstr ""
+
+msgid "PipelineSource|External Pull Request"
+msgstr ""
+
+msgid "PipelineSource|Merge Request"
+msgstr ""
+
+msgid "PipelineSource|On-Demand DAST Scan"
+msgstr ""
+
+msgid "PipelineSource|On-Demand DAST Validation"
+msgstr ""
+
+msgid "PipelineSource|Parent Pipeline"
+msgstr ""
+
+msgid "PipelineSource|Pipeline"
+msgstr ""
+
+msgid "PipelineSource|Push"
+msgstr ""
+
+msgid "PipelineSource|Schedule"
+msgstr ""
+
+msgid "PipelineSource|Trigger"
+msgstr ""
+
+msgid "PipelineSource|Web"
+msgstr ""
+
+msgid "PipelineSource|Web IDE"
+msgstr ""
+
msgid "PipelineStatusTooltip|Pipeline: %{ciStatus}"
msgstr ""
@@ -25162,51 +25207,9 @@ msgstr ""
msgid "Pipeline|Source"
msgstr ""
-msgid "Pipeline|Source|API"
-msgstr ""
-
-msgid "Pipeline|Source|Chat"
-msgstr ""
-
-msgid "Pipeline|Source|External"
-msgstr ""
-
-msgid "Pipeline|Source|External Pull Request"
-msgstr ""
-
-msgid "Pipeline|Source|Merge Request"
-msgstr ""
-
-msgid "Pipeline|Source|On-Demand DAST Scan"
-msgstr ""
-
-msgid "Pipeline|Source|On-Demand DAST Validation"
-msgstr ""
-
-msgid "Pipeline|Source|Parent Pipeline"
-msgstr ""
-
-msgid "Pipeline|Source|Pipeline"
-msgstr ""
-
-msgid "Pipeline|Source|Push"
-msgstr ""
-
-msgid "Pipeline|Source|Schedule"
-msgstr ""
-
msgid "Pipeline|Source|Security Policy"
msgstr ""
-msgid "Pipeline|Source|Trigger"
-msgstr ""
-
-msgid "Pipeline|Source|Web"
-msgstr ""
-
-msgid "Pipeline|Source|Web IDE"
-msgstr ""
-
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default."
msgstr ""
@@ -25933,6 +25936,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 ""
@@ -29748,16 +29754,16 @@ msgstr ""
msgid "SearchCodeResults|of %{link_to_project}"
msgstr ""
-msgid "SearchResults|Showing %{count} %{scope} for%{term_element}"
+msgid "SearchResults|Showing %{count} %{scope} for %{term_element}"
msgstr ""
-msgid "SearchResults|Showing %{count} %{scope} for%{term_element} in your personal and project snippets"
+msgid "SearchResults|Showing %{count} %{scope} for %{term_element} in your personal and project snippets"
msgstr ""
-msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}"
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element}"
msgstr ""
-msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element} in your personal and project snippets"
+msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element} in your personal and project snippets"
msgstr ""
msgid "SearchResults|code result"
@@ -31274,6 +31280,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 +35159,7 @@ msgstr ""
msgid "TimeTrackingEstimated|Est"
msgstr ""
-msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
+msgid "TimeTracking|%{spentStart}Spent: %{spentEnd}"
msgstr ""
msgid "TimeTracking|Estimated:"
@@ -35388,6 +35397,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 +35505,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 ""
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index b4019643baf..4959003d788 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -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
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 04bacbe14e7..325f62f6028 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -139,6 +139,8 @@ FactoryBot.define do
end
factory :omniauth_user do
+ password_automatically_set { true }
+
transient do
extern_uid { '123456' }
provider { 'ldapmain' }
diff --git a/spec/features/admin/admin_mode/login_spec.rb b/spec/features/admin/admin_mode/login_spec.rb
index 5b2dfdb2941..c8ee6c14499 100644
--- a/spec/features/admin/admin_mode/login_spec.rb
+++ b/spec/features/admin/admin_mode/login_spec.rb
@@ -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
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index af085b63155..afb6fa26138 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -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
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index afd750d02eb..2c88860aef2 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -874,7 +874,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
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index d25e1b9ca4b..749e319f9c7 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -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
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
index cf554717127..2719e917a9b 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/__snapshots__/utils_spec.js.snap
@@ -100,6 +100,12 @@ Array [
"variable": 30,
},
Object {
+ "default": false,
+ "key": "SIXTY_DAYS",
+ "label": "60 days",
+ "variable": 60,
+ },
+ Object {
"default": true,
"key": "NINETY_DAYS",
"label": "90 days",
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
index 1009db46401..9938357ed24 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -44,7 +44,7 @@ exports[`Settings Form Keep Regex matches snapshot 1`] = `
exports[`Settings Form OlderThan matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
- formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
+ formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
value="FOURTEEN_DAYS"
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
new file mode 100644
index 00000000000..b722ac1e97b
--- /dev/null
+++ b/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
@@ -0,0 +1,92 @@
+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);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
index 8f6fe3cd37a..63df63a9b00 100644
--- a/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
+++ b/spec/frontend/vue_mr_widget/components/extensions/index_spec.js
@@ -1,4 +1,7 @@
-import { registerExtension, extensions } from '~/vue_merge_request_widget/components/extensions';
+import {
+ registerExtension,
+ registeredExtensions,
+} from '~/vue_merge_request_widget/components/extensions';
import ExtensionBase from '~/vue_merge_request_widget/components/extensions/base.vue';
describe('MR widget extension registering', () => {
@@ -14,7 +17,7 @@ describe('MR widget extension registering', () => {
},
});
- expect(extensions[0]).toEqual(
+ expect(registeredExtensions.extensions[0]).toEqual(
expect.objectContaining({
extends: ExtensionBase,
name: 'Test',
diff --git a/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js b/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js
new file mode 100644
index 00000000000..84fd3c7eb21
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/extensions/status_icon_spec.js
@@ -0,0 +1,36 @@
+import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = shallowMount(StatusIcon, {
+ propsData,
+ });
+}
+
+describe('MR widget extensions status icon', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders loading icon', () => {
+ factory({ name: 'test', isLoading: true, iconName: 'status-failed' });
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('renders status icon', () => {
+ factory({ name: 'test', isLoading: false, iconName: 'status-failed' });
+
+ expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlIcon).props('name')).toBe('status-failed');
+ });
+
+ it('sets aria-label for status icon', () => {
+ factory({ name: 'test', isLoading: false, iconName: 'status-failed' });
+
+ expect(wrapper.findComponent(GlIcon).props('ariaLabel')).toBe('Failed test');
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index c50cf7cb076..24d2b39ce87 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,13 +1,16 @@
+import { GlBadge, GlLink, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
@@ -15,6 +18,7 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import mockData from './mock_data';
+import testExtension from './test_extension';
jest.mock('~/smart_interval');
@@ -879,4 +883,46 @@ describe('MrWidgetOptions', () => {
});
});
});
+
+ describe('mock extension', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders collapsed data', async () => {
+ registerExtension(testExtension);
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Test extension summary count: 1');
+ });
+
+ it('renders full data', async () => {
+ registerExtension(testExtension);
+
+ await waitForPromises();
+
+ wrapper
+ .find('[data-testid="widget-extension"] [data-testid="toggle-button"]')
+ .trigger('click');
+
+ await Vue.nextTick();
+
+ const collapsedSection = wrapper.find('[data-testid="widget-extension-collapsed-section"]');
+ expect(collapsedSection.exists()).toBe(true);
+ expect(collapsedSection.text()).toContain('Hello world');
+
+ // Renders icon in the row
+ expect(collapsedSection.find(GlIcon).exists()).toBe(true);
+ expect(collapsedSection.find(GlIcon).props('name')).toBe('status_failed_borderless');
+
+ // Renders badge in the row
+ expect(collapsedSection.find(GlBadge).exists()).toBe(true);
+ expect(collapsedSection.find(GlBadge).text()).toBe('Closed');
+
+ // Renders a link in the row
+ expect(collapsedSection.find(GlLink).exists()).toBe(true);
+ expect(collapsedSection.find(GlLink).text()).toBe('GitLab.com');
+ });
+ });
});
diff --git a/spec/frontend/vue_mr_widget/test_extension.js b/spec/frontend/vue_mr_widget/test_extension.js
new file mode 100644
index 00000000000..7ea42e33103
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/test_extension.js
@@ -0,0 +1,36 @@
+export default {
+ name: 'WidgetTestExtension',
+ props: ['targetProjectFullPath'],
+ computed: {
+ summary({ count, targetProjectFullPath }) {
+ return `Test extension summary count: ${count} & ${targetProjectFullPath}`;
+ },
+ statusIcon({ count }) {
+ return count > 0 ? 'warning' : 'success';
+ },
+ },
+ methods: {
+ fetchCollapsedData({ targetProjectFullPath }) {
+ return Promise.resolve({ targetProjectFullPath, count: 1 });
+ },
+ fetchFullData() {
+ return Promise.resolve([
+ {
+ id: 1,
+ text: 'Hello world',
+ icon: {
+ name: 'status_failed_borderless',
+ class: 'text-danger',
+ },
+ badge: {
+ text: 'Closed',
+ },
+ link: {
+ href: 'https://gitlab.com',
+ text: 'GitLab.com',
+ },
+ },
+ ]);
+ },
+ },
+};
diff --git a/spec/graphql/mutations/clusters/agent_tokens/create_spec.rb b/spec/graphql/mutations/clusters/agent_tokens/create_spec.rb
new file mode 100644
index 00000000000..fc025c8e3d3
--- /dev/null
+++ b/spec/graphql/mutations/clusters/agent_tokens/create_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Clusters::AgentTokens::Create do
+ subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
+
+ let_it_be(:cluster_agent) { create(:cluster_agent) }
+ let_it_be(:user) { create(:user) }
+
+ let(:context) do
+ GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: { current_user: user },
+ object: nil
+ )
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:create_cluster) }
+
+ describe '#resolve' do
+ let(:description) { 'new token!' }
+ let(:name) { 'new name' }
+
+ subject { mutation.resolve(cluster_agent_id: cluster_agent.to_global_id, description: description, name: name) }
+
+ context 'without token permissions' do
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ cluster_agent.project.add_maintainer(user)
+ end
+
+ it 'creates a new token', :aggregate_failures do
+ expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
+ expect(subject[:errors]).to eq([])
+ end
+
+ it 'returns token information', :aggregate_failures do
+ token = subject[:token]
+
+ expect(subject[:secret]).not_to be_nil
+ expect(token.created_by_user).to eq(user)
+ expect(token.description).to eq(description)
+ expect(token.name).to eq(name)
+ end
+
+ context 'invalid params' do
+ subject { mutation.resolve(cluster_agent_id: cluster_agent.id) }
+
+ it 'generates an error message when id invalid', :aggregate_failures do
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb b/spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb
new file mode 100644
index 00000000000..5cdbc0f6d72
--- /dev/null
+++ b/spec/graphql/mutations/clusters/agent_tokens/delete_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Clusters::AgentTokens::Delete do
+ let(:token) { create(:cluster_agent_token) }
+ let(:user) { create(:user) }
+
+ let(:mutation) do
+ described_class.new(
+ object: double,
+ context: { current_user: user },
+ field: double
+ )
+ end
+
+ it { expect(described_class.graphql_name).to eq('ClusterAgentTokenDelete') }
+ it { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+
+ describe '#resolve' do
+ let(:global_id) { token.to_global_id }
+
+ subject { mutation.resolve(id: global_id) }
+
+ context 'without user permissions' do
+ it 'fails to delete the cluster agent', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect { token.reload }.not_to raise_error
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ token.agent.project.add_maintainer(user)
+ end
+
+ it 'deletes a cluster agent', :aggregate_failures do
+ expect { subject }.to change { ::Clusters::AgentToken.count }.by(-1)
+ expect { token.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid params' do
+ let(:global_id) { token.id }
+
+ it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ expect { token.reload }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/clusters/agents/create_spec.rb b/spec/graphql/mutations/clusters/agents/create_spec.rb
new file mode 100644
index 00000000000..c80b6f6cdad
--- /dev/null
+++ b/spec/graphql/mutations/clusters/agents/create_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Clusters::Agents::Create do
+ subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+ let(:context) do
+ GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: { current_user: user },
+ object: nil
+ )
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:create_cluster) }
+
+ describe '#resolve' do
+ subject { mutation.resolve(project_path: project.full_path, name: 'test-agent') }
+
+ context 'without project permissions' do
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates a new clusters_agent', :aggregate_failures do
+ expect { subject }.to change { ::Clusters::Agent.count }.by(1)
+ expect(subject[:cluster_agent].name).to eq('test-agent')
+ expect(subject[:errors]).to eq([])
+ end
+
+ context 'invalid params' do
+ subject { mutation.resolve(project_path: project.full_path, name: '@bad_name!') }
+
+ it 'generates an error message when name is invalid', :aggregate_failures do
+ expect(subject[:clusters_agent]).to be_nil
+ expect(subject[:errors]).to eq(["Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/clusters/agents/delete_spec.rb b/spec/graphql/mutations/clusters/agents/delete_spec.rb
new file mode 100644
index 00000000000..0aabf53391a
--- /dev/null
+++ b/spec/graphql/mutations/clusters/agents/delete_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Clusters::Agents::Delete do
+ subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
+
+ let(:cluster_agent) { create(:cluster_agent) }
+ let(:project) { cluster_agent.project }
+ let(:user) { create(:user) }
+ let(:context) do
+ GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: { current_user: user },
+ object: nil
+ )
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:admin_cluster) }
+
+ describe '#resolve' do
+ subject { mutation.resolve(id: cluster_agent.to_global_id) }
+
+ context 'without user permissions' do
+ it 'fails to delete the cluster agent', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect { cluster_agent.reload }.not_to raise_error
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'deletes a cluster agent', :aggregate_failures do
+ expect { subject }.to change { ::Clusters::Agent.count }.by(-1)
+ expect { cluster_agent.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid params' do
+ subject { mutation.resolve(id: cluster_agent.id) }
+
+ it 'raises an error if the cluster agent id is invalid', :aggregate_failures do
+ expect { subject }.to raise_error(::GraphQL::CoercionError)
+ expect { cluster_agent.reload }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb b/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb
index 72ab605f2e6..1989b87a28f 100644
--- a/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb
+++ b/spec/graphql/types/container_expiration_policy_older_than_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerExpirationPolicyOlderThanEnum'] do
- let_it_be(:expected_values) { %w[SEVEN_DAYS FOURTEEN_DAYS THIRTY_DAYS NINETY_DAYS] }
+ let_it_be(:expected_values) { %w[SEVEN_DAYS FOURTEEN_DAYS THIRTY_DAYS SIXTY_DAYS NINETY_DAYS] }
it_behaves_like 'exposing container expiration policy option', :older_than
end
diff --git a/spec/helpers/container_expiration_policies_helper_spec.rb b/spec/helpers/container_expiration_policies_helper_spec.rb
index 7ad3804e3a9..acb6b017d2c 100644
--- a/spec/helpers/container_expiration_policies_helper_spec.rb
+++ b/spec/helpers/container_expiration_policies_helper_spec.rb
@@ -40,6 +40,7 @@ RSpec.describe ContainerExpirationPoliciesHelper do
{ key: '7d', label: '7 days until tags are automatically removed' },
{ key: '14d', label: '14 days until tags are automatically removed' },
{ key: '30d', label: '30 days until tags are automatically removed' },
+ { key: '60d', label: '60 days until tags are automatically removed' },
{ key: '90d', label: '90 days until tags are automatically removed', default: true }
]
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 7b2334ab79e..9e870658870 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -248,13 +248,13 @@ RSpec.describe SearchHelper do
it 'uses the correct singular label' do
collection = Kaminari.paginate_array([:foo]).page(1).per(10)
- expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for<span>&nbsp;<code>foo</code>&nbsp;</span>")
+ expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for <span>&nbsp;<code>foo</code>&nbsp;</span>")
end
it 'uses the correct plural label' do
collection = Kaminari.paginate_array([:foo] * 23).page(1).per(10)
- expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for<span>&nbsp;<code>foo</code>&nbsp;</span>")
+ expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for <span>&nbsp;<code>foo</code>&nbsp;</span>")
end
end
diff --git a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
index 7c24cd0a5db..91479d23f43 100644
--- a/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/diff_note_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/representation/issue_spec.rb b/spec/lib/gitlab/github_import/representation/issue_spec.rb
index 3d306a4a3a3..f3052efea70 100644
--- a/spec/lib/gitlab/github_import/representation/issue_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/issue_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb
new file mode 100644
index 00000000000..b59ea513436
--- /dev/null
+++ b/spec/lib/gitlab/github_import/representation/lfs_object_spec.rb
@@ -0,0 +1,17 @@
+# 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
diff --git a/spec/lib/gitlab/github_import/representation/note_spec.rb b/spec/lib/gitlab/github_import/representation/note_spec.rb
index 112bb7eb908..97addcc1c98 100644
--- a/spec/lib/gitlab/github_import/representation/note_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/note_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
index cad9b13774e..f812fd85fbc 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_review_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
index 27a82951b01..925dba5b5a7 100644
--- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
+++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/pagination/keyset/iterator_spec.rb b/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
index c6d34d43264..a0a2c0d6a63 100644
--- a/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/iterator_spec.rb
@@ -32,8 +32,11 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
])
end
+ let(:iterator_params) { nil }
let(:scope) { project.issues.reorder(custom_reorder) }
+ subject(:iterator) { described_class.new(**iterator_params) }
+
shared_examples 'iterator examples' do
describe '.each_batch' do
it 'yields an ActiveRecord::Relation when a block is given' do
@@ -56,6 +59,30 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
expect(count).to eq(9)
end
+ it 'continues after the cursor' do
+ loaded_records = []
+ cursor = nil
+
+ # stopping the iterator after the first batch and storing the cursor
+ iterator.each_batch(of: 2) do |relation| # rubocop: disable Lint/UnreachableLoop
+ loaded_records.concat(relation.to_a)
+ record = loaded_records.last
+
+ cursor = custom_reorder.cursor_attributes_for_node(record)
+ break
+ end
+
+ expect(loaded_records).to eq(project.issues.order(custom_reorder).take(2))
+
+ # continuing the iteration
+ new_iterator = described_class.new(**iterator_params.merge(cursor: cursor))
+ new_iterator.each_batch(of: 2) do |relation|
+ loaded_records.concat(relation.to_a)
+ end
+
+ expect(loaded_records).to eq(project.issues.order(custom_reorder))
+ end
+
it 'allows updating of the yielded relations' do
time = Time.current
@@ -131,13 +158,13 @@ RSpec.describe Gitlab::Pagination::Keyset::Iterator do
end
context 'when use_union_optimization is used' do
- subject(:iterator) { described_class.new(scope: scope, use_union_optimization: true) }
+ let(:iterator_params) { { scope: scope, use_union_optimization: true } }
include_examples 'iterator examples'
end
context 'when use_union_optimization is not used' do
- subject(:iterator) { described_class.new(scope: scope, use_union_optimization: false) }
+ let(:iterator_params) { { scope: scope, use_union_optimization: false } }
include_examples 'iterator examples'
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a70b68a181f..5bd2d6fda4b 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -662,13 +662,13 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.data[:counts] }
it 'gathers usage data' do
- expect(subject[:projects_with_expiration_policy_enabled]).to eq 18
+ expect(subject[:projects_with_expiration_policy_enabled]).to eq 19
expect(subject[:projects_with_expiration_policy_disabled]).to eq 5
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 12
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 13
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
@@ -676,9 +676,10 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_60d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 14
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 14
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 15
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 8864994c15f..bfe06c44839 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -432,9 +432,9 @@ RSpec.describe Namespace do
end
describe '.search' do
- let_it_be(:first_group) { build(:group, name: 'my first namespace', path: 'old-path').tap(&:save!) }
- let_it_be(:parent_group) { build(:group, name: 'my parent namespace', path: 'parent-path').tap(&:save!) }
- let_it_be(:second_group) { build(:group, name: 'my second namespace', path: 'new-path', parent: parent_group).tap(&:save!) }
+ let_it_be(:first_group) { create(:group, name: 'my first namespace', path: 'old-path') }
+ let_it_be(:parent_group) { create(:group, name: 'my parent namespace', path: 'parent-path') }
+ let_it_be(:second_group) { create(:group, name: 'my second namespace', path: 'new-path', parent: parent_group) }
let_it_be(:project_with_same_path) { create(:project, id: second_group.id, path: first_group.path) }
it 'returns namespaces with a matching name' do
diff --git a/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
new file mode 100644
index 00000000000..aac8eb22771
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/clusters/agent_tokens/agent_tokens/create_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a new cluster agent token' do
+ include GraphqlHelpers
+
+ let_it_be(:cluster_agent) { create(:cluster_agent) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:description) { 'create token' }
+ let(:name) { 'token name' }
+ let(:mutation) do
+ graphql_mutation(
+ :cluster_agent_token_create,
+ { cluster_agent_id: cluster_agent.to_global_id.to_s, description: description, name: name }
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:cluster_agent_token_create)
+ end
+
+ context 'without user permissions' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ["The resource that you are attempting to access does not exist "\
+ "or you don't have permission to perform this action"]
+
+ it 'does not create a token' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::AgentToken, :count)
+ end
+ end
+
+ context 'with project permissions' do
+ before do
+ cluster_agent.project.add_maintainer(current_user)
+ end
+
+ it 'creates a new token', :aggregate_failures do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::AgentToken.count }.by(1)
+ expect(mutation_response['errors']).to eq([])
+ end
+
+ it 'returns token information', :aggregate_failures do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['secret']).not_to be_nil
+ expect(mutation_response.dig('token', 'description')).to eq(description)
+ expect(mutation_response.dig('token', 'name')).to eq(name)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
new file mode 100644
index 00000000000..c2ef2362d66
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/clusters/agents/create_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Create a new cluster agent' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:project_name) { 'agent-test' }
+ let(:current_user) { create(:user) }
+
+ let(:mutation) do
+ graphql_mutation(
+ :create_cluster_agent,
+ { project_path: project.full_path, name: project_name }
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:create_cluster_agent)
+ end
+
+ context 'without project permissions' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create cluster agent' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(Clusters::Agent, :count)
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'creates a new cluster agent', :aggregate_failures do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::Agent.count }.by(1)
+ expect(mutation_response.dig('clusterAgent', 'name')).to eq(project_name)
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
new file mode 100644
index 00000000000..5f6822223ca
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/clusters/agents/delete_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Delete a cluster agent' do
+ include GraphqlHelpers
+
+ let(:cluster_agent) { create(:cluster_agent) }
+ let(:project) { cluster_agent.project }
+ let(:current_user) { create(:user) }
+
+ let(:mutation) do
+ graphql_mutation(
+ :cluster_agent_delete,
+ { id: cluster_agent.to_global_id.uri }
+ )
+ end
+
+ def mutation_response
+ graphql_mutation_response(:cluster_agent_delete)
+ end
+
+ context 'without project permissions' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['The resource that you are attempting to access does not exist '\
+ 'or you don\'t have permission to perform this action']
+
+ it 'does not delete cluster agent' do
+ expect { cluster_agent.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_maintainer(current_user)
+ end
+
+ it 'deletes a cluster agent', :aggregate_failures do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.to change { Clusters::Agent.count }.by(-1)
+ expect(mutation_response['errors']).to eq([])
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/cluster_agents_spec.rb b/spec/requests/api/graphql/project/cluster_agents_spec.rb
new file mode 100644
index 00000000000..dc7254dd552
--- /dev/null
+++ b/spec/requests/api/graphql/project/cluster_agents_spec.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Project.cluster_agents' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:current_user) { create(:user, maintainer_projects: [project]) }
+ let_it_be(:agents) { create_list(:cluster_agent, 5, project: project) }
+
+ let(:first) { var('Int') }
+ let(:cluster_agents_fields) { nil }
+ let(:project_fields) do
+ query_nodes(:cluster_agents, cluster_agents_fields, args: { first: first }, max_depth: 3)
+ end
+
+ let(:query) do
+ args = { full_path: project.full_path }
+
+ with_signature([first], graphql_query_for(:project, args, project_fields))
+ end
+
+ before do
+ allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: []))
+ end
+
+ it 'can retrieve cluster agents' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(:project, :cluster_agents, :nodes)).to match_array(
+ agents.map { |agent| a_hash_including('id' => global_id_of(agent)) }
+ )
+ end
+
+ context 'selecting page info' do
+ let(:project_fields) do
+ query_nodes(:cluster_agents, args: { first: first }, include_pagination_info: true)
+ end
+
+ it 'can paginate cluster agents' do
+ post_graphql(query, current_user: current_user, variables: first.with(2))
+
+ expect(graphql_data_at(:project, :cluster_agents, :page_info)).to include(
+ 'hasNextPage' => be_truthy,
+ 'hasPreviousPage' => be_falsey
+ )
+ expect(graphql_data_at(:project, :cluster_agents, :nodes)).to have_attributes(size: 2)
+ end
+ end
+
+ context 'selecting tokens' do
+ let_it_be(:token_1) { create(:cluster_agent_token, agent: agents.second) }
+ let_it_be(:token_2) { create(:cluster_agent_token, agent: agents.second, last_used_at: 3.days.ago) }
+ let_it_be(:token_3) { create(:cluster_agent_token, agent: agents.second, last_used_at: 2.days.ago) }
+
+ let(:cluster_agents_fields) { [:id, query_nodes(:tokens, of: 'ClusterAgentToken')] }
+
+ it 'can select tokens in last_used_at order' do
+ post_graphql(query, current_user: current_user)
+
+ tokens = graphql_data_at(:project, :cluster_agents, :nodes, :tokens, :nodes)
+
+ expect(tokens).to match([
+ a_hash_including('id' => global_id_of(token_3)),
+ a_hash_including('id' => global_id_of(token_2)),
+ a_hash_including('id' => global_id_of(token_1))
+ ])
+ end
+
+ it 'does not suffer from N+1 performance issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect do
+ post_graphql(query, current_user: current_user)
+ end.to issue_same_number_of_queries_as { post_graphql(query, current_user: current_user, variables: [first.with(1)]) }
+ end
+ end
+
+ context 'selecting connections' do
+ let(:agent_meta) { double(version: '1', commit_id: 'abc', pod_namespace: 'namespace', pod_name: 'pod') }
+ let(:connected_agent) { double(agent_id: agents.first.id, connected_at: 123456, connection_id: 1, agent_meta: agent_meta) }
+
+ let(:metadata_fields) { query_graphql_field(:metadata, {}, [:version, :commit, :pod_namespace, :pod_name], 'AgentMetadata') }
+ let(:cluster_agents_fields) { [:id, query_nodes(:connections, [:connection_id, :connected_at, metadata_fields])] }
+
+ before do
+ allow(Gitlab::Kas::Client).to receive(:new).and_return(double(get_connected_agents: [connected_agent]))
+ end
+
+ it 'can retrieve connections and agent metadata' do
+ post_graphql(query, current_user: current_user)
+
+ connection = graphql_data_at(:project, :cluster_agents, :nodes, :connections, :nodes).first
+
+ expect(connection).to include({
+ 'connectionId' => connected_agent.connection_id.to_s,
+ 'connectedAt' => Time.at(connected_agent.connected_at),
+ 'metadata' => {
+ 'version' => agent_meta.version,
+ 'commit' => agent_meta.commit_id,
+ 'podNamespace' => agent_meta.pod_namespace,
+ 'podName' => agent_meta.pod_name
+ }
+ })
+ end
+ end
+end
diff --git a/spec/services/clusters/agent_tokens/create_service_spec.rb b/spec/services/clusters/agent_tokens/create_service_spec.rb
new file mode 100644
index 00000000000..92629af06c8
--- /dev/null
+++ b/spec/services/clusters/agent_tokens/create_service_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::AgentTokens::CreateService do
+ subject(:service) { described_class.new(container: project, current_user: user, params: params) }
+
+ let_it_be(:user) { create(:user) }
+
+ let(:cluster_agent) { create(:cluster_agent) }
+ let(:project) { cluster_agent.project }
+ let(:params) { { agent_id: cluster_agent.id, description: 'token description', name: 'token name' } }
+
+ describe '#execute' do
+ subject { service.execute }
+
+ it 'does not create a new token due to user permissions' do
+ expect { subject }.not_to change(::Clusters::AgentToken, :count)
+ end
+
+ it 'returns permission errors', :aggregate_failures do
+ expect(subject.status).to eq(:error)
+ expect(subject.message).to eq('User has insufficient permissions to create a token for this project')
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates a new token' do
+ expect { subject }.to change { ::Clusters::AgentToken.count }.by(1)
+ end
+
+ it 'returns success status', :aggregate_failures do
+ expect(subject.status).to eq(:success)
+ expect(subject.message).to be_nil
+ end
+
+ it 'returns token information', :aggregate_failures do
+ token = subject.payload[:token]
+
+ expect(subject.payload[:secret]).not_to be_nil
+
+ expect(token.created_by_user).to eq(user)
+ expect(token.description).to eq(params[:description])
+ expect(token.name).to eq(params[:name])
+ end
+
+ context 'when params are invalid' do
+ let(:params) { { agent_id: 'bad_id' } }
+
+ it 'does not create a new token' do
+ expect { subject }.not_to change(::Clusters::AgentToken, :count)
+ end
+
+ it 'returns validation errors', :aggregate_failures do
+ expect(subject.status).to eq(:error)
+ expect(subject.message).to eq(["Agent must exist", "Name can't be blank"])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/create_service_spec.rb b/spec/services/clusters/agents/create_service_spec.rb
new file mode 100644
index 00000000000..2b3bbcae13c
--- /dev/null
+++ b/spec/services/clusters/agents/create_service_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::CreateService do
+ subject(:service) { described_class.new(project, user) }
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ context 'without user permissions' do
+ it 'returns errors when user does not have permissions' do
+ expect(service.execute(name: 'missing-permissions')).to eq({
+ status: :error,
+ message: 'You have insufficient permissions to create a cluster agent for this project'
+ })
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'creates a new clusters_agent' do
+ expect { service.execute(name: 'with-user') }.to change { ::Clusters::Agent.count }.by(1)
+ end
+
+ it 'returns success status', :aggregate_failures do
+ result = service.execute(name: 'success')
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:message]).to be_nil
+ end
+
+ it 'returns agent values', :aggregate_failures do
+ new_agent = service.execute(name: 'new-agent')[:cluster_agent]
+
+ expect(new_agent.name).to eq('new-agent')
+ expect(new_agent.created_by_user).to eq(user)
+ end
+
+ it 'generates an error message when name is invalid' do
+ expect(service.execute(name: '@bad_agent_name!')).to eq({
+ status: :error,
+ message: ["Name can contain only lowercase letters, digits, and '-', but cannot start or end with '-'"]
+ })
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/agents/delete_service_spec.rb b/spec/services/clusters/agents/delete_service_spec.rb
new file mode 100644
index 00000000000..1d6bc9618dd
--- /dev/null
+++ b/spec/services/clusters/agents/delete_service_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Clusters::Agents::DeleteService do
+ subject(:service) { described_class.new(container: project, current_user: user) }
+
+ let(:cluster_agent) { create(:cluster_agent) }
+ let(:project) { cluster_agent.project }
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ context 'without user permissions' do
+ it 'fails to delete when the user has no permissions', :aggregate_failures do
+ response = service.execute(cluster_agent)
+
+ expect(response.status).to eq(:error)
+ expect(response.message).to eq('You have insufficient permissions to delete this cluster agent')
+
+ expect { cluster_agent.reload }.not_to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with user permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'deletes a cluster agent', :aggregate_failures do
+ expect { service.execute(cluster_agent) }.to change { ::Clusters::Agent.count }.by(-1)
+ expect { cluster_agent.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index b30b7e6eb56..3244db4c1fb 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -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
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index b1a9aade043..e772fd237fa 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -112,6 +112,7 @@ module UsageDataHelpers
projects_with_expiration_policy_enabled_with_older_than_set_to_7d
projects_with_expiration_policy_enabled_with_older_than_set_to_14d
projects_with_expiration_policy_enabled_with_older_than_set_to_30d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_60d
projects_with_expiration_policy_enabled_with_older_than_set_to_90d
projects_with_expiration_policy_enabled_with_cadence_set_to_1d
projects_with_expiration_policy_enabled_with_cadence_set_to_7d
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index c1ac5ffebe8..b5252294b27 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -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