diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-11 12:09:05 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-11 12:09:05 +0000 |
commit | 28e90894e1e6f17320f5b1d2fff6fe736bf65dff (patch) | |
tree | 21d63bf124b6064eb1650acc3e2aabe6dbc99f58 | |
parent | a48957b317edf23b1bcfc6df0c098a824eae86f4 (diff) | |
download | gitlab-ce-28e90894e1e6f17320f5b1d2fff6fe736bf65dff.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 @@ -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 Binary files differindex 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 diff --git a/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz b/spec/fixtures/gitlab/import_export/corrupted_project_export.tar.gz Binary files differindex 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 diff --git a/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz b/spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz Binary files differindex 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 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 Binary files differindex fff7a7e45a6..ee0689a6e45 100644 --- a/vendor/project_templates/android.tar.gz +++ b/vendor/project_templates/android.tar.gz diff --git a/vendor/project_templates/gitbook.tar.gz b/vendor/project_templates/gitbook.tar.gz Binary files differindex 07037a83db6..64290e92f96 100644 --- a/vendor/project_templates/gitbook.tar.gz +++ b/vendor/project_templates/gitbook.tar.gz diff --git a/vendor/project_templates/gomicro.tar.gz b/vendor/project_templates/gomicro.tar.gz Binary files differindex c7d8687fdd8..50899e661c7 100644 --- a/vendor/project_templates/gomicro.tar.gz +++ b/vendor/project_templates/gomicro.tar.gz diff --git a/vendor/project_templates/hexo.tar.gz b/vendor/project_templates/hexo.tar.gz Binary files differindex 489da1a34ec..cc30ff0a67e 100644 --- a/vendor/project_templates/hexo.tar.gz +++ b/vendor/project_templates/hexo.tar.gz diff --git a/vendor/project_templates/hipaa_audit_protocol.tar.gz b/vendor/project_templates/hipaa_audit_protocol.tar.gz Binary files differindex 7ca94675d35..00c6c9f324c 100644 --- a/vendor/project_templates/hipaa_audit_protocol.tar.gz +++ b/vendor/project_templates/hipaa_audit_protocol.tar.gz diff --git a/vendor/project_templates/iosswift.tar.gz b/vendor/project_templates/iosswift.tar.gz Binary files differindex 76f32a3a681..80a71b432b5 100644 --- a/vendor/project_templates/iosswift.tar.gz +++ b/vendor/project_templates/iosswift.tar.gz diff --git a/vendor/project_templates/jekyll.tar.gz b/vendor/project_templates/jekyll.tar.gz Binary files differindex 0a97723712a..8dc68699baa 100644 --- a/vendor/project_templates/jekyll.tar.gz +++ b/vendor/project_templates/jekyll.tar.gz diff --git a/vendor/project_templates/nfgitbook.tar.gz b/vendor/project_templates/nfgitbook.tar.gz Binary files differindex 71f526ac43d..f09a5f41171 100644 --- a/vendor/project_templates/nfgitbook.tar.gz +++ b/vendor/project_templates/nfgitbook.tar.gz diff --git a/vendor/project_templates/nfhexo.tar.gz b/vendor/project_templates/nfhexo.tar.gz Binary files differindex 79cc74f8d72..3a241f68df4 100644 --- a/vendor/project_templates/nfhexo.tar.gz +++ b/vendor/project_templates/nfhexo.tar.gz diff --git a/vendor/project_templates/nfhugo.tar.gz b/vendor/project_templates/nfhugo.tar.gz Binary files differindex 1a4aab028a8..093ecdea96a 100644 --- a/vendor/project_templates/nfhugo.tar.gz +++ b/vendor/project_templates/nfhugo.tar.gz diff --git a/vendor/project_templates/nfjekyll.tar.gz b/vendor/project_templates/nfjekyll.tar.gz Binary files differindex 56bf955afbe..f554181e1db 100644 --- a/vendor/project_templates/nfjekyll.tar.gz +++ b/vendor/project_templates/nfjekyll.tar.gz diff --git a/vendor/project_templates/nfplainhtml.tar.gz b/vendor/project_templates/nfplainhtml.tar.gz Binary files differindex 3a90983bd06..13dd13a6830 100644 --- a/vendor/project_templates/nfplainhtml.tar.gz +++ b/vendor/project_templates/nfplainhtml.tar.gz diff --git a/vendor/project_templates/pelican.tar.gz b/vendor/project_templates/pelican.tar.gz Binary files differindex 1877d3fb24b..bc87d498ced 100644 --- a/vendor/project_templates/pelican.tar.gz +++ b/vendor/project_templates/pelican.tar.gz diff --git a/vendor/project_templates/plainhtml.tar.gz b/vendor/project_templates/plainhtml.tar.gz Binary files differindex 1ed17ddc140..dc0354172be 100644 --- a/vendor/project_templates/plainhtml.tar.gz +++ b/vendor/project_templates/plainhtml.tar.gz diff --git a/vendor/project_templates/salesforcedx.tar.gz b/vendor/project_templates/salesforcedx.tar.gz Binary files differindex f92721a453f..9486b410507 100644 --- a/vendor/project_templates/salesforcedx.tar.gz +++ b/vendor/project_templates/salesforcedx.tar.gz diff --git a/vendor/project_templates/serverless_framework.tar.gz b/vendor/project_templates/serverless_framework.tar.gz Binary files differindex b09de0ec3a2..279d0f2eb5c 100644 --- a/vendor/project_templates/serverless_framework.tar.gz +++ b/vendor/project_templates/serverless_framework.tar.gz diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz Binary files differindex c1198bf13b7..12960e91f85 100644 --- a/vendor/project_templates/spring.tar.gz +++ b/vendor/project_templates/spring.tar.gz |