diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 980da48a317..83440c58bb3 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -162,9 +162,7 @@ retrieve-frontend-fixtures: echoinfo "INFO: Reusing frontend fixtures from 'retrieve-frontend-fixtures'." exit 0 fi - - run_timed_command "gem install knapsack --no-document" - - section_start "gitaly-test-spawn" "Spawning Gitaly"; scripts/gitaly-test-spawn; section_end "gitaly-test-spawn"; # Do not use 'bundle exec' here - - source ./scripts/rspec_helpers.sh + - !reference [.base-script, script] - rspec_parallelized_job artifacts: name: frontend-fixtures diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 7c8ed3b0fc4..882c7736756 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -240,12 +240,14 @@ - ".gitlab/ci/review-apps/qa.gitlab-ci.yml" - ".gitlab/ci/review-apps/rules.gitlab-ci.yml" - ".gitlab/ci/test-on-gdk/*.yml" + - ".gitlab/ci/version.yml" .gitaly-patterns: &gitaly-patterns - "GITALY_SERVER_VERSION" - "lib/gitlab/setup_helper.rb" .workhorse-patterns: &workhorse-patterns + - ".gitlab/ci/version.yml" - ".gitlab/ci/workhorse.gitlab-ci.yml" - "GITLAB_WORKHORSE_VERSION" - "workhorse/**/*" @@ -713,7 +715,8 @@ - "ee/{lib/,spec/}tasks/gitlab/custom_roles/*" .cng-orchestrator-patterns: &cng-orchestrator-patterns - - qa/gems/gitlab-cng/**/*.rb + - "qa/gems/gitlab-cng/**/*.rb" + - "qa/gems/gitlab-cng/{Gemfile,Gemfile.lock}" ################## # Conditions set # diff --git a/Gemfile b/Gemfile index bfc8e6e4406..dd2a598437e 100644 --- a/Gemfile +++ b/Gemfile @@ -228,7 +228,7 @@ gem 'elasticsearch-rails', '~> 7.2', require: 'elasticsearch/rails/instrumentati gem 'elasticsearch-api', '7.17.11', feature_category: :global_search gem 'aws-sdk-core', '~> 3.201.0' # rubocop:todo Gemfile/MissingFeatureCategory gem 'aws-sdk-cloudformation', '~> 1' # rubocop:todo Gemfile/MissingFeatureCategory -gem 'aws-sdk-s3', '~> 1.156.0' # rubocop:todo Gemfile/MissingFeatureCategory +gem 'aws-sdk-s3', '~> 1.157.0' # rubocop:todo Gemfile/MissingFeatureCategory gem 'faraday-typhoeus', '~> 1.1', feature_category: :global_search gem 'faraday_middleware-aws-sigv4', '~> 1.0.1', feature_category: :global_search # Used with Elasticsearch to support http keep-alive connections diff --git a/Gemfile.checksum b/Gemfile.checksum index 398738fb47e..57b66606be2 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -39,7 +39,7 @@ {"name":"aws-sdk-cloudformation","version":"1.41.0","platform":"ruby","checksum":"31e47539719734413671edf9b1a31f8673fbf9688549f50c41affabbcb1c6b26"}, {"name":"aws-sdk-core","version":"3.201.3","platform":"ruby","checksum":"c045a7ff37b4a6f1de5742e64def0841bdf70d215cb17d3875b2c5bdd9e99d52"}, {"name":"aws-sdk-kms","version":"1.76.0","platform":"ruby","checksum":"e7f75013cba9ba357144f66bbc600631c192e2cda9dd572794be239654e2cf49"}, -{"name":"aws-sdk-s3","version":"1.156.0","platform":"ruby","checksum":"9302da1d1a70363308854d5065035f6c72cf8b8af895d8789487cd5c6b076a46"}, +{"name":"aws-sdk-s3","version":"1.157.0","platform":"ruby","checksum":"e1e0c7a268e710a7ccf4a0f9d2c33e3ca685b06968c3048d907e3a792580e990"}, {"name":"aws-sigv4","version":"1.8.0","platform":"ruby","checksum":"84dd99768b91b93b63d1d8e53ee837cfd06ab402812772a7899a78f9f9117cbc"}, {"name":"axe-core-api","version":"4.9.1","platform":"ruby","checksum":"9ea7ac16bfee1cb3545345d210878aa8cccfb41b493e00fe1faab79af4d9fed8"}, {"name":"axe-core-rspec","version":"4.9.1","platform":"ruby","checksum":"31ef067bee36d6efb3f156a83aa2fb6ac721270a53fb9473f0268e325a3e6efd"}, diff --git a/Gemfile.lock b/Gemfile.lock index 1dd453da2b6..2c454342847 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -330,7 +330,7 @@ GEM aws-sdk-kms (1.76.0) aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.156.0) + aws-sdk-s3 (1.157.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -1968,7 +1968,7 @@ DEPENDENCIES awesome_print aws-sdk-cloudformation (~> 1) aws-sdk-core (~> 3.201.0) - aws-sdk-s3 (~> 1.156.0) + aws-sdk-s3 (~> 1.157.0) axe-core-rspec (~> 4.9.0) babosa (~> 2.0) base32 (~> 0.3.0) diff --git a/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue b/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue index 9899e941fbf..24dfba58578 100644 --- a/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue +++ b/app/assets/javascripts/ide/components/oauth_domain_mismatch_error.vue @@ -1,41 +1,38 @@ + + diff --git a/app/assets/javascripts/search/results/components/blob_chunks.vue b/app/assets/javascripts/search/results/components/blob_chunks.vue new file mode 100644 index 00000000000..66939e14209 --- /dev/null +++ b/app/assets/javascripts/search/results/components/blob_chunks.vue @@ -0,0 +1,81 @@ + + + diff --git a/app/assets/javascripts/search/results/components/blob_footer.vue b/app/assets/javascripts/search/results/components/blob_footer.vue new file mode 100644 index 00000000000..8ac8b6856a1 --- /dev/null +++ b/app/assets/javascripts/search/results/components/blob_footer.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/assets/javascripts/search/results/components/blob_header.vue b/app/assets/javascripts/search/results/components/blob_header.vue new file mode 100644 index 00000000000..6721202a259 --- /dev/null +++ b/app/assets/javascripts/search/results/components/blob_header.vue @@ -0,0 +1,59 @@ + + diff --git a/app/assets/javascripts/search/results/components/result_empty.vue b/app/assets/javascripts/search/results/components/result_empty.vue new file mode 100644 index 00000000000..7d02e15f087 --- /dev/null +++ b/app/assets/javascripts/search/results/components/result_empty.vue @@ -0,0 +1,73 @@ + + + diff --git a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue index aa404e20663..a87f01d51c2 100644 --- a/app/assets/javascripts/search/results/components/zoekt_blob_results.vue +++ b/app/assets/javascripts/search/results/components/zoekt_blob_results.vue @@ -1,5 +1,5 @@ @@ -76,5 +106,41 @@ export default { diff --git a/app/assets/javascripts/search/results/constants.js b/app/assets/javascripts/search/results/constants.js index ce8287a5f71..35e1f8bb826 100644 --- a/app/assets/javascripts/search/results/constants.js +++ b/app/assets/javascripts/search/results/constants.js @@ -2,3 +2,4 @@ export const DEFAULT_FETCH_CHUNKS = 50; export const PROJECT_GRAPHQL_ID_TYPE = 'Project'; export const GROUP_GRAPHQL_ID_TYPE = 'Group'; export const SEARCH_RESULTS_DEBOUNCE = 500; +export const DEFAULT_SHOW_CHUNKS = 3; diff --git a/app/assets/javascripts/search/results/event_hub.js b/app/assets/javascripts/search/results/event_hub.js new file mode 100644 index 00000000000..e31806ad199 --- /dev/null +++ b/app/assets/javascripts/search/results/event_hub.js @@ -0,0 +1,3 @@ +import createEventHub from '~/helpers/event_hub_factory'; + +export default createEventHub(); diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js index 6518c85643c..dd7e541e51d 100644 --- a/app/assets/javascripts/search/store/constants.js +++ b/app/assets/javascripts/search/store/constants.js @@ -4,6 +4,7 @@ import { languageFilterData } from '~/search/sidebar/components/language_filter/ import { LABEL_FILTER_PARAM } from '~/search/sidebar/components/label_filter/data'; import { archivedFilterData } from '~/search/sidebar/components/archived_filter/data'; import { INCLUDE_FORKED_FILTER_PARAM } from '~/search/sidebar/components/forks_filter/index.vue'; +import { s__ } from '~/locale'; export const MAX_FREQUENT_ITEMS = 5; @@ -40,6 +41,20 @@ export const ICON_MAP = { snippet_titles: 'snippet', }; +export const SCOPE_NAVIGATION_MAP = { + blobs: s__(`GlobalSearch|Code`), + issues: s__(`GlobalSearch|Issues`), + epics: s__(`GlobalSearch|'Epics`), + merge_requests: s__(`GlobalSearch|Merge request`), + commits: s__(`GlobalSearch|Commits`), + notes: s__(`GlobalSearch|Comments`), + milestones: s__(`GlobalSearch|Milestones`), + users: s__(`GlobalSearch|Users`), + projects: s__(`GlobalSearch|Projects`), + wiki_blobs: s__(`GlobalSearch|Wiki`), + snippet_titles: s__(`GlobalSearch|Snippets`), +}; + export const ZOEKT_SEARCH_TYPE = 'zoekt'; export const ADVANCED_SEARCH_TYPE = 'advanced'; export const BASIC_SEARCH_TYPE = 'basic'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js index e1cffbcbef2..27a9c492d1d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js @@ -25,4 +25,5 @@ export const FAILURE_REASONS = { requested_changes: __('The change requests must be completed or resolved.'), approvals_syncing: __('The merge request approvals are currently syncing.'), locked_lfs_files: __('All LFS files must be unlocked.'), + security_policy_evaluation: __('All security policies must be evaluated.'), }; diff --git a/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue b/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue new file mode 100644 index 00000000000..190fe3cae10 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_abuse_modal.vue @@ -0,0 +1,103 @@ + + diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue index 39e944744ab..67348d17bfc 100644 --- a/app/assets/javascripts/work_items/components/work_item_actions.vue +++ b/app/assets/javascripts/work_items/components/work_item_actions.vue @@ -37,6 +37,7 @@ import { I18N_WORK_ITEM_ERROR_COPY_REFERENCE, I18N_WORK_ITEM_ERROR_COPY_EMAIL, TEST_ID_LOCK_ACTION, + TEST_ID_REPORT_ABUSE, } from '../constants'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql'; @@ -55,6 +56,7 @@ export default { referenceCopied: __('Reference copied'), emailAddressCopied: __('Email address copied'), moreActions: __('More actions'), + reportAbuse: __('Report abuse'), }, components: { GlDisclosureDropdown, @@ -79,6 +81,7 @@ export default { promoteActionTestId: TEST_ID_PROMOTE_ACTION, lockDiscussionTestId: TEST_ID_LOCK_ACTION, stateToggleTestId: TEST_ID_TOGGLE_ACTION, + reportAbuseActionTestId: TEST_ID_REPORT_ABUSE, props: { fullPath: { type: String, @@ -164,6 +167,11 @@ export default { required: false, default: false, }, + workItemAuthorId: { + type: Number, + required: false, + default: 0, + }, }, data() { return { @@ -228,6 +236,9 @@ export default { showDropdownTooltip() { return !this.isDropdownVisible ? this.$options.i18n.moreActions : ''; }, + isAuthor() { + return this.workItemAuthorId === window.gon.current_user_id; + }, }, methods: { copyToClipboard(text, message) { @@ -356,6 +367,10 @@ export default { emitStateToggleError(error) { this.$emit('error', error); }, + handleToggleReportAbuseModal() { + this.$emit('toggleReportAbuseModal', true); + this.closeDropdown(); + }, }, }; @@ -452,8 +467,16 @@ export default { + + + + + diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue index d4feb8619a3..7230e96a2c4 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue @@ -97,8 +97,7 @@ export default { updateHasNotes() { this.hasNotes = true; }, - openReportAbuseDrawer(reply) { - this.hide(); + openReportAbuseModal(reply) { this.$emit('openReportAbuse', reply); }, }, @@ -132,7 +131,7 @@ export default { @deleteWorkItem="deleteWorkItem" @update-modal="updateModal" @has-notes="updateHasNotes" - @openReportAbuse="openReportAbuseDrawer" + @openReportAbuse="openReportAbuseModal" /> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index 086e6517023..5f27743c27c 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -14,7 +14,6 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants'; import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql'; import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility'; -import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; import { FORM_TYPES, @@ -30,6 +29,7 @@ import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql'; import WorkItemChildrenLoadMore from '../shared/work_item_children_load_more.vue'; import WidgetWrapper from '../widget_wrapper.vue'; import WorkItemDetailModal from '../work_item_detail_modal.vue'; +import WorkItemAbuseModal from '../work_item_abuse_modal.vue'; import WorkItemLinksForm from './work_item_links_form.vue'; import WorkItemChildrenWrapper from './work_item_children_wrapper.vue'; @@ -42,7 +42,7 @@ export default { WidgetWrapper, WorkItemLinksForm, WorkItemDetailModal, - AbuseCategorySelector, + WorkItemAbuseModal, WorkItemChildrenWrapper, WorkItemChildrenLoadMore, GlToggle, @@ -109,7 +109,7 @@ export default { parentIssue: null, formType: null, workItem: null, - isReportDrawerOpen: false, + isReportModalOpen: false, reportedUserId: 0, reportedUrl: '', widgetName: TASKS_ANCHOR, @@ -206,13 +206,13 @@ export default { updateWorkItemIdUrlQuery({ iid } = {}) { updateHistory({ url: setUrlParams({ work_item_iid: iid }), replace: true }); }, - toggleReportAbuseDrawer(isOpen, reply = {}) { - this.isReportDrawerOpen = isOpen; + toggleReportAbuseModal(isOpen, reply = {}) { + this.isReportModalOpen = isOpen; this.reportedUrl = reply.url; this.reportedUserId = reply.author ? getIdFromGraphQLId(reply.author.id) : 0; }, - openReportAbuseDrawer(reply) { - this.toggleReportAbuseDrawer(true, reply); + openReportAbuseModal(reply) { + this.toggleReportAbuseModal(true, reply); }, async fetchNextPage() { if (this.hasNextPage && !this.fetchNextPageInProgress) { @@ -355,14 +355,14 @@ export default { :work-item-full-path="activeChildNamespaceFullPath" @close="closeModal" @workItemDeleted="handleWorkItemDeleted(activeChild)" - @openReportAbuse="openReportAbuseDrawer" + @openReportAbuse="openReportAbuseModal" /> - diff --git a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue index 97db3dea3f9..1f379433554 100644 --- a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue +++ b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue @@ -62,6 +62,11 @@ export default { required: false, default: () => [], }, + workItemAuthorId: { + type: Number, + required: false, + default: 0, + }, }, computed: { canUpdate() { @@ -153,6 +158,7 @@ export default { :work-item-create-note-email="workItem.createNoteEmail" :work-item-state="workItem.state" :is-modal="isModal" + :work-item-author-id="workItemAuthorId" @deleteWorkItem="$emit('deleteWorkItem')" @toggleWorkItemConfidentiality=" $emit('toggleWorkItemConfidentiality', !workItem.confidential) @@ -160,6 +166,7 @@ export default { @error="$emit('error')" @promotedToObjective="$emit('promotedToObjective')" @workItemStateUpdated="$emit('workItemStateUpdated')" + @toggleReportAbuseModal="$emit('toggleReportAbuseModal', true)" /> diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index f4574076d3e..8be385f3636 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -277,6 +277,7 @@ export const TEST_ID_LOCK_ACTION = 'lock-action'; export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action'; export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action'; export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action'; +export const TEST_ID_REPORT_ABUSE = 'report-abuse-action'; export const TODO_ADD_ICON = 'todo-add'; export const TODO_DONE_ICON = 'todo-done'; diff --git a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb index 190417bdaeb..1d17f38cc6f 100644 --- a/app/graphql/types/merge_requests/detailed_merge_status_enum.rb +++ b/app/graphql/types/merge_requests/detailed_merge_status_enum.rb @@ -60,6 +60,9 @@ module Types value 'LOCKED_LFS_FILES', value: :locked_lfs_files, description: 'Merge request includes locked LFS files.' + value 'SECURITY_POLICIES_EVALUATING', + value: :security_policy_evaluation, + description: 'All security policies must be evaluated.' end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c4959cd6ee1..dc970922168 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1274,7 +1274,8 @@ class MergeRequest < ApplicationRecord skip_external_status_check: merge_when_checks_pass_strat, skip_requested_changes_check: merge_when_checks_pass_strat, skip_jira_check: merge_when_checks_pass_strat, - skip_locked_lfs_files_check: merge_when_checks_pass_strat + skip_locked_lfs_files_check: merge_when_checks_pass_strat, + skip_security_policy_check: merge_when_checks_pass_strat } end diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb index 7723cfbcf9c..d6e0fbcd6e7 100644 --- a/config/initializers_before_autoloader/000_inflections.rb +++ b/config/initializers_before_autoloader/000_inflections.rb @@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections do |inflect| dependency_proxy_blob_registry design_management_repository_registry dependency_proxy_manifest_registry + duo_enterprise duo_pro event_log file_registry diff --git a/config/routes.rb b/config/routes.rb index fb3427696f4..ab750a7fcb3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -145,6 +145,7 @@ InitializerConnections.raise_if_new_database_connection do scope :ide, as: :ide, format: false do get '/', to: 'ide#index' get '/project', to: 'ide#index' + # note: This path has a hardcoded reference in the FE `app/assets/javascripts/ide/constants.js` get '/oauth_redirect', to: 'ide#oauth_redirect' scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do diff --git a/danger/plugins/settings_sections.rb b/danger/plugins/settings_sections.rb new file mode 100644 index 00000000000..c58e2832e4a --- /dev/null +++ b/danger/plugins/settings_sections.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative '../../tooling/danger/settings_sections' + +module Danger + class SettingsSections < ::Danger::Plugin + include Tooling::Danger::SettingsSections + end +end diff --git a/danger/settings_sections/Dangerfile b/danger/settings_sections/Dangerfile new file mode 100644 index 00000000000..05b5f8e1d5d --- /dev/null +++ b/danger/settings_sections/Dangerfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +settings_sections.check! diff --git a/db/docs/ci_instance_variables.yml b/db/docs/ci_instance_variables.yml index 328a8b8378b..f61e587566f 100644 --- a/db/docs/ci_instance_variables.yml +++ b/db/docs/ci_instance_variables.yml @@ -8,4 +8,4 @@ description: CI/CD variables available to all projects and groups in an instance introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30156 milestone: '13.0' gitlab_schema: gitlab_ci -sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/459039 +exempt_from_sharding: true # table not used in .com nor Cells. \ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 601261a86de..12be1235faa 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -35344,6 +35344,7 @@ Detailed representation of whether a GitLab merge request can be merged. | `NOT_OPEN` | Merge request must be open before merging. | | `PREPARING` | Merge request diff is being created. | | `REQUESTED_CHANGES` | Indicates a reviewer has requested changes. | +| `SECURITY_POLICIES_EVALUATING` | All security policies must be evaluated. | | `UNCHECKED` | Merge status has not been checked. | ### `DiffPositionType` @@ -36119,6 +36120,7 @@ Representation of mergeability check identifier. | `NOT_APPROVED` | Checks whether the merge request is approved. | | `NOT_OPEN` | Checks whether the merge request is open. | | `REQUESTED_CHANGES` | Checks whether the merge request has changes requested. | +| `SECURITY_POLICY_EVALUATION` | Checks whether the security policies are evaluated. | | `STATUS_CHECKS_MUST_PASS` | Checks whether the external status checks pass. | ### `MergeabilityCheckStatus` diff --git a/doc/development/documentation/styleguide/deprecations_and_removals.md b/doc/development/documentation/styleguide/deprecations_and_removals.md index 4977824b549..f91c96553fc 100644 --- a/doc/development/documentation/styleguide/deprecations_and_removals.md +++ b/doc/development/documentation/styleguide/deprecations_and_removals.md @@ -5,7 +5,7 @@ group: unassigned description: 'Guidelines for deprecations and page removals' --- -## Deprecations and removals +# Deprecations and removals When GitLab deprecates or removes a feature, use the following process to update the documentation. This process requires temporarily changing content to be "deprecated" or "removed" before it's deleted. @@ -16,7 +16,7 @@ NOTE: A separate process exists for [GraphQL docs](../../api_graphql_styleguide.md#deprecating-schema-items) and [REST API docs](../restful_api_styleguide.md#deprecations). -### Deprecate a page or topic +## Deprecate a page or topic To deprecate a page or topic: @@ -67,7 +67,7 @@ To deprecate a page or topic: 1. Open a merge request to add the word `(deprecated)` to the left nav, after the page title. -### Remove a page +## Remove a page Mark content as removed during the release the feature was removed. The title and a removed indicator remains until three months after the removal. @@ -107,7 +107,7 @@ To remove a page: This content is removed from the documentation as part of the Technical Writing team's [regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks). -### Remove a topic +## Remove a topic To remove a topic: @@ -136,7 +136,7 @@ To remove a topic: This content is removed from the documentation as part of the Technical Writing team's [regularly scheduled tasks](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#regularly-scheduled-tasks). -### Removing version-specific upgrade pages +## Removing version-specific upgrade pages Version-specific upgrade pages are in the `doc/update/versions/` directory. diff --git a/doc/development/work_items.md b/doc/development/work_items.md index ba21402b56a..86f951dd993 100644 --- a/doc/development/work_items.md +++ b/doc/development/work_items.md @@ -4,7 +4,7 @@ group: Project Management info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review. --- -## Work items development +# Work items development - Work item lists are only available at group level `http://gdk.test:3000/groups/flightjs/-/work_items`, they are enabled with feature flags: `namespace_level_work_items` and `work_item_epics_rollout`. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 47810c59fa2..5e3c5ae109c 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -95,12 +95,23 @@ Prerequisites: - You must be an administrator. -In GitLab 15.7 and later, you can [use the application settings API to disable personal access tokens](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls). +Depending on your GitLab version, you can use either the application settings API +or the Admin UI to disable personal access tokens. + +### Use the application settings API + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384201) in GitLab 15.7. + +In GitLab 15.7 and later, you can use the [`disable_personal_access_tokens` attribute in the application settings API](../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls) to disable personal access tokens. NOTE: After you have used the API to disable personal access tokens, those tokens cannot be used in subsequent API calls to manage this setting. To re-enable personal access tokens, you must use the [GitLab Rails console](../../administration/operations/rails_console.md). You can also upgrade to GitLab 17.3 or later so you can use the Admin UI instead. -In GitLab 17.3 and later, you can disable personal access tokens in the Admin UI: +### Use the Admin UI + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436991) in GitLab 17.3. + +In GitLab 17.3 and later, you can use the Admin UI to disable personal access tokens: 1. On the left sidebar, at the bottom, select **Admin**. 1. Select **Settings > General**. diff --git a/doc/user/project/repository/code_suggestions/supported_extensions.md b/doc/user/project/repository/code_suggestions/supported_extensions.md index 3b5fd9b2d00..9097143b7de 100644 --- a/doc/user/project/repository/code_suggestions/supported_extensions.md +++ b/doc/user/project/repository/code_suggestions/supported_extensions.md @@ -121,7 +121,7 @@ When you're ready to start coding: 1. Open relevant files, including configuration files, to provide better context. 1. Close any files you don't want to be used as context. -## View Multiple Code Suggestions +## View multiple code suggestions > - [Introduced](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/1325) in GitLab 17.1. @@ -130,10 +130,14 @@ might be available. To view all available suggestions: 1. Hover over the code completion suggestion. 1. Scroll through the alternatives. Either: - - Use keyboard shortcuts. Press Option + `]` to view the - next suggestion, and Option + `[` to view the previous - suggestions. - - Select the right or left arrow to see next or previous options. + - Use keyboard shortcuts: + - On a Mac, press Option + ] to view the + next suggestion, and Option + [ to view the previous + suggestions. + - On Windows, press Alt + ] to view the + next suggestion, and Alt + [ to view the previous + suggestions. + - On the dialog that's displayed, select the right or left arrow to see next or previous options. 1. Press Tab to apply the suggestion you prefer. ## Add additional languages for Code Suggestions diff --git a/lib/search/group_settings.rb b/lib/search/group_settings.rb index 32903860e7d..96c3fb8878b 100644 --- a/lib/search/group_settings.rb +++ b/lib/search/group_settings.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Search + # Generates a list of all available setting sections of a group. + # This list is used by the command palette's search functionality. class GroupSettings include Rails.application.routes.url_helpers diff --git a/lib/search/project_settings.rb b/lib/search/project_settings.rb index 413b69a0ac8..645783c83b9 100644 --- a/lib/search/project_settings.rb +++ b/lib/search/project_settings.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Search + # Generates a list of all available setting sections of a project. + # This list is used by the command palette's search functionality. class ProjectSettings include Rails.application.routes.url_helpers diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aff58159ae8..55d5b2460af 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1716,9 +1716,6 @@ msgstr "" msgid "- Push code to the repository." msgstr "" -msgid "- Select -" -msgstr "" - msgid "- User" msgid_plural "- Users" msgstr[0] "" @@ -5449,6 +5446,9 @@ msgstr "" msgid "All required approvals must be given." msgstr "" +msgid "All security policies must be evaluated." +msgstr "" + msgid "All threads resolved!" msgstr "" @@ -13375,7 +13375,7 @@ msgstr "" msgid "Company" msgstr "" -msgid "Company Name" +msgid "Company name" msgstr "" msgid "Compare" @@ -15525,7 +15525,7 @@ msgstr "" msgid "Couldn't reorder child due to an internal error." msgstr "" -msgid "Country / Region" +msgid "Country or region" msgstr "" msgid "Counts" @@ -19665,6 +19665,18 @@ msgstr "" msgid "DuoCodeReview|I have encountered some issues while I was reviewing. Please try again later." msgstr "" +msgid "DuoEnterpriseTrial|Start your free Duo Enterprise trial" +msgstr "" + +msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial" +msgstr "" + +msgid "DuoEnterpriseTrial|Start your free GitLab Duo Enterprise trial on %{group_name}" +msgstr "" + +msgid "DuoEnterpriseTrial|We just need some additional information to activate your trial." +msgstr "" + msgid "DuoProDiscover|Accelerate your path to market" msgstr "" @@ -22826,9 +22838,6 @@ msgstr "" msgid "Finished" msgstr "" -msgid "First Name" -msgstr "" - msgid "First Seen" msgstr "" @@ -24392,12 +24401,18 @@ msgstr "" msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list." msgstr "" +msgid "GlobalSearch|%{lessButtonStart}Show less%{lessButtonEnd} - Too many matches found. Showing %{showingMatches} chunks out of %{fileMatches} results. %{fileLinkStart}Open the file to view all.%{fileLinkEnd}" +msgstr "" + msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is disabled since %{ref_elem} is not the default branch. %{docs_link}" msgstr "" msgid "GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is enabled." msgstr "" +msgid "GlobalSearch|'Epics" +msgstr "" + msgid "GlobalSearch|Aggregations load error." msgstr "" @@ -24410,12 +24425,21 @@ msgstr "" msgid "GlobalSearch|Change context %{kbdStart}↵%{kbdEnd}" msgstr "" +msgid "GlobalSearch|Code" +msgstr "" + msgid "GlobalSearch|Command palette" msgstr "" msgid "GlobalSearch|Commands %{superKey} %{link2Start}k%{link2End}" msgstr "" +msgid "GlobalSearch|Comments" +msgstr "" + +msgid "GlobalSearch|Commits" +msgstr "" + msgid "GlobalSearch|Could not load search results. Please refresh the page to try again." msgstr "" @@ -24500,6 +24524,9 @@ msgstr "" msgid "GlobalSearch|Merge Requests" msgstr "" +msgid "GlobalSearch|Merge request" +msgstr "" + msgid "GlobalSearch|Merge requests I've created" msgstr "" @@ -24509,9 +24536,15 @@ msgstr "" msgid "GlobalSearch|Merge requests that I'm a reviewer" msgstr "" +msgid "GlobalSearch|Milestones" +msgstr "" + msgid "GlobalSearch|No labels found" msgstr "" +msgid "GlobalSearch|No results found" +msgstr "" + msgid "GlobalSearch|No results found. Edit your search and try again." msgstr "" @@ -24521,6 +24554,9 @@ msgstr "" msgid "GlobalSearch|Only first %{max_shown} of not indexed projects is shown" msgstr "" +msgid "GlobalSearch|Open file in repository" +msgstr "" + msgid "GlobalSearch|Pages or actions" msgstr "" @@ -24587,12 +24623,21 @@ msgstr "" msgid "GlobalSearch|Settings" msgstr "" +msgid "GlobalSearch|Show %{matches} more matches" +msgstr "" + +msgid "GlobalSearch|Show less" +msgstr "" + msgid "GlobalSearch|Show more" msgstr "" msgid "GlobalSearch|Showing top %{maxItems}" msgstr "" +msgid "GlobalSearch|Snippets" +msgstr "" + msgid "GlobalSearch|The search term must be at least 3 characters long." msgstr "" @@ -24629,9 +24674,21 @@ msgstr "" msgid "GlobalSearch|View syntax options." msgstr "" +msgid "GlobalSearch|We couldn't find any %{scope} matching %{term}" +msgstr "" + +msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in group %{group}" +msgstr "" + +msgid "GlobalSearch|We couldn't find any %{scope} matching %{term} in project %{project}" +msgstr "" + msgid "GlobalSearch|What are you searching for?" msgstr "" +msgid "GlobalSearch|Wiki" +msgstr "" + msgid "GlobalSearch|Your work" msgstr "" @@ -26750,15 +26807,15 @@ msgstr "" msgid "IDE|Contact your administrator or try to open the Web IDE again with another domain." msgstr "" +msgid "IDE|Could not find a callback URL entry for %{expectedCallbackUrl}." +msgstr "" + msgid "IDE|Edit" msgstr "" msgid "IDE|Editing this application might affect the functionality of the Web IDE. Ensure the configuration meets the following conditions:" msgstr "" -msgid "IDE|Error reloading page" -msgstr "" - msgid "IDE|GitLab logo" msgstr "" @@ -30876,9 +30933,6 @@ msgstr "" msgid "Last GitLab activity" msgstr "" -msgid "Last Name" -msgstr "" - msgid "Last Seen" msgstr "" @@ -39888,9 +39942,6 @@ msgstr "" msgid "Please select a Jira project" msgstr "" -msgid "Please select a country / region" -msgstr "" - msgid "Please select a group" msgstr "" @@ -49240,6 +49291,9 @@ msgstr "" msgid "Select a comment template" msgstr "" +msgid "Select a country or region" +msgstr "" + msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes." msgstr "" @@ -56302,12 +56356,15 @@ msgstr "" msgid "Trials|Your trial ends on %{boldStart}%{trialEndDate}%{boldEnd}. We hope you’re enjoying the features of GitLab %{planName}. To keep those features after your trial ends, you’ll need to buy a subscription. (You can also choose GitLab Premium if it meets your needs.)" msgstr "" -msgid "Trial| By selecting Continue or registering through a third party, you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}" +msgid "Trial|Activate my trial" msgstr "" msgid "Trial|Allowed characters: +, 0-9, -, and spaces." msgstr "" +msgid "Trial|By clicking \"%{buttonText}\" you accept the %{gitlabSubscriptionAgreement} and acknowledge the %{privacyStatement} and %{cookiePolicy}" +msgstr "" + msgid "Trial|Continue" msgstr "" @@ -56317,16 +56374,19 @@ msgstr "" msgid "Trial|GitLab Subscription Agreement" msgstr "" -msgid "Trial|Please select" +msgid "Trial|Privacy Statement" msgstr "" -msgid "Trial|Privacy Statement" +msgid "Trial|Select number of employees" +msgstr "" + +msgid "Trial|Select state or province" msgstr "" msgid "Trial|Start free GitLab Ultimate trial" msgstr "" -msgid "Trial|State/Province" +msgid "Trial|State or province" msgstr "" msgid "Trial|To activate your trial, we need additional details from you." @@ -58668,6 +58728,9 @@ msgstr "" msgid "View File Metadata" msgstr "" +msgid "View Line in repository" +msgstr "" + msgid "View Stage: %{title}" msgstr "" @@ -59527,6 +59590,9 @@ msgstr "" msgid "We're experiencing difficulties and this tab content is currently unavailable." msgstr "" +msgid "We're sorry, your GitLab Duo Enterprise trial could not be created because our system did not respond successfully." +msgstr "" + msgid "We're sorry, your GitLab Duo Pro trial could not be created because our system did not respond successfully." msgstr "" diff --git a/package.json b/package.json index 4bb4b06cfae..ef1d1db066e 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/svgs": "3.109.0", - "@gitlab/ui": "87.8.0", + "@gitlab/ui": "88.0.0", "@gitlab/web-ide": "^0.0.1-dev-20240613133550", "@mattiasbuelens/web-streams-adapter": "^0.1.0", "@rails/actioncable": "7.0.8-4", diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb index 9d7085067cd..0378aa6e718 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_components_catalog/run_component_in_project_pipeline_spec.rb @@ -71,7 +71,7 @@ module QA runner.remove_via_api! end - it 'runs in project pipeline with correct inputs', :aggregate_failures, + it 'runs in project pipeline with correct inputs', :blocking, :aggregate_failures, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/451582' do Flow::Pipeline.visit_latest_pipeline diff --git a/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js b/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js index 1ce0201f5e9..2de27b26aae 100644 --- a/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js +++ b/spec/frontend/ide/components/oauth_domain_mismatch_error_spec.js @@ -1,87 +1,111 @@ -import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; +import { GlButton, GlDisclosureDropdown } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import OAuthDomainMismatchError from '~/ide/components/oauth_domain_mismatch_error.vue'; -const MOCK_CALLBACK_URL_ORIGIN = 'https://example1.com'; -const MOCK_PATH_NAME = '/path/to/ide'; +const MOCK_CALLBACK_URLS = [ + { + base: 'https://example1.com/', + }, + { + base: 'https://example2.com/', + }, + { + base: 'https://example3.com/relative-path/', + }, +]; +const MOCK_CALLBACK_URL = 'https://example.com'; +const MOCK_PATH_NAME = 'path/to/ide'; + +const EXPECTED_DROPDOWN_ITEMS = MOCK_CALLBACK_URLS.map(({ base }) => ({ + text: base, + href: `${base}${MOCK_PATH_NAME}`, +})); describe('OAuthDomainMismatchError', () => { - useMockLocationHelper(); - let wrapper; - let originalLocation; const findButton = () => wrapper.findComponent(GlButton); - const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox); - const findDropdownItems = () => wrapper.findAllComponents(GlListboxItem); + const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); const createWrapper = (props = {}) => { wrapper = mount(OAuthDomainMismatchError, { propsData: { - callbackUrlOrigins: [MOCK_CALLBACK_URL_ORIGIN], + expectedCallbackUrl: MOCK_CALLBACK_URL, + callbackUrls: MOCK_CALLBACK_URLS, ...props, }, }); }; beforeEach(() => { - originalLocation = window.location; - window.location.pathname = MOCK_PATH_NAME; - }); - - afterEach(() => { - window.location = originalLocation; + setWindowLocation(`/${MOCK_PATH_NAME}`); }); describe('single callback URL domain passed', () => { beforeEach(() => { - createWrapper(); + createWrapper({ + callbackUrls: MOCK_CALLBACK_URLS.slice(0, 1), + }); + }); + + it('renders expected callback URL message', () => { + expect(wrapper.text()).toContain( + `Could not find a callback URL entry for ${MOCK_CALLBACK_URL}.`, + ); }); it('does not render dropdown', () => { expect(findDropdown().exists()).toBe(false); }); - it('reloads page with correct url on button click', async () => { - findButton().vm.$emit('click'); - await nextTick(); - - expect(window.location.replace).toHaveBeenCalledTimes(1); - expect(window.location.replace).toHaveBeenCalledWith( - new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(), - ); + it('renders button with correct attributes', () => { + const button = findButton(); + expect(button.exists()).toBe(true); + const baseUrl = MOCK_CALLBACK_URLS[0].base; + expect(button.text()).toContain(baseUrl); + expect(button.attributes('href')).toBe(`${baseUrl}${MOCK_PATH_NAME}`); }); }); describe('multiple callback URL domains passed', () => { - const MOCK_CALLBACK_URL_ORIGINS = [MOCK_CALLBACK_URL_ORIGIN, 'https://example2.com']; - beforeEach(() => { - createWrapper({ callbackUrlOrigins: MOCK_CALLBACK_URL_ORIGINS }); + createWrapper(); }); - it('renders dropdown', () => { - expect(findDropdown().exists()).toBe(true); + it('renders dropdown with correct items', () => { + const dropdown = findDropdown(); + + expect(dropdown.exists()).toBe(true); + expect(dropdown.props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS); + }); + }); + + describe('with erroneous callback from current origin', () => { + beforeEach(() => { + createWrapper({ + callbackUrls: MOCK_CALLBACK_URLS.concat({ + base: `${TEST_HOST}/foo`, + }), + }); }); - it('renders dropdown items', () => { - const dropdownItems = findDropdownItems(); - expect(dropdownItems.length).toBe(MOCK_CALLBACK_URL_ORIGINS.length); - expect(dropdownItems.at(0).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[0]); - expect(dropdownItems.at(1).text()).toBe(MOCK_CALLBACK_URL_ORIGINS[1]); + it('filters out item with current origin', () => { + expect(findDropdown().props('items')).toStrictEqual(EXPECTED_DROPDOWN_ITEMS); + }); + }); + + describe('when no callback URL passed', () => { + beforeEach(() => { + createWrapper({ + callbackUrls: [], + }); }); - it('reloads page with correct url on dropdown item click', async () => { - const dropdownItem = findDropdownItems().at(0); - dropdownItem.vm.$emit('select', MOCK_CALLBACK_URL_ORIGIN); - await nextTick(); - - expect(window.location.replace).toHaveBeenCalledTimes(1); - expect(window.location.replace).toHaveBeenCalledWith( - new URL(MOCK_CALLBACK_URL_ORIGIN + MOCK_PATH_NAME).toString(), - ); + it('does not render dropdown or button', () => { + expect(findDropdown().exists()).toBe(false); + expect(findButton().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js index 3c390bdcf41..08c49bb4f3c 100644 --- a/spec/frontend/ide/helpers.js +++ b/spec/frontend/ide/helpers.js @@ -1,7 +1,6 @@ import * as pathUtils from 'path'; -import { commitActionTypes } from '~/ide/constants'; +import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, commitActionTypes } from '~/ide/constants'; import { decorateData } from '~/ide/stores/utils'; -import { WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/lib/gitlab_web_ide/get_oauth_config'; export const file = (name = 'name', id = name, type = '', parent = null) => decorateData({ diff --git a/spec/frontend/ide/index_spec.js b/spec/frontend/ide/index_spec.js index 414c963aeb7..8f79af84a63 100644 --- a/spec/frontend/ide/index_spec.js +++ b/spec/frontend/ide/index_spec.js @@ -2,12 +2,14 @@ import { startIde } from '~/ide/index'; import { IDE_ELEMENT_ID } from '~/ide/constants'; import { OAuthCallbackDomainMismatchErrorApp } from '~/ide/oauth_callback_domain_mismatch_error'; import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; jest.mock('~/ide/init_gitlab_web_ide'); -const MOCK_CALLBACK_URL = `${window.location.origin}/ide/redirect`; +const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect'; const MOCK_DATA_SET = { - callbackUrls: JSON.stringify([MOCK_CALLBACK_URL]), + callbackUrls: JSON.stringify([`${TEST_HOST}/-/ide/oauth_redirect`]), useNewWebIde: true, }; /** @@ -27,12 +29,20 @@ const setupMockIdeElement = (customData = MOCK_DATA_SET) => { }; describe('startIde', () => { + let renderErrorSpy; + + beforeEach(() => { + setWindowLocation(`${TEST_HOST}/-/ide/edit/gitlab-org/gitlab`); + renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError'); + }); + afterEach(() => { - document.getElementById(IDE_ELEMENT_ID).remove(); + document.getElementById(IDE_ELEMENT_ID)?.remove(); }); describe('when useNewWebIde feature flag is true', () => { let ideElement; + beforeEach(async () => { ideElement = setupMockIdeElement(); @@ -43,34 +53,14 @@ describe('startIde', () => { expect(initGitlabWebIDE).toHaveBeenCalledTimes(1); expect(initGitlabWebIDE).toHaveBeenCalledWith(ideElement); }); + + it('does not render error page', () => { + expect(renderErrorSpy).not.toHaveBeenCalled(); + }); }); - describe('OAuth callback origin mismatch check', () => { - let renderErrorSpy; - - beforeEach(() => { - renderErrorSpy = jest.spyOn(OAuthCallbackDomainMismatchErrorApp.prototype, 'renderError'); - }); - - it('does not render error page if no callbackUrl provided', async () => { - setupMockIdeElement({ useNewWebIde: true }); - await startIde(); - - expect(renderErrorSpy).not.toHaveBeenCalled(); - expect(initGitlabWebIDE).toHaveBeenCalledTimes(1); - }); - - it('does not call renderOAuthDomainMismatchError if no mismatch detected', async () => { - setupMockIdeElement(); - await startIde(); - - expect(renderErrorSpy).not.toHaveBeenCalled(); - expect(initGitlabWebIDE).toHaveBeenCalledTimes(1); - }); - - it('renders error page if OAuth callback origin does not match window.location.origin', async () => { - const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect'; - renderErrorSpy.mockImplementation(() => {}); + describe('with mismatch callback url', () => { + it('renders error page', async () => { setupMockIdeElement({ callbackUrls: JSON.stringify([MOCK_MISMATCH_CALLBACK_URL]), useNewWebIde: true, @@ -82,4 +72,17 @@ describe('startIde', () => { expect(initGitlabWebIDE).not.toHaveBeenCalled(); }); }); + + describe('with relative URL location and mismatch callback url', () => { + it('renders error page', async () => { + setWindowLocation(`${TEST_HOST}/relative-path/-/ide/edit/project`); + + setupMockIdeElement(); + + await startIde(); + + expect(renderErrorSpy).toHaveBeenCalledTimes(1); + expect(initGitlabWebIDE).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js index 5f7e4caed19..3f905574d2d 100644 --- a/spec/frontend/ide/init_gitlab_web_ide_spec.js +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -135,7 +135,7 @@ describe('ide/init_gitlab_web_ide', () => { mrTargetProject: '', forkInfo: null, username: gon.current_username, - gitlabUrl: TEST_HOST, + gitlabUrl: `${TEST_HOST}/`, nonce: TEST_NONCE, httpHeaders: { 'mock-csrf-header': 'mock-csrf-token', diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js index 3c42f54a1f7..e50868d69ad 100644 --- a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js +++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js @@ -16,7 +16,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => { expect(actual).toEqual({ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, - gitlabUrl: TEST_HOST, + gitlabUrl: `${TEST_HOST}/`, }); }); @@ -27,7 +27,7 @@ describe('~/ide/lib/gitlab_web_ide/get_base_config', () => { expect(actual).toEqual({ baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, - gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}`, + gitlabUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/`, }); }); }); diff --git a/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js new file mode 100644 index 00000000000..61a8e3288d8 --- /dev/null +++ b/spec/frontend/ide/lib/gitlab_web_ide/oauth_callback_urls_spec.js @@ -0,0 +1,89 @@ +import { + parseCallbackUrls, + getOAuthCallbackUrl, +} from '~/ide/lib/gitlab_web_ide/oauth_callback_urls'; +import { logError } from '~/lib/logger'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { IDE_PATH, WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/constants'; +import setWindowLocation from 'helpers/set_window_location_helper'; + +jest.mock('~/lib/logger'); + +const MOCK_IDE_PATH = joinPaths(IDE_PATH, 'some/path'); + +describe('ide/lib/oauth_callback_urls', () => { + describe('getOAuthCallbackUrl', () => { + const mockPath = MOCK_IDE_PATH; + const MOCK_RELATIVE_PATH = 'relative-path'; + const mockPathWithRelative = joinPaths(MOCK_RELATIVE_PATH, MOCK_IDE_PATH); + + const originalHref = window.location.href; + + afterEach(() => { + setWindowLocation(originalHref); + }); + + const expectedBaseUrlWithRelative = joinPaths(window.location.origin, MOCK_RELATIVE_PATH); + + it.each` + path | expectedCallbackBaseUrl + ${mockPath} | ${window.location.origin} + ${mockPathWithRelative} | ${expectedBaseUrlWithRelative} + `( + 'retrieves expected callback URL based on window url', + ({ path, expectedCallbackBaseUrl }) => { + setWindowLocation(path); + + const actual = getOAuthCallbackUrl(); + const expected = joinPaths(expectedCallbackBaseUrl, WEB_IDE_OAUTH_CALLBACK_URL_PATH); + expect(actual).toEqual(expected); + }, + ); + }); + describe('parseCallbackUrls', () => { + it('parses the given JSON URL array and returns some metadata for them', () => { + const actual = parseCallbackUrls( + JSON.stringify([ + 'https://gitlab.com/-/ide/oauth_redirect', + 'not a url', + 'https://gdk.test:3443/-/ide/oauth_redirect/', + 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo', + 'https://example.com/not-a-real-one-/ide/oauth_redirectz', + ]), + ); + + expect(actual).toEqual([ + { + base: 'https://gitlab.com/', + url: 'https://gitlab.com/-/ide/oauth_redirect', + }, + { + base: 'https://gdk.test:3443/', + url: 'https://gdk.test:3443/-/ide/oauth_redirect/', + }, + { + base: 'https://gdk.test:3443/gitlab/', + url: 'https://gdk.test:3443/gitlab/-/ide/oauth_redirect#1234?query=foo', + }, + { + base: 'https://example.com/', + url: 'https://example.com/not-a-real-one-/ide/oauth_redirectz', + }, + ]); + }); + + it('returns empty when given empty', () => { + expect(parseCallbackUrls('')).toEqual([]); + expect(logError).not.toHaveBeenCalled(); + }); + + it('returns empty when not valid JSON', () => { + expect(parseCallbackUrls('babar')).toEqual([]); + expect(logError).toHaveBeenCalledWith('Failed to parse callback URLs JSON'); + }); + + it('returns empty when not array JSON', () => { + expect(parseCallbackUrls('{}')).toEqual([]); + }); + }); +}); diff --git a/spec/frontend/ide/mount_oauth_callback_spec.js b/spec/frontend/ide/mount_oauth_callback_spec.js index 9187fffca81..e63ef9c0496 100644 --- a/spec/frontend/ide/mount_oauth_callback_spec.js +++ b/spec/frontend/ide/mount_oauth_callback_spec.js @@ -46,7 +46,7 @@ describe('~/ide/mount_oauth_callback', () => { clientId: TEST_OAUTH_CLIENT_ID, protectRefreshToken: true, }, - gitlabUrl: TEST_HOST, + gitlabUrl: `${TEST_HOST}/`, baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, username: TEST_USERNAME, }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 75f8a1f342d..f29fa1f76cf 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -1304,4 +1304,21 @@ describe('URL utility', () => { expect(urlUtils.buildURLwithRefType({ base, path, refType })).toBe(output); }); }); + + describe('stripRelativeUrlRootFromPath', () => { + it.each` + relativeUrlRoot | path | expectation + ${''} | ${'/foo/bar'} | ${'/foo/bar'} + ${'/'} | ${'/foo/bar'} | ${'/foo/bar'} + ${'/foo'} | ${'/foo/bar'} | ${'/bar'} + ${'/gitlab/'} | ${'/gitlab/-/ide/foo'} | ${'/-/ide/foo'} + `( + 'with relative_url_root="$relativeUrlRoot", "$path" should return "$expectation"', + ({ relativeUrlRoot, path, expectation }) => { + window.gon.relative_url_root = relativeUrlRoot; + + expect(urlUtils.stripRelativeUrlRootFromPath(path)).toBe(expectation); + }, + ); + }); }); diff --git a/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap b/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap new file mode 100644 index 00000000000..b2590297b8f --- /dev/null +++ b/spec/frontend/search/results/components/__snapshots__/result_empty_spec.js.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GlobalSearchResultsEmpty component basics renders all parts of header 1`] = ` + + + +`; diff --git a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap index e3f226345bd..be8f34ad824 100644 --- a/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap +++ b/spec/frontend/search/results/components/__snapshots__/zoekt_blob_results_spec.js.snap @@ -3,5 +3,89 @@ exports[`ZoektBlobResults when component loads normally renders component properly 1`] = `
+> +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ +
+
+
+
+ +
`; diff --git a/spec/frontend/search/results/components/blob_body_spec.js b/spec/frontend/search/results/components/blob_body_spec.js new file mode 100644 index 00000000000..f821c497277 --- /dev/null +++ b/spec/frontend/search/results/components/blob_body_spec.js @@ -0,0 +1,45 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BlobChunks from '~/search/results/components/blob_chunks.vue'; +import ZoektBlobResultsChunks from '~/search/results/components/blob_body.vue'; +import eventHub from '~/search/results/event_hub'; +import { mockDataForBlobBody } from '../../mock_data'; + +describe('BlobChunks', () => { + let wrapper; + + const createComponent = (file = {}) => { + wrapper = shallowMountExtended(ZoektBlobResultsChunks, { + propsData: { + file, + }, + }); + }; + + const findBlobChunks = () => wrapper.findAllComponents(BlobChunks); + + describe('component basics', () => { + beforeEach(() => { + createComponent(mockDataForBlobBody); + }); + + it(`renders default amount of chunks`, () => { + expect(findBlobChunks()).toHaveLength(3); + expect(findBlobChunks().at(0).props()).toMatchObject({ + chunk: { + lines: expect.any(Array), + matchCountInChunk: expect.any(Number), + __typename: expect.any(String), + }, + blameLink: 'blame/test.js', + fileUrl: 'https://gitlab.com/file/test.js', + }); + }); + + it(`renders all chunks`, async () => { + eventHub.$emit('showMore', { id: 'Testjs/Test:file/test.js', state: true }); + await nextTick(); + expect(findBlobChunks()).toHaveLength(4); + }); + }); +}); diff --git a/spec/frontend/search/results/components/blob_chunks_spec.js b/spec/frontend/search/results/components/blob_chunks_spec.js new file mode 100644 index 00000000000..0bd5f63d767 --- /dev/null +++ b/spec/frontend/search/results/components/blob_chunks_spec.js @@ -0,0 +1,77 @@ +import { GlIcon, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BlobChunks from '~/search/results/components/blob_chunks.vue'; + +describe('BlobChunks', () => { + let wrapper; + + const createComponent = (props) => { + wrapper = shallowMountExtended(BlobChunks, { + propsData: { + ...props, + }, + stubs: { + GlLink, + }, + }); + }; + + const findGlIcon = () => wrapper.findAllComponents(GlIcon); + const findGlLink = () => wrapper.findAllComponents(GlLink); + const findLine = () => wrapper.findAllByTestId('search-blob-line'); + const findLineNumbers = () => wrapper.findAllByTestId('search-blob-line-numbers'); + const findLineCode = () => wrapper.findAllByTestId('search-blob-line-code'); + const findRootElement = () => wrapper.find('#search-blob-content'); + + describe('component basics', () => { + beforeEach(() => { + createComponent({ + chunk: { + lines: [ + { + lineNumber: 1, + richText: '', + text: '', + __typename: 'SearchBlobLine', + }, + { + lineNumber: 2, + richText: 'test1', + text: 'test1', + __typename: 'SearchBlobLine', + }, + { lineNumber: 3, richText: '', text: '', __typename: 'SearchBlobLine' }, + ], + matchCountInChunk: 1, + __typename: 'SearchBlobChunk', + }, + blameLink: 'https://gitlab.com/blame/test.js', + fileUrl: 'https://gitlab.com/file/test.js', + }); + }); + + it(`renders default state`, () => { + expect(findLine()).toHaveLength(3); + expect(findLineNumbers()).toHaveLength(3); + expect(findLineCode()).toHaveLength(3); + expect(findGlLink()).toHaveLength(6); + expect(findGlIcon()).toHaveLength(3); + }); + + it(`renders proper colors`, () => { + expect(findRootElement().classes('white')).toBe(true); + expect(findLineCode().at(1).find('b').classes('hll')).toBe(true); + }); + + it(`renders links correctly`, () => { + expect(findGlLink().at(0).attributes('href')).toBe('https://gitlab.com/blame/test.js#L1'); + expect(findGlLink().at(0).attributes('title')).toBe('View blame'); + expect(findGlLink().at(0).findComponent(GlIcon).exists()).toBe(true); + expect(findGlLink().at(0).findComponent(GlIcon).props('name')).toBe('git'); + + expect(findGlLink().at(1).attributes('href')).toBe('https://gitlab.com/file/test.js#L1'); + expect(findGlLink().at(1).attributes('title')).toBe('View Line in repository'); + expect(findGlLink().at(1).text()).toBe('1'); + }); + }); +}); diff --git a/spec/frontend/search/results/components/blob_footer_spec.js b/spec/frontend/search/results/components/blob_footer_spec.js new file mode 100644 index 00000000000..d494b434e29 --- /dev/null +++ b/spec/frontend/search/results/components/blob_footer_spec.js @@ -0,0 +1,107 @@ +import { nextTick } from 'vue'; +import { GlSprintf, GlButton, GlLink } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BlobFooter from '~/search/results/components/blob_footer.vue'; +import eventHub from '~/search/results/event_hub'; +import { mockDataForBlobBody } from '../../mock_data'; + +describe('BlobFooter', () => { + let wrapper; + let spy; + + const createComponent = (props) => { + wrapper = shallowMountExtended(BlobFooter, { + propsData: { + ...props, + }, + stubs: { + GlSprintf, + GlLink, + }, + }); + }; + + const findGlButton = () => wrapper.findComponent(GlButton); + const findGlLink = () => wrapper.findComponent(GlLink); + + describe('component basics', () => { + beforeEach(() => { + createComponent({ + file: mockDataForBlobBody, + }); + spy = jest.spyOn(eventHub, '$emit'); + }); + + it(`renders default closed state`, () => { + expect(findGlButton().exists()).toBe(true); + expect(wrapper.text()).toContain('Show 1 more matches'); + }); + + it(`renders default open state`, async () => { + findGlButton().vm.$emit('click'); + await nextTick(); + expect(spy).toHaveBeenCalledWith('showMore', { + id: 'Testjs/Test:file/test.js', + state: true, + }); + expect(wrapper.text()).toContain('Show less'); + }); + }); + + describe('component with too many results', () => { + beforeEach(() => { + createComponent({ + // matchCountTotal: 100, + // matchCount: 100, + // filePath: 'test/file.js', + // projectPath: 'Testjs/Test', + // fileLink: 'https://gitlab.com/test/file.js', + file: { + ...mockDataForBlobBody, + chunks: [ + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ...mockDataForBlobBody.chunks, + ], + matchCountTotal: 200, + }, + }); + }); + + it(`renders closed state`, () => { + expect(findGlButton().exists()).toBe(true); + expect(wrapper.text()).toContain('Show 97 more matches'); + }); + + it(`renders open state`, async () => { + findGlButton().vm.$emit('click'); + await nextTick(); + expect(findGlLink().exists()).toBe(true); + expect(wrapper.text()).toContain( + 'Show less - Too many matches found. Showing 50 chunks out of 200 results. Open the file to view all.', + ); + }); + }); +}); diff --git a/spec/frontend/search/results/components/blob_header_spec.js b/spec/frontend/search/results/components/blob_header_spec.js new file mode 100644 index 00000000000..e94119228b0 --- /dev/null +++ b/spec/frontend/search/results/components/blob_header_spec.js @@ -0,0 +1,54 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import BlobHeader from '~/search/results/components/blob_header.vue'; + +describe('BlobHeader', () => { + let wrapper; + + const createComponent = (props) => { + wrapper = shallowMountExtended(BlobHeader, { + propsData: { + ...props, + }, + }); + }; + + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); + const findFileIcon = () => wrapper.findComponent(FileIcon); + const findProjectPath = () => wrapper.findByTestId('project-path-content'); + const findProjectName = () => wrapper.findByTestId('file-name-content'); + + describe('component basics', () => { + beforeEach(() => { + createComponent({ + filePath: 'test/file.js', + projectPath: 'Testjs/Test', + fileUrl: 'https://gitlab.com/test/file.js', + }); + }); + + it(`renders all parts of header`, () => { + expect(findClipboardButton().exists()).toBe(true); + expect(findFileIcon().exists()).toBe(true); + expect(findProjectPath().exists()).toBe(true); + expect(findProjectName().exists()).toBe(true); + }); + }); + + describe('limited component', () => { + beforeEach(() => { + createComponent({ + filePath: 'test/file.js', + fileUrl: 'https://gitlab.com/test/file.js', + }); + }); + + it(`renders withough projectPath`, () => { + expect(findClipboardButton().exists()).toBe(true); + expect(findFileIcon().exists()).toBe(true); + expect(findProjectPath().exists()).toBe(false); + expect(findProjectName().exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/search/results/components/result_empty_spec.js b/spec/frontend/search/results/components/result_empty_spec.js new file mode 100644 index 00000000000..067ba0447d4 --- /dev/null +++ b/spec/frontend/search/results/components/result_empty_spec.js @@ -0,0 +1,48 @@ +import Vue from 'vue'; +// eslint-disable-next-line no-restricted-imports +import Vuex from 'vuex'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import GlobalSearchResultsEmpty from '~/search/results/components/result_empty.vue'; +import { MOCK_QUERY } from '../../mock_data'; + +Vue.use(Vuex); + +describe('GlobalSearchResultsEmpty', () => { + let wrapper; + + const getterSpies = { + currentScope: jest.fn(() => 'blobs'), + }; + + const createComponent = ( + props, + initialState = { query: { scope: 'blobs' }, searchType: 'zoekt' }, + ) => { + const store = new Vuex.Store({ + state: { + query: MOCK_QUERY, + ...initialState, + }, + getters: getterSpies, + }); + + wrapper = shallowMountExtended(GlobalSearchResultsEmpty, { + store, + propsData: { + ...props, + }, + }); + }; + + describe('component basics', () => { + beforeEach(() => { + createComponent(); + }); + + it(`renders all parts of header`, () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/search/results/components/zoekt_blob_results_spec.js b/spec/frontend/search/results/components/zoekt_blob_results_spec.js index de8144037ae..9ad36971c40 100644 --- a/spec/frontend/search/results/components/zoekt_blob_results_spec.js +++ b/spec/frontend/search/results/components/zoekt_blob_results_spec.js @@ -10,6 +10,7 @@ import ZoektBlobResults from '~/search/results/components/zoekt_blob_results.vue import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; +import EmptyResult from '~/search/results/components/result_empty.vue'; import { MOCK_QUERY, mockGetBlobSearchQuery } from '../../mock_data'; jest.mock('~/alert'); @@ -26,6 +27,7 @@ describe('ZoektBlobResults', () => { const blobSearchHandler = jest.fn().mockResolvedValue(mockGetBlobSearchQuery); const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {})); + const mockQueryEmpty = jest.fn().mockReturnValue({}); const mockQueryError = jest.fn().mockRejectedValue(new Error('Network error')); const createComponent = ({ @@ -42,7 +44,7 @@ describe('ZoektBlobResults', () => { }, getters: getterSpies, }); - + // apolloMock = createMockApollo([[getBlobSearchQuery, blobSearchHandler]]); wrapper = shallowMountExtended(ZoektBlobResults, { apolloProvider, store, @@ -53,6 +55,7 @@ describe('ZoektBlobResults', () => { }; const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findEmptyResult = () => wrapper.findComponent(EmptyResult); describe('when loading results', () => { beforeEach(async () => { @@ -81,6 +84,21 @@ describe('ZoektBlobResults', () => { }); }); + describe('when component has no results', () => { + beforeEach(async () => { + createComponent({ + queryHandler: mockQueryEmpty, + }); + jest.advanceTimersByTime(500); + await waitForPromises(); + }); + + it(`renders component properly`, async () => { + await nextTick(); + expect(findEmptyResult().exists()).toBe(true); + }); + }); + describe('when component has load error', () => { beforeEach(async () => { createComponent({ queryHandler: mockQueryError }); diff --git a/spec/frontend/work_items/components/work_item_abuse_modal_spec.js b/spec/frontend/work_items/components/work_item_abuse_modal_spec.js new file mode 100644 index 00000000000..59d36a279fa --- /dev/null +++ b/spec/frontend/work_items/components/work_item_abuse_modal_spec.js @@ -0,0 +1,101 @@ +import { GlModal, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue'; +import { CATEGORY_OPTIONS } from '~/abuse_reports/components/constants'; + +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); + +describe('WorkItemAbuseModal', () => { + let wrapper; + + const ACTION_PATH = '/abuse_reports/add_category'; + const USER_ID = 1; + const REPORTED_FROM_URL = 'http://example.com'; + + const createComponent = (props) => { + wrapper = shallowMountExtended(WorkItemAbuseModal, { + propsData: { + reportedUserId: USER_ID, + reportedFromUrl: REPORTED_FROM_URL, + ...props, + }, + provide: { + reportAbusePath: ACTION_PATH, + }, + }); + }; + + beforeEach(() => { + createComponent({ showModal: true }); + }); + + const findAbuseModal = () => wrapper.findComponent(GlModal); + const findForm = () => wrapper.findComponent(GlForm); + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + + const findCSRFToken = () => findForm().find('input[name="authenticity_token"]'); + const findUserId = () => wrapper.findByTestId('input-user-id'); + const findReferer = () => wrapper.findByTestId('input-referer'); + + describe('Modal', () => { + it('renders report abuse modal with the form', () => { + expect(findAbuseModal().exists()).toBe(true); + expect(findForm().exists()).toBe(true); + }); + + it('should set the modal title when the `title` prop is set', () => { + const title = 'Report abuse to administrator'; + createComponent({ title, showModal: true }); + + expect(findAbuseModal().props().title).toBe(title); + }); + + it('should set modal size to `sm` by default', () => { + expect(findAbuseModal().props('size')).toBe('sm'); + }); + + it('renders radio form group with the first option selected by default', () => { + const firstOption = CATEGORY_OPTIONS[0].value; + expect(findRadioGroup().attributes('checked')).toBe(firstOption); + }); + }); + + describe('Select category form', () => { + it('renders POST form with path', () => { + expect(findForm().attributes()).toMatchObject({ + method: 'post', + action: ACTION_PATH, + }); + }); + + it('renders csrf token', () => { + expect(findCSRFToken().attributes('value')).toBe('mock-csrf-token'); + }); + + it('renders label', () => { + expect(findFormGroup().attributes('label')).toBe('Why are you reporting this user?'); + }); + + it('renders radio group', () => { + expect(findRadioGroup().props('options')).toEqual(CATEGORY_OPTIONS); + expect(findRadioGroup().attributes('name')).toBe('abuse_report[category]'); + }); + + it('renders userId as a hidden fields', () => { + expect(findUserId().attributes()).toMatchObject({ + type: 'hidden', + name: 'user_id', + value: USER_ID.toString(), + }); + }); + + it('renders referer as a hidden fields', () => { + expect(findReferer().attributes()).toMatchObject({ + type: 'hidden', + name: 'abuse_report[reported_from_url]', + value: REPORTED_FROM_URL, + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index bda391a71fa..bb276b0f6da 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -5,7 +5,7 @@ import { GlToggle, GlDisclosureDropdownItem, } from '@gitlab/ui'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import namespaceWorkItemTypesQueryResponse from 'test_fixtures/graphql/work_items/namespace_work_item_types.query.graphql.json'; @@ -19,6 +19,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { isLoggedIn } from '~/lib/utils/common_utils'; import toast from '~/vue_shared/plugins/global_toast'; import WorkItemActions from '~/work_items/components/work_item_actions.vue'; +import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue'; import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue'; import { STATE_OPEN, @@ -30,6 +31,7 @@ import { TEST_ID_NOTIFICATIONS_TOGGLE_FORM, TEST_ID_PROMOTE_ACTION, TEST_ID_TOGGLE_ACTION, + TEST_ID_REPORT_ABUSE, } from '~/work_items/constants'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql'; @@ -64,6 +66,8 @@ describe('WorkItemActions component', () => { const findWorkItemToggleOption = () => wrapper.findComponent(WorkItemStateToggle); const findCopyCreateNoteEmailButton = () => wrapper.findByTestId(TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION); + const findReportAbuseButton = () => wrapper.findByTestId(TEST_ID_REPORT_ABUSE); + const findReportAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal); const findMoreDropdown = () => wrapper.findByTestId('work-item-actions-dropdown'); const findMoreDropdownTooltip = () => getBinding(findMoreDropdown().element, 'gl-tooltip'); const findDropdownItems = () => wrapper.findAll('[data-testid="work-item-actions-dropdown"] > *'); @@ -217,6 +221,10 @@ describe('WorkItemActions component', () => { { divider: true, }, + { + testId: TEST_ID_REPORT_ABUSE, + text: 'Report abuse', + }, { testId: TEST_ID_DELETE_ACTION, text: 'Delete task', @@ -502,4 +510,22 @@ describe('WorkItemActions component', () => { expect(findMoreDropdownTooltip().value).toBe('More actions'); }); }); + + describe('report abuse action', () => { + it('renders the report abuse button', () => { + createComponent(); + + expect(findReportAbuseButton().exists()).toBe(true); + expect(findReportAbuseModal().exists()).toBe(false); + }); + + it('opens the report abuse modal', async () => { + createComponent(); + + findReportAbuseButton().vm.$emit('action'); + await nextTick(); + + expect(wrapper.emitted('toggleReportAbuseModal')).toEqual([[true]]); + }); + }); }); diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 97f1e8753dc..285c03899b0 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -20,7 +20,7 @@ import WorkItemNotes from '~/work_items/components/work_item_notes.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; import WorkItemStickyHeader from '~/work_items/components/work_item_sticky_header.vue'; import WorkItemTitle from '~/work_items/components/work_item_title.vue'; -import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; +import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue'; import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; import DesignWidget from '~/work_items/components/design_management/design_management_widget.vue'; import { i18n } from '~/work_items/constants'; @@ -83,7 +83,7 @@ describe('WorkItemDetail component', () => { const findWorkItemRelationships = () => wrapper.findComponent(WorkItemRelationships); const findNotesWidget = () => wrapper.findComponent(WorkItemNotes); const findModal = () => wrapper.findComponent(WorkItemDetailModal); - const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); + const findWorkItemAbuseModal = () => wrapper.findComponent(WorkItemAbuseModal); const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos); const findStickyHeader = () => wrapper.findComponent(WorkItemStickyHeader); const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview'); @@ -696,7 +696,7 @@ describe('WorkItemDetail component', () => { }); it('should not be visible by default', () => { - expect(findAbuseCategorySelector().exists()).toBe(false); + expect(findWorkItemAbuseModal().exists()).toBe(false); }); it('should be visible when the work item modal emits `openReportAbuse` event', async () => { @@ -704,13 +704,25 @@ describe('WorkItemDetail component', () => { await nextTick(); - expect(findAbuseCategorySelector().exists()).toBe(true); + expect(findWorkItemAbuseModal().exists()).toBe(true); - findAbuseCategorySelector().vm.$emit('close-drawer'); + findWorkItemAbuseModal().vm.$emit('close-modal'); await nextTick(); - expect(findAbuseCategorySelector().exists()).toBe(false); + expect(findWorkItemAbuseModal().exists()).toBe(false); + }); + + it('should be visible when the work item actions button emits `toggleReportAbuseModal` event', async () => { + findWorkItemActions().vm.$emit('toggleReportAbuseModal', true); + await nextTick(); + + expect(findWorkItemAbuseModal().exists()).toBe(true); + + findWorkItemAbuseModal().vm.$emit('close-modal'); + await nextTick(); + + expect(findWorkItemAbuseModal().exists()).toBe(false); }); }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 0fc21a9d095..128ba9218bc 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -16,7 +16,7 @@ import WidgetWrapper from '~/work_items/components/widget_wrapper.vue'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue'; import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue'; -import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; +import WorkItemAbuseModal from '~/work_items/components/work_item_abuse_modal.vue'; import { FORM_TYPES } from '~/work_items/constants'; import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql'; @@ -94,7 +94,7 @@ describe('WorkItemLinks', () => { const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); const findChildrenCount = () => wrapper.findByTestId('children-count'); const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal); - const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); + const findAbuseCategoryModal = () => wrapper.findComponent(WorkItemAbuseModal); const findWorkItemLinkChildrenWrapper = () => wrapper.findComponent(WorkItemChildrenWrapper); const findShowLabelsToggle = () => wrapper.findComponent(GlToggle); @@ -237,7 +237,7 @@ describe('WorkItemLinks', () => { }); it('should not be visible by default', () => { - expect(findAbuseCategorySelector().exists()).toBe(false); + expect(findAbuseCategoryModal().exists()).toBe(false); }); it('should be visible when the work item modal emits `openReportAbuse` event', async () => { @@ -245,13 +245,13 @@ describe('WorkItemLinks', () => { await nextTick(); - expect(findAbuseCategorySelector().exists()).toBe(true); + expect(findAbuseCategoryModal().exists()).toBe(true); - findAbuseCategorySelector().vm.$emit('close-drawer'); + findAbuseCategoryModal().vm.$emit('close-modal'); await nextTick(); - expect(findAbuseCategorySelector().exists()).toBe(false); + expect(findAbuseCategoryModal().exists()).toBe(false); }); }); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a9b5b4fcead..23295cdf69e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -270,6 +270,9 @@ merge_requests: - scan_result_policy_violations - applicable_post_merge_approval_rules - requested_changes +- scan_result_policy_reads_through_violations +- scan_result_policy_reads_through_approval_rules +- running_scan_result_policy_violations external_pull_requests: - project merge_request_diff: diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f97a2c20b1e..75f48ddf5e0 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3946,19 +3946,18 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev context 'when auto_merge_requested is true' do let(:options) { { auto_merge_requested: true, auto_merge_strategy: auto_merge_strategy } } - where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check, - :skip_discussions_check, :skip_external_status_check, :skip_requested_changes_check, :skip_jira_check, :skip_locked_lfs_files_check) do - '' | false | false | false | false | false | false | false | false - AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false | false | false | false | false | false | false | false - AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | true | true | true | true | true | true | true + where(:auto_merge_strategy, :skip_checks) do + '' | false + AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false + AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true end with_them do it do - is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check, - skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check, - skip_external_status_check: skip_external_status_check, skip_requested_changes_check: skip_requested_changes_check, - skip_jira_check: skip_jira_check) + is_expected.to include(skip_approved_check: skip_checks, skip_draft_check: skip_checks, + skip_blocked_check: skip_checks, skip_discussions_check: skip_checks, + skip_external_status_check: skip_checks, skip_requested_changes_check: skip_checks, + skip_jira_check: skip_checks, skip_security_policy_check: skip_checks) end end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index 29e2f7c7693..244b6320dfd 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -59,7 +59,7 @@ RSpec.describe 'getting merge request listings nested in a project', feature_cat # We cannot disable SQL query limiting here, since the transaction does not # begin until we enter the controller. headers = { - 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '205,https://gitlab.com/gitlab-org/gitlab/-/issues/469250' + 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => '206,https://gitlab.com/gitlab-org/gitlab/-/issues/469250' } post_graphql(query, current_user: current_user, headers: headers) diff --git a/spec/tooling/danger/settings_sections_spec.rb b/spec/tooling/danger/settings_sections_spec.rb new file mode 100644 index 00000000000..1fbea0e1f3a --- /dev/null +++ b/spec/tooling/danger/settings_sections_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'gitlab/dangerfiles/spec_helper' +require 'fast_spec_helper' + +require_relative '../../../tooling/danger/settings_sections' + +RSpec.describe Tooling::Danger::SettingsSections, feature_category: :tooling do + include_context 'with dangerfile' + + subject(:settings_section_check) { fake_danger.new(helper: fake_helper) } + + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + let(:matching_changed_files) { ['app/views/foo/bar.html.haml', 'app/assets/js/foo/bar.vue'] } + let(:changed_lines) { ['-render SettingsBlockComponent.new(id: "foo") do', ''] } + let(:stable_branch?) { false } + + before do + allow(fake_helper).to receive(:changed_files).and_return(matching_changed_files) + allow(fake_helper).to receive(:changed_lines).and_return(changed_lines) + allow(fake_helper).to receive(:stable_branch?).and_return(stable_branch?) + end + + context 'when on stable branch' do + let(:stable_branch?) { true } + + it 'does not write any markdown' do + expect(settings_section_check).not_to receive(:markdown) + settings_section_check.check! + end + end + + context 'when none of the changed files are Haml or Vue files' do + let(:matching_changed_files) { [] } + + it 'does not write any markdown' do + expect(settings_section_check).not_to receive(:markdown) + settings_section_check.check! + end + end + + context 'when none of the changed lines match the pattern' do + let(:changed_lines) { ['-foo', '+bar'] } + + it 'does not write any markdown' do + expect(settings_section_check).not_to receive(:markdown) + settings_section_check.check! + end + end + + it 'adds a new markdown section listing every matching line' do + expect(settings_section_check).to receive(:markdown).with(/Searchable setting sections/) + expect(settings_section_check).to receive(:markdown).with(/SettingsBlock/) + expect(settings_section_check).to receive(:markdown).with(/settings-block/) + settings_section_check.check! + end +end diff --git a/tooling/danger/settings_sections.rb b/tooling/danger/settings_sections.rb new file mode 100644 index 00000000000..b05fb721e94 --- /dev/null +++ b/tooling/danger/settings_sections.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +module Tooling + module Danger + module SettingsSections + def check! + return if helper.stable_branch? + + changed_code_files = helper.changed_files(/\.(haml|vue)$/) + return if changed_code_files.empty? + + vc_regexp = /(SettingsBlockComponent|settings-block)/ + lines_with_matches = filter_changed_lines(changed_code_files, vc_regexp) + return if lines_with_matches.empty? + + markdown(<<~MARKDOWN) + ## Searchable setting sections + + Looks like you have edited the template of some settings section. Please check that all changed sections are still searchable: + + - If you created a new section, make sure to add it to either `lib/search/project_settings.rb` or `lib/search/group_settings.rb`, or in their counterparts in `ee/` if this section is only available behind a licensed feature. + - If you removed a section, make sure to also remove it from the files above. + - If you changed a section's id, please update it also in the files above. + - If you just moved code around within the same page, there is nothing to do. + + MARKDOWN + + lines_with_matches.each do |file, lines| + markdown(<<~MARKDOWN) + #### `#{file}` + + ```shell + #{lines.join("\n")} + ``` + + MARKDOWN + end + end + + def filter_changed_lines(files, pattern) + files_with_lines = {} + files.each do |file| + next if file.start_with?('spec/', 'ee/spec/', 'qa/') + + matching_changed_lines = helper.changed_lines(file).select { |line| line =~ pattern } + next unless matching_changed_lines.any? + + files_with_lines[file] = matching_changed_lines + end + + files_with_lines + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 48c7c0b3f6d..a220c87885d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1359,10 +1359,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.109.0.tgz#af953d8114768343034f1f02bc8e2d93eb613c65" integrity sha512-MmBTsco2LIh/l16iJQy6R98YDOlE3C++AE0Z1+KCpAX/3+fLAmULx2sWp+JnmM0ws8J0LaeLN6+vWiPaEWA16Q== -"@gitlab/ui@87.8.0": - version "87.8.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-87.8.0.tgz#94b9e301330b22d466fffddaa4f9838385b68ae0" - integrity sha512-oGWyFmI87IbTYb6uYGt79MwV/hkl/vVKqLtMCgx2JLnzYSXWxYAdCKPhmQiO8Fib5RpfYwLzsxZ5qfaazTq4ig== +"@gitlab/ui@88.0.0": + version "88.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-88.0.0.tgz#aab7b5ef169d02d65e80901680052c1f755eeec3" + integrity sha512-O9K5UalOBLboPnskMPezt2nttKL/YXiSlADcDZ/MKTfw9usxRVMm+COuS+zrmqXfOEZfmNMd2lbQnXW9uJlyRQ== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2"