summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 12:09:05 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-11 12:09:05 +0000
commit28e90894e1e6f17320f5b1d2fff6fe736bf65dff (patch)
tree21d63bf124b6064eb1650acc3e2aabe6dbc99f58
parenta48957b317edf23b1bcfc6df0c098a824eae86f4 (diff)
downloadgitlab-ce-28e90894e1e6f17320f5b1d2fff6fe736bf65dff.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/package-and-test/rules.gitlab-ci.yml19
-rw-r--r--.gitlab/ci/review-apps/rules.gitlab-ci.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue28
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js8
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue68
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue108
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue1
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue39
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql4
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql2
-rw-r--r--app/assets/stylesheets/page_bundles/issues_list.scss5
-rw-r--r--app/channels/awareness_channel.rb85
-rw-r--r--app/graphql/types/permission_types/work_item.rb2
-rw-r--r--app/helpers/auth_helper.rb1
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/models/awareness_session.rb245
-rw-r--r--app/models/concerns/awareness.rb41
-rw-r--r--app/models/user.rb1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--config/feature_flags/development/project_export_as_ndjson.yml8
-rw-r--r--config/feature_flags/development/project_import_ndjson.yml8
-rw-r--r--doc/administration/auth/index.md4
-rw-r--r--doc/administration/auth/ldap/ldap_synchronization.md100
-rw-r--r--doc/administration/auth/test_oidc_oauth.md57
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/users.md41
-rw-r--r--lib/gitlab/import_export/group/relation_tree_restorer.rb10
-rw-r--r--lib/gitlab/import_export/json/legacy_reader.rb123
-rw-r--r--lib/gitlab/import_export/json/legacy_writer.rb88
-rw-r--r--lib/gitlab/import_export/json/ndjson_reader.rb10
-rw-r--r--lib/gitlab/import_export/project/sample/relation_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/project/tree_restorer.rb21
-rw-r--r--lib/gitlab/import_export/project/tree_saver.rb11
-rw-r--r--locale/gitlab.pot23
-rw-r--r--qa/Dockerfile7
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/page/main/login.rb6
-rw-r--r--qa/qa/runtime/env.rb20
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/oauth_login_with_github_spec.rb18
-rw-r--r--qa/qa/vendor/github/page/base.rb14
-rw-r--r--qa/qa/vendor/github/page/login.rb36
-rw-r--r--qa/qa/vendor/one_password/cli.rb59
-rw-r--r--qa/tasks/ci.rake2
-rwxr-xr-xscripts/generate-e2e-pipeline1
-rw-r--r--scripts/utils.sh2
-rw-r--r--spec/channels/awareness_channel_spec.rb81
-rw-r--r--spec/features/admin/admin_runners_spec.rb11
-rw-r--r--spec/features/nav/top_nav_responsive_spec.rb4
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb61
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin3176 -> 4799 bytes
-rw-r--r--spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gzbin4603 -> 5288 bytes
-rw-r--r--spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gzbin3758 -> 4950 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/tree/project.json15
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/tree/project/issues.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/tree/project/project_members.ndjson2
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_instructions_spec.js40
-rw-r--r--spec/frontend/issues/issue_spec.js8
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js11
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js92
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js8
-rw-r--r--spec/graphql/types/permission_types/work_item_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb59
-rw-r--r--spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/import_export_equivalence_spec.rb67
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb32
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb35
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/shared_example.rb102
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_writer_spec.rb102
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb51
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb194
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb30
-rw-r--r--spec/models/awareness_session_spec.rb163
-rw-r--r--spec/models/concerns/awareness_spec.rb39
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb22
-rw-r--r--spec/support/import_export/common_util.rb25
-rw-r--r--spec/support/rspec_order_todo.yml3
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb8
-rw-r--r--spec/views/profiles/preferences/show.html.haml_spec.rb4
-rw-r--r--vendor/project_templates/android.tar.gzbin133429 -> 131955 bytes
-rw-r--r--vendor/project_templates/gitbook.tar.gzbin13068 -> 12129 bytes
-rw-r--r--vendor/project_templates/gomicro.tar.gzbin5388 -> 4495 bytes
-rw-r--r--vendor/project_templates/hexo.tar.gzbin547436 -> 546434 bytes
-rw-r--r--vendor/project_templates/hipaa_audit_protocol.tar.gzbin114488 -> 1523 bytes
-rw-r--r--vendor/project_templates/iosswift.tar.gzbin2422200 -> 2421857 bytes
-rw-r--r--vendor/project_templates/jekyll.tar.gzbin60465 -> 59551 bytes
-rw-r--r--vendor/project_templates/nfgitbook.tar.gzbin122562 -> 121618 bytes
-rw-r--r--vendor/project_templates/nfhexo.tar.gzbin655028 -> 654150 bytes
-rw-r--r--vendor/project_templates/nfhugo.tar.gzbin1159251 -> 1158440 bytes
-rw-r--r--vendor/project_templates/nfjekyll.tar.gzbin132262 -> 131307 bytes
-rw-r--r--vendor/project_templates/nfplainhtml.tar.gzbin122392 -> 121444 bytes
-rw-r--r--vendor/project_templates/pelican.tar.gzbin43159 -> 42448 bytes
-rw-r--r--vendor/project_templates/plainhtml.tar.gzbin11389 -> 10445 bytes
-rw-r--r--vendor/project_templates/salesforcedx.tar.gzbin432083 -> 431023 bytes
-rw-r--r--vendor/project_templates/serverless_framework.tar.gzbin92193 -> 91058 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin49518 -> 48912 bytes
107 files changed, 994 insertions, 1718 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index e88df6f0574..7abcca3a891 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -455,6 +455,15 @@ group-saml:
- if: $QA_SUITES =~ /Test::Integration::GroupSAML/
- !reference [.rules:test:manual, rules]
+oauth:
+ extends: .qa
+ variables:
+ QA_SCENARIO: Test::Integration::OAuth
+ rules:
+ - !reference [.rules:test:qa-default-branch, rules]
+ - if: $QA_SUITES =~ /Test::Integration::OAuth/
+ - !reference [.rules:test:manual, rules]
+
instance-saml:
extends: .qa
variables:
diff --git a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml b/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
index 42bc5eeb7e0..640f5f53bfa 100644
--- a/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/rules.gitlab-ci.yml
@@ -10,16 +10,23 @@
.feature-flags-set: &feature-flags-set
if: $QA_FEATURE_FLAGS =~ /enabled|disabled/
-
# Manually trigger job on ff changes but with default ff state instead of inverted
.feature-flags-set-manual: &feature-flags-set-manual
<<: *feature-flags-set
when: manual
allow_failure: true
-# Run all tests when framework changes present, full suite execution is explicitly enabled or a feature flag file is removed
+# Run the job on master pipeline
+.default-branch: &default-branch
+ if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+
+# Run all tests when QA framework changes present, full suite execution is explicitly enabled or a feature flag file is removed
.qa-run-all-tests: &qa-run-all-tests
- if: $QA_FRAMEWORK_CHANGES == "true" || $QA_RUN_ALL_TESTS == "true" || $QA_FEATURE_FLAGS =~ /deleted/
+ if: $QA_FRAMEWORK_CHANGES == "true" || $QA_RUN_ALL_TESTS == "true" || $QA_RUN_ALL_E2E_LABEL == "true" || $QA_FEATURE_FLAGS =~ /deleted/
+
+# Run job when MR has pipeline:run-all-e2e label
+.qa-run-all-e2e-label: &qa-run-all-e2e-label
+ if: $QA_RUN_ALL_E2E_LABEL == "true"
# Process test results (notify failure to slack, create test session report, relate test failures)
.process-test-results: &process-test-results
@@ -122,6 +129,12 @@
- !reference [.rules:test:ee-only, rules]
- !reference [.rules:test:qa, rules]
+.rules:test:qa-default-branch:
+ rules:
+ - *qa-run-all-e2e-label
+ - *default-branch
+ - *feature-flags-set-manual
+
# ------------------------------------------
# Report
# ------------------------------------------
diff --git a/.gitlab/ci/review-apps/rules.gitlab-ci.yml b/.gitlab/ci/review-apps/rules.gitlab-ci.yml
index a3ae31cb14c..a4b667c6645 100644
--- a/.gitlab/ci/review-apps/rules.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/rules.gitlab-ci.yml
@@ -20,7 +20,7 @@
# Run all tests when framework changes present or explicitly enabled full suite execution
.qa-run-all-tests: &qa-run-all-tests
- if: $QA_FRAMEWORK_CHANGES == "true" || $QA_RUN_ALL_TESTS == "true"
+ if: $QA_FRAMEWORK_CHANGES == "true" || $QA_RUN_ALL_TESTS == "true" || $QA_RUN_ALL_E2E_LABEL == "true"
.default-branch: &default-branch
if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5f84ef38652..42a6ec90baf 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-13f08896b308c617c4a4ca585212069808422367
+0270b952e5bff22707247670520c695401715e25
diff --git a/Gemfile.lock b/Gemfile.lock
index 59813dd6c97..5d10f5f4655 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1958,4 +1958,4 @@ DEPENDENCIES
yajl-ruby (~> 1.4.3)
BUNDLED WITH
- 2.4.10
+ 2.4.11
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index 6e7c87b8515..69021dde0e9 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -70,7 +70,7 @@ export default {
captureException({ error, component: this.$options.name });
},
pollInterval() {
- if (this.runner?.status === STATUS_ONLINE) {
+ if (this.isRunnerOnline) {
// stop polling
return 0;
}
@@ -97,9 +97,6 @@ export default {
}
return s__('Runners|Register runner');
},
- status() {
- return this.runner?.status;
- },
tokenMessage() {
if (this.token) {
return s__(
@@ -122,15 +119,34 @@ export default {
runCommand() {
return runCommand({ platform: this.platform });
},
+ isRunnerOnline() {
+ return this.runner?.status === STATUS_ONLINE;
+ },
+ },
+ created() {
+ window.addEventListener('beforeunload', this.onBeforeunload);
+ },
+ destroyed() {
+ window.removeEventListener('beforeunload', this.onBeforeunload);
},
methods: {
toggleDrawer() {
this.$emit('toggleDrawer');
},
+ onBeforeunload(event) {
+ if (this.isRunnerOnline) {
+ return undefined;
+ }
+
+ const str = s__('Runners|You may lose access to the runner token if you leave this page.');
+ event.preventDefault();
+ // eslint-disable-next-line no-param-reassign
+ event.returnValue = str; // Chrome requires returnValue to be set
+ return str;
+ },
},
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
- STATUS_ONLINE,
I18N_REGISTRATION_SUCCESS,
};
</script>
@@ -225,7 +241,7 @@ export default {
</gl-sprintf>
</p>
</section>
- <section v-if="status == $options.STATUS_ONLINE">
+ <section v-if="isRunnerOnline">
<h2 class="gl-font-size-h2">🎉 {{ $options.I18N_REGISTRATION_SUCCESS }}</h2>
<p class="gl-pl-6">
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index f482fabc5f6..3e310f941ec 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -81,6 +81,14 @@ export const config = {
});
},
},
+ userPermissions: {
+ read(permission = {}) {
+ return {
+ ...permission,
+ setWorkItemMetadata: false,
+ };
+ },
+ },
},
},
MemberInterfaceConnection: {
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 34b7d95322e..d1c0e757a91 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -92,6 +92,6 @@ export default {
{{ $options.i18n.emptyHint }}
</div>
</gl-collapse>
- <hr class="gl-my-2" />
+ <hr class="gl-my-2 gl-mx-4" />
</section>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 2bb355736fd..3fdc5124111 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -134,7 +134,7 @@ export default {
<ul class="gl-p-0 gl-m-0">
<nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static />
</ul>
- <hr class="gl-my-2" />
+ <hr class="gl-my-2 gl-mx-4" />
</section>
<pinned-section
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 082c261977b..650fa798db6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -130,7 +130,7 @@ export default {
<span v-if="approvalLeftMessage">{{ message }}</span>
<span v-else class="gl-font-weight-bold">{{ message }}</span>
<user-avatar-list
- class="gl-display-inline-block gl-vertical-align-middle gl-pt-1"
+ class="gl-display-inline-flex gl-vertical-align-middle"
:img-size="24"
:items="approvers"
/>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 71cf85c75a7..6552a874c3a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -90,7 +90,7 @@ export default {
</script>
<template>
- <span ref="userAvatar" class="gl-display-inline-flex">
+ <span ref="userAvatar">
<gl-avatar
:class="{
lazy: lazy,
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index 8a05960869c..713c08e20e9 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -1,8 +1,8 @@
<script>
-import { GlAvatar, GlButton } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
+import { __ } from '~/locale';
import { clearDraft } from '~/lib/utils/autosave';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getWorkItemQuery } from '../../utils';
@@ -17,8 +17,6 @@ export default {
avatarUrl: window.gon.current_user_avatar_url,
},
components: {
- GlAvatar,
- GlButton,
WorkItemNoteSignedOut,
WorkItemCommentLocked,
WorkItemCommentForm,
@@ -75,11 +73,16 @@ export default {
required: false,
default: () => ({}),
},
+ isNewDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
workItem: {},
- isEditing: false,
+ isEditing: this.isNewDiscussion,
isSubmitting: false,
isSubmittingWithKeydown: false,
};
@@ -118,23 +121,9 @@ export default {
property: `type_${this.workItemType}`,
};
},
- isLockedOutOrSignedOut() {
- return !this.signedIn || !this.canUpdate;
- },
- lockedOutUserWarningInReplies() {
- return this.addPadding && this.isLockedOutOrSignedOut;
- },
- timelineEntryClass() {
- return {
- 'timeline-entry gl-mb-3 note note-wrapper note-comment': true,
- 'gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-p-5! gl-mx-n3 gl-mb-n2!': this
- .lockedOutUserWarningInReplies,
- };
- },
timelineEntryInnerClass() {
return {
- 'timeline-entry-inner': true,
- 'gl-pb-3': this.addPadding,
+ 'timeline-entry-inner': this.isNewDiscussion,
};
},
timelineContentClass() {
@@ -155,6 +144,18 @@ export default {
canUpdate() {
return this.workItem?.userPermissions?.updateWorkItem;
},
+ workItemState() {
+ return this.workItem?.state;
+ },
+ commentButtonText() {
+ return this.isNewDiscussion ? __('Comment') : __('Reply');
+ },
+ timelineEntryClass() {
+ return this.isNewDiscussion
+ ? 'timeline-entry note-form'
+ : // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix';
+ },
},
watch: {
autofocus: {
@@ -226,9 +227,13 @@ export default {
}
},
cancelEditing() {
- this.isEditing = false;
+ this.isEditing = this.isNewDiscussion;
this.$emit('cancelEditing');
},
+ showReplyForm() {
+ this.isEditing = true;
+ this.$emit('startReplying');
+ },
},
};
</script>
@@ -242,9 +247,6 @@ export default {
:is-project-archived="isProjectArchived"
/>
<div v-else :class="timelineEntryInnerClass">
- <div class="timeline-avatar gl-float-left">
- <gl-avatar :src="$options.constantOptions.avatarUrl" :size="32" class="gl-mr-3" />
- </div>
<div :class="timelineContentClass">
<div :class="parentClass">
<work-item-comment-form
@@ -253,17 +255,27 @@ export default {
:aria-label="__('Add a reply')"
:is-submitting="isSubmitting"
:autosave-key="autosaveKey"
+ :is-new-discussion="isNewDiscussion"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :work-item-state="workItemState"
+ :work-item-id="workItemId"
+ :autofocus="autofocus"
+ :comment-button-text="commentButtonText"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
/>
- <gl-button
+ <textarea
v-else
- class="gl-flex-grow-1 gl-justify-content-start! gl-text-secondary!"
- @click="isEditing = true"
- >{{ __('Add a reply') }}</gl-button
- >
+ ref="textarea"
+ rows="1"
+ class="reply-placeholder-text-field gl-font-regular!"
+ data-testid="note-reply-textarea"
+ :placeholder="__('Reply')"
+ :aria-label="__('Reply to comment')"
+ @focus="showReplyForm"
+ @click="showReplyForm"
+ ></textarea>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index 8390ae5b2e1..f9f24366725 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -1,10 +1,22 @@
<script>
import { GlButton } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
+import Tracking from '~/tracking';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ STATE_OPEN,
+ STATE_EVENT_REOPEN,
+ STATE_EVENT_CLOSE,
+ TRACKING_CATEGORY_SHOW,
+ i18n,
+} from '~/work_items/constants';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
export default {
constantOptions: {
@@ -14,8 +26,13 @@ export default {
GlButton,
MarkdownEditor,
},
+ mixins: [Tracking.mixin()],
inject: ['fullPath'],
props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
workItemType: {
type: String,
required: true,
@@ -52,13 +69,36 @@ export default {
required: false,
default: () => ({}),
},
+ isNewDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ workItemState: {
+ type: String,
+ required: false,
+ default: STATE_OPEN,
+ },
+ autofocus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
commentText: getDraft(this.autosaveKey) || this.initialValue || '',
+ updateInProgress: false,
};
},
computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_task_status',
+ property: `type_${this.workItemType}`,
+ };
+ },
formFieldProps() {
return {
'aria-label': this.ariaLabel,
@@ -67,6 +107,17 @@ export default {
name: 'work-item-add-or-edit-comment',
};
},
+ isWorkItemOpen() {
+ return this.workItemState === STATE_OPEN;
+ },
+ toggleWorkItemStateText() {
+ return this.isWorkItemOpen
+ ? sprintf(__('Close %{workItemType}'), { workItemType: this.workItemType.toLowerCase() })
+ : sprintf(__('Reopen %{workItemType}'), { workItemType: this.workItemType.toLowerCase() });
+ },
+ cancelButtonText() {
+ return this.isNewDiscussion ? this.toggleWorkItemStateText : __('Cancel');
+ },
},
methods: {
setCommentText(newText) {
@@ -99,13 +150,55 @@ export default {
this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
+ async toggleWorkItemState() {
+ const input = {
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
+ };
+
+ this.updateInProgress = true;
+
+ try {
+ this.track('updated_state');
+
+ const { mutation, variables } = getUpdateWorkItemMutation({
+ workItemParentId: this.workItemParentId,
+ input,
+ });
+
+ const { data } = await this.$apollo.mutate({
+ mutation,
+ variables,
+ });
+
+ const errors = data.workItemUpdate?.errors;
+
+ if (errors?.length) {
+ this.$emit('error', i18n.updateError);
+ }
+ } catch (error) {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ }
+
+ this.updateInProgress = false;
+ },
+ cancelButtonAction() {
+ if (this.isNewDiscussion) {
+ this.toggleWorkItemState();
+ } else {
+ this.cancelEditing();
+ }
+ },
},
};
</script>
<template>
- <div class="timeline-discussion-body">
- <div class="note-body">
+ <div class="timeline-discussion-body gl-overflow-visible!">
+ <div class="note-body gl-p-0! gl-overflow-visible!">
<form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
<markdown-editor
:value="commentText"
@@ -113,11 +206,12 @@ export default {
:markdown-docs-path="$options.constantOptions.markdownDocsPath"
:autocomplete-data-sources="autocompleteDataSources"
:form-field-props="formFieldProps"
+ :add-spacing-classes="false"
data-testid="work-item-add-comment"
class="gl-mb-3"
- autofocus
use-bottom-toolbar
supports-quick-actions
+ :autofocus="autofocus"
@input="setCommentText"
@keydown.meta.enter="$emit('submitForm', commentText)"
@keydown.ctrl.enter="$emit('submitForm', commentText)"
@@ -127,6 +221,7 @@ export default {
category="primary"
variant="confirm"
data-testid="confirm-button"
+ :disabled="!commentText.length"
:loading="isSubmitting"
@click="$emit('submitForm', commentText)"
>{{ commentButtonText }}
@@ -135,8 +230,9 @@ export default {
data-testid="cancel-button"
category="primary"
class="gl-ml-3"
- @click="cancelEditing"
- >{{ __('Cancel') }}
+ :loading="updateInProgress"
+ @click="cancelButtonAction"
+ >{{ cancelButtonText }}
</gl-button>
</form>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index 6cf15ba50ec..21fc8f99366 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -248,6 +248,7 @@ export default {
:add-padding="true"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ @startReplying="showReplyForm"
@cancelEditing="hideReplyForm"
@replied="onReplied"
@replying="onReplying"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
index 8b25d305398..5ccc5526ce8 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -310,6 +310,9 @@ export default {
:comment-button-text="__('Save comment')"
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
+ :work-item-id="workItemId"
+ :autofocus="isEditing"
+ class="gl-pl-3 gl-mt-3"
@cancelEditing="isEditing = false"
@submitForm="updateNote"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index a1f1eda8bc5..00cdc224320 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,6 +1,7 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import { uniqueId } from 'lodash';
import { __ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
@@ -96,6 +97,7 @@ export default {
sortOrder: ASC,
noteToDelete: null,
discussionFilter: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ addNoteKey: uniqueId(`work-item-add-note-${this.workItemId}`),
};
},
computed: {
@@ -134,6 +136,7 @@ export default {
fetchByIid: this.fetchByIid,
workItemType: this.workItemType,
sortOrder: this.sortOrder,
+ isNewDiscussion: true,
markdownPreviewPath: this.markdownPreviewPath,
autocompleteDataSources: this.autocompleteDataSources,
};
@@ -278,6 +281,9 @@ export default {
filterDiscussions(filterValue) {
this.discussionFilter = filterValue;
},
+ updateKey() {
+ this.addNoteKey = uniqueId(`work-item-add-note-${this.workItemId}`);
+ },
async fetchMoreNotes() {
this.isLoadingMore = true;
// copied from discussions batch logic - every fetchMore call has a higher
@@ -361,12 +367,17 @@ export default {
</div>
<div v-else class="issuable-discussion gl-mb-5 gl-clearfix!">
<template v-if="!initialLoading">
- <ul class="notes main-notes-list timeline gl-clearfix!">
- <work-item-add-note
- v-if="formAtTop && !commentsDisabled"
- v-bind="workItemCommentFormProps"
- @error="$emit('error', $event)"
- />
+ <div v-if="formAtTop && !commentsDisabled" class="js-comment-form">
+ <ul class="notes notes-form timeline">
+ <work-item-add-note
+ v-bind="workItemCommentFormProps"
+ :key="addNoteKey"
+ @cancelEditing="updateKey"
+ @error="$emit('error', $event)"
+ />
+ </ul>
+ </div>
+ <ul class="notes main-notes-list timeline">
<template v-for="discussion in notesArray">
<system-note
v-if="isSystemNote(discussion)"
@@ -393,17 +404,21 @@ export default {
</template>
</template>
- <work-item-add-note
- v-if="!formAtTop && !commentsDisabled"
- v-bind="workItemCommentFormProps"
- @error="$emit('error', $event)"
- />
-
<work-item-history-only-filter-note
v-if="commentsDisabled"
@changeFilter="filterDiscussions"
/>
</ul>
+ <div v-if="!formAtTop && !commentsDisabled" class="js-comment-form">
+ <ul class="notes notes-form timeline">
+ <work-item-add-note
+ v-bind="workItemCommentFormProps"
+ :key="addNoteKey"
+ @cancelEditing="updateKey"
+ @error="$emit('error', $event)"
+ />
+ </ul>
+ </div>
</template>
<template v-if="showLoadingMoreSkeleton">
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index fda71fabe22..40fb0fbc91d 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -15,6 +15,10 @@ extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
+extend type WorkItemPermissions {
+ setWorkItemMetadata: Boolean
+}
+
input LocalUserInput {
id: ID!
name: String
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index 3651cad48f6..86640a6d994 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -27,7 +27,7 @@ fragment WorkItem on WorkItem {
userPermissions {
deleteWorkItem
updateWorkItem
- setWorkItemMetadata
+ setWorkItemMetadata @client
}
widgets {
...WorkItemWidgets
diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss
index f39dee12126..41515a98e0a 100644
--- a/app/assets/stylesheets/page_bundles/issues_list.scss
+++ b/app/assets/stylesheets/page_bundles/issues_list.scss
@@ -23,6 +23,11 @@
margin-bottom: 2px;
}
+ .issue-labels,
+ .author-link {
+ display: inline-block;
+ }
+
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
diff --git a/app/channels/awareness_channel.rb b/app/channels/awareness_channel.rb
deleted file mode 100644
index cf7ba0e5aaf..00000000000
--- a/app/channels/awareness_channel.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-class AwarenessChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
- REFRESH_INTERVAL = ENV.fetch("GITLAB_AWARENESS_REFRESH_INTERVAL_SEC", 60)
- private_constant :REFRESH_INTERVAL
-
- # Produces a refresh interval value, based of the
- # GITLAB_AWARENESS_REFRESH_INTERVAL_SEC environment variable or the given
- # default. Makes sure, that the interval after a jitter is applied, is never
- # less than half the predefined interval.
- def self.refresh_interval(range: -10..10)
- min = REFRESH_INTERVAL / 2.to_f
- [min.to_i, REFRESH_INTERVAL.to_i + rand(range)].max.seconds
- end
- private_class_method :refresh_interval
-
- # keep clients updated about session membership
- periodically every: refresh_interval do
- transmit payload
- end
-
- def subscribed
- reject unless valid_subscription?
- return if subscription_rejected?
-
- stream_for session, coder: ActiveSupport::JSON
-
- session.join(current_user)
- AwarenessChannel.broadcast_to(session, payload)
- end
-
- def unsubscribed
- return if subscription_rejected?
-
- session.leave(current_user)
- AwarenessChannel.broadcast_to(session, payload)
- end
-
- # Allows a client to let the server know they are still around. This is not
- # like a heartbeat mechanism. This can be triggered by any action that results
- # in a meaningful "presence" update. Like scrolling the screen (debounce),
- # window becoming active, user starting to type in a text field, etc.
- def touch
- session.touch!(current_user)
-
- transmit payload
- end
-
- private
-
- def valid_subscription?
- current_user.present? && path.present?
- end
-
- def payload
- { collaborators: collaborators }
- end
-
- def collaborators
- session.online_users_with_last_activity.map do |user, last_activity|
- collaborator(user, last_activity)
- end
- end
-
- def collaborator(user, last_activity)
- {
- id: user.id,
- name: user.name,
- username: user.username,
- avatar_url: user.avatar_url(size: 36),
- last_activity: last_activity,
- last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
- Time.zone.now, last_activity
- )
- }
- end
-
- def session
- @session ||= AwarenessSession.for(path)
- end
-
- def path
- params[:path]
- end
-end
diff --git a/app/graphql/types/permission_types/work_item.rb b/app/graphql/types/permission_types/work_item.rb
index 25d6b3e924d..f35f42001e0 100644
--- a/app/graphql/types/permission_types/work_item.rb
+++ b/app/graphql/types/permission_types/work_item.rb
@@ -6,7 +6,7 @@ module Types
graphql_name 'WorkItemPermissions'
description 'Check permissions for the current user on a work item'
- abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item, :set_work_item_metadata
+ abilities :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
end
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index e2e89c9abca..00cf8e395bb 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -47,6 +47,7 @@ module AuthHelper
def qa_class_for_provider(provider)
{
+ github: 'qa-github-login-button',
saml: 'qa-saml-login-button'
}[provider.to_sym]
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 2442856d7fe..f2fa82aebdb 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -132,7 +132,7 @@ module PreferencesHelper
Gitlab::CurrentSettings.gitpod_url.presence || 'https://gitpod.io/'
end
- # Ensure that anyone adding new options updates `DASHBOARD_CHOICES` too
+ # Ensure that anyone adding new options updates `localized_dashboard_choices` too
def validate_dashboard_choices!(user_dashboards)
if user_dashboards.size != localized_dashboard_choices.size
raise "`User` defines #{user_dashboards.size} dashboard choices," \
diff --git a/app/models/awareness_session.rb b/app/models/awareness_session.rb
deleted file mode 100644
index 0b652984630..00000000000
--- a/app/models/awareness_session.rb
+++ /dev/null
@@ -1,245 +0,0 @@
-# frozen_string_literal: true
-
-# A Redis backed session store for real-time collaboration. A session is defined
-# by its documents and the users that join this session. An online user can have
-# two states within the session: "active" and "away".
-#
-# By design, session must eventually be cleaned up. If this doesn't happen
-# explicitly, all keys used within the session model must have an expiry
-# timestamp set.
-class AwarenessSession # rubocop:disable Gitlab/NamespacedClass
- # An awareness session expires automatically after 1 hour of no activity
- SESSION_LIFETIME = 1.hour
- private_constant :SESSION_LIFETIME
-
- # Expire user awareness keys after some time of inactivity
- USER_LIFETIME = 1.hour
- private_constant :USER_LIFETIME
-
- PRESENCE_LIFETIME = 10.minutes
- private_constant :PRESENCE_LIFETIME
-
- KEY_NAMESPACE = "gitlab:awareness"
- private_constant :KEY_NAMESPACE
-
- class << self
- def for(value = nil)
- # Creates a unique value for situations where we have no unique value to
- # create a session with. This could be when creating a new issue, a new
- # merge request, etc.
- value = SecureRandom.uuid unless value.present?
-
- # We use SHA-256 based session identifiers (similar to abbreviated git
- # hashes). There is always a chance for Hash collisions (birthday
- # problem), we therefore have to pick a good tradeoff between the amount
- # of data stored and the probability of a collision.
- #
- # The approximate probability for a collision can be calculated:
- #
- # p ~= n^2 / 2m
- # ~= (2^18)^2 / (2 * 16^15)
- # ~= 2^36 / 2^61
- #
- # n is the number of awareness sessions and m the number of possibilities
- # for each item. For a hex number, this is 16^c, where c is the number of
- # characters. With 260k (~2^18) sessions, the probability for a collision
- # is ~2^-25.
- #
- # The number of 15 is selected carefully. The integer representation fits
- # nicely into a signed 64 bit integer and eventually allows Redis to
- # optimize its memory usage. 16 chars would exceed the space for
- # this datatype.
- id = Digest::SHA256.hexdigest(value.to_s)[0, 15]
-
- AwarenessSession.new(id)
- end
- end
-
- def initialize(id)
- @id = id
- end
-
- def join(user)
- user_key = user_sessions_key(user.id)
-
- with_redis do |redis|
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.sadd?(user_key, id_i)
- pipeline.expire(user_key, USER_LIFETIME.to_i)
-
- pipeline.zadd(users_key, timestamp.to_f, user.id)
-
- # We also mark for expiry when a session key is created (first user joins),
- # because some users might never actively leave a session and the key could
- # therefore become stale, w/o us noticing.
- reset_session_expiry(pipeline)
- end
- end
- end
-
- nil
- end
-
- def leave(user)
- user_key = user_sessions_key(user.id)
-
- with_redis do |redis|
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.srem?(user_key, id_i)
- pipeline.zrem(users_key, user.id)
- end
- end
-
- # cleanup orphan sessions and users
- #
- # this needs to be a second pipeline due to the delete operations being
- # dependent on the result of the cardinality checks
- user_sessions_count, session_users_count =
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.scard(user_key)
- pipeline.zcard(users_key)
- end
- end
-
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.pipelined do |pipeline|
- pipeline.del(user_key) unless user_sessions_count > 0
-
- unless session_users_count > 0
- pipeline.del(users_key)
- @id = nil
- end
- end
- end
- end
-
- nil
- end
-
- def present?(user, threshold: PRESENCE_LIFETIME)
- with_redis do |redis|
- user_timestamp = redis.zscore(users_key, user.id)
- break false unless user_timestamp.present?
-
- timestamp - user_timestamp < threshold
- end
- end
-
- def away?(user, threshold: PRESENCE_LIFETIME)
- !present?(user, threshold: threshold)
- end
-
- # Updates the last_activity timestamp for a user in this session
- def touch!(user)
- with_redis do |redis|
- redis.pipelined do |pipeline|
- pipeline.zadd(users_key, timestamp.to_f, user.id)
-
- # extend the session lifetime due to user activity
- reset_session_expiry(pipeline)
- end
- end
-
- nil
- end
-
- def size
- with_redis do |redis|
- redis.zcard(users_key)
- end
- end
-
- def to_param
- id&.to_s
- end
-
- def to_s
- "awareness_session=#{id}"
- end
-
- def online_users_with_last_activity(threshold: PRESENCE_LIFETIME)
- users_with_last_activity.filter do |_user, last_activity|
- user_online?(last_activity, threshold: threshold)
- end
- end
-
- def users
- User.where(id: user_ids)
- end
-
- def users_with_last_activity
- # where in (x, y, [...z]) is a set and does not maintain any order, we need
- # to make sure to establish a stable order for both, the pairs returned from
- # redis and the ActiveRecord query. Using IDs in ascending order.
- user_ids, last_activities = user_ids_with_last_activity
- .sort_by(&:first)
- .transpose
-
- return [] if user_ids.blank?
-
- users = User.where(id: user_ids).order(id: :asc)
- users.zip(last_activities)
- end
-
- private
-
- attr_reader :id
-
- def user_online?(last_activity, threshold:)
- last_activity.to_i + threshold.to_i > Time.zone.now.to_i
- end
-
- # converts session id from hex to integer representation
- def id_i
- Integer(id, 16) if id.present?
- end
-
- def users_key
- "#{KEY_NAMESPACE}:session:#{id}:users"
- end
-
- def user_sessions_key(user_id)
- "#{KEY_NAMESPACE}:user:#{user_id}:sessions"
- end
-
- def with_redis
- Gitlab::Redis::SharedState.with do |redis|
- yield redis if block_given?
- end
- end
-
- def timestamp
- Time.now.to_i
- end
-
- def user_ids
- with_redis do |redis|
- redis.zrange(users_key, 0, -1)
- end
- end
-
- # Returns an array of tuples, where the first element in the tuple represents
- # the user ID and the second part the last_activity timestamp.
- def user_ids_with_last_activity
- pairs = with_redis do |redis|
- redis.zrange(users_key, 0, -1, with_scores: true)
- end
-
- # map data type of score (float) to Time
- pairs.map do |user_id, score|
- [user_id, Time.zone.at(score.to_i)]
- end
- end
-
- # We want sessions to cleanup automatically after a certain period of
- # inactivity. This sets the expiry timestamp for this session to
- # [SESSION_LIFETIME].
- def reset_session_expiry(redis)
- redis.expire(users_key, SESSION_LIFETIME)
-
- nil
- end
-end
diff --git a/app/models/concerns/awareness.rb b/app/models/concerns/awareness.rb
deleted file mode 100644
index da87d87e838..00000000000
--- a/app/models/concerns/awareness.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-
-module Awareness
- extend ActiveSupport::Concern
-
- KEY_NAMESPACE = "gitlab:awareness"
- private_constant :KEY_NAMESPACE
-
- def join(session)
- session.join(self)
-
- nil
- end
-
- def leave(session)
- session.leave(self)
-
- nil
- end
-
- def session_ids
- with_redis do |redis|
- redis
- .smembers(user_sessions_key)
- # converts session ids from (internal) integer to hex presentation
- .map { |key| key.to_i.to_s(16) }
- end
- end
-
- private
-
- def user_sessions_key
- "#{KEY_NAMESPACE}:user:#{id}:sessions"
- end
-
- def with_redis
- Gitlab::Redis::SharedState.with do |redis|
- yield redis if block_given?
- end
- end
-end
diff --git a/app/models/user.rb b/app/models/user.rb
index 86e8aace514..71ea185b6f1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -9,7 +9,6 @@ class User < ApplicationRecord
include Gitlab::SQL::Pattern
include AfterCommitQueue
include Avatarable
- include Awareness
include Referable
include Sortable
include CaseSensitivity
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 95d9caa686d..7156a0e5931 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -8,7 +8,7 @@
.title-container.hide-when-top-nav-responsive-open.gl-transition-medium.gl-display-flex.gl-align-items-stretch.gl-pt-0.gl-mr-3
.title
%span.gl-sr-only GitLab
- = link_to root_path, title: _('Dashboard'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
+ = link_to root_path, title: _('Homepage'), id: 'logo', class: 'has-tooltip', **tracking_attrs('main_navigation', 'click_gitlab_logo_link', 'navigation_top') do
= brand_header_logo
.gl-display-flex.gl-align-items-center
- if Gitlab.com_and_canary?
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 23dd824c268..c16469bbf79 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -78,7 +78,7 @@
= f.select :layout, layout_choices, {}, class: 'gl-form-select custom-select'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
- .js-listbox-input{ data: { label: s_('Preferences|Dashboard'), description: s_('Preferences|Choose what content you want to see by default on your dashboard.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard } }
+ .js-listbox-input{ data: { label: s_('Preferences|Homepage'), description: s_('Preferences|Choose what content you want to see by default on your homepage.'), name: 'user[dashboard]', items: dashboard_choices.to_json, value: current_user.dashboard } }
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
diff --git a/config/feature_flags/development/project_export_as_ndjson.yml b/config/feature_flags/development/project_export_as_ndjson.yml
deleted file mode 100644
index f77c1979e55..00000000000
--- a/config/feature_flags/development/project_export_as_ndjson.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: project_export_as_ndjson
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26995
-rollout_issue_url:
-milestone: '12.10'
-type: development
-group: group::import
-default_enabled: true
diff --git a/config/feature_flags/development/project_import_ndjson.yml b/config/feature_flags/development/project_import_ndjson.yml
deleted file mode 100644
index 756c6c1aaa5..00000000000
--- a/config/feature_flags/development/project_import_ndjson.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: project_import_ndjson
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27206
-rollout_issue_url:
-milestone: '12.10'
-type: development
-group: group::import
-default_enabled: true
diff --git a/doc/administration/auth/index.md b/doc/administration/auth/index.md
index b4e3103f853..4a8e230a944 100644
--- a/doc/administration/auth/index.md
+++ b/doc/administration/auth/index.md
@@ -33,3 +33,7 @@ For more information, see the links shown on this page for each external provide
| **User Removal** | SCIM (remove user from top-level group) | LDAP (remove user from groups and block from the instance)<br>SCIM |
1. Using Just-In-Time (JIT) provisioning, user accounts are created when the user first signs in.
+
+## Test OIDC/OAuth in GitLab
+
+See [Test OIDC/OAuth in GitLab](test_oidc_oauth.md) to learn how to test OIDC/OAuth authentication in your GitLab instance using your client application.
diff --git a/doc/administration/auth/ldap/ldap_synchronization.md b/doc/administration/auth/ldap/ldap_synchronization.md
index c61783e507e..f54b0b02d7f 100644
--- a/doc/administration/auth/ldap/ldap_synchronization.md
+++ b/doc/administration/auth/ldap/ldap_synchronization.md
@@ -38,11 +38,109 @@ For more information, see [Bitmask Searches in LDAP](https://ctovswild.com/2009/
The process also updates the following user information:
- Name. Because of a [sync issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342598), `name` is not synchronized if
- [**Prevent users from changing their profile name**](../../../user/admin_area/settings/account_and_limit_settings.md#disable-user-profile-name-changes) is enabled.
+ [**Prevent users from changing their profile name**](../../../user/admin_area/settings/account_and_limit_settings.md#disable-user-profile-name-changes) is enabled or `sync_name` is set to `false`.
- Email address.
- SSH public keys if `sync_ssh_keys` is set.
- Kerberos identity if Kerberos is enabled.
+### Synchronize LDAP username
+
+By default, GitLab synchronizes the LDAP username field.
+
+To prevent this synchronization, you can set `sync_name` to `false`.
+
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['ldap_servers'] = {
+ 'main' => {
+ 'sync_name' => false,
+ }
+ }
+ ```
+
+1. Save the file and reconfigure GitLab:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+:::TabTitle Helm chart (Kubernetes)
+
+1. Export the Helm values:
+
+ ```shell
+ helm get values gitlab > gitlab_values.yaml
+ ```
+
+1. Edit `gitlab_values.yaml`:
+
+ ```yaml
+ global:
+ appConfig:
+ ldap:
+ servers:
+ main:
+ sync_name: false
+ ```
+
+1. Save the file and apply the new values:
+
+ ```shell
+ helm upgrade -f gitlab_values.yaml gitlab gitlab/gitlab
+ ```
+
+:::TabTitle Docker
+
+1. Edit `docker-compose.yml`:
+
+ ```yaml
+ version: "3.6"
+ services:
+ gitlab:
+ environment:
+ GITLAB_OMNIBUS_CONFIG: |
+ gitlab_rails['ldap_servers'] = {
+ 'main' => {
+ 'sync_name' => false,
+ }
+ }
+ ```
+
+1. Save the file and restart GitLab:
+
+ ```shell
+ docker compose up -d
+ ```
+
+:::TabTitle Self-compiled (source)
+
+1. Edit `/home/git/gitlab/config/gitlab.yml`:
+
+ ```yaml
+ production: &base
+ ldap:
+ servers:
+ main:
+ sync_name: false
+ ```
+
+1. Save the file and restart GitLab:
+
+ ```shell
+ # For systems running systemd
+ sudo systemctl restart gitlab.target
+
+ # For systems running SysV init
+ sudo service gitlab restart
+ ```
+
+::EndTabs
+
### Blocked users
A user is blocked if either the:
diff --git a/doc/administration/auth/test_oidc_oauth.md b/doc/administration/auth/test_oidc_oauth.md
new file mode 100644
index 00000000000..95cca1ced86
--- /dev/null
+++ b/doc/administration/auth/test_oidc_oauth.md
@@ -0,0 +1,57 @@
+---
+stage: Manage
+group: Authentication and Authorization
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Test OIDC/OAuth in GitLab **(FREE)**
+
+To test OIDC/OAuth in GitLab, you must:
+
+1. [Enable OIDC/OAuth](#enable-oidcoauth-in-gitlab)
+1. [Test OIDC/OAuth with your client application](#test-oidcoauth-with-your-client-application)
+1. [Verify OIDC/OAuth authentication](#verify-oidcoauth-authentication)
+
+## Prerequisites
+
+Before you can test OIDC/OAuth on GitLab, you'll need the following:
+
+- Publicly accessible GitLab instance
+- A client application that you want to use to test OIDC/OAuth
+- A user account on the GitLab instance that you can use to log in and test OIDC/OAuth
+
+## Enable OIDC/OAuth in GitLab
+
+First, you must create OIDC/OAuth application on your GitLab instance. To do this:
+
+1. Sign in to GitLab as an administrator.
+1. Select **Profile > Preferences > Applications**.
+1. Fill in the details for your client application, including the name, redirect URI, and allowed scopes.
+1. Make sure the `openid` scope is enabled.
+1. Select **Save application** to create the new OAuth application.
+
+## Test OIDC/OAuth with your client application
+
+After you've created your OAuth application in GitLab, you can use it to test OIDC/OAuth:
+
+1. You can use <https://openidconnect.net> as the OIDC/OAuth playground.
+1. Sign out of GitLab.
+1. Visit your client application and initiate the OIDC/OAuth flow, using the GitLab OAuth application you created in the previous step.
+1. Follow the prompts to sign in to GitLab and authorize the client application to access your GitLab account.
+1. After you've completed the OIDC/OAuth flow, your client application should have received an access token that it can use to authenticate with GitLab.
+
+## Verify OIDC/OAuth authentication
+
+To verify that OIDC/OAuth authentication is working correctly on GitLab, you can perform the following checks:
+
+1. Check that the access token you received in the previous step is valid and can be used to authenticate with GitLab. You can do this by making a test API request to GitLab, using the access token to authenticate. For example:
+
+ ```shell
+ curl --header "Authorization: Bearer <access_token>" https://mygitlabinstance.com/api/v4/user
+ ```
+
+ Replace `<access_token>` with the actual access token you received in the previous step. If the API request succeeds and returns information about the authenticated user, then OIDC/OAuth authentication is working correctly.
+
+1. Check that the scopes you specified in your OAuth application are being enforced correctly. You can do this by making API requests that require the specific scopes and checking that they succeed or fail as expected.
+
+That's it! With these steps, you should be able to test OIDC/OAuth authentication on your GitLab instance using your client application.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c5a53b458cb..2e945f5ba66 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22477,7 +22477,6 @@ Check permissions for the current user on a work item.
| <a id="workitempermissionsadminworkitem"></a>`adminWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_work_item` on this resource. |
| <a id="workitempermissionsdeleteworkitem"></a>`deleteWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `delete_work_item` on this resource. |
| <a id="workitempermissionsreadworkitem"></a>`readWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `read_work_item` on this resource. |
-| <a id="workitempermissionssetworkitemmetadata"></a>`setWorkItemMetadata` | [`Boolean!`](#boolean) | Indicates the user can perform `set_work_item_metadata` on this resource. |
| <a id="workitempermissionsupdateworkitem"></a>`updateWorkItem` | [`Boolean!`](#boolean) | Indicates the user can perform `update_work_item` on this resource. |
### `WorkItemType`
diff --git a/doc/api/users.md b/doc/api/users.md
index f877aa29b9c..dba1c30f7e8 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1301,7 +1301,25 @@ Parameters:
| `key` | string | yes | New GPG key |
```shell
-curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." \
+
+export KEY="$( gpg --armor --export <your_gpg_key_id>)"
+
+curl --data-urlencode "key=-----BEGIN PGP PUBLIC KEY BLOCK-----
+> xsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj
+> t1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O
+> CfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa
+> qKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO
+> VaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57
+> vilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp
+> IDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV
+> CAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/
+> oO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5
+> crfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4
+> bjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn
+> iE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp
+> o4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=
+> =XQoy
+> -----END PGP PUBLIC KEY BLOCK-----" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/user/gpg_keys"
```
@@ -1311,7 +1329,7 @@ Example response:
[
{
"id": 1,
- "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\n=XQoy\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
@@ -1413,7 +1431,22 @@ Parameters:
| `key_id` | integer | yes | ID of the GPG key |
```shell
-curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." \
+curl --data-urlencode "key=-----BEGIN PGP PUBLIC KEY BLOCK-----
+> xsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj
+> t1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O
+> CfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa
+> qKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO
+> VaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57
+> vilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp
+> IDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV
+> CAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/
+> oO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5
+> crfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4
+> bjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn
+> iE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp
+> o4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=
+> =XQoy
+> -----END PGP PUBLIC KEY BLOCK-----" \
--header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/users/2/gpg_keys"
```
@@ -1423,7 +1456,7 @@ Example response:
[
{
"id": 1,
- "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\n=XQoy\n-----END PGP PUBLIC KEY BLOCK-----",
"created_at": "2017-09-05T09:17:46.264Z"
}
]
diff --git a/lib/gitlab/import_export/group/relation_tree_restorer.rb b/lib/gitlab/import_export/group/relation_tree_restorer.rb
index 624acd3bb2a..5825db89201 100644
--- a/lib/gitlab/import_export/group/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/group/relation_tree_restorer.rb
@@ -34,7 +34,6 @@ module Gitlab
update_params!
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_insert_enabled) do
- fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
@@ -275,15 +274,6 @@ module Gitlab
}
end
- # Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
- # This should be removed once legacy JSON format is deprecated.
- # Ndjson export file will fix the order during project export.
- def fix_ci_pipelines_not_sorted_on_legacy_project_json!
- return unless @relation_reader.legacy?
-
- @relation_reader.sort_ci_pipelines_by_id
- end
-
# Enable logging of each top-level relation creation when Importing into a Group
def log_relation_creation(importable, relation_key, relation_object)
root_ancestor_group = importable.try(:root_ancestor)
diff --git a/lib/gitlab/import_export/json/legacy_reader.rb b/lib/gitlab/import_export/json/legacy_reader.rb
deleted file mode 100644
index ee360020556..00000000000
--- a/lib/gitlab/import_export/json/legacy_reader.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- module Json
- class LegacyReader
- class File < LegacyReader
- include Gitlab::Utils::StrongMemoize
-
- def initialize(path, relation_names:, allowed_path: nil)
- @path = path
- super(
- relation_names: relation_names,
- allowed_path: allowed_path)
- end
-
- def exist?
- ::File.exist?(@path)
- end
-
- protected
-
- def tree_hash
- strong_memoize(:tree_hash) do
- read_hash
- end
- end
-
- def read_hash
- Gitlab::Json.parse(::File.read(@path))
- rescue StandardError => e
- Gitlab::ErrorTracking.log_exception(e)
- raise Gitlab::ImportExport::Error, 'Incorrect JSON format'
- end
- end
-
- class Hash < LegacyReader
- def initialize(tree_hash, relation_names:, allowed_path: nil)
- @tree_hash = tree_hash
- super(
- relation_names: relation_names,
- allowed_path: allowed_path)
- end
-
- def exist?
- @tree_hash.present?
- end
-
- protected
-
- attr_reader :tree_hash
- end
-
- def initialize(relation_names:, allowed_path:)
- @relation_names = relation_names.map(&:to_s)
- @consumed_relations = Set.new
-
- # This is legacy reader, to be used in transition
- # period before `.ndjson`,
- # we strong validate what is being readed
- @allowed_path = allowed_path
- end
-
- def exist?
- raise NotImplementedError
- end
-
- def legacy?
- true
- end
-
- def consume_attributes(importable_path)
- unless importable_path == @allowed_path
- raise ArgumentError, "Invalid #{importable_path} passed to `consume_attributes`. Use #{@allowed_path} instead."
- end
-
- attributes
- end
-
- def consume_relation(importable_path, key)
- unless importable_path == @allowed_path
- raise ArgumentError, "Invalid #{importable_name} passed to `consume_relation`. Use #{@allowed_path} instead."
- end
-
- Enumerator.new do |documents|
- next unless @consumed_relations.add?("#{importable_path}/#{key}")
-
- value = relations.delete(key)
- next if value.nil?
-
- if value.is_a?(Array)
- value.each.with_index do |item, idx|
- documents << [item, idx]
- end
- else
- documents << [value, 0]
- end
- end
- end
-
- def sort_ci_pipelines_by_id
- relations['ci_pipelines']&.sort_by! { |hash| hash['id'] }
- end
-
- private
-
- attr_reader :relation_names, :allowed_path
-
- def tree_hash
- raise NotImplementedError
- end
-
- def attributes
- @attributes ||= tree_hash.slice!(*relation_names)
- end
-
- def relations
- @relations ||= tree_hash.extract!(*relation_names)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/json/legacy_writer.rb b/lib/gitlab/import_export/json/legacy_writer.rb
deleted file mode 100644
index e03ab9f7650..00000000000
--- a/lib/gitlab/import_export/json/legacy_writer.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- module Json
- class LegacyWriter
- include Gitlab::ImportExport::CommandLineUtil
-
- attr_reader :path
-
- def initialize(path, allowed_path:)
- @path = path
- @keys = Set.new
-
- # This is legacy writer, to be used in transition
- # period before `.ndjson`,
- # we strong validate what is being written
- @allowed_path = allowed_path
-
- mkdir_p(File.dirname(@path))
- file.write('{}')
- end
-
- def close
- @file&.close
- @file = nil
- end
-
- def write_attributes(exportable_path, hash)
- unless exportable_path == @allowed_path
- raise ArgumentError, "Invalid #{exportable_path}"
- end
-
- hash.each do |key, value|
- write(key, value)
- end
- end
-
- def write_relation(exportable_path, key, value)
- unless exportable_path == @allowed_path
- raise ArgumentError, "Invalid #{exportable_path}"
- end
-
- write(key, value)
- end
-
- def write_relation_array(exportable_path, key, items)
- unless exportable_path == @allowed_path
- raise ArgumentError, "Invalid #{exportable_path}"
- end
-
- write(key, [])
-
- # rewind by two bytes, to overwrite ']}'
- file.pos = file.size - 2
-
- items.each_with_index do |item, idx|
- file.write(',') if idx > 0
- file.write(item.to_json)
- end
-
- file.write(']}')
- end
-
- private
-
- def write(key, value)
- raise ArgumentError, "key '#{key}' already written" if @keys.include?(key)
-
- # rewind by one byte, to overwrite '}'
- file.pos = file.size - 1
-
- file.write(',') if @keys.any?
- file.write(key.to_json)
- file.write(':')
- file.write(value.to_json)
- file.write('}')
-
- @keys.add(key)
- end
-
- def file
- @file ||= File.open(@path, "wb")
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb
index 510da61d3ab..3de56aacf18 100644
--- a/lib/gitlab/import_export/json/ndjson_reader.rb
+++ b/lib/gitlab/import_export/json/ndjson_reader.rb
@@ -17,14 +17,12 @@ module Gitlab
Dir.exist?(@dir_path)
end
- # This can be removed once legacy_reader is deprecated.
- def legacy?
- false
- end
-
def consume_attributes(importable_path)
# This reads from `tree/project.json`
path = file_path("#{importable_path}.json")
+
+ raise Gitlab::ImportExport::Error, 'Invalid file' if !File.exist?(path) || File.symlink?(path)
+
data = File.read(path, MAX_JSON_DOCUMENT_SIZE)
json_decode(data)
end
@@ -36,7 +34,7 @@ module Gitlab
# This reads from `tree/project/merge_requests.ndjson`
path = file_path(importable_path, "#{key}.ndjson")
- next unless File.exist?(path)
+ next if !File.exist?(path) || File.symlink?(path)
File.foreach(path, MAX_JSON_DOCUMENT_SIZE).with_index do |line, line_num|
documents << [json_decode(line), line_num]
diff --git a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
index 034122a9f14..639f34980ff 100644
--- a/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/project/sample/relation_tree_restorer.rb
@@ -18,8 +18,6 @@ module Gitlab
end
def dates
- return [] if @relation_reader.legacy?
-
RelationFactory::DATE_MODELS.flat_map do |tag|
@relation_reader.consume_relation(@importable_path, tag, mark_as_consumed: false).map do |model|
model.first['due_date']
diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb
index 47f82a901b7..e791424875a 100644
--- a/lib/gitlab/import_export/project/tree_restorer.rb
+++ b/lib/gitlab/import_export/project/tree_restorer.rb
@@ -17,7 +17,7 @@ module Gitlab
end
def restore
- unless relation_reader
+ unless relation_reader.exist?
raise Gitlab::ImportExport::Error, 'invalid import format'
end
@@ -47,28 +47,11 @@ module Gitlab
private
def relation_reader
- strong_memoize(:relation_reader) do
- [ndjson_relation_reader, legacy_relation_reader]
- .compact.find(&:exist?)
- end
- end
-
- def ndjson_relation_reader
- return unless Feature.enabled?(:project_import_ndjson, project.namespace)
-
- ImportExport::Json::NdjsonReader.new(
+ @relation_reader ||= ImportExport::Json::NdjsonReader.new(
File.join(shared.export_path, 'tree')
)
end
- def legacy_relation_reader
- ImportExport::Json::LegacyReader::File.new(
- File.join(shared.export_path, 'project.json'),
- relation_names: reader.project_relation_names,
- allowed_path: importable_path
- )
- end
-
def relation_tree_restorer
@relation_tree_restorer ||= relation_tree_restorer_class.new(
user: @user,
diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb
index 05b96f7e8ce..fd5fa73764e 100644
--- a/lib/gitlab/import_export/project/tree_saver.rb
+++ b/lib/gitlab/import_export/project/tree_saver.rb
@@ -81,13 +81,10 @@ module Gitlab
end
def json_writer
- @json_writer ||= if ::Feature.enabled?(:project_export_as_ndjson, @project.namespace)
- full_path = File.join(@shared.export_path, 'tree')
- Gitlab::ImportExport::Json::NdjsonWriter.new(full_path)
- else
- full_path = File.join(@shared.export_path, ImportExport.project_filename)
- Gitlab::ImportExport::Json::LegacyWriter.new(full_path, allowed_path: 'project')
- end
+ @json_writer ||= begin
+ full_path = File.join(@shared.export_path, 'tree')
+ Gitlab::ImportExport::Json::NdjsonWriter.new(full_path)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index aa59278aa39..8505e029f3a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -9403,6 +9403,9 @@ msgstr ""
msgid "Close %{tabname}"
msgstr ""
+msgid "Close %{workItemType}"
+msgstr ""
+
msgid "Close design"
msgstr ""
@@ -10790,6 +10793,9 @@ msgstr ""
msgid "ComplianceFrameworks|Cancel"
msgstr ""
+msgid "ComplianceFrameworks|Compliance framework created"
+msgstr ""
+
msgid "ComplianceFrameworks|Compliance framework deleted successfully"
msgstr ""
@@ -10841,6 +10847,9 @@ msgstr ""
msgid "ComplianceFrameworks|Name is required"
msgstr ""
+msgid "ComplianceFrameworks|New compliance framework"
+msgstr ""
+
msgid "ComplianceFrameworks|No compliance frameworks are set up yet"
msgstr ""
@@ -33007,7 +33016,7 @@ msgstr ""
msgid "Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout."
msgstr ""
-msgid "Preferences|Choose what content you want to see by default on your dashboard."
+msgid "Preferences|Choose what content you want to see by default on your homepage."
msgstr ""
msgid "Preferences|Choose what content you want to see on a project’s overview page."
@@ -33040,9 +33049,6 @@ msgstr ""
msgid "Preferences|Customize the colors of removed and added lines in diffs."
msgstr ""
-msgid "Preferences|Dashboard"
-msgstr ""
-
msgid "Preferences|Diff colors"
msgstr ""
@@ -33064,6 +33070,9 @@ msgstr ""
msgid "Preferences|Gitpod"
msgstr ""
+msgid "Preferences|Homepage"
+msgstr ""
+
msgid "Preferences|Instead of all the files changed, show only one file at a time. To switch between files, use the file browser."
msgstr ""
@@ -36747,6 +36756,9 @@ msgstr ""
msgid "Reopen %{noteable}"
msgstr ""
+msgid "Reopen %{workItemType}"
+msgstr ""
+
msgid "Reopen epic"
msgstr ""
@@ -38395,6 +38407,9 @@ msgstr ""
msgid "Runners|You have used %{quotaUsed} out of %{quotaLimit} of your shared Runners pipeline minutes."
msgstr ""
+msgid "Runners|You may lose access to the runner token if you leave this page."
+msgstr ""
+
msgid "Runners|You've created a new runner!"
msgstr ""
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 6efc8ac09fa..2bf668abc49 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -22,6 +22,13 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
##
+# Install 1Password CLI
+#
+RUN wget -P /tmp/ https://downloads.1password.com/linux/debian/$(dpkg --print-architecture)/stable/1password-cli-$(dpkg --print-architecture)-latest.deb
+RUN dpkg -i /tmp/1password-cli-$(dpkg --print-architecture)-latest.deb
+RUN op --version
+
+##
# Install root certificate
#
RUN mkdir -p /usr/share/ca-certificates/gitlab
diff --git a/qa/qa.rb b/qa/qa.rb
index a395dc6e0b0..f6fba30c079 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -83,7 +83,8 @@ module QA
"vscode" => "VSCode",
"registry_with_cdn" => "RegistryWithCDN",
"fips" => "FIPS",
- "ci_cd_settings" => "CICDSettings"
+ "ci_cd_settings" => "CICDSettings",
+ "cli" => "CLI"
)
loader.setup
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 4a5f9d90630..9fb1179373d 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -40,6 +40,7 @@ module QA
view 'app/helpers/auth_helper.rb' do
element :saml_login_button
+ element :github_login_button
end
view 'app/views/layouts/devise.html.haml' do
@@ -177,6 +178,11 @@ module QA
click_element :standard_tab
end
+ def sign_in_with_github
+ set_initial_password_if_present
+ click_element :github_login_button
+ end
+
def sign_in_with_saml
set_initial_password_if_present
click_element :saml_login_button
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 7da529becbd..34e392c6263 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -210,11 +210,11 @@ module QA
end
def github_username
- ENV['GITHUB_USERNAME']
+ ENV['QA_GITHUB_USERNAME']
end
def github_password
- ENV['GITHUB_PASSWORD']
+ ENV['QA_GITHUB_PASSWORD']
end
def forker?
@@ -541,6 +541,22 @@ module QA
raise "Missing Slack env: #{missing_env.map(&:upcase).join(', ')}"
end
+ def one_p_email
+ ENV['QA_1P_EMAIL']
+ end
+
+ def one_p_password
+ ENV['QA_1P_PASSWORD']
+ end
+
+ def one_p_secret
+ ENV['QA_1P_SECRET']
+ end
+
+ def one_p_github_uuid
+ ENV['QA_1P_GITHUB_UUID']
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/oauth_login_with_github_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/oauth_login_with_github_spec.rb
new file mode 100644
index 00000000000..3ac050c1649
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/oauth_login_with_github_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Manage', :orchestrated, :oauth, product_group: :authentication_and_authorization do
+ describe 'OAuth' do
+ it 'connects and logs in with GitHub OAuth',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/402405' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+ Page::Main::Login.perform(&:sign_in_with_github)
+
+ Vendor::Github::Page::Login.perform(&:login)
+
+ expect(page).to have_content('Welcome to GitLab')
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/github/page/base.rb b/qa/qa/vendor/github/page/base.rb
new file mode 100644
index 00000000000..3b96180afe9
--- /dev/null
+++ b/qa/qa/vendor/github/page/base.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module QA
+ module Vendor
+ module Github
+ module Page
+ class Base
+ include Capybara::DSL
+ include Scenario::Actable
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb
new file mode 100644
index 00000000000..17a7471e251
--- /dev/null
+++ b/qa/qa/vendor/github/page/login.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'capybara/dsl'
+require 'benchmark'
+
+module QA
+ module Vendor
+ module Github
+ module Page
+ class Login < Page::Base
+ def login
+ fill_in 'login', with: QA::Runtime::Env.github_username
+ fill_in 'password', with: QA::Runtime::Env.github_password
+ click_on 'Sign in'
+
+ current_otp = OnePassword::CLI.instance.current_otp
+
+ fill_in 'app_otp', with: current_otp
+
+ if has_text?('Two-factor authentication failed', wait: 2)
+ new_otp = OnePassword::CLI.instance.new_otp(otp)
+
+ fill_in 'app_otp', with: new_otp
+ end
+
+ authorize_app
+ end
+
+ def authorize_app
+ click_on 'Authorize' if has_button?('Authorize')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/one_password/cli.rb b/qa/qa/vendor/one_password/cli.rb
new file mode 100644
index 00000000000..f443ba05492
--- /dev/null
+++ b/qa/qa/vendor/one_password/cli.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'benchmark'
+
+module QA
+ module Vendor
+ module OnePassword
+ class CLI
+ include Singleton
+
+ def initialize
+ @email = QA::Runtime::Env.one_p_email
+ @password = QA::Runtime::Env.one_p_password
+ @secret = QA::Runtime::Env.one_p_secret
+ @github_uuid = QA::Runtime::Env.one_p_github_uuid
+ @address = 'gitlab.1password.com'
+ end
+
+ def new_otp(old_otp = "")
+ # Fetches a new OTP that is not equal to the old OTP
+ new_otp = ""
+ time = Benchmark.realtime do
+ # An otp is valid for 30 seconds so 64 attempts with 0.5 interval are enough to ensure a new OTP is obtained
+ Support::Retrier.retry_until(max_attempts: 64, sleep_interval: 0.5) do
+ new_otp = current_otp
+ new_otp != old_otp
+ end
+ end
+
+ QA::Runtime::Logger.info("Fetched new OTP in: #{time} seconds")
+
+ new_otp
+ end
+
+ def current_otp
+ result = nil
+
+ time = Benchmark.realtime do
+ result = `op item get #{@github_uuid} --otp --session #{session_token}`.chop
+ end
+
+ QA::Runtime::Logger.info("Fetched current OTP in: #{time} seconds")
+
+ result
+ end
+
+ private
+
+ # OP session tokens are valid for 30 minutes. We are caching the session token here and this is fine currently
+ # as we just have one test that is not expected to go over 30 minutes.
+ # But note that if we add more tests that use this class, we might need to add a mechanism to invalidate
+ # the cache after 30 minutes or if the session_token is rejected by op CLI.
+ def session_token
+ @session_token ||= `echo '#{@password}' | op account add --address #{@address} --email #{@email} --secret-key #{@secret} --signin --raw` # rubocop:disable Layout/LineLength
+ end
+ end
+ end
+ end
+end
diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake
index aaf691de1b5..e5f4acb158b 100644
--- a/qa/tasks/ci.rake
+++ b/qa/tasks/ci.rake
@@ -32,7 +32,7 @@ namespace :ci do
if run_all_label_present
logger.info(" merge request has pipeline:run-all-e2e label, full test suite will be executed")
- append_to_file(env_file, "QA_RUN_ALL_TESTS=true\n")
+ append_to_file(env_file, "QA_RUN_ALL_E2E_LABEL=true\n")
elsif qa_changes.framework_changes? # run all tests when framework changes detected
logger.info(" merge request contains qa framework changes, full test suite will be executed")
append_to_file(env_file, "QA_FRAMEWORK_CHANGES=true\n")
diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline
index 001644ddc14..c0d17443ba9 100755
--- a/scripts/generate-e2e-pipeline
+++ b/scripts/generate-e2e-pipeline
@@ -32,6 +32,7 @@ variables:
QA_FEATURE_FLAGS: "${QA_FEATURE_FLAGS}"
QA_FRAMEWORK_CHANGES: "${QA_FRAMEWORK_CHANGES:-false}"
QA_RUN_ALL_TESTS: "${QA_RUN_ALL_TESTS:-false}"
+ QA_RUN_ALL_E2E_LABEL: "${QA_RUN_ALL_E2E_LABEL:-false}"
QA_SAVE_TEST_METRICS: "${QA_SAVE_TEST_METRICS:-false}"
QA_SUITES: "$QA_SUITES"
QA_TESTS: "$QA_TESTS"
diff --git a/scripts/utils.sh b/scripts/utils.sh
index df8a5825dab..80057842c28 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -76,7 +76,7 @@ function bundle_install_script() {
gem --version
bundle --version
- gem install bundler --no-document --conservative --version 2.3.15
+ gem install bundler --no-document --conservative --version 2.4.11
test -d jh && bundle config set --local gemfile 'jh/Gemfile'
bundle config set path "$(pwd)/vendor"
bundle config set clean 'true'
diff --git a/spec/channels/awareness_channel_spec.rb b/spec/channels/awareness_channel_spec.rb
deleted file mode 100644
index 47b1cd0188f..00000000000
--- a/spec/channels/awareness_channel_spec.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AwarenessChannel, :clean_gitlab_redis_shared_state, type: :channel do
- before do
- stub_action_cable_connection(current_user: user)
- end
-
- context "with user" do
- let(:user) { create(:user) }
-
- describe "when no path parameter given" do
- it "rejects subscription" do
- subscribe path: nil
-
- expect(subscription).to be_rejected
- end
- end
-
- describe "with valid path parameter" do
- it "successfully subscribes" do
- subscribe path: "/test"
-
- session = AwarenessSession.for("/test")
-
- expect(subscription).to be_confirmed
- # check if we can use session object instead
- expect(subscription).to have_stream_from("awareness:#{session.to_param}")
- end
-
- it "broadcasts set of collaborators when subscribing" do
- session = AwarenessSession.for("/test")
-
- freeze_time do
- collaborator = {
- id: user.id,
- name: user.name,
- username: user.username,
- avatar_url: user.avatar_url(size: 36),
- last_activity: Time.zone.now,
- last_activity_humanized: ActionController::Base.helpers.distance_of_time_in_words(
- Time.zone.now, Time.zone.now
- )
- }
-
- expect do
- subscribe path: "/test"
- end.to have_broadcasted_to("awareness:#{session.to_param}")
- .with(collaborators: [collaborator])
- end
- end
-
- it "transmits payload when user is touched" do
- subscribe path: "/test"
-
- perform :touch
-
- expect(transmissions.size).to be 1
- end
-
- it "unsubscribes from channel" do
- subscribe path: "/test"
- session = AwarenessSession.for("/test")
-
- expect { subscription.unsubscribe_from_channel }
- .to change { session.size }.by(-1)
- end
- end
- end
-
- context "with guest" do
- let(:user) { nil }
-
- it "rejects subscription" do
- subscribe path: "/test"
-
- expect(subscription).to be_rejected
- end
- end
-end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 6d775f1ebff..a07eb4036c2 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -511,6 +511,17 @@ RSpec.describe "Admin Runners", feature_category: :runner_fleet do
click_on 'How do I install GitLab Runner?'
expect(page.find('[data-testid="runner-platforms-drawer"]')).to have_content('gitlab-runner install')
end
+
+ it 'warns from leaving page without finishing registration' do
+ click_on s_('Runners|Go to runners page')
+
+ alert = page.driver.browser.switch_to.alert
+
+ expect(alert).not_to be_nil
+ alert.dismiss
+
+ expect(current_url).to match(register_admin_runner_path(Ci::Runner.last))
+ end
end
end
diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb
index f6187672f0e..ff8132dc087 100644
--- a/spec/features/nav/top_nav_responsive_spec.rb
+++ b/spec/features/nav/top_nav_responsive_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
context 'when menu is closed' do
it 'has page content and hides responsive menu', :aggregate_failures do
expect(page).to have_css('.page-title', text: 'Explore projects')
- expect(page).to have_link('Dashboard', id: 'logo')
+ expect(page).to have_link('Homepage', id: 'logo')
expect(page).to have_no_css('.top-nav-responsive')
end
@@ -35,7 +35,7 @@ RSpec.describe 'top nav responsive', :js, feature_category: :navigation do
it 'hides everything and shows responsive menu', :aggregate_failures do
expect(page).to have_no_css('.page-title', text: 'Explore projects')
- expect(page).to have_no_link('Dashboard', id: 'logo')
+ expect(page).to have_no_link('Homepage', id: 'logo')
within '.top-nav-responsive' do
expect(page).to have_link(nil, href: search_path)
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 6630956f835..3c39d8745a4 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -40,59 +40,28 @@ RSpec.describe 'Import/Export - project export integration test', :js, feature_c
sign_in(user)
end
- context "with streaming serializer" do
- before do
- stub_feature_flags(project_export_as_ndjson: false)
- end
-
- it 'exports a project successfully', :sidekiq_inline do
- export_project_and_download_file(page, project)
-
- in_directory_with_expanded_export(project) do |exit_status, tmpdir|
- expect(exit_status).to eq(0)
+ it 'exports a project successfully', :sidekiq_inline do
+ export_project_and_download_file(page, project)
- project_json_path = File.join(tmpdir, 'project.json')
- expect(File).to exist(project_json_path)
+ in_directory_with_expanded_export(project) do |exit_status, tmpdir|
+ expect(exit_status).to eq(0)
- project_hash = Gitlab::Json.parse(File.read(project_json_path))
-
- sensitive_words.each do |sensitive_word|
- found = find_sensitive_attributes(sensitive_word, project_hash)
+ project_json_path = File.join(tmpdir, 'tree', 'project.json')
+ expect(File).to exist(project_json_path)
- expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
+ relations = []
+ relations << Gitlab::Json.parse(File.read(project_json_path))
+ Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename|
+ File.foreach(rb_filename) do |line|
+ relations << Gitlab::Json.parse(line)
end
end
- end
- end
- context "with ndjson" do
- before do
- stub_feature_flags(project_export_as_ndjson: true)
- end
-
- it 'exports a project successfully', :sidekiq_inline do
- export_project_and_download_file(page, project)
-
- in_directory_with_expanded_export(project) do |exit_status, tmpdir|
- expect(exit_status).to eq(0)
-
- project_json_path = File.join(tmpdir, 'tree', 'project.json')
- expect(File).to exist(project_json_path)
-
- relations = []
- relations << Gitlab::Json.parse(File.read(project_json_path))
- Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename|
- File.foreach(rb_filename) do |line|
- relations << Gitlab::Json.parse(line)
- end
- end
-
- relations.each do |relation_hash|
- sensitive_words.each do |sensitive_word|
- found = find_sensitive_attributes(sensitive_word, relation_hash)
+ relations.each do |relation_hash|
+ sensitive_words.each do |sensitive_word|
+ found = find_sensitive_attributes(sensitive_word, relation_hash)
- expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
- end
+ expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
end
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index b93da033aea..d34d72920dd 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
index d6632c5121a..1ecfa5a80f9 100644
--- a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
+++ b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
index e5f6f195fe5..71a0ade3eba 100644
--- a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
+++ b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/tree/project.json b/spec/fixtures/lib/gitlab/import_export/designs/tree/project.json
new file mode 100644
index 00000000000..3adcb693aeb
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/designs/tree/project.json
@@ -0,0 +1,15 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
+ "visibility_level": 10,
+ "archived": false,
+ "deploy_keys": [
+
+ ],
+ "hooks": [
+
+ ],
+ "shared_runners_enabled": true,
+ "ci_config_path": "config/path"
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/designs/tree/project/issues.ndjson
new file mode 100644
index 00000000000..3f767505bfb
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/designs/tree/project/issues.ndjson
@@ -0,0 +1,2 @@
+{"id":469,"title":"issue 1","author_id":1,"project_id":30,"created_at":"2019-08-07T03:57:55.007Z","updated_at":"2019-08-07T03:57:55.007Z","description":"","state":"opened","iid":1,"updated_by_id":null,"weight":null,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":0,"time_estimate":0,"relative_position":1073742323,"external_author":null,"last_edited_at":null,"last_edited_by_id":null,"discussion_locked":null,"closed_at":null,"closed_by_id":null,"state_id":1,"events":[{"id":1775,"project_id":30,"author_id":1,"target_id":469,"created_at":"2019-08-07T03:57:55.158Z","updated_at":"2019-08-07T03:57:55.158Z","target_type":"Issue","action":1}],"timelogs":[],"notes":[],"label_links":[],"resource_label_events":[],"issue_assignees":[],"designs":[{"id":38,"iid":1,"project_id":30,"issue_id":469,"filename":"chirrido3.jpg","notes":[]},{"id":39,"iid":2,"project_id":30,"issue_id":469,"filename":"jonathan_richman.jpg","notes":[]},{"id":40,"iid":3,"project_id":30,"issue_id":469,"filename":"mariavontrap.jpeg","notes":[]}],"design_versions":[{"id":24,"sha":"9358d1bac8ff300d3d2597adaa2572a20f7f8703","issue_id":469,"author_id":1,"actions":[{"design_id":38,"version_id":24,"event":0,"design":{"id":38,"iid":1,"project_id":30,"issue_id":469,"filename":"chirrido3.jpg"}}]},{"id":25,"sha":"e1a4a501bcb42f291f84e5d04c8f927821542fb6","issue_id":469,"author_id":2,"actions":[{"design_id":38,"version_id":25,"event":1,"design":{"id":38,"iid":1,"project_id":30,"issue_id":469,"filename":"chirrido3.jpg"}},{"design_id":39,"version_id":25,"event":0,"design":{"id":39,"iid":2,"project_id":30,"issue_id":469,"filename":"jonathan_richman.jpg"}}]},{"id":26,"sha":"27702d08f5ee021ae938737f84e8fe7c38599e85","issue_id":469,"author_id":1,"actions":[{"design_id":38,"version_id":26,"event":1,"design":{"id":38,"iid":1,"project_id":30,"issue_id":469,"filename":"chirrido3.jpg"}},{"design_id":39,"version_id":26,"event":2,"design":{"id":39,"iid":2,"project_id":30,"issue_id":469,"filename":"jonathan_richman.jpg"}},{"design_id":40,"version_id":26,"event":0,"design":{"id":40,"iid":3,"project_id":30,"issue_id":469,"filename":"mariavontrap.jpeg"}}]}]}
+{"id":470,"title":"issue 2","author_id":1,"project_id":30,"created_at":"2019-08-07T04:15:57.607Z","updated_at":"2019-08-07T04:15:57.607Z","description":"","state":"opened","iid":2,"updated_by_id":null,"weight":null,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":0,"time_estimate":0,"relative_position":1073742823,"external_author":null,"last_edited_at":null,"last_edited_by_id":null,"discussion_locked":null,"closed_at":null,"closed_by_id":null,"state_id":1,"events":[{"id":1776,"project_id":30,"author_id":1,"target_id":470,"created_at":"2019-08-07T04:15:57.789Z","updated_at":"2019-08-07T04:15:57.789Z","target_type":"Issue","action":1}],"timelogs":[],"notes":[],"label_links":[],"resource_label_events":[],"issue_assignees":[],"designs":[{"id":42,"project_id":30,"issue_id":470,"filename":"1 (1).jpeg","notes":[]},{"id":43,"project_id":30,"issue_id":470,"filename":"2099743.jpg","notes":[]},{"id":44,"project_id":30,"issue_id":470,"filename":"a screenshot (1).jpg","notes":[]},{"id":41,"project_id":30,"issue_id":470,"filename":"chirrido3.jpg","notes":[]}],"design_versions":[{"id":27,"sha":"8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8","issue_id":470,"author_id":1,"actions":[{"design_id":41,"version_id":27,"event":0,"design":{"id":41,"project_id":30,"issue_id":470,"filename":"chirrido3.jpg"}}]},{"id":28,"sha":"73f871b4c8c1d65c62c460635e023179fb53abc4","issue_id":470,"author_id":2,"actions":[{"design_id":42,"version_id":28,"event":0,"design":{"id":42,"project_id":30,"issue_id":470,"filename":"1 (1).jpeg"}},{"design_id":43,"version_id":28,"event":0,"design":{"id":43,"project_id":30,"issue_id":470,"filename":"2099743.jpg"}}]},{"id":29,"sha":"c9b5f067f3e892122a4b12b0a25a8089192f3ac8","issue_id":470,"author_id":2,"actions":[{"design_id":42,"version_id":29,"event":1,"design":{"id":42,"project_id":30,"issue_id":470,"filename":"1 (1).jpeg"}},{"design_id":44,"version_id":29,"event":0,"design":{"id":44,"project_id":30,"issue_id":470,"filename":"a screenshot (1).jpg"}}]}]} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/tree/project/project_members.ndjson b/spec/fixtures/lib/gitlab/import_export/designs/tree/project/project_members.ndjson
new file mode 100644
index 00000000000..570fd4a0c05
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/designs/tree/project/project_members.ndjson
@@ -0,0 +1,2 @@
+{"id":95,"access_level":40,"source_id":30,"source_type":"Project","user_id":1,"notification_level":3,"created_at":"2019-08-07T03:57:32.825Z","updated_at":"2019-08-07T03:57:32.825Z","created_by_id":1,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1,"public_email":"admin@example.com","username":"root"}}
+{"id":96,"access_level":40,"source_id":30,"source_type":"Project","user_id":2,"notification_level":3,"created_at":"2019-08-07T03:57:32.825Z","updated_at":"2019-08-07T03:57:32.825Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":2,"public_email":"user_2@gitlabexample.com","username":"user_2"}} \ No newline at end of file
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
index 0a5b16575df..629272c0bf0 100644
--- a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -50,6 +50,18 @@ describe('RegistrationInstructions', () => {
await waitForPromises();
};
+ const mockBeforeunload = () => {
+ const event = new Event('beforeunload');
+ const preventDefault = jest.spyOn(event, 'preventDefault');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
+
+ return {
+ event,
+ preventDefault,
+ returnValueSetter,
+ };
+ };
+
const mockResolvedRunner = (runner = mockRunner) => {
mockRunnerQuery.mockResolvedValue({
data: {
@@ -266,6 +278,20 @@ describe('RegistrationInstructions', () => {
it('does not show success message', () => {
expect(wrapper.text()).not.toContain(I18N_REGISTRATION_SUCCESS);
});
+
+ describe('when the page is closing', () => {
+ it('warns the user against closing', async () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).toHaveBeenCalledWith();
+ expect(returnValueSetter).toHaveBeenCalledWith(expect.any(String));
+ });
+ });
});
describe('when the runner has been registered', () => {
@@ -281,6 +307,20 @@ describe('RegistrationInstructions', () => {
expect(wrapper.text()).toContain('🎉');
expect(wrapper.text()).toContain(I18N_REGISTRATION_SUCCESS);
});
+
+ describe('when the page is closing', () => {
+ it('does not warn the user against closing', () => {
+ const { event, preventDefault, returnValueSetter } = mockBeforeunload();
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+
+ window.dispatchEvent(event);
+
+ expect(preventDefault).not.toHaveBeenCalled();
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ });
+ });
});
});
});
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index f04e766a78c..3b8a09714a7 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,6 +1,8 @@
import { getByText } from '@testing-library/dom';
+import htmlOpenIssue from 'test_fixtures/issues/open-issue.html';
+import htmlClosedIssue from 'test_fixtures/issues/closed-issue.html';
import MockAdapter from 'axios-mock-adapter';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
@@ -40,9 +42,9 @@ describe('Issue', () => {
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
- loadHTMLFixture('issues/open-issue.html');
+ setHTMLFixture(htmlOpenIssue);
} else {
- loadHTMLFixture('issues/closed-issue.html');
+ setHTMLFixture(htmlClosedIssue);
}
testContext.issueCounter = getIssueCounter();
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 908c6510bb2..a97164f9dce 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -1,7 +1,6 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -28,7 +27,7 @@ jest.mock('~/lib/utils/autosave');
const workItemId = workItemQueryResponse.data.workItem.id;
-describe('WorkItemCommentForm', () => {
+describe('Work item add note', () => {
let wrapper;
Vue.use(VueApollo);
@@ -38,6 +37,7 @@ describe('WorkItemCommentForm', () => {
let workItemResponseHandler;
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -50,7 +50,6 @@ describe('WorkItemCommentForm', () => {
workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
-
if (signedIn) {
window.gon.current_user_id = '1';
window.gon.current_user_avatar_url = 'avatar.png';
@@ -76,7 +75,7 @@ describe('WorkItemCommentForm', () => {
});
const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemAddNote, {
+ wrapper = shallowMountExtended(WorkItemAddNote, {
apolloProvider,
propsData: {
workItemId: id,
@@ -95,7 +94,7 @@ describe('WorkItemCommentForm', () => {
await waitForPromises();
if (isEditing) {
- wrapper.findComponent(GlButton).vm.$emit('click');
+ findTextarea().trigger('click');
}
};
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index bf36e3999d4..87db3d4573b 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -1,11 +1,23 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import * as autosave from '~/lib/utils/autosave';
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import {
+ STATE_OPEN,
+ STATE_CLOSED,
+ STATE_EVENT_REOPEN,
+ STATE_EVENT_CLOSE,
+} from '~/work_items/constants';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import { updateWorkItemMutationResponse, workItemQueryResponse } from 'jest/work_items/mock_data';
+
+Vue.use(VueApollo);
const draftComment = 'draft comment';
@@ -18,6 +30,8 @@ jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
confirmAction: jest.fn().mockResolvedValue(true),
}));
+const workItemId = 'gid://gitlab/WorkItem/1';
+
describe('Work item comment form component', () => {
let wrapper;
@@ -27,16 +41,29 @@ describe('Work item comment form component', () => {
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
- const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
+
+ const createComponent = ({
+ isSubmitting = false,
+ initialValue = '',
+ isNewDiscussion = false,
+ workItemState = STATE_OPEN,
+ workItemType = 'Task',
+ mutationHandler = mutationSuccessHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemCommentForm, {
+ apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
- workItemType: 'Issue',
+ workItemState,
+ workItemId,
+ workItemType,
ariaLabel: 'test-aria-label',
autosaveKey: mockAutosaveKey,
isSubmitting,
initialValue,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
+ isNewDiscussion,
},
provide: {
fullPath: 'test-project-path',
@@ -163,4 +190,63 @@ describe('Work item comment form component', () => {
expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
});
+
+ describe('when used as a top level/is a new discussion', () => {
+ describe('cancel button text', () => {
+ it.each`
+ workItemState | workItemType | buttonText
+ ${STATE_OPEN} | ${'Task'} | ${'Close task'}
+ ${STATE_CLOSED} | ${'Task'} | ${'Reopen task'}
+ ${STATE_OPEN} | ${'Objective'} | ${'Close objective'}
+ ${STATE_CLOSED} | ${'Objective'} | ${'Reopen objective'}
+ ${STATE_OPEN} | ${'Key result'} | ${'Close key result'}
+ ${STATE_CLOSED} | ${'Key result'} | ${'Reopen key result'}
+ `(
+ 'is "$buttonText" when "$workItemType" state is "$workItemState"',
+ ({ workItemState, workItemType, buttonText }) => {
+ createComponent({ isNewDiscussion: true, workItemState, workItemType });
+
+ expect(findCancelButton().text()).toBe(buttonText);
+ },
+ );
+ });
+
+ describe('Close/reopen button click', () => {
+ it.each`
+ workItemState | stateEvent
+ ${STATE_OPEN} | ${STATE_EVENT_CLOSE}
+ ${STATE_CLOSED} | ${STATE_EVENT_REOPEN}
+ `(
+ 'calls mutation with "$stateEvent" when workItemState is "$workItemState"',
+ async ({ workItemState, stateEvent }) => {
+ createComponent({ isNewDiscussion: true, workItemState });
+
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: workItemQueryResponse.data.workItem.id,
+ stateEvent,
+ },
+ });
+ },
+ );
+
+ it('emits an error message when the mutation was unsuccessful', async () => {
+ createComponent({
+ isNewDiscussion: true,
+ mutationHandler: jest.fn().mockRejectedValue('Error!'),
+ });
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the task. Please try again.'],
+ ]);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 17bbdf78458..69b7c7b0828 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,4 +1,3 @@
-import { GlAvatarLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -54,7 +53,6 @@ describe('Work Item Note', () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
- const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
@@ -75,10 +73,10 @@ describe('Work Item Note', () => {
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
+ workItemId,
note,
isFirstNote,
workItemType: 'Task',
- workItemId,
markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem',
autocompleteDataSources: {},
assignees,
@@ -245,10 +243,6 @@ describe('Work Item Note', () => {
expect(findNoteActions().exists()).toBe(true);
});
- it('should have the Avatar link for comment threads', () => {
- expect(findAuthorAvatarLink().exists()).toBe(true);
- });
-
it('should not have the reply button props', () => {
expect(findNoteActions().props('showReply')).toBe(false);
});
diff --git a/spec/graphql/types/permission_types/work_item_spec.rb b/spec/graphql/types/permission_types/work_item_spec.rb
index c710f7d169e..db6d78b1538 100644
--- a/spec/graphql/types/permission_types/work_item_spec.rb
+++ b/spec/graphql/types/permission_types/work_item_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Types::PermissionTypes::WorkItem do
it do
expected_permissions = [
- :read_work_item, :update_work_item, :delete_work_item, :admin_work_item, :set_work_item_metadata
+ :read_work_item, :update_work_item, :delete_work_item, :admin_work_item
]
expected_permissions.each do |permission|
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
deleted file mode 100644
index 9d766eb3af1..00000000000
--- a/spec/lib/gitlab/import_export/fork_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'forked project import' do
- include ProjectForksHelper
-
- let(:user) { create(:user) }
- let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
- let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
- let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
- let(:shared) { project.import_export_shared }
- let(:forked_from_project) { create(:project, :repository) }
- let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
- let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(exportable: project_with_repo, shared: shared) }
- let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
-
- let(:repo_restorer) do
- Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, importable: project)
- end
-
- let!(:merge_request) do
- create(:merge_request, source_project: forked_project, target_project: project_with_repo)
- end
-
- let(:saver) do
- Gitlab::ImportExport::Project::TreeSaver.new(project: project_with_repo, current_user: user, shared: shared)
- end
-
- let(:restorer) do
- Gitlab::ImportExport::Project::TreeRestorer.new(user: user, shared: shared, project: project)
- end
-
- before do
- stub_feature_flags(project_export_as_ndjson: false)
-
- allow_next_instance_of(Gitlab::ImportExport) do |instance|
- allow(instance).to receive(:storage_path).and_return(export_path)
- end
-
- saver.save # rubocop:disable Rails/SaveBang
- repo_saver.save # rubocop:disable Rails/SaveBang
-
- repo_restorer.restore
- restorer.restore
- end
-
- after do
- FileUtils.rm_rf(export_path)
- project_with_repo.repository.remove
- project.repository.remove
- end
-
- it 'can access the MR', :sidekiq_might_not_need_inline do
- project.merge_requests.first.fetch_ref!
-
- expect(project.repository.ref_exists?('refs/merge-requests/1/head')).to be_truthy
- end
-end
diff --git a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
index 07971d6271c..495cefa002a 100644
--- a/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/relation_tree_restorer_spec.rb
@@ -14,20 +14,26 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_catego
let(:importable) { create(:group, parent: group) }
include_context 'relation tree restorer shared context' do
- let(:importable_name) { nil }
+ let(:importable_name) { 'groups/4353' }
end
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/group_exports/no_children/group.json' }
+ let(:path) { Rails.root.join('spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree') }
let(:relation_reader) do
- Gitlab::ImportExport::Json::LegacyReader::File.new(
- path,
- relation_names: reader.group_relation_names)
+ Gitlab::ImportExport::Json::NdjsonReader.new(path)
end
let(:reader) do
Gitlab::ImportExport::Reader.new(
shared: shared,
- config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
+ config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.group_config_file).to_h
+ )
+ end
+
+ let(:members_mapper) do
+ Gitlab::ImportExport::MembersMapper.new(
+ exported_members: relation_reader.consume_relation(importable_name, 'members').map(&:first),
+ user: user,
+ importable: importable
)
end
@@ -41,7 +47,7 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_catego
relation_factory: Gitlab::ImportExport::Group::RelationFactory,
reader: reader,
importable: importable,
- importable_path: nil,
+ importable_path: importable_name,
importable_attributes: attributes
)
end
@@ -62,20 +68,13 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_catego
end
describe 'relation object saving' do
- let(:importable) { create(:group) }
- let(:relation_reader) do
- Gitlab::ImportExport::Json::LegacyReader::File.new(
- path,
- relation_names: [:labels])
- end
-
before do
allow(shared.logger).to receive(:info).and_call_original
allow(relation_reader).to receive(:consume_relation).and_call_original
allow(relation_reader)
.to receive(:consume_relation)
- .with(nil, 'labels')
+ .with(importable_name, 'labels')
.and_return([[label, 0]])
end
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
index aa30e24296e..a6afd0a36ec 100644
--- a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Group::TreeRestorer, feature: :subgroups do
+RSpec.describe Gitlab::ImportExport::Group::TreeRestorer, feature: :subgroups, feature_category: :importers do
include ImportExport::CommonUtil
shared_examples 'group restoration' do
@@ -171,7 +171,7 @@ RSpec.describe Gitlab::ImportExport::Group::TreeRestorer, feature: :subgroups do
allow(shared).to receive(:export_path).and_return(tmpdir)
expect(group_tree_restorer.restore).to eq(false)
- expect(shared.errors).to include('Incorrect JSON format')
+ expect(shared.errors).to include('Invalid file')
end
end
end
diff --git a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb b/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
deleted file mode 100644
index 6c997dc1361..00000000000
--- a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# Verifies that given an exported project meta-data tree, when importing this
-# tree and then exporting it again, we should obtain the initial tree.
-#
-# This equivalence only works up to a certain extent, for instance we need
-# to ignore:
-#
-# - row IDs and foreign key IDs
-# - some timestamps
-# - randomly generated fields like tokens
-#
-# as these are expected to change between import/export cycles.
-RSpec.describe Gitlab::ImportExport, feature_category: :importers do
- include ImportExport::CommonUtil
- include ConfigurationHelper
- include ImportExport::ProjectTreeExpectations
-
- let(:json_fixture) { 'complex' }
-
- before do
- stub_feature_flags(project_export_as_ndjson: false)
- end
-
- it 'yields the initial tree when importing and exporting it again' do
- project = create(:project)
- user = create(:user, :admin)
-
- # We first generate a test fixture dynamically from a seed-fixture, so as to
- # account for any fields in the initial fixture that are missing and set to
- # defaults during import (ideally we should have realistic test fixtures
- # that "honestly" represent exports)
- expect(
- restore_then_save_project(
- project,
- user,
- import_path: seed_fixture_path,
- export_path: test_fixture_path)
- ).to be true
- # Import, then export again from the generated fixture. Any residual changes
- # in the JSON will count towards comparison i.e. test failures.
- expect(
- restore_then_save_project(
- project,
- user,
- import_path: test_fixture_path,
- export_path: test_tmp_path)
- ).to be true
-
- imported_json = Gitlab::Json.parse(File.read("#{test_fixture_path}/project.json"))
- exported_json = Gitlab::Json.parse(File.read("#{test_tmp_path}/project.json"))
-
- assert_relations_match(imported_json, exported_json)
- end
-
- private
-
- def seed_fixture_path
- "#{fixtures_path}/#{json_fixture}"
- end
-
- def test_fixture_path
- "#{test_tmp_path}/#{json_fixture}"
- end
-end
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
deleted file mode 100644
index 793b3ebfb9e..00000000000
--- a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative 'shared_example'
-
-RSpec.describe Gitlab::ImportExport::Json::LegacyReader::File do
- it_behaves_like 'import/export json legacy reader' do
- let(:valid_path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
- let(:data) { valid_path }
- let(:json_data) { Gitlab::Json.parse(File.read(valid_path)) }
- end
-
- describe '#exist?' do
- let(:legacy_reader) do
- described_class.new(path, relation_names: [])
- end
-
- subject { legacy_reader.exist? }
-
- context 'given valid path' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
-
- it { is_expected.to be true }
- end
-
- context 'given invalid path' do
- let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' }
-
- it { is_expected.to be false }
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
deleted file mode 100644
index 57d66dc0f50..00000000000
--- a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require_relative 'shared_example'
-
-RSpec.describe Gitlab::ImportExport::Json::LegacyReader::Hash do
- it_behaves_like 'import/export json legacy reader' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
-
- # the hash is modified by the `LegacyReader`
- # we need to deep-dup it
- let(:json_data) { Gitlab::Json.parse(File.read(path)) }
- let(:data) { Gitlab::Json.parse(File.read(path)) }
- end
-
- describe '#exist?' do
- let(:legacy_reader) do
- described_class.new(tree_hash, relation_names: [])
- end
-
- subject { legacy_reader.exist? }
-
- context 'tree_hash is nil' do
- let(:tree_hash) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'tree_hash presents' do
- let(:tree_hash) { { "issues": [] } }
-
- it { is_expected.to be_truthy }
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/shared_example.rb b/spec/lib/gitlab/import_export/json/legacy_reader/shared_example.rb
deleted file mode 100644
index 3e9bd3fe741..00000000000
--- a/spec/lib/gitlab/import_export/json/legacy_reader/shared_example.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'import/export json legacy reader' do
- let(:relation_names) { [] }
-
- let(:legacy_reader) do
- described_class.new(
- data,
- relation_names: relation_names,
- allowed_path: "project")
- end
-
- describe '#consume_attributes' do
- context 'when valid path is passed' do
- subject { legacy_reader.consume_attributes("project") }
-
- context 'no excluded attributes' do
- let(:relation_names) { [] }
-
- it 'returns the whole tree from parsed JSON' do
- expect(subject).to eq(json_data)
- end
- end
-
- context 'some attributes are excluded' do
- let(:relation_names) { %w[milestones labels] }
-
- it 'returns hash without excluded attributes and relations' do
- expect(subject).not_to include('milestones', 'labels')
- end
- end
- end
-
- context 'when invalid path is passed' do
- it 'raises an exception' do
- expect { legacy_reader.consume_attributes("invalid-path") }
- .to raise_error(ArgumentError)
- end
- end
- end
-
- describe '#consume_relation' do
- context 'when valid path is passed' do
- let(:key) { 'labels' }
-
- subject { legacy_reader.consume_relation("project", key) }
-
- context 'key has not been consumed' do
- it 'returns an Enumerator' do
- expect(subject).to be_an_instance_of(Enumerator)
- end
-
- context 'value is nil' do
- before do
- expect(legacy_reader).to receive(:relations).and_return({ key => nil })
- end
-
- it 'yields nothing to the Enumerator' do
- expect(subject.to_a).to eq([])
- end
- end
-
- context 'value is an array' do
- before do
- expect(legacy_reader).to receive(:relations).and_return({ key => %w[label1 label2] })
- end
-
- it 'yields every relation value to the Enumerator' do
- expect(subject.to_a).to eq([['label1', 0], ['label2', 1]])
- end
- end
-
- context 'value is not array' do
- before do
- expect(legacy_reader).to receive(:relations).and_return({ key => 'non-array value' })
- end
-
- it 'yields the value with index 0 to the Enumerator' do
- expect(subject.to_a).to eq([['non-array value', 0]])
- end
- end
- end
-
- context 'key has been consumed' do
- before do
- legacy_reader.consume_relation("project", key).first
- end
-
- it 'yields nothing to the Enumerator' do
- expect(subject.to_a).to eq([])
- end
- end
- end
-
- context 'when invalid path is passed' do
- it 'raises an exception' do
- expect { legacy_reader.consume_relation("invalid") }
- .to raise_error(ArgumentError)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb b/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
deleted file mode 100644
index 2c0f023ad2c..00000000000
--- a/spec/lib/gitlab/import_export/json/legacy_writer_spec.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'tmpdir'
-
-RSpec.describe Gitlab::ImportExport::Json::LegacyWriter, feature_category: :importers do
- let(:path) { "#{Dir.tmpdir}/legacy_writer_spec/test.json" }
-
- subject do
- described_class.new(path, allowed_path: "project")
- end
-
- after do
- FileUtils.rm_rf(path)
- end
-
- describe "#write_attributes" do
- it "writes correct json" do
- expected_hash = { "key" => "value_1", "key_1" => "value_2" }
- subject.write_attributes("project", expected_hash)
-
- expect(subject_json).to eq(expected_hash)
- end
-
- context 'when invalid path is used' do
- it 'raises an exception' do
- expect { subject.write_attributes("invalid", { "key" => "value" }) }
- .to raise_error(ArgumentError)
- end
- end
- end
-
- describe "#write_relation" do
- context "when key is already written" do
- it "raises exception" do
- subject.write_relation("project", "key", "old value")
-
- expect { subject.write_relation("project", "key", "new value") }
- .to raise_exception("key 'key' already written")
- end
- end
-
- context "when key is not already written" do
- context "when multiple key value pairs are stored" do
- it "writes correct json" do
- expected_hash = { "key" => "value_1", "key_1" => "value_2" }
- expected_hash.each do |key, value|
- subject.write_relation("project", key, value)
- end
-
- expect(subject_json).to eq(expected_hash)
- end
- end
- end
-
- context 'when invalid path is used' do
- it 'raises an exception' do
- expect { subject.write_relation("invalid", "key", "value") }
- .to raise_error(ArgumentError)
- end
- end
- end
-
- describe "#write_relation_array" do
- context 'when array is used' do
- it 'writes correct json' do
- subject.write_relation_array("project", "key", ["value"])
-
- expect(subject_json).to eq({ "key" => ["value"] })
- end
- end
-
- context 'when enumerable is used' do
- it 'writes correct json' do
- values = %w(value1 value2)
-
- enumerator = Enumerator.new do |items|
- values.each { |value| items << value }
- end
-
- subject.write_relation_array("project", "key", enumerator)
-
- expect(subject_json).to eq({ "key" => values })
- end
- end
-
- context "when key is already written" do
- it "raises an exception" do
- subject.write_relation_array("project", "key", %w(old_value))
-
- expect { subject.write_relation_array("project", "key", %w(new_value)) }
- .to raise_error(ArgumentError)
- end
- end
- end
-
- def subject_json
- subject.close
-
- ::JSON.parse(File.read(subject.path))
- end
-end
diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
index 0ca4c4ccc87..98afe01c08b 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::ImportExport::Json::NdjsonReader do
+RSpec.describe Gitlab::ImportExport::Json::NdjsonReader, feature_category: :importers do
include ImportExport::CommonUtil
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/tree' }
@@ -26,14 +26,6 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonReader do
end
end
- describe '#legacy?' do
- let(:dir_path) { fixture }
-
- subject { ndjson_reader.legacy? }
-
- it { is_expected.to be false }
- end
-
describe '#consume_attributes' do
let(:dir_path) { fixture }
@@ -42,6 +34,20 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonReader do
it 'returns the whole root tree from parsed JSON' do
expect(subject).to eq(root_tree)
end
+
+ context 'when project.json is symlink' do
+ it 'raises error an error' do
+ Dir.mktmpdir do |tmpdir|
+ FileUtils.touch(File.join(tmpdir, 'passwd'))
+ File.symlink(File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project.json'))
+
+ ndjson_reader = described_class.new(tmpdir)
+
+ expect { ndjson_reader.consume_attributes(importable_path) }
+ .to raise_error(Gitlab::ImportExport::Error, 'Invalid file')
+ end
+ end
+ end
end
describe '#consume_relation' do
@@ -91,6 +97,22 @@ RSpec.describe Gitlab::ImportExport::Json::NdjsonReader do
end
end
+ context 'when relation file is a symlink' do
+ it 'yields nothing to the Enumerator' do
+ Dir.mktmpdir do |tmpdir|
+ Dir.mkdir(File.join(tmpdir, 'project'))
+ File.write(File.join(tmpdir, 'passwd'), "{}\n{}")
+ File.symlink(File.join(tmpdir, 'passwd'), File.join(tmpdir, 'project', 'issues.ndjson'))
+
+ ndjson_reader = described_class.new(tmpdir)
+
+ result = ndjson_reader.consume_relation(importable_path, 'issues')
+
+ expect(result.to_a).to eq([])
+ end
+ end
+ end
+
context 'relation file is empty' do
let(:key) { 'empty' }
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index 103d3512e8b..f4c9189030b 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer, feature_category
let(:exportable_path) { 'project' }
let(:logger) { Gitlab::Export::Logger.build }
- let(:json_writer) { instance_double('Gitlab::ImportExport::Json::LegacyWriter') }
+ let(:json_writer) { instance_double('Gitlab::ImportExport::Json::NdjsonWriter') }
let(:hash) { { name: exportable.name, description: exportable.description }.stringify_keys }
let(:include) { [] }
let(:custom_orderer) { nil }
diff --git a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
index 75012aa80ec..180a6b6ff0a 100644
--- a/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/relation_tree_restorer_spec.rb
@@ -55,54 +55,19 @@ RSpec.describe Gitlab::ImportExport::Project::RelationTreeRestorer, feature_cate
end
end
- context 'with legacy reader' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
- let(:relation_reader) do
- Gitlab::ImportExport::Json::LegacyReader::File.new(
- path,
- relation_names: reader.project_relation_names,
- allowed_path: 'project'
- )
- end
-
- let(:attributes) { relation_reader.consume_attributes('project') }
-
- it_behaves_like 'import project successfully'
-
- context 'with logging of relations creation' do
- let_it_be(:group) { create(:group).tap { |g| g.add_maintainer(user) } }
- let_it_be(:importable) do
- create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project', group: group)
- end
-
- it 'logs top-level relation creation' do
- expect(shared.logger)
- .to receive(:info)
- .with(hash_including(message: '[Project/Group Import] Created new object relation'))
- .at_least(:once)
-
- subject
- end
- end
- end
-
- context 'with ndjson reader' do
+ context 'when inside a group' do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
let(:relation_reader) { Gitlab::ImportExport::Json::NdjsonReader.new(path) }
- it_behaves_like 'import project successfully'
-
- context 'when inside a group' do
- let_it_be(:group) do
- create(:group, :disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
- end
-
- before do
- importable.update!(shared_runners_enabled: false, group: group)
- end
+ let_it_be(:group) do
+ create(:group, :disabled_and_unoverridable).tap { |g| g.add_maintainer(user) }
+ end
- it_behaves_like 'import project successfully'
+ before do
+ importable.update!(shared_runners_enabled: false, group: group)
end
+
+ it_behaves_like 'import project successfully'
end
context 'with invalid relations' do
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index a07fe4fd29c..5aa16f9508d 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
let(:shared) { project.import_export_shared }
- RSpec.shared_examples 'project tree restorer work properly' do |reader, ndjson_enabled|
+ RSpec.shared_examples 'project tree restorer work properly' do
describe 'restore project tree' do
before_all do
# Using an admin for import, so we can check assignment of existing members
@@ -27,10 +27,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
@shared = @project.import_export_shared
stub_all_feature_flags
- stub_feature_flags(project_import_ndjson: ndjson_enabled)
setup_import_export_config('complex')
- setup_reader(reader)
+ setup_reader
allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
@@ -606,23 +605,15 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
end
end
- context 'project.json file access check' do
+ context 'when expect tree structure is not present in the export path' do
let(:user) { create(:user) }
- let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
- let(:project_tree_restorer) do
- described_class.new(user: user, shared: shared, project: project)
- end
+ let_it_be(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
- let(:restored_project_json) { project_tree_restorer.restore }
+ it 'fails to restore the project' do
+ result = described_class.new(user: user, shared: shared, project: project).restore
- it 'does not read a symlink' do
- Dir.mktmpdir do |tmpdir|
- setup_symlink(tmpdir, 'project.json')
- allow(shared).to receive(:export_path).and_call_original
-
- expect(project_tree_restorer.restore).to eq(false)
- expect(shared.errors).to include('invalid import format')
- end
+ expect(result).to eq(false)
+ expect(shared.errors).to include('invalid import format')
end
end
@@ -635,7 +626,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'with a simple project' do
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
expect(restored_project_json).to eq(true)
end
@@ -670,7 +661,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'multiple pipelines reference the same external pull request' do
before do
setup_import_export_config('multi_pipeline_ref_one_external_pr')
- setup_reader(reader)
+ setup_reader
expect(restored_project_json).to eq(true)
end
@@ -698,7 +689,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
expect(project).to receive(:merge_requests).and_call_original
expect(project).to receive(:merge_requests).and_raise(exception)
@@ -715,7 +706,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
expect(project).to receive(:merge_requests).and_call_original
expect(project).to receive(:merge_requests).and_raise(exception)
@@ -747,7 +738,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'when the project has overridden params in import data' do
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
end
it 'handles string versions of visibility_level' do
@@ -813,7 +804,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('group')
- setup_reader(reader)
+ setup_reader
expect(restored_project_json).to eq(true)
end
@@ -849,7 +840,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
end
it 'imports labels' do
@@ -885,7 +876,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('milestone-iid')
- setup_reader(reader)
+ setup_reader
end
it 'preserves the project milestone IID' do
@@ -901,7 +892,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
context 'with external authorization classification labels' do
before do
setup_import_export_config('light')
- setup_reader(reader)
+ setup_reader
end
it 'converts empty external classification authorization labels to nil' do
@@ -928,76 +919,80 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
described_class.new(user: user, shared: shared, project: project)
end
- before do
- allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(true)
- allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(false)
- allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:tree_hash) { tree_hash }
- end
-
- context 'no group visibility' do
- let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
+ describe 'visibility level' do
+ before do
+ setup_import_export_config('light')
- it 'uses the project visibility' do
- expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(visibility)
+ allow_next_instance_of(Gitlab::ImportExport::Json::NdjsonReader) do |relation_reader|
+ allow(relation_reader).to receive(:consume_attributes).and_return(tree_hash)
+ end
end
- end
-
- context 'with restricted internal visibility' do
- describe 'internal project' do
- let(:visibility) { Gitlab::VisibilityLevel::INTERNAL }
- it 'uses private visibility' do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ context 'no group visibility' do
+ let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
+ it 'uses the project visibility' do
expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(restorer.project.visibility_level).to eq(visibility)
end
end
- end
- context 'with group visibility' do
- before do
- group = create(:group, visibility_level: group_visibility)
- group.add_members([user], GroupMember::MAINTAINER)
- project.update!(group: group)
- end
+ context 'with restricted internal visibility' do
+ describe 'internal project' do
+ let(:visibility) { Gitlab::VisibilityLevel::INTERNAL }
- context 'private group visibility' do
- let(:group_visibility) { Gitlab::VisibilityLevel::PRIVATE }
- let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+ it 'uses private visibility' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
- it 'uses the group visibility' do
- expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(group_visibility)
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
end
end
- context 'public group visibility' do
- let(:group_visibility) { Gitlab::VisibilityLevel::PUBLIC }
- let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
+ context 'with group visibility' do
+ before do
+ group = create(:group, visibility_level: group_visibility)
+ group.add_members([user], GroupMember::MAINTAINER)
+ project.update!(group: group)
+ end
- it 'uses the project visibility' do
- expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(visibility)
+ context 'private group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+
+ it 'uses the group visibility' do
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(group_visibility)
+ end
end
- end
- context 'internal group visibility' do
- let(:group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
- let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+ context 'public group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:visibility) { Gitlab::VisibilityLevel::PRIVATE }
- it 'uses the group visibility' do
- expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(group_visibility)
+ it 'uses the project visibility' do
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(visibility)
+ end
end
- context 'with restricted internal visibility' do
- it 'sets private visibility' do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ context 'internal group visibility' do
+ let(:group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
+ let(:visibility) { Gitlab::VisibilityLevel::PUBLIC }
+ it 'uses the group visibility' do
expect(restorer.restore).to eq(true)
- expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(restorer.project.visibility_level).to eq(group_visibility)
+ end
+
+ context 'with restricted internal visibility' do
+ it 'sets private visibility' do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+
+ expect(restorer.restore).to eq(true)
+ expect(restorer.project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
end
end
end
@@ -1008,24 +1003,35 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
let(:user2) { create(:user) }
let(:project_members) do
[
- {
- "id" => 2,
- "access_level" => 40,
- "source_type" => "Project",
- "notification_level" => 3,
- "user" => {
- "id" => user2.id,
- "email" => user2.email,
- "username" => 'test'
- }
- }
+ [
+ {
+ "id" => 2,
+ "access_level" => 40,
+ "source_type" => "Project",
+ "notification_level" => 3,
+ "user" => {
+ "id" => user2.id,
+ "email" => user2.email,
+ "username" => 'test'
+ }
+ },
+ 0
+ ]
]
end
- let(:tree_hash) { { 'project_members' => project_members } }
-
before do
project.add_maintainer(user)
+
+ setup_import_export_config('light')
+
+ allow_next_instance_of(Gitlab::ImportExport::Json::NdjsonReader) do |relation_reader|
+ allow(relation_reader).to receive(:consume_relation).and_call_original
+
+ allow(relation_reader).to receive(:consume_relation)
+ .with('project', 'project_members')
+ .and_return(project_members)
+ end
end
it 'restores project members' do
@@ -1045,7 +1051,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
before do
setup_import_export_config('with_invalid_records')
- setup_reader(reader)
+ setup_reader
subject
end
@@ -1138,13 +1144,5 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer, feature_category: :i
end
end
- context 'enable ndjson import' do
- it_behaves_like 'project tree restorer work properly', :legacy_reader, true
-
- it_behaves_like 'project tree restorer work properly', :ndjson_reader, true
- end
-
- context 'disable ndjson import' do
- it_behaves_like 'project tree restorer work properly', :legacy_reader, false
- end
+ it_behaves_like 'project tree restorer work properly'
end
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index b87992c4594..4166eba4e8e 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -9,28 +9,21 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let_it_be(:group) { create(:group) }
let_it_be(:project) { setup_project }
- shared_examples 'saves project tree successfully' do |ndjson_enabled|
+ shared_examples 'saves project tree successfully' do
include ImportExport::CommonUtil
- subject { get_json(full_path, exportable_path, relation_name, ndjson_enabled) }
+ subject { get_json(full_path, exportable_path, relation_name) }
describe 'saves project tree attributes' do
let_it_be(:shared) { project.import_export_shared }
let(:relation_name) { :projects }
- let_it_be(:full_path) do
- if ndjson_enabled
- File.join(shared.export_path, 'tree')
- else
- File.join(shared.export_path, Gitlab::ImportExport.project_filename)
- end
- end
+ let_it_be(:full_path) { File.join(shared.export_path, 'tree') }
before_all do
RSpec::Mocks.with_temporary_scope do
stub_all_feature_flags
- stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project.add_maintainer(user)
@@ -300,13 +293,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let_it_be(:group) { create(:group) }
let(:project) { setup_project }
- let(:full_path) do
- if ndjson_enabled
- File.join(shared.export_path, 'tree')
- else
- File.join(shared.export_path, Gitlab::ImportExport.project_filename)
- end
- end
+ let(:full_path) { File.join(shared.export_path, 'tree') }
let(:shared) { project.import_export_shared }
let(:params) { {} }
@@ -314,7 +301,6 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
let(:project_tree_saver ) { described_class.new(project: project, current_user: user, shared: shared, params: params) }
before do
- stub_feature_flags(project_export_as_ndjson: ndjson_enabled)
project.add_maintainer(user)
FileUtils.rm_rf(export_path)
@@ -425,13 +411,7 @@ RSpec.describe Gitlab::ImportExport::Project::TreeSaver, :with_license, feature_
end
end
- context 'with JSON' do
- it_behaves_like "saves project tree successfully", false
- end
-
- context 'with NDJSON' do
- it_behaves_like "saves project tree successfully", true
- end
+ it_behaves_like "saves project tree successfully"
context 'when streaming has to retry', :aggregate_failures do
let(:shared) { double('shared', export_path: exportable_path) }
diff --git a/spec/models/awareness_session_spec.rb b/spec/models/awareness_session_spec.rb
deleted file mode 100644
index 854ce5957f7..00000000000
--- a/spec/models/awareness_session_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe AwarenessSession, :clean_gitlab_redis_shared_state do
- subject { AwarenessSession.for(session_id) }
-
- let!(:user) { create(:user) }
- let(:session_id) { 1 }
-
- describe "when initiating a session" do
- it "provides a string representation of the model instance" do
- expected = "awareness_session=6b86b273ff34fce"
-
- expect(subject.to_s).to eql(expected)
- end
-
- it "provides a parameterized version of the session identifier" do
- expected = "6b86b273ff34fce"
-
- expect(subject.to_param).to eql(expected)
- end
- end
-
- describe "when a user joins a session" do
- let(:user2) { create(:user) }
-
- let(:presence_ttl) { 15.minutes }
-
- it "changes number of session members" do
- expect { subject.join(user) }.to change(subject, :size).by(1)
- end
-
- it "returns user as member of session with last_activity timestamp" do
- freeze_time do
- subject.join(user)
-
- session_users = subject.users_with_last_activity
- session_user, last_activity = session_users.first
-
- expect(session_user.id).to be(user.id)
- expect(last_activity).to be_eql(Time.now.utc)
- end
- end
-
- it "maintains user ID and last_activity pairs" do
- now = Time.zone.now
-
- travel_to now - 1.minute do
- subject.join(user2)
- end
-
- travel_to now do
- subject.join(user)
- end
-
- session_users = subject.users_with_last_activity
-
- expect(session_users[0].first.id).to eql(user.id)
- expect(session_users[0].last.to_i).to eql(now.to_i)
-
- expect(session_users[1].first.id).to eql(user2.id)
- expect(session_users[1].last.to_i).to eql((now - 1.minute).to_i)
- end
-
- it "reports user as present" do
- freeze_time do
- subject.join(user)
-
- expect(subject.present?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "reports user as away after a certain time on inactivity" do
- subject.join(user)
-
- travel_to((presence_ttl + 1.minute).from_now) do
- expect(subject.away?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "reports user as present still when there was some activity" do
- subject.join(user)
-
- travel_to((presence_ttl - 1.minute).from_now) do
- subject.touch!(user)
- end
-
- travel_to((presence_ttl + 1.minute).from_now) do
- expect(subject.present?(user, threshold: presence_ttl)).to be true
- end
- end
-
- it "creates user and session awareness keys in store" do
- subject.join(user)
-
- Gitlab::Redis::SharedState.with do |redis|
- keys = redis.scan_each(match: "gitlab:awareness:*").to_a
-
- expect(keys.size).to be(2)
- end
- end
-
- it "sets a timeout for user and session key" do
- subject.join(user)
- subject_id = Digest::SHA256.hexdigest(session_id.to_s)[0, 15]
-
- Gitlab::Redis::SharedState.with do |redis|
- ttl_session = redis.ttl("gitlab:awareness:session:#{subject_id}:users")
- ttl_user = redis.ttl("gitlab:awareness:user:#{user.id}:sessions")
-
- expect(ttl_session).to be > 0
- expect(ttl_user).to be > 0
- end
- end
-
- it "fetches user(s) from database" do
- subject.join(user)
-
- expect(subject.users.first).to eql(user)
- end
-
- it "fetches and filters online user(s) from database" do
- subject.join(user)
-
- travel 2.hours do
- subject.join(user2)
-
- online_users = subject.online_users_with_last_activity
- online_user, _ = online_users.first
-
- expect(online_users.size).to be 1
- expect(online_user).to eql(user2)
- end
- end
- end
-
- describe "when a user leaves a session" do
- it "changes number of session members" do
- subject.join(user)
-
- expect { subject.leave(user) }.to change(subject, :size).by(-1)
- end
-
- it "destroys the session when it was the last user" do
- subject.join(user)
-
- expect { subject.leave(user) }.to change(subject, :id).to(nil)
- end
- end
-
- describe "when last user leaves a session" do
- it "session and user keys are removed" do
- subject.join(user)
-
- Gitlab::Redis::SharedState.with do |redis|
- expect { subject.leave(user) }
- .to change { redis.scan_each(match: "gitlab:awareness:*").to_a.size }
- .to(0)
- end
- end
- end
-end
diff --git a/spec/models/concerns/awareness_spec.rb b/spec/models/concerns/awareness_spec.rb
deleted file mode 100644
index 67acacc7bb1..00000000000
--- a/spec/models/concerns/awareness_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Awareness, :clean_gitlab_redis_shared_state do
- subject { create(:user) }
-
- let(:session) { AwarenessSession.for(1) }
-
- describe "when joining a session" do
- it "increases the number of sessions" do
- expect { subject.join(session) }
- .to change { subject.session_ids.size }
- .by(1)
- end
- end
-
- describe "when leaving session" do
- it "decreases the number of sessions" do
- subject.join(session)
-
- expect { subject.leave(session) }
- .to change { subject.session_ids.size }
- .by(-1)
- end
- end
-
- describe "when joining multiple sessions" do
- let(:session2) { AwarenessSession.for(2) }
-
- it "increases number of active sessions for user" do
- expect do
- subject.join(session)
- subject.join(session2)
- end.to change { subject.session_ids.size }
- .by(2)
- end
- end
-end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 8dfa577bc35..fe6f75548a5 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -59,8 +59,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'readWorkItem' => true,
'updateWorkItem' => true,
'deleteWorkItem' => false,
- 'adminWorkItem' => true,
- 'setWorkItemMetadata' => true
+ 'adminWorkItem' => true
},
'project' => hash_including('id' => project.to_gid.to_s, 'fullPath' => project.full_path)
)
@@ -498,25 +497,6 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
end
end
- context 'when the user cannot set work item metadata' do
- let(:current_user) { guest }
-
- before do
- project.add_guest(guest)
- post_graphql(query, current_user: current_user)
- end
-
- it 'returns correct user permission' do
- expect(work_item_data).to include(
- 'id' => work_item.to_gid.to_s,
- 'userPermissions' =>
- hash_including(
- 'setWorkItemMetadata' => false
- )
- )
- end
- end
-
context 'when the user can not read the work item' do
let(:current_user) { create(:user) }
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index f8f32fa59d1..53e943dc3bc 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -18,14 +18,8 @@ module ImportExport
allow(Gitlab::ImportExport).to receive(:export_path) { export_path }
end
- def setup_reader(reader)
- if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
- allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(false)
- allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(true)
- else
- allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(true)
- allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(false)
- end
+ def setup_reader
+ allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(true)
end
def fixtures_path
@@ -36,19 +30,12 @@ module ImportExport
"tmp/tests/gitlab-test/import_export"
end
- def get_json(path, exportable_path, key, ndjson_enabled)
- if ndjson_enabled
- json = if key == :projects
- consume_attributes(path, exportable_path)
- else
- consume_relations(path, exportable_path, key)
- end
+ def get_json(path, exportable_path, key)
+ if key == :projects
+ consume_attributes(path, exportable_path)
else
- json = project_json(path)
- json = json[key.to_s] unless key == :projects
+ consume_relations(path, exportable_path, key)
end
-
- json
end
def restore_then_save_project(project, user, import_path:, export_path:)
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index d6824bc2cd4..15743e6b695 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -3310,7 +3310,6 @@
- './spec/bin/feature_flag_spec.rb'
- './spec/bin/sidekiq_cluster_spec.rb'
- './spec/channels/application_cable/connection_spec.rb'
-- './spec/channels/awareness_channel_spec.rb'
- './spec/commands/metrics_server/metrics_server_spec.rb'
- './spec/commands/sidekiq_cluster/cli_spec.rb'
- './spec/components/diffs/overflow_warning_component_spec.rb'
@@ -7713,7 +7712,6 @@
- './spec/models/audit_event_spec.rb'
- './spec/models/authentication_event_spec.rb'
- './spec/models/award_emoji_spec.rb'
-- './spec/models/awareness_session_spec.rb'
- './spec/models/aws/role_spec.rb'
- './spec/models/badges/group_badge_spec.rb'
- './spec/models/badge_spec.rb'
@@ -7845,7 +7843,6 @@
- './spec/models/concerns/atomic_internal_id_spec.rb'
- './spec/models/concerns/avatarable_spec.rb'
- './spec/models/concerns/awardable_spec.rb'
-- './spec/models/concerns/awareness_spec.rb'
- './spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
- './spec/models/concerns/batch_nullify_dependent_associations_spec.rb'
- './spec/models/concerns/blob_language_from_git_attributes_spec.rb'
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index d2270a124b7..759cdc423e2 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -38,8 +38,6 @@ RSpec.shared_examples 'work items comments' do |type|
let(:comment) { 'Test comment' }
def set_comment
- click_button 'Add a reply'
-
find(form_selector).fill_in(with: comment)
end
@@ -69,7 +67,7 @@ RSpec.shared_examples 'work items comments' do |type|
find('[data-testid="work-item-note-actions"]', match: :first).click
expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).to have_selector('[data-testid="assign-note-action"]')
+ expect(page).not_to have_selector('[data-testid="assign-note-action"]')
end
end
end
@@ -85,8 +83,6 @@ RSpec.shared_examples 'work items comments' do |type|
expect(page).to have_content comment
end
- click_button 'Add a reply'
-
expect(find(textarea_selector)).to have_content ""
end
@@ -134,8 +130,6 @@ RSpec.shared_examples 'work items comments' do |type|
end
def click_reply_and_enter_slash
- click_button 'Add a reply'
-
find(form_selector).fill_in(with: "/")
wait_for_all_requests
diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb
index 6e0c6d67d85..9a177ba0394 100644
--- a/spec/views/profiles/preferences/show.html.haml_spec.rb
+++ b/spec/views/profiles/preferences/show.html.haml_spec.rb
@@ -54,9 +54,9 @@ RSpec.describe 'profiles/preferences/show' do
end
it 'has helpful homepage setup guidance' do
- expect(rendered).to have_selector('[data-label="Dashboard"]')
+ expect(rendered).to have_selector('[data-label="Homepage"]')
expect(rendered).to have_selector("[data-description=" \
- "'Choose what content you want to see by default on your dashboard.']")
+ "'Choose what content you want to see by default on your homepage.']")
end
end
diff --git a/vendor/project_templates/android.tar.gz b/vendor/project_templates/android.tar.gz
index fff7a7e45a6..ee0689a6e45 100644
--- a/vendor/project_templates/android.tar.gz
+++ b/vendor/project_templates/android.tar.gz
Binary files differ
diff --git a/vendor/project_templates/gitbook.tar.gz b/vendor/project_templates/gitbook.tar.gz
index 07037a83db6..64290e92f96 100644
--- a/vendor/project_templates/gitbook.tar.gz
+++ b/vendor/project_templates/gitbook.tar.gz
Binary files differ
diff --git a/vendor/project_templates/gomicro.tar.gz b/vendor/project_templates/gomicro.tar.gz
index c7d8687fdd8..50899e661c7 100644
--- a/vendor/project_templates/gomicro.tar.gz
+++ b/vendor/project_templates/gomicro.tar.gz
Binary files differ
diff --git a/vendor/project_templates/hexo.tar.gz b/vendor/project_templates/hexo.tar.gz
index 489da1a34ec..cc30ff0a67e 100644
--- a/vendor/project_templates/hexo.tar.gz
+++ b/vendor/project_templates/hexo.tar.gz
Binary files differ
diff --git a/vendor/project_templates/hipaa_audit_protocol.tar.gz b/vendor/project_templates/hipaa_audit_protocol.tar.gz
index 7ca94675d35..00c6c9f324c 100644
--- a/vendor/project_templates/hipaa_audit_protocol.tar.gz
+++ b/vendor/project_templates/hipaa_audit_protocol.tar.gz
Binary files differ
diff --git a/vendor/project_templates/iosswift.tar.gz b/vendor/project_templates/iosswift.tar.gz
index 76f32a3a681..80a71b432b5 100644
--- a/vendor/project_templates/iosswift.tar.gz
+++ b/vendor/project_templates/iosswift.tar.gz
Binary files differ
diff --git a/vendor/project_templates/jekyll.tar.gz b/vendor/project_templates/jekyll.tar.gz
index 0a97723712a..8dc68699baa 100644
--- a/vendor/project_templates/jekyll.tar.gz
+++ b/vendor/project_templates/jekyll.tar.gz
Binary files differ
diff --git a/vendor/project_templates/nfgitbook.tar.gz b/vendor/project_templates/nfgitbook.tar.gz
index 71f526ac43d..f09a5f41171 100644
--- a/vendor/project_templates/nfgitbook.tar.gz
+++ b/vendor/project_templates/nfgitbook.tar.gz
Binary files differ
diff --git a/vendor/project_templates/nfhexo.tar.gz b/vendor/project_templates/nfhexo.tar.gz
index 79cc74f8d72..3a241f68df4 100644
--- a/vendor/project_templates/nfhexo.tar.gz
+++ b/vendor/project_templates/nfhexo.tar.gz
Binary files differ
diff --git a/vendor/project_templates/nfhugo.tar.gz b/vendor/project_templates/nfhugo.tar.gz
index 1a4aab028a8..093ecdea96a 100644
--- a/vendor/project_templates/nfhugo.tar.gz
+++ b/vendor/project_templates/nfhugo.tar.gz
Binary files differ
diff --git a/vendor/project_templates/nfjekyll.tar.gz b/vendor/project_templates/nfjekyll.tar.gz
index 56bf955afbe..f554181e1db 100644
--- a/vendor/project_templates/nfjekyll.tar.gz
+++ b/vendor/project_templates/nfjekyll.tar.gz
Binary files differ
diff --git a/vendor/project_templates/nfplainhtml.tar.gz b/vendor/project_templates/nfplainhtml.tar.gz
index 3a90983bd06..13dd13a6830 100644
--- a/vendor/project_templates/nfplainhtml.tar.gz
+++ b/vendor/project_templates/nfplainhtml.tar.gz
Binary files differ
diff --git a/vendor/project_templates/pelican.tar.gz b/vendor/project_templates/pelican.tar.gz
index 1877d3fb24b..bc87d498ced 100644
--- a/vendor/project_templates/pelican.tar.gz
+++ b/vendor/project_templates/pelican.tar.gz
Binary files differ
diff --git a/vendor/project_templates/plainhtml.tar.gz b/vendor/project_templates/plainhtml.tar.gz
index 1ed17ddc140..dc0354172be 100644
--- a/vendor/project_templates/plainhtml.tar.gz
+++ b/vendor/project_templates/plainhtml.tar.gz
Binary files differ
diff --git a/vendor/project_templates/salesforcedx.tar.gz b/vendor/project_templates/salesforcedx.tar.gz
index f92721a453f..9486b410507 100644
--- a/vendor/project_templates/salesforcedx.tar.gz
+++ b/vendor/project_templates/salesforcedx.tar.gz
Binary files differ
diff --git a/vendor/project_templates/serverless_framework.tar.gz b/vendor/project_templates/serverless_framework.tar.gz
index b09de0ec3a2..279d0f2eb5c 100644
--- a/vendor/project_templates/serverless_framework.tar.gz
+++ b/vendor/project_templates/serverless_framework.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index c1198bf13b7..12960e91f85 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ